UNPKG

astro

Version:

Astro is a modern site builder with web best practices, performance, and DX front-of-mind.

244 lines (243 loc) • 11.2 kB
import { prependForwardSlash } from "@astrojs/internal-helpers/path"; import { ASTRO_VITE_ENVIRONMENT_NAMES } from "../core/constants.js"; import { wrapId } from "../core/util.js"; import { inlineRE, isBuildableCSSRequest, rawRE } from "../vite-plugin-astro-server/util.js"; import { getVirtualModulePageNameForComponent } from "../vite-plugin-pages/util.js"; import { getDevCSSModuleName } from "./util.js"; import { CSS_LANGS_RE } from "../core/viteUtils.js"; import { PROPAGATED_ASSET_QUERY_PARAM } from "../content/consts.js"; import { VIRTUAL_PAGE_RESOLVED_MODULE_ID } from "../vite-plugin-pages/const.js"; import { RESOLVED_SERVER_ISLAND_MANIFEST } from "../core/server-islands/vite-plugin-server-islands.js"; import { ASTRO_CSS_EXTENSION_POST_PATTERN, MODULE_DEV_CSS, MODULE_DEV_CSS_ALL, MODULE_DEV_CSS_PREFIX, RESOLVED_MODULE_DEV_CSS, RESOLVED_MODULE_DEV_CSS_ALL, RESOLVED_MODULE_DEV_CSS_PREFIX } from "./const.js"; /** * Extract the original component path from a masked virtual module name. * Inverse function of getVirtualModulePageName(). */ function getComponentFromVirtualModuleCssName(virtualModulePrefix, id) { return id.slice(virtualModulePrefix.length).replace(new RegExp(ASTRO_CSS_EXTENSION_POST_PATTERN, "g"), "."); } /** * Ensure all modules reachable from the given module have been fetched and transformed. * This is needed for dynamically imported components whose modules may be registered * in the graph (via Vite's import analysis) but not yet transformed, meaning their * own imports (including CSS) would not be visible during the CSS graph walk. */ async function ensureModulesLoaded(env, mod, seen = new Set()) { const id = mod.id ?? mod.url; if (seen.has(id)) return; seen.add(id); for (const imp of mod.importedModules) { if (!imp.id) continue; if (seen.has(imp.id)) continue; // Mirror the stopping point used by collectCSSWithOrder — don't descend into propagated // asset modules, as the CSS walk intentionally stops there too. if (imp.id.includes(PROPAGATED_ASSET_QUERY_PARAM)) continue; // Stop at page virtual module boundaries. When a page imports something that // transitively pulls in virtual:astro:pages (e.g. via astro:config/server → // virtual:astro:manifest), the walk would otherwise descend into every other // page's module graph, fetching their CSS dependencies. This is the dev-mode // counterpart of isBuildCssBoundary in plugin-css.ts. See #18060, #16116. if (imp.id.startsWith(VIRTUAL_PAGE_RESOLVED_MODULE_ID)) continue; // Skip the server island manifest. The server islands plugin refreshes it by clearing its // transformResult so the module runner's next import re-transforms it and drops its stale // copy. Fetching it here would repopulate transformResult first, so Vite would answer the // runner with `cache: true` and it would keep an island map missing newly discovered // islands. The manifest contains no CSS. See #18387. if (imp.id === RESOLVED_SERVER_ISLAND_MANIFEST) continue; // Skip virtual dev-css modules to prevent circular deadlocks with the Cloudflare adapter. // The dev-css-all module statically imports all per-route dev-css:* modules, and those // modules' load handlers may already be running (waiting on ensureModulesLoaded), causing // a permanent deadlock via Vite's _pendingRequests map. These are CSS collector virtuals // that don't contain CSS themselves, so skipping them has no effect on CSS injection. if (imp.id === RESOLVED_MODULE_DEV_CSS || imp.id === RESOLVED_MODULE_DEV_CSS_ALL || imp.id.startsWith(RESOLVED_MODULE_DEV_CSS_PREFIX)) continue; // If this module hasn't been transformed yet, fetch it to populate its importedModules if (!imp.transformResult) { try { await env.fetchModule(imp.id); } catch {} } // Recursively ensure child modules are loaded await ensureModulesLoaded(env, imp, seen); } } /** * Walk down the dependency tree to collect CSS with depth/order. * Performs depth-first traversal to ensure correct CSS ordering based on import order. */ function* collectCSSWithOrder(id, mod, seen = new Set()) { seen.add(id); // Stop traversing if we reach an asset propagation stopping point to ensure we only collect CSS // relevant to a content collection entry, if any. Not doing so could cause CSS from other // entries to potentially be collected and bleed into the CSS included on the page, causing // unexpected styles, for example when a module shared between 2 pages would import // `astro:content` and thus potentially adding multiple content collection entry assets to the // module graph. if (id.includes(PROPAGATED_ASSET_QUERY_PARAM)) { return; } // Keep all of the imported modules into an array so we can go through them one at a time const imported = Array.from(mod.importedModules); // Check if this module is CSS and should be collected if (isBuildableCSSRequest(id)) { yield { id: wrapId(mod.id ?? mod.url), idKey: id, content: "", url: prependForwardSlash(wrapId(mod.url)) }; return; } else if (id.endsWith("?raw")) { return; } // Recursively walk imported modules (depth-first) for (const imp of imported) { if (imp.id && !seen.has(imp?.id)) { // Stop at page virtual module boundaries to prevent CSS from other pages // leaking in when the graph passes through virtual:astro:pages. See #18060. if (imp.id.startsWith(VIRTUAL_PAGE_RESOLVED_MODULE_ID)) continue; yield* collectCSSWithOrder(imp.id, imp, seen); } } } /** * This plugin tracks the CSS that should be applied by route. * * The virtual module should be used only during development. * Per-route virtual modules are created to avoid invalidation loops. * * The second plugin imports all of the individual CSS modules in a map. * * @param routesList */ export function astroDevCssPlugin({ routesList, command, cssContentCache }) { let server; function getCurrentEnvironment(pluginEnv) { return pluginEnv ?? server?.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr]; } return [{ name: MODULE_DEV_CSS, async configureServer(viteServer) { server = viteServer; }, applyToEnvironment(env) { return command === "dev" && env.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender; }, resolveId: { filter: { id: new RegExp(`^(${MODULE_DEV_CSS}|${MODULE_DEV_CSS_PREFIX}.*)$`) }, handler(id) { if (id === MODULE_DEV_CSS) { return RESOLVED_MODULE_DEV_CSS; } return RESOLVED_MODULE_DEV_CSS_PREFIX + id.slice(MODULE_DEV_CSS_PREFIX.length); } }, load: { filter: { id: new RegExp(`^(${RESOLVED_MODULE_DEV_CSS}|${RESOLVED_MODULE_DEV_CSS_PREFIX}.*)$`) }, async handler(id) { if (id === RESOLVED_MODULE_DEV_CSS) { return { code: `export const css = new Set()` }; } if (id.startsWith(RESOLVED_MODULE_DEV_CSS_PREFIX)) { const componentPath = getComponentFromVirtualModuleCssName(RESOLVED_MODULE_DEV_CSS_PREFIX, id); // Collect CSS by walking the dependency tree from the component const cssWithOrder = new Map(); // The virtual module name for this page, like virtual:astro:dev-css:index@_@astro const componentPageId = getVirtualModulePageNameForComponent(componentPath); const env = getCurrentEnvironment(this.environment); // Ensure the page module is loaded. This will populate the graph and allow us to walk through. await env?.fetchModule(componentPageId); const resolved = await env?.pluginContainer.resolveId(componentPageId); if (!resolved?.id) { return { code: "export const css = new Set()" }; } // the vite.EnvironmentModuleNode has all of the info we need const mod = env?.moduleGraph.getModuleById(resolved.id); if (!mod) { return { code: "export const css = new Set()" }; } // Ensure all reachable modules have been transformed. // Dynamically imported components may be in the graph (detected by Vite's // import analysis) but not yet transformed, so their own CSS imports would // be invisible during the graph walk. This eagerly fetches them. if (env) { await ensureModulesLoaded(env, mod); } // Walk through the graph depth-first for (const collected of collectCSSWithOrder(componentPageId, mod)) { // Use the CSS file ID as the key to deduplicate while keeping best ordering if (!cssWithOrder.has(collected.idKey)) { // Look up actual content from cache if available. // Use `idKey` (the raw module ID) for cache lookup because that matches the key // used by the transform hook. `collected.id` is wrapped via `wrapId()` which // transforms the `\0` prefix of virtual modules, causing a cache miss. const content = cssContentCache.get(collected.idKey) || collected.content; cssWithOrder.set(collected.idKey, { ...collected, content }); } } const cssArray = Array.from(cssWithOrder.values()); // Remove the temporary fields added during collection const cleanedCss = cssArray.map(({ content, id: cssId, url }) => ({ content, id: cssId, url })); return { code: `export const css = new Set(${JSON.stringify(cleanedCss)})` }; } } } }, { name: MODULE_DEV_CSS_ALL, applyToEnvironment(env) { return command === "dev" && env.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender; }, resolveId: { filter: { id: new RegExp(`^${MODULE_DEV_CSS_ALL}$`) }, handler() { return RESOLVED_MODULE_DEV_CSS_ALL; } }, load: { filter: { id: new RegExp(`^${RESOLVED_MODULE_DEV_CSS_ALL}$`) }, handler() { // Creates a map of the component name to a function to import it let code = `export const devCSSMap = new Map([`; for (const route of routesList.routes) { code += `\n\t[${JSON.stringify(route.component)}, () => import(${JSON.stringify(getDevCSSModuleName(route.component))})],`; } code += "]);"; return { code }; } } }]; } /** * Caches the final CSS of each module in `cssContentCache` during dev, so it can be inlined * into server-rendered HTML. * * Must be registered after all integration and user plugins: its normal-order `transform` then * runs after framework transforms (e.g. `@vitejs/plugin-vue` scoping SFC styles) and before * `vite:css-post` turns the CSS into JS. See #18333. */ export function astroDevCssContentCachePlugin({ cssContentCache }) { return { name: "astro:dev-css-content-cache", applyToEnvironment(env) { return env.name === ASTRO_VITE_ENVIRONMENT_NAMES.astro || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.ssr || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.client || env.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender; }, transform: { filter: { id: { include: [CSS_LANGS_RE], exclude: [rawRE, inlineRE] } }, handler(code, id) { const mod = this.environment.moduleGraph.getModuleById(id); if (mod) { cssContentCache.set(id, code); } } } }; }