astro
Version:
Astro is a modern site builder with web best practices, performance, and DX front-of-mind.
274 lines (273 loc) • 11.1 kB
JavaScript
import { defaultClientConditions, defaultServerConditions, normalizePath } from "vite";
import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js";
import { isAstroServerEnvironment } from "../environments.js";
import { normalizeFilename, specialQueriesRE } from "../vite-plugin-utils/index.js";
import { compileAstro } from "./compile.js";
import { handleHotUpdate } from "./hmr.js";
import { parseAstroRequest } from "./query.js";
import { loadId } from "./utils.js";
export { getAstroMetadata } from "./metadata.js";
const astroFileToCompileMetadataWeakMap = new WeakMap();
/** Transform .astro files for Vite */
export default function astro({ settings, logger }) {
const { config } = settings;
let server;
let compile;
// Each Astro file has its own compile metadata so that its scripts and styles virtual module
// can retrieve their code from here.
// NOTE: We need to initialize a map here and in `buildStart` because our unit tests don't
// call `buildStart` (test bug)
let astroFileToCompileMetadata = new Map();
// Variables for determining if an id starts with /src...
const srcRootWeb = config.srcDir.pathname.slice(config.root.pathname.length - 1);
const isBrowserPath = (path) => path.startsWith(srcRootWeb) && srcRootWeb !== "/";
const notAstroComponent = (component) => !component.resolvedPath.endsWith(".astro");
return [
{
name: "astro:build:css-hmr",
enforce: "pre",
applyToEnvironment(environment) {
return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client;
},
transform: {
filter: { id: {
include: [/(?:\?|&)astro(?:&|=|$)/],
// ignore astro file sub-requests, e.g. Foo.astro?astro&type=script&index=0&lang.ts
exclude: [specialQueriesRE]
} },
async handler(_source, id) {
const parsedId = parseAstroRequest(id);
// Special edge case handling for Vite 6 beta, the style dependencies need to be registered here to take effect
// TODO: Remove this when Vite fixes it (https://github.com/vitejs/vite/pull/18103)
const astroFilename = normalizePath(normalizeFilename(parsedId.filename, config.root));
const compileMetadata = astroFileToCompileMetadata.get(astroFilename);
if (compileMetadata && parsedId.query.type === "style" && parsedId.query.index != null) {
const result = compileMetadata.css[parsedId.query.index];
// Register dependencies from preprocessing this style
result.dependencies?.forEach((dep) => this.addWatchFile(dep));
}
}
}
},
{
name: "astro:build",
enforce: "pre",
async configEnvironment(name, viteConfig, opts) {
viteConfig.resolve ??= {};
// Emulate Vite default fallback for `resolve.conditions` if not set
if (viteConfig.resolve.conditions == null) {
if (viteConfig.consumer === "client" || name === "client" || opts.isSsrTargetWebworker) {
viteConfig.resolve.conditions = [...defaultClientConditions];
} else {
viteConfig.resolve.conditions = [...defaultServerConditions];
}
}
// `configEnvironment` can run again on dev server restart with the
// resolved `conditions` carried over, so only add `astro` if missing to
// avoid a duplicate that changes the optimizeDeps config hash.
if (!viteConfig.resolve.conditions.includes("astro")) {
viteConfig.resolve.conditions.push("astro");
}
},
async configResolved(viteConfig) {
const toolbarEnabled = await settings.preferences.get("devToolbar.enabled");
// Initialize `compile` function to simplify usage later
compile = (code, filename, inlineComponentAssets) => {
return compileAstro({
compileProps: {
astroConfig: config,
viteConfig,
toolbarEnabled,
filename,
source: code,
inlineComponentAssets
},
astroFileToCompileMetadata
});
};
},
configureServer(_server) {
server = _server;
// Make sure deleted files are removed from the compile metadata to save memory
server.watcher.on("unlink", (filename) => {
astroFileToCompileMetadata.delete(filename);
});
},
buildStart() {
astroFileToCompileMetadata = new Map();
// Share the `astroFileToCompileMetadata` across the same Astro config as Astro performs
// multiple builds and its hoisted scripts analyzer requires the compile metadata from
// previous builds. Ideally this should not be needed when we refactor hoisted scripts analysis.
if (astroFileToCompileMetadataWeakMap.has(config)) {
astroFileToCompileMetadata = astroFileToCompileMetadataWeakMap.get(config);
} else {
astroFileToCompileMetadataWeakMap.set(config, astroFileToCompileMetadata);
}
},
load: {
filter: { id: /(?:\?|&)astro(?:&|=|$)/ },
async handler(id) {
const parsedId = parseAstroRequest(id);
const query = parsedId.query;
// Astro scripts and styles virtual module code comes from the main Astro compilation
// through the metadata from `astroFileToCompileMetadata`. It should always exist as Astro
// modules are compiled first, then its virtual modules.
const filename = normalizePath(normalizeFilename(parsedId.filename, config.root));
let compileMetadata = astroFileToCompileMetadata.get(filename);
if (!compileMetadata) {
// If `compileMetadata` doesn't exist in dev, that means the virtual module may have been invalidated.
// We try to re-compile the main Astro module (`filename`) first before retrieving the metadata again.
if (server) {
const code = await loadId(server.pluginContainer, filename);
// `compile` should re-set `filename` in `astroFileToCompileMetadata`
if (code != null) await compile(code, filename);
}
compileMetadata = astroFileToCompileMetadata.get(filename);
}
// If the metadata still doesn't exist, that means the virtual modules are somehow compiled first,
// throw an error and we should investigate it.
if (!compileMetadata) {
throw new Error(`No cached compile metadata found for "${id}". The main Astro module "${filename}" should have ` + `compiled and filled the metadata first, before its virtual modules can be requested.`);
}
switch (query.type) {
case "style": {
if (typeof query.index === "undefined") {
throw new Error(`Requests for Astro CSS must include an index.`);
}
const result = compileMetadata.css[query.index];
if (!result) {
throw new Error(`No Astro CSS at index ${query.index}`);
}
// Register dependencies from preprocessing this style
result.dependencies?.forEach((dep) => this.addWatchFile(dep));
return {
code: result.code,
// `vite.cssScopeTo` is a Vite feature that allows this CSS to be treeshaken
// if the Astro component's default export is not used
meta: result.isGlobal ? undefined : { vite: { cssScopeTo: [filename, "default"] } }
};
}
case "script": {
if (typeof query.index === "undefined") {
throw new Error(`Requests for scripts must include an index`);
}
// SSR script only exists to make them appear in the module graph.
if (isAstroServerEnvironment(this.environment)) {
return {
code: `/* client script, empty in SSR: ${id} */`,
moduleType: "ts"
};
}
const script = compileMetadata.scripts[query.index];
if (!script) {
throw new Error(`No script at index ${query.index}`);
}
if (script.type === "external") {
const src = script.src;
if (src.startsWith("/") && !isBrowserPath(src)) {
const publicDir = config.publicDir.pathname.replace(/\/$/, "").split("/").pop() + "/";
throw new Error(`\n\n<script src="${src}"> references an asset in the "${publicDir}" directory. Please add the "is:inline" directive to keep this asset from being bundled.\n\nFile: ${id}`);
}
}
const result = {
code: "",
moduleType: "ts",
meta: { vite: { lang: "ts" } }
};
switch (script.type) {
case "inline": {
result.code = script.code ?? "";
break;
}
case "external": {
result.code = `import "${script.src}"`;
break;
}
}
return result;
}
case "custom":
case "template":
case undefined:
default: return null;
}
}
},
transform: {
filter: { id: {
include: [/\.astro$/, /\.astro\?/],
// ignore astro file sub-requests, e.g. Foo.astro?astro&type=script&index=0&lang.ts
exclude: [specialQueriesRE, /(?:\?|&)astro(?:&|=|$)/]
} },
async handler(source, id) {
const parsedId = parseAstroRequest(id);
if (!parsedId.filename.endsWith(".astro")) {
return;
}
const filename = normalizePath(parsedId.filename);
// If an Astro component is imported in code used on the client, we return an empty
// module so that Vite doesn’t bundle the server-side Astro code for the client.
if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) {
return {
code: `export default import.meta.env.DEV
? () => {
throw new Error(
'Astro components cannot be used in the browser.\\nTried to render "${filename}".'
);
}
: {};`,
moduleType: "ts",
meta: { vite: { lang: "ts" } }
};
}
const transformResult = await compile(source, filename, parsedId.query.container);
if (parsedId.query.container) {
for (const style of transformResult.css) {
style.dependencies?.forEach((dependency) => this.addWatchFile(dependency));
}
}
const astroMetadata = {
// Remove Astro components that have been mistakenly given client directives
// We'll warn the user about this later, but for now we'll prevent them from breaking the build
clientOnlyComponents: transformResult.clientOnlyComponents.filter(notAstroComponent),
hydratedComponents: transformResult.hydratedComponents.filter(notAstroComponent),
serverComponents: transformResult.serverComponents,
scripts: transformResult.scripts,
containsHead: transformResult.containsHead,
propagation: transformResult.propagation ? "self" : "none",
pageOptions: {}
};
return {
code: transformResult.code,
map: transformResult.map,
moduleType: "ts",
meta: {
astro: astroMetadata,
vite: {
// Setting this vite metadata to `ts` causes Vite to resolve .js
// extensions to .ts files.
lang: "ts" }
}
};
}
},
async handleHotUpdate(ctx) {
return handleHotUpdate(ctx, {
logger,
compile,
astroFileToCompileMetadata
});
}
},
{
name: "astro:build:normal",
// If Vite resolver can't resolve the Astro request, it's likely a virtual Astro file, fallback here instead
resolveId: {
filter: { id: /(?:\?|&)astro(?:&|=|$)/ },
handler(id) {
return id;
}
}
}
];
}