From ee1ebab21cfa2a6d235c019df0a8f59cc95f99ab Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 11 Dec 2025 15:21:23 +0100 Subject: [PATCH 1/9] transform --- .../src/content/vite-plugin-content-assets.ts | 9 +- .../content/vite-plugin-content-imports.ts | 102 ++++++++------- .../src/core/build/plugins/plugin-css.ts | 10 +- .../vite-plugin-server-islands.ts | 122 ++++++++++-------- packages/astro/src/core/viteUtils.ts | 4 + .../src/env/vite-plugin-import-meta-env.ts | 111 ++++++++-------- .../src/prefetch/vite-plugin-prefetch.ts | 13 +- .../transitions/vite-plugin-transitions.ts | 9 +- .../src/vite-plugin-astro-server/plugin.ts | 18 ++- packages/astro/src/vite-plugin-astro/index.ts | 112 ++++++++-------- 10 files changed, 284 insertions(+), 226 deletions(-) diff --git a/packages/astro/src/content/vite-plugin-content-assets.ts b/packages/astro/src/content/vite-plugin-content-assets.ts index c6f89e85d895..61afe06d9017 100644 --- a/packages/astro/src/content/vite-plugin-content-assets.ts +++ b/packages/astro/src/content/vite-plugin-content-assets.ts @@ -79,8 +79,11 @@ export function astroContentAssetPropagationPlugin({ server.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr] as RunnableDevEnvironment, ); }, - async transform(_, id, options) { - if (hasContentFlag(id, PROPAGATED_ASSET_FLAG)) { + transform: { + filter: { + id: new RegExp(`(?:\\?|&)${PROPAGATED_ASSET_FLAG}(?:&|=|$)`), + }, + async handler(_, id, options) { const basePath = id.split('?')[0]; let stringifiedLinks: string, stringifiedStyles: string; @@ -128,7 +131,7 @@ export function astroContentAssetPropagationPlugin({ // ^ 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 bfc032897a37..ba89d1989e49 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'; interface EnvPluginOptions { envLoader: EnvLoader; @@ -72,7 +72,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 }) { @@ -99,68 +98,70 @@ export function importMetaEnv({ envLoader }: EnvPluginOptions): vite.Plugin { } }, - transform(source, id, options) { - if ( - !options?.ssr || - !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: { + include: [/import\.meta\.env/], + exclude: [/.*\.(html|htm|json)$/, CSS_LANGS_RE], + }, + }, + handler(source, id, options) { + if (!options?.ssr || 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 b87c968e7a87..abbf8f8eb055 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -56,7 +56,9 @@ export default function createVitePluginAstroServer({ if (!isRunnableDevEnvironment(viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr])) { return; } - const environment = viteServer.environments[ASTRO_VITE_ENVIRONMENT_NAMES.ssr] as RunnableDevEnvironment; + const environment = viteServer.environments[ + ASTRO_VITE_ENVIRONMENT_NAMES.ssr + ] as RunnableDevEnvironment; const loader = createViteLoader(viteServer, environment); const { default: createAstroServerApp } = await environment.runner.import(ASTRO_DEV_APP_ID); const controller = createController({ loader }); @@ -158,12 +160,16 @@ export default function createVitePluginAstroServer({ }); }; }, - transform(code, id, opts = {}) { - if (opts.ssr) return; - if (!id.includes('vite/dist/client/client.mjs')) return; + transform: { + filter: { + id: /id\/vite\/dist\/client\/client\.mjs/, + }, + handler(code, _id, opts) { + if (opts?.ssr) return; - // Replace the Vite overlay with ours - return patchOverlay(code); + // Replace the Vite overlay with ours + return patchOverlay(code); + }, }, }; } diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index b1a469360858..fbec2958fa45 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -6,7 +6,7 @@ import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; import type { Logger } from '../core/logger/core.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'; @@ -210,71 +210,81 @@ export default function astro({ settings, logger }: AstroPluginOptions): vite.Pl } }, }, - async transform(source, id) { - if (hasSpecialQueries(id)) return; - - const parsedId = parseAstroRequest(id); - // ignore astro file sub-requests, e.g. Foo.astro?astro&type=script&index=0&lang.ts - if (!parsedId.filename.endsWith('.astro') || parsedId.query.astro) { - // 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) - if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) { - 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]; + transform: { + filter: { + id: { + include: [/(?:\?|&)astro(?:&|=|$)/, /\.astro\?/], + exclude: [specialQueriesRE], + }, + }, + async handler(source, id) { + const parsedId = parseAstroRequest(id); + // ignore astro file sub-requests, e.g. Foo.astro?astro&type=script&index=0&lang.ts + if (!parsedId.filename.endsWith('.astro') || parsedId.query.astro) { + // 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) + if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) { + 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)); + // Register dependencies from preprocessing this style + result.dependencies?.forEach((dep) => this.addWatchFile(dep)); + } } - } - return; - } + return; + } - const filename = normalizePath(parsedId.filename); + 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 + // 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}".' ); } : {};`, - meta: { vite: { lang: 'ts' } }, - }; - } + meta: { vite: { lang: 'ts' } }, + }; + } - const transformResult = await compile(source, filename); + const transformResult = await compile(source, filename); - const astroMetadata: AstroPluginMetadata['astro'] = { - // 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: {}, - }; + const astroMetadata: AstroPluginMetadata['astro'] = { + // 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, - meta: { - astro: astroMetadata, - vite: { - // Setting this vite metadata to `ts` causes Vite to resolve .js - // extensions to .ts files. - lang: 'ts', + return { + code: transformResult.code, + map: transformResult.map, + 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, astroFileToCompileMetadata }); From 11c2adb66ffe49332165f03ba05e600aadc91ad0 Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 11 Dec 2025 15:36:13 +0100 Subject: [PATCH 2/9] wip --- .../src/vite-plugin-astro-server/plugin.ts | 25 ++---- .../src/vite-plugin-astro-server/util.ts | 4 +- packages/astro/src/vite-plugin-css/index.ts | 23 ++++-- packages/astro/src/vite-plugin-html/index.ts | 8 +- .../mdx/src/vite-plugin-mdx-postprocess.ts | 33 ++++---- .../integrations/mdx/src/vite-plugin-mdx.ts | 77 ++++++++++--------- packages/integrations/vue/src/index.ts | 11 ++- 7 files changed, 95 insertions(+), 86 deletions(-) diff --git a/packages/astro/src/vite-plugin-astro-server/plugin.ts b/packages/astro/src/vite-plugin-astro-server/plugin.ts index abbf8f8eb055..8d0aeed57825 100644 --- a/packages/astro/src/vite-plugin-astro-server/plugin.ts +++ b/packages/astro/src/vite-plugin-astro-server/plugin.ts @@ -160,17 +160,6 @@ export default function createVitePluginAstroServer({ }); }; }, - transform: { - filter: { - id: /id\/vite\/dist\/client\/client\.mjs/, - }, - handler(code, _id, opts) { - if (opts?.ssr) return; - - // Replace the Vite overlay with ours - return patchOverlay(code); - }, - }, }; } @@ -180,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: /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-css/index.ts b/packages/astro/src/vite-plugin-css/index.ts index fa723784ada0..9c195ed3529b 100644 --- a/packages/astro/src/vite-plugin-css/index.ts +++ b/packages/astro/src/vite-plugin-css/index.ts @@ -2,11 +2,12 @@ import type { Plugin, RunnableDevEnvironment } from 'vite'; import { wrapId } from '../core/util.js'; import type { ImportedDevStyle, RoutesList } from '../types/astro.js'; import type * as vite from 'vite'; -import { isBuildableCSSRequest } from '../vite-plugin-astro-server/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 { prependForwardSlash } from '@astrojs/internal-helpers/path'; import { ASTRO_VITE_ENVIRONMENT_NAMES } from '../core/constants.js'; +import { CSS_LANGS_RE } from '../core/viteUtils.js'; interface AstroVitePluginOptions { routesList: RoutesList; @@ -170,18 +171,24 @@ export function astroDevCssPlugin({ routesList, command }: AstroVitePluginOption }, }, - async transform(code, id) { - if (command === 'build') { - return; - } + transform: { + filter: { + id: { + include: [CSS_LANGS_RE], + exclude: [rawRE, inlineRE], + }, + }, + handler(code, id) { + if (command === 'build') { + return; + } - // Cache CSS content as we see it - if (isBuildableCSSRequest(id)) { + // Cache CSS content as we see it const mod = environment?.moduleGraph.getModuleById(id); if (mod) { cssContentCache.set(id, code); } - } + }, }, }, { diff --git a/packages/astro/src/vite-plugin-html/index.ts b/packages/astro/src/vite-plugin-html/index.ts index b44a28acea02..2407e3b262d2 100644 --- a/packages/astro/src/vite-plugin-html/index.ts +++ b/packages/astro/src/vite-plugin-html/index.ts @@ -6,9 +6,11 @@ export default function html() { options(options: any) { options.plugins = options.plugins?.filter((p: any) => p.name !== 'vite:build-html'); }, - async transform(source: string, id: string) { - if (!id.endsWith('.html')) return; - return await transform(source, id); + transform: { + filter: { + id: /\.html$/, + }, + handler: transform, }, }; } diff --git a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts index e00173fbeebf..d7b8bda13c06 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx-postprocess.ts @@ -15,21 +15,24 @@ const astroTagComponentImportRegex = /[\s,{]__astro_tag_component__[\s,}]/; export function vitePluginMdxPostprocess(astroConfig: AstroConfig): Plugin { return { name: '@astrojs/mdx-postprocess', - transform(code, id, opts) { - if (!id.endsWith('.mdx')) return; - - const fileInfo = getFileInfo(id, astroConfig); - const [imports, exports] = parse(code); - - // Call a series of functions that transform the code - code = injectUnderscoreFragmentImport(code, imports); - code = injectMetadataExports(code, exports, fileInfo); - code = transformContentExport(code, exports); - code = annotateContentExport(code, id, !!opts?.ssr, imports); - - // The code transformations above are append-only, so the line/column mappings are the same - // and we can omit the sourcemap for performance. - return { code, map: null }; + transform: { + filter: { + id: /\.mdx$/ + }, + handler(code, id, opts) { + const fileInfo = getFileInfo(id, astroConfig); + const [imports, exports] = parse(code); + + // Call a series of functions that transform the code + code = injectUnderscoreFragmentImport(code, imports); + code = injectMetadataExports(code, exports, fileInfo); + code = transformContentExport(code, exports); + code = annotateContentExport(code, id, !!opts?.ssr, imports); + + // The code transformations above are append-only, so the line/column mappings are the same + // and we can omit the sourcemap for performance. + return { code, map: null }; + }, }, }; } diff --git a/packages/integrations/mdx/src/vite-plugin-mdx.ts b/packages/integrations/mdx/src/vite-plugin-mdx.ts index 12ac4668c286..e881d900386e 100644 --- a/packages/integrations/mdx/src/vite-plugin-mdx.ts +++ b/packages/integrations/mdx/src/vite-plugin-mdx.ts @@ -47,51 +47,54 @@ export function vitePluginMdx(opts: VitePluginMdxOptions): Plugin { }, // Override transform to alter code before MDX compilation // ex. inject layouts - async transform(code, id) { - if (!id.endsWith('.mdx')) return; - - const { frontmatter, content } = safeParseFrontmatter(code, id); + transform: { + filter: { + id: /\.mdx$/, + }, + async handler(code, id) { + const { frontmatter, content } = safeParseFrontmatter(code, id); - const vfile = new VFile({ - value: content, - path: id, - data: { - astro: { - frontmatter, + const vfile = new VFile({ + value: content, + path: id, + data: { + astro: { + frontmatter, + }, + applyFrontmatterExport: { + srcDir: opts.srcDir, + }, }, - applyFrontmatterExport: { - srcDir: opts.srcDir, - }, - }, - }); - - // Lazily initialize the MDX processor - if (!processor) { - processor = createMdxProcessor(opts.mdxOptions, { - sourcemap: sourcemapEnabled, }); - } - try { - const compiled = await processor.process(vfile); + // Lazily initialize the MDX processor + if (!processor) { + processor = createMdxProcessor(opts.mdxOptions, { + sourcemap: sourcemapEnabled, + }); + } - return { - code: String(compiled.value), - map: compiled.map, - meta: getMdxMeta(vfile), - }; - } catch (e: any) { - const err: SSRError = e; + try { + const compiled = await processor.process(vfile); - // For some reason MDX puts the error location in the error's name, not very useful for us. - err.name = 'MDXError'; - err.loc = { file: id, line: e.line, column: e.column }; + return { + code: String(compiled.value), + map: compiled.map, + meta: getMdxMeta(vfile), + }; + } catch (e: any) { + const err: SSRError = e; - // For another some reason, MDX doesn't include a stack trace. Weird - Error.captureStackTrace(err); + // For some reason MDX puts the error location in the error's name, not very useful for us. + err.name = 'MDXError'; + err.loc = { file: id, line: e.line, column: e.column }; - throw err; - } + // For another some reason, MDX doesn't include a stack trace. Weird + Error.captureStackTrace(err); + + throw err; + } + }, }, }; } diff --git a/packages/integrations/vue/src/index.ts b/packages/integrations/vue/src/index.ts index 69b5f327d0f2..eed408e4f239 100644 --- a/packages/integrations/vue/src/index.ts +++ b/packages/integrations/vue/src/index.ts @@ -89,16 +89,19 @@ export const setup = async (app) => { // Ensure that Vue components reference appEntrypoint directly // This allows Astro to associate global styles imported in this file // with the pages they should be injected to - transform(code, id) { - if (!appEntrypoint) return; - if (id.endsWith('.vue')) { + transform: { + filter: { + id: /\.vue$/, + }, + handler(code) { + if (!appEntrypoint) return; const s = new MagicString(code); s.prepend(`import ${JSON.stringify(appEntrypoint)};\n`); return { code: s.toString(), map: s.generateMap({ hires: 'boundary' }), }; - } + }, }, }; } From 3b348a8e839639873cbdcc9e3665949e70753f2d Mon Sep 17 00:00:00 2001 From: Florian Lefebvre Date: Thu, 11 Dec 2025 16:08:25 +0100 Subject: [PATCH 3/9] fix: regexes --- packages/astro/src/vite-plugin-astro/index.ts | 459 +++++++++--------- 1 file changed, 235 insertions(+), 224 deletions(-) diff --git a/packages/astro/src/vite-plugin-astro/index.ts b/packages/astro/src/vite-plugin-astro/index.ts index fbec2958fa45..d715def34433 100644 --- a/packages/astro/src/vite-plugin-astro/index.ts +++ b/packages/astro/src/vite-plugin-astro/index.ts @@ -40,271 +40,282 @@ 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', + transform: { + filter: { + id: { + include: [/(?:\?|&)astro(?:&|=|$)/], + // ignore astro file sub-requests, e.g. Foo.astro?astro&type=script&index=0&lang.ts + exclude: [specialQueriesRE, /\.astro$/, /\.astro\?/], }, - 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) + if (this.environment.name === ASTRO_VITE_ENVIRONMENT_NAMES.client) { + 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, opts) { - 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, opts) { + 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 (opts?.ssr) { 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 (opts?.ssr) { + 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