diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index cd204871609f..829f032d24d6 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -80,44 +80,48 @@ export function astroContentAssetPropagationPlugin({ server.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr] as RunnableDevEnvironment, ); }, - async transform(_, id) { - if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) { - const basePath = id.split('?')[0]; - let stringifiedLinks: string, stringifiedStyles: string; + transform: { + filter: { + id: new RegExp(`(?:\\?|&)${PROPAGATED_ASSET_FLAG}(?:&|=|$)`), + }, + async handler(_, id) { + if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) { + const basePath = id.split('?')[0]; + let stringifiedLinks: string, stringifiedStyles: string; - // We can access the server in dev, - // so resolve collected styles and scripts here. - if (isAstroServerEnvironment(this.environment) && devModuleLoader) { - if (!devModuleLoader.getModuleById(basePath)?.ssrModule) { - await devModuleLoader.import(basePath); - } - const { - styles, - urls, - crawledFiles: styleCrawledFiles, - } = await getStylesForURL(basePath, devModuleLoader.getSSREnvironment()); + // We can access the server in dev, + // so resolve collected styles and scripts here. + if (isAstroServerEnvironment(this.environment) && devModuleLoader) { + if (!devModuleLoader.getModuleById(basePath)?.ssrModule) { + await devModuleLoader.import(basePath); + } + const { + styles, + urls, + crawledFiles: styleCrawledFiles, + } = await getStylesForURL(basePath, devModuleLoader.getSSREnvironment()); - // Register files we crawled to be able to retrieve the rendered styles and scripts, - // as when they get updated, we need to re-transform ourselves. - // We also only watch files within the user source code, as changes in node_modules - // are usually also ignored by Vite. - for (const file of styleCrawledFiles) { - if (!file.includes('node_modules')) { - this.addWatchFile(file); + // Register files we crawled to be able to retrieve the rendered styles and scripts, + // as when they get updated, we need to re-transform ourselves. + // We also only watch files within the user source code, as changes in node_modules + // are usually also ignored by Vite. + for (const file of styleCrawledFiles) { + if (!file.includes('node_modules')) { + this.addWatchFile(file); + } } - } - stringifiedLinks = JSON.stringify([...urls]); - stringifiedStyles = JSON.stringify(styles.map((s) => s.content)); - } else { - // Otherwise, use placeholders to inject styles and scripts - // during the production bundle step. - // @see the `astro:content-build-plugin` below. - stringifiedLinks = JSON.stringify(LINKS_PLACEHOLDER); - stringifiedStyles = JSON.stringify(STYLES_PLACEHOLDER); - } + stringifiedLinks = JSON.stringify([...urls]); + stringifiedStyles = JSON.stringify(styles.map((s) => s.content)); + } else { + // Otherwise, use placeholders to inject styles and scripts + // during the production bundle step. + // @see the `astro:content-build-plugin` below. + stringifiedLinks = JSON.stringify(LINKS_PLACEHOLDER); + stringifiedStyles = JSON.stringify(STYLES_PLACEHOLDER); + } - const code = ` + const code = ` async function getMod() { return import(${JSON.stringify(basePath)}); } @@ -126,10 +130,11 @@ export function astroContentAssetPropagationPlugin({ const defaultMod = { __astroPropagation: true, getMod, collectedLinks, collectedStyles, collectedScripts: [] }; export default defaultMod; `; - // ^ Use a default export for tools like Markdoc - // to catch the `__astroPropagation` identifier - return { code, map: { mappings: '' } }; - } + // ^ Use a default export for tools like Markdoc + // to catch the `__astroPropagation` identifier + return { code, map: { mappings: '' } }; + } + }, }, }; } diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index ab34ac0ca92c..6782c965fd92 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -91,28 +91,32 @@ export function astroContentImportPlugin({ // Get symlinks once at build start symlinks = await getSymlinkedContentCollections({ contentDir, logger, fs }); }, - async transform(_, viteId) { - if (hasContentFlag(viteId, DATA_FLAG)) { - // By default, Vite will resolve symlinks to their targets. We need to reverse this for - // content entries, so we can get the path relative to the content directory. - const fileId = reverseSymlink({ - entry: viteId.split('?')[0] ?? viteId, - contentDir, - symlinks, - }); - // Data collections don't need to rely on the module cache. - // This cache only exists for the `render()` function specific to content. - const { id, data, collection, _internal } = await getDataEntryModule({ - fileId, - entryConfigByExt: dataEntryConfigByExt, - contentDir, - config: settings.config, - fs, - pluginContext: this, - shouldEmitFile, - }); - - const code = ` + transform: { + filter: { + id: new RegExp(`(?:\\?|&)(?:${DATA_FLAG}|${CONTENT_FLAG})(?:&|=|$)`), + }, + async handler(_, viteId) { + if (hasContentFlag(viteId, DATA_FLAG)) { + // By default, Vite will resolve symlinks to their targets. We need to reverse this for + // content entries, so we can get the path relative to the content directory. + const fileId = reverseSymlink({ + entry: viteId.split('?')[0] ?? viteId, + contentDir, + symlinks, + }); + // Data collections don't need to rely on the module cache. + // This cache only exists for the `render()` function specific to content. + const { id, data, collection, _internal } = await getDataEntryModule({ + fileId, + entryConfigByExt: dataEntryConfigByExt, + contentDir, + config: settings.config, + fs, + pluginContext: this, + shouldEmitFile, + }); + + const code = ` export const id = ${JSON.stringify(id)}; export const collection = ${JSON.stringify(collection)}; export const data = ${stringifyEntryData(data, settings.buildOutput === 'server')}; @@ -122,20 +126,20 @@ export const _internal = { rawData: ${JSON.stringify(_internal.rawData)}, }; `; - return code; - } else if (hasContentFlag(viteId, CONTENT_FLAG)) { - const fileId = reverseSymlink({ entry: viteId.split('?')[0], contentDir, symlinks }); - const { id, slug, collection, body, data, _internal } = await getContentEntryModule({ - fileId, - entryConfigByExt: contentEntryConfigByExt, - contentDir, - config: settings.config, - fs, - pluginContext: this, - shouldEmitFile, - }); - - const code = ` + return code; + } else if (hasContentFlag(viteId, CONTENT_FLAG)) { + const fileId = reverseSymlink({ entry: viteId.split('?')[0], contentDir, symlinks }); + const { id, slug, collection, body, data, _internal } = await getContentEntryModule({ + fileId, + entryConfigByExt: contentEntryConfigByExt, + contentDir, + config: settings.config, + fs, + pluginContext: this, + shouldEmitFile, + }); + + const code = ` export const id = ${JSON.stringify(id)}; export const collection = ${JSON.stringify(collection)}; export const slug = ${JSON.stringify(slug)}; @@ -147,8 +151,9 @@ export const _internal = { rawData: ${JSON.stringify(_internal.rawData)}, };`; - return { code, map: { mappings: '' } }; - } + return { code, map: { mappings: '' } }; + } + }, }, configureServer(viteServer) { viteServer.watcher.on('all', async (event, entry) => { @@ -200,12 +205,21 @@ export const _internal = { if (settings.contentEntryTypes.some((t) => t.getRenderModule)) { plugins.push({ name: 'astro:content-render-imports', - async transform(contents, viteId) { - const contentRenderer = getContentRendererByViteId(viteId, settings); - if (!contentRenderer) return; - - const fileId = viteId.split('?')[0]; - return contentRenderer.bind(this)({ viteId, contents, fileUrl: pathToFileURL(fileId) }); + transform: { + filter: { + id: { + include: settings.contentEntryTypes + .filter((t) => t.getRenderModule) + .map((t) => new RegExp(`\\.(${t.extensions.map((e) => e.slice(1)).join('|')})$`)), + }, + }, + async handler(contents, viteId) { + const contentRenderer = getContentRendererByViteId(viteId, settings); + if (!contentRenderer) return; + + const fileId = viteId.split('?')[0]; + return contentRenderer.bind(this)({ viteId, contents, fileUrl: pathToFileURL(fileId) }); + }, }, }); } diff --git a/packages/astro/src/core/build/plugins/plugin-css.ts b/packages/astro/src/core/build/plugins/plugin-css.ts index d4163c8b278c..4b9d93ceeeb4 100644 --- a/packages/astro/src/core/build/plugins/plugin-css.ts +++ b/packages/astro/src/core/build/plugins/plugin-css.ts @@ -12,6 +12,7 @@ import { getPageDataByViteID, getPageDatasByClientOnlyID } from '../internal.js' import type { PageBuildData, StaticBuildOptions, StylesheetAsset } from '../types.js'; import { shouldInlineAsset } from './util.js'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../../constants.js'; +import { CSS_LANGS_RE } from '../../viteUtils.js'; /***** ASTRO PLUGIN *****/ @@ -53,8 +54,11 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { ); }, - transform(_code, id) { - if (isCSSRequest(id)) { + transform: { + filter: { + id: CSS_LANGS_RE, + }, + handler(_code, id) { // In prerender, don't rebundle CSS that was already bundled in SSR. // Return an empty string here to prevent it. if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.prerender) { @@ -65,7 +69,7 @@ function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin[] { } } cssModulesInBundles.add(id); - } + }, }, async generateBundle(_outputOptions, bundle) { diff --git a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts index 090ced0ec356..421e9160b143 100644 --- a/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts +++ b/packages/astro/src/core/server-islands/vite-plugin-server-islands.ts @@ -45,81 +45,91 @@ export function vitePluginServerIslands({ settings }: AstroPluginOptions): ViteP }, }, - async transform(_code, id) { - // We run the transform for all file extensions to support transformed files, eg. mdx - const info = this.getModuleInfo(id); + transform: { + filter: { + id: { + include: [ + // Allows server islands in astro and mdx files + /\.(astro|mdx)$/, + new RegExp(`^${RESOLVED_SERVER_ISLAND_MANIFEST}$`), + ], + }, + }, + async handler(_code, id) { + const info = this.getModuleInfo(id); - const astro = info ? (info.meta.astro as AstroPluginMetadata['astro']) : undefined; + const astro = info ? (info.meta.astro as AstroPluginMetadata['astro']) : undefined; - if (astro) { - for (const comp of astro.serverComponents) { - if (!serverIslandNameMap.has(comp.resolvedPath)) { - if (!settings.adapter) { - throw new AstroError(AstroErrorData.NoAdapterInstalledServerIslands); - } - let name = comp.localName; - let idx = 1; + if (astro) { + for (const comp of astro.serverComponents) { + if (!serverIslandNameMap.has(comp.resolvedPath)) { + if (!settings.adapter) { + throw new AstroError(AstroErrorData.NoAdapterInstalledServerIslands); + } + let name = comp.localName; + let idx = 1; - while (true) { - // Name not taken, let's use it. - if (!serverIslandMap.has(name)) { - break; + while (true) { + // Name not taken, let's use it. + if (!serverIslandMap.has(name)) { + break; + } + // Increment a number onto the name: Avatar -> Avatar1 + name += idx++; } - // Increment a number onto the name: Avatar -> Avatar1 - name += idx++; - } - // Append the name map, for prod - serverIslandNameMap.set(comp.resolvedPath, name); - serverIslandMap.set(name, comp.resolvedPath); + // Append the name map, for prod + serverIslandNameMap.set(comp.resolvedPath, name); + serverIslandMap.set(name, comp.resolvedPath); - // Build mode - if (command === 'build') { - let referenceId = this.emitFile({ - type: 'chunk', - id: comp.specifier, - importer: id, - name: comp.localName, - }); - referenceIdMap.set(comp.resolvedPath, referenceId); + // Build mode + if (command === 'build') { + let referenceId = this.emitFile({ + type: 'chunk', + id: comp.specifier, + importer: id, + name: comp.localName, + }); + referenceIdMap.set(comp.resolvedPath, referenceId); + } } } } - } - - if (serverIslandNameMap.size > 0 && serverIslandMap.size > 0 && ssrEnvironment) { - // In dev, we need to clear the module graph so that Vite knows to re-transform - // the module with the new island information. - const mod = ssrEnvironment.moduleGraph.getModuleById(RESOLVED_SERVER_ISLAND_MANIFEST); - if (mod) { - ssrEnvironment.moduleGraph.invalidateModule(mod); - } - } - if (id === RESOLVED_SERVER_ISLAND_MANIFEST) { - if (command === 'build' && settings.buildOutput) { - const hasServerIslands = serverIslandNameMap.size > 0; - // Error if there are server islands but no adapter provided. - if (hasServerIslands && settings.buildOutput !== 'server') { - throw new AstroError(AstroErrorData.NoAdapterInstalledServerIslands); + if (serverIslandNameMap.size > 0 && serverIslandMap.size > 0 && ssrEnvironment) { + // In dev, we need to clear the module graph so that Vite knows to re-transform + // the module with the new island information. + const mod = ssrEnvironment.moduleGraph.getModuleById(RESOLVED_SERVER_ISLAND_MANIFEST); + if (mod) { + ssrEnvironment.moduleGraph.invalidateModule(mod); } } - if (serverIslandNameMap.size > 0 && serverIslandMap.size > 0) { - let mapSource = 'new Map([\n\t'; - for (let [name, path] of serverIslandMap) { - mapSource += `\n\t['${name}', () => import('${path}')],`; + if (id === RESOLVED_SERVER_ISLAND_MANIFEST) { + if (command === 'build' && settings.buildOutput) { + const hasServerIslands = serverIslandNameMap.size > 0; + // Error if there are server islands but no adapter provided. + if (hasServerIslands && settings.buildOutput !== 'server') { + throw new AstroError(AstroErrorData.NoAdapterInstalledServerIslands); + } } - mapSource += ']);'; - return { - code: ` + if (serverIslandNameMap.size > 0 && serverIslandMap.size > 0) { + let mapSource = 'new Map([\n\t'; + for (let [name, path] of serverIslandMap) { + mapSource += `\n\t['${name}', () => import('${path}')],`; + } + mapSource += ']);'; + + return { + code: ` export const serverIslandMap = ${mapSource}; \n\nexport const serverIslandNameMap = new Map(${JSON.stringify(Array.from(serverIslandNameMap.entries()), null, 2)}); `, - }; + }; + } } - } + }, }, renderChunk(code, chunk) { diff --git a/packages/astro/src/core/viteUtils.ts b/packages/astro/src/core/viteUtils.ts index 0fd7c4cb7faa..301edacb15fd 100644 --- a/packages/astro/src/core/viteUtils.ts +++ b/packages/astro/src/core/viteUtils.ts @@ -68,3 +68,7 @@ export async function resolveIdToUrl(loader: ModuleLoader, id: string, root?: UR } return VALID_ID_PREFIX + resultId; } + +// https://github.com/vitejs/vite/blob/2f9428d1ffd988e30cb253d5bb84844fb1654e86/packages/vite/src/node/constants.ts#L108 +// Used by isCSSRequest() under the hood +export const CSS_LANGS_RE = /\.(css|less|sass|scss|styl|stylus|pcss|postcss|sss)(?:$|\?)/; diff --git a/packages/astro/src/env/vite-plugin-import-meta-env.ts b/packages/astro/src/env/vite-plugin-import-meta-env.ts index 4bf6fd5b1f18..d92d3f20bfda 100644 --- a/packages/astro/src/env/vite-plugin-import-meta-env.ts +++ b/packages/astro/src/env/vite-plugin-import-meta-env.ts @@ -1,8 +1,8 @@ import { transform } from 'esbuild'; import MagicString from 'magic-string'; import type * as vite from 'vite'; -import { createFilter, isCSSRequest } from 'vite'; import type { EnvLoader } from './env-loader.js'; +import { CSS_LANGS_RE } from '../core/viteUtils.js'; import { isAstroClientEnvironment } from '../environments.js'; interface EnvPluginOptions { @@ -73,7 +73,6 @@ export function importMetaEnv({ envLoader }: EnvPluginOptions): vite.Plugin { let isDev: boolean; let devImportMetaEnvPrepend: string; let viteConfig: vite.ResolvedConfig; - const filter = createFilter(null, ['**/*.html', '**/*.htm', '**/*.json']); return { name: 'astro:vite-plugin-env', config(_, { command }) { @@ -100,68 +99,70 @@ export function importMetaEnv({ envLoader }: EnvPluginOptions): vite.Plugin { } }, - transform(source, id) { - if ( - isAstroClientEnvironment(this.environment) || - !source.includes('import.meta.env') || - !filter(id) || - isCSSRequest(id) || - viteConfig.assetsInclude(id) - ) { - return; - } - // Find matches for *private* env and do our own replacement. - // Env is retrieved before process.env is populated by astro:env - // so that import.meta.env is first replaced by values, not process.env - privateEnv ??= envLoader.getPrivateEnv(); + transform: { + filter: { + id: { + exclude: [/.*\.(html|htm|json)$/, CSS_LANGS_RE], + }, + code: /import\.meta\.env/, + }, + handler(source, id) { + if (isAstroClientEnvironment(this.environment) || viteConfig.assetsInclude(id)) { + return; + } + // Find matches for *private* env and do our own replacement. + // Env is retrieved before process.env is populated by astro:env + // so that import.meta.env is first replaced by values, not process.env + privateEnv ??= envLoader.getPrivateEnv(); + + // In dev, we can assign the private env vars to `import.meta.env` directly for performance + if (isDev) { + const s = new MagicString(source); + + if (!devImportMetaEnvPrepend) { + devImportMetaEnvPrepend = `Object.assign(import.meta.env,{`; + for (const key in privateEnv) { + devImportMetaEnvPrepend += `${key}:${privateEnv[key]},`; + } + devImportMetaEnvPrepend += '});'; + } + s.prepend(devImportMetaEnvPrepend); - // In dev, we can assign the private env vars to `import.meta.env` directly for performance - if (isDev) { - const s = new MagicString(source); + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + }; + } - if (!devImportMetaEnvPrepend) { - devImportMetaEnvPrepend = `Object.assign(import.meta.env,{`; + // In build, use esbuild to perform replacements. Compute the default defines for esbuild here as a + // separate object as it could be extended by `import.meta.env` later. + if (!defaultDefines) { + defaultDefines = {}; for (const key in privateEnv) { - devImportMetaEnvPrepend += `${key}:${privateEnv[key]},`; + defaultDefines[`import.meta.env.${key}`] = privateEnv[key]; } - devImportMetaEnvPrepend += '});'; } - s.prepend(devImportMetaEnvPrepend); - return { - code: s.toString(), - map: s.generateMap({ hires: 'boundary' }), - }; - } + let defines = defaultDefines; - // In build, use esbuild to perform replacements. Compute the default defines for esbuild here as a - // separate object as it could be extended by `import.meta.env` later. - if (!defaultDefines) { - defaultDefines = {}; - for (const key in privateEnv) { - defaultDefines[`import.meta.env.${key}`] = privateEnv[key]; - } - } - - let defines = defaultDefines; - - // If reference the `import.meta.env` object directly, we want to inject private env vars - // into Vite's injected `import.meta.env` object. To do this, we use `Object.assign` and keeping - // the `import.meta.env` identifier so Vite sees it. - if (importMetaEnvOnlyRe.test(source)) { - const references = getReferencedPrivateKeys(source, privateEnv); - let replacement = `(Object.assign(import.meta.env,{`; - for (const key of references.values()) { - replacement += `${key}:${privateEnv[key]},`; + // If reference the `import.meta.env` object directly, we want to inject private env vars + // into Vite's injected `import.meta.env` object. To do this, we use `Object.assign` and keeping + // the `import.meta.env` identifier so Vite sees it. + if (importMetaEnvOnlyRe.test(source)) { + const references = getReferencedPrivateKeys(source, privateEnv); + let replacement = `(Object.assign(import.meta.env,{`; + for (const key of references.values()) { + replacement += `${key}:${privateEnv[key]},`; + } + replacement += '}))'; + defines = { + ...defaultDefines, + 'import.meta.env': replacement, + }; } - replacement += '}))'; - defines = { - ...defaultDefines, - 'import.meta.env': replacement, - }; - } - return replaceDefine(source, id, defines, viteConfig); + return replaceDefine(source, id, defines, viteConfig); + }, }, }; } diff --git a/packages/astro/src/prefetch/vite-plugin-prefetch.ts b/packages/astro/src/prefetch/vite-plugin-prefetch.ts index 49c5c884b55c..7e43f9330c45 100644 --- a/packages/astro/src/prefetch/vite-plugin-prefetch.ts +++ b/packages/astro/src/prefetch/vite-plugin-prefetch.ts @@ -49,10 +49,13 @@ export default function astroPrefetch({ settings }: { settings: AstroSettings }) return { code: `export { prefetch } from "astro/virtual-modules/prefetch.js";` }; }, }, - transform(code, id) { - // NOTE: Handle replacing the specifiers even if prefetch is disabled so View Transitions - // can import the internal module and not hit runtime issues. - if (id.includes(prefetchInternalModuleFsSubpath)) { + transform: { + filter: { + // NOTE: Handle replacing the specifiers even if prefetch is disabled so View Transitions + // can import the internal module and not hit runtime issues. + id: new RegExp(`${prefetchInternalModuleFsSubpath}`), + }, + handler(code) { // We perform a simple replacement with padding so that the code offset is not changed and // we don't have to generate a sourcemap. This has the assumption that the replaced string // will always be shorter than the search string to work. @@ -70,7 +73,7 @@ export default function astroPrefetch({ settings }: { settings: AstroSettings }) `${JSON.stringify(settings.config.experimental.clientPrerender)}`.padEnd(33), ); return { code, map: null }; - } + }, }, }; } diff --git a/packages/astro/src/transitions/vite-plugin-transitions.ts b/packages/astro/src/transitions/vite-plugin-transitions.ts index 3d38d7683f17..d8a270cbb04e 100644 --- a/packages/astro/src/transitions/vite-plugin-transitions.ts +++ b/packages/astro/src/transitions/vite-plugin-transitions.ts @@ -60,11 +60,14 @@ export default function astroTransitions({ settings }: { settings: AstroSettings } }, }, - transform(code, id) { - if (id.includes('ClientRouter.astro') && id.endsWith('.ts')) { + transform: { + filter: { + id: /ClientRouter\.astro.*\.ts$/, + }, + handler(code) { const prefetchDisabled = settings.config.prefetch === false; return code.replace('__PREFETCH_DISABLED__', JSON.stringify(prefetchDisabled)); - } + }, }, }; } diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index 9160cf99401b..5c8cea9a9686 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -160,13 +160,6 @@ export default function createVitePluginAstroServer({ }); }; }, - transform(code, id, opts = {}) { - if (opts.ssr) return; - if (!id.includes('vite/dist/client/client.mjs')) return; - - // Replace the Vite overlay with ours - return patchOverlay(code); - }, }; } @@ -176,12 +169,14 @@ export function createVitePluginAstroServerClient(): vite.Plugin { applyToEnvironment(environment) { return environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client; }, - transform(code, id, opts = {}) { - if (opts.ssr) return; - if (!id.includes('vite/dist/client/client.mjs')) return; - - // Replace the Vite overlay with ours - return patchOverlay(code); + transform: { + filter: { + id: /vite\/dist\/client\/client\.mjs/, + }, + handler(code) { + // Replace the Vite overlay with ours + return patchOverlay(code); + }, }, }; } diff --git a/packages/astro/src/vite-plugin-astro-server/util.ts b/packages/astro/src/vite-plugin-astro-server/util.ts index 4331c4ee449f..d2d0c1df98fc 100644 --- a/packages/astro/src/vite-plugin-astro-server/util.ts +++ b/packages/astro/src/vite-plugin-astro-server/util.ts @@ -1,7 +1,7 @@ import { isCSSRequest } from 'vite'; -const rawRE = /(?:\?|&)raw(?:&|$)/; -const inlineRE = /(?:\?|&)inline\b/; +export const rawRE = /(?:\?|&)raw(?:&|$)/; +export const inlineRE = /(?:\?|&)inline\b/; export const isBuildableCSSRequest = (request: string): boolean => isCSSRequest(request) && !rawRE.test(request) && !inlineRE.test(request); diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index 5f1b5e162e3a..0e302c10fdd1 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -7,7 +7,7 @@ import type { Logger } from '../core/logger/core.js'; import { isAstroServerEnvironment } from '../environments.js'; import type { AstroSettings } from '../types/astro.js'; import type { AstroConfig } from '../types/public/config.js'; -import { hasSpecialQueries, normalizeFilename } from '../vite-plugin-utils/index.js'; +import { normalizeFilename, specialQueriesRE } from '../vite-plugin-utils/index.js'; import { type CompileAstroResult, compileAstro } from './compile.js'; import { handleHotUpdate } from './hmr.js'; import { parseAstroRequest } from './query.js'; @@ -41,261 +41,283 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl const notAstroComponent = (component: HydratedComponent) => !component.resolvedPath.endsWith('.astro'); - const prePlugin: vite.Plugin = { - name: 'astro:build', - enforce: 'pre', // run transforms before other plugins can - 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]; - } - } - 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) => { - return compileAstro({ - compileProps: { - astroConfig: config, - viteConfig, - toolbarEnabled, - filename, - source: code, + 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], }, - astroFileToCompileMetadata, - logger, - }); - }; - }, - 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(); + }, + async handler(_source, id) { + const parsedId = parseAstroRequest(id); + // Special edge case handling for Vite 6 beta, the style dependencies need to be registered here take affect + // 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]; - // 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(?:&|=|$)/, + // Register dependencies from preprocessing this style + result.dependencies?.forEach((dep) => this.addWatchFile(dep)); + } + }, }, - 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); + }, + { + name: 'astro:build', + enforce: 'pre', // run transforms before other plugins can + 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]; } - - 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.`, - ); + 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) => { + return compileAstro({ + compileProps: { + astroConfig: config, + viteConfig, + toolbarEnabled, + filename, + source: code, + }, + astroFileToCompileMetadata, + logger, + }); + }; + }, + 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; - switch (query.type) { - case 'style': { - if (typeof query.index === 'undefined') { - throw new Error(`Requests for Astro CSS must include an index.`); + // 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); } - const result = compileMetadata.css[query.index]; - if (!result) { - throw new Error(`No Astro CSS at index ${query.index}`); - } + 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.`, + ); + } - // Register dependencies from preprocessing this style - result.dependencies?.forEach((dep) => this.addWatchFile(dep)); + 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} */`, + 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} */`, + }; + } - const script = compileMetadata.scripts[query.index]; - if (!script) { - throw new Error(`No script at index ${query.index}`); - } + 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