UNPKG

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
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; } } } ]; }