diff --git a/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts b/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts index 37dc870372da0f..38355b38fe6b31 100644 --- a/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/assetImportMetaUrl.spec.ts @@ -10,8 +10,8 @@ async function createAssetImportMetaurlPluginTransform() { const environment = new PartialEnvironment('client', config) return async (code: string) => { - // @ts-expect-error transform should exist - const result = await instance.transform.call( + // @ts-expect-error transform.handler should exist + const result = await instance.transform.handler.call( { environment, parse: parseAst }, code, 'foo.ts', diff --git a/packages/vite/src/node/__tests__/plugins/define.spec.ts b/packages/vite/src/node/__tests__/plugins/define.spec.ts index 166cabac83376f..ca88a177ff7343 100644 --- a/packages/vite/src/node/__tests__/plugins/define.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/define.spec.ts @@ -16,8 +16,8 @@ async function createDefinePluginTransform( const environment = new PartialEnvironment(ssr ? 'ssr' : 'client', config) return async (code: string) => { - // @ts-expect-error transform should exist - const result = await instance.transform.call( + // @ts-expect-error transform.handler should exist + const result = await instance.transform.handler.call( { environment }, code, 'foo.ts', diff --git a/packages/vite/src/node/__tests__/plugins/json.spec.ts b/packages/vite/src/node/__tests__/plugins/json.spec.ts index e90bcb39c22737..644fd1a925084d 100644 --- a/packages/vite/src/node/__tests__/plugins/json.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/json.spec.ts @@ -36,7 +36,8 @@ describe('transform', () => { isBuild: boolean, ) => { const plugin = jsonPlugin(opts, isBuild) - return (plugin.transform! as Function)(input, 'test.json').code + // @ts-expect-error transform.handler should exist + return plugin.transform.handler(input, 'test.json').code } test("namedExports: true, stringify: 'auto' should not transformed an array input", () => { diff --git a/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts index 559b51a8d51cbd..1c35a178ed2ade 100644 --- a/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts @@ -10,8 +10,8 @@ async function createWorkerImportMetaUrlPluginTransform() { const environment = new PartialEnvironment('client', config) return async (code: string) => { - // @ts-expect-error transform should exist - const result = await instance.transform.call( + // @ts-expect-error transform.handler should exist + const result = await instance.transform.handler.call( { environment, parse: parseAst }, code, 'foo.ts', diff --git a/packages/vite/src/node/plugins/asset.ts b/packages/vite/src/node/plugins/asset.ts index 9706c2b49c5972..305ba49dc66ac6 100644 --- a/packages/vite/src/node/plugins/asset.ts +++ b/packages/vite/src/node/plugins/asset.ts @@ -148,60 +148,64 @@ export function assetPlugin(config: ResolvedConfig): Plugin { cssEntriesMap.set(this.environment, new Set()) }, - resolveId(id) { - if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) { - return - } - // imports to absolute urls pointing to files in /public - // will fail to resolve in the main resolver. handle them here. - const publicFile = checkPublicFile(id, config) - if (publicFile) { - return id - } + resolveId: { + handler(id) { + if (!config.assetsInclude(cleanUrl(id)) && !urlRE.test(id)) { + return + } + // imports to absolute urls pointing to files in /public + // will fail to resolve in the main resolver. handle them here. + const publicFile = checkPublicFile(id, config) + if (publicFile) { + return id + } + }, }, - async load(id) { - if (id[0] === '\0') { - // Rollup convention, this id should be handled by the - // plugin that marked it with \0 - return - } + load: { + async handler(id) { + if (id[0] === '\0') { + // Rollup convention, this id should be handled by the + // plugin that marked it with \0 + return + } - // raw requests, read from disk - if (rawRE.test(id)) { - const file = checkPublicFile(id, config) || cleanUrl(id) - this.addWatchFile(file) - // raw query, read file and return as string - return `export default ${JSON.stringify( - await fsp.readFile(file, 'utf-8'), - )}` - } + // raw requests, read from disk + if (rawRE.test(id)) { + const file = checkPublicFile(id, config) || cleanUrl(id) + this.addWatchFile(file) + // raw query, read file and return as string + return `export default ${JSON.stringify( + await fsp.readFile(file, 'utf-8'), + )}` + } - if (!urlRE.test(id) && !config.assetsInclude(cleanUrl(id))) { - return - } + if (!urlRE.test(id) && !config.assetsInclude(cleanUrl(id))) { + return + } - id = removeUrlQuery(id) - let url = await fileToUrl(this, id) + id = removeUrlQuery(id) + let url = await fileToUrl(this, id) - // Inherit HMR timestamp if this asset was invalidated - if (!url.startsWith('data:') && this.environment.mode === 'dev') { - const mod = this.environment.moduleGraph.getModuleById(id) - if (mod && mod.lastHMRTimestamp > 0) { - url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) + // Inherit HMR timestamp if this asset was invalidated + if (!url.startsWith('data:') && this.environment.mode === 'dev') { + const mod = this.environment.moduleGraph.getModuleById(id) + if (mod && mod.lastHMRTimestamp > 0) { + url = injectQuery(url, `t=${mod.lastHMRTimestamp}`) + } } - } - return { - code: `export default ${JSON.stringify(encodeURIPath(url))}`, - // Force rollup to keep this module from being shared between other entry points if it's an entrypoint. - // If the resulting chunk is empty, it will be removed in generateBundle. - moduleSideEffects: - config.command === 'build' && this.getModuleInfo(id)?.isEntry - ? 'no-treeshake' - : false, - meta: config.command === 'build' ? { 'vite:asset': true } : undefined, - } + return { + code: `export default ${JSON.stringify(encodeURIPath(url))}`, + // Force rollup to keep this module from being shared between other entry points if it's an entrypoint. + // If the resulting chunk is empty, it will be removed in generateBundle. + moduleSideEffects: + config.command === 'build' && this.getModuleInfo(id)?.isEntry + ? 'no-treeshake' + : false, + meta: config.command === 'build' ? { 'vite:asset': true } : undefined, + } + }, }, renderChunk(code, chunk, opts) { diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index a772c8531b4f47..50a13e1b85aa78 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -49,122 +49,124 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { return environment.config.consumer === 'client' }, - async transform(code, id) { - if ( - id !== preloadHelperId && - id !== CLIENT_ENTRY && - code.includes('new URL') && - code.includes(`import.meta.url`) - ) { - let s: MagicString | undefined - const assetImportMetaUrlRE = - /\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg - const cleanString = stripLiteral(code) + transform: { + async handler(code, id) { + if ( + id !== preloadHelperId && + id !== CLIENT_ENTRY && + code.includes('new URL') && + code.includes(`import.meta.url`) + ) { + let s: MagicString | undefined + const assetImportMetaUrlRE = + /\bnew\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*(?:,\s*)?\)/dg + const cleanString = stripLiteral(code) - let match: RegExpExecArray | null - while ((match = assetImportMetaUrlRE.exec(cleanString))) { - const [[startIndex, endIndex], [urlStart, urlEnd]] = match.indices! - if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue + let match: RegExpExecArray | null + while ((match = assetImportMetaUrlRE.exec(cleanString))) { + const [[startIndex, endIndex], [urlStart, urlEnd]] = match.indices! + if (hasViteIgnoreRE.test(code.slice(startIndex, urlStart))) continue - const rawUrl = code.slice(urlStart, urlEnd) + const rawUrl = code.slice(urlStart, urlEnd) - if (!s) s = new MagicString(code) + if (!s) s = new MagicString(code) - // potential dynamic template string - if (rawUrl[0] === '`' && rawUrl.includes('${')) { - const queryDelimiterIndex = getQueryDelimiterIndex(rawUrl) - const hasQueryDelimiter = queryDelimiterIndex !== -1 - const pureUrl = hasQueryDelimiter - ? rawUrl.slice(0, queryDelimiterIndex) + '`' - : rawUrl - const queryString = hasQueryDelimiter - ? rawUrl.slice(queryDelimiterIndex, -1) - : '' - const ast = this.parse(pureUrl) - const templateLiteral = (ast as any).body[0].expression - if (templateLiteral.expressions.length) { - const pattern = buildGlobPattern(templateLiteral) - if (pattern.startsWith('*')) { - // don't transform for patterns like this - // because users won't intend to do that in most cases + // potential dynamic template string + if (rawUrl[0] === '`' && rawUrl.includes('${')) { + const queryDelimiterIndex = getQueryDelimiterIndex(rawUrl) + const hasQueryDelimiter = queryDelimiterIndex !== -1 + const pureUrl = hasQueryDelimiter + ? rawUrl.slice(0, queryDelimiterIndex) + '`' + : rawUrl + const queryString = hasQueryDelimiter + ? rawUrl.slice(queryDelimiterIndex, -1) + : '' + const ast = this.parse(pureUrl) + const templateLiteral = (ast as any).body[0].expression + if (templateLiteral.expressions.length) { + const pattern = buildGlobPattern(templateLiteral) + if (pattern.startsWith('*')) { + // don't transform for patterns like this + // because users won't intend to do that in most cases + continue + } + + const globOptions = { + eager: true, + import: 'default', + // A hack to allow 'as' & 'query' exist at the same time + query: injectQuery(queryString, 'url'), + } + s.update( + startIndex, + endIndex, + `new URL((import.meta.glob(${JSON.stringify( + pattern, + )}, ${JSON.stringify( + globOptions, + )}))[${pureUrl}], import.meta.url)`, + ) continue } + } - const globOptions = { - eager: true, - import: 'default', - // A hack to allow 'as' & 'query' exist at the same time - query: injectQuery(queryString, 'url'), - } - s.update( - startIndex, - endIndex, - `new URL((import.meta.glob(${JSON.stringify( - pattern, - )}, ${JSON.stringify( - globOptions, - )}))[${pureUrl}], import.meta.url)`, - ) + const url = rawUrl.slice(1, -1) + if (isDataUrl(url)) { continue } - } - - const url = rawUrl.slice(1, -1) - if (isDataUrl(url)) { - continue - } - let file: string | undefined - if (url[0] === '.') { - file = slash(path.resolve(path.dirname(id), url)) - file = tryFsResolve(file, fsResolveOptions) ?? file - } else { - assetResolver ??= createBackCompatIdResolver(config, { - extensions: [], - mainFields: [], - tryIndex: false, - preferRelative: true, - }) - file = await assetResolver(this.environment, url, id) - file ??= - url[0] === '/' - ? slash(path.join(publicDir, url)) - : slash(path.resolve(path.dirname(id), url)) - } + let file: string | undefined + if (url[0] === '.') { + file = slash(path.resolve(path.dirname(id), url)) + file = tryFsResolve(file, fsResolveOptions) ?? file + } else { + assetResolver ??= createBackCompatIdResolver(config, { + extensions: [], + mainFields: [], + tryIndex: false, + preferRelative: true, + }) + file = await assetResolver(this.environment, url, id) + file ??= + url[0] === '/' + ? slash(path.join(publicDir, url)) + : slash(path.resolve(path.dirname(id), url)) + } - // Get final asset URL. If the file does not exist, - // we fall back to the initial URL and let it resolve in runtime - let builtUrl: string | undefined - if (file) { - try { - if (publicDir && isParentDirectory(publicDir, file)) { - const publicPath = '/' + path.posix.relative(publicDir, file) - builtUrl = await fileToUrl(this, publicPath) - } else { - builtUrl = await fileToUrl(this, file) + // Get final asset URL. If the file does not exist, + // we fall back to the initial URL and let it resolve in runtime + let builtUrl: string | undefined + if (file) { + try { + if (publicDir && isParentDirectory(publicDir, file)) { + const publicPath = '/' + path.posix.relative(publicDir, file) + builtUrl = await fileToUrl(this, publicPath) + } else { + builtUrl = await fileToUrl(this, file) + } + } catch { + // do nothing, we'll log a warning after this } - } catch { - // do nothing, we'll log a warning after this } - } - if (!builtUrl) { - const rawExp = code.slice(startIndex, endIndex) - config.logger.warnOnce( - `\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime. ` + - `If this is intended, you can use the /* @vite-ignore */ comment to suppress this warning.`, + if (!builtUrl) { + const rawExp = code.slice(startIndex, endIndex) + config.logger.warnOnce( + `\n${rawExp} doesn't exist at build time, it will remain unchanged to be resolved at runtime. ` + + `If this is intended, you can use the /* @vite-ignore */ comment to suppress this warning.`, + ) + builtUrl = url + } + s.update( + startIndex, + endIndex, + `new URL(${JSON.stringify(builtUrl)}, import.meta.url)`, ) - builtUrl = url } - s.update( - startIndex, - endIndex, - `new URL(${JSON.stringify(builtUrl)}, import.meta.url)`, - ) - } - if (s) { - return transformStableResult(s, id, config) + if (s) { + return transformStableResult(s, id, config) + } } - } - return null + return null + }, }, } } diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index b78f52ebc96380..f7fd8e208c44a4 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -313,7 +313,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { }) } - return { + const plugin: Plugin = { name: 'vite:css', buildStart() { @@ -336,33 +336,37 @@ export function cssPlugin(config: ResolvedConfig): Plugin { preprocessorWorkerController?.close() }, - async load(id) { - if (!isCSSRequest(id)) return + load: { + async handler(id) { + if (!isCSSRequest(id)) return - if (urlRE.test(id)) { - if (isModuleCSSRequest(id)) { - throw new Error( - `?url is not supported with CSS modules. (tried to import ${JSON.stringify( - id, - )})`, - ) - } + if (urlRE.test(id)) { + if (isModuleCSSRequest(id)) { + throw new Error( + `?url is not supported with CSS modules. (tried to import ${JSON.stringify( + id, + )})`, + ) + } - // *.css?url - // in dev, it's handled by assets plugin. - if (isBuild) { - id = injectQuery(removeUrlQuery(id), 'transform-only') - return ( - `import ${JSON.stringify(id)};` + - `export default "__VITE_CSS_URL__${Buffer.from(id).toString( - 'hex', - )}__"` - ) + // *.css?url + // in dev, it's handled by assets plugin. + if (isBuild) { + id = injectQuery(removeUrlQuery(id), 'transform-only') + return ( + `import ${JSON.stringify(id)};` + + `export default "__VITE_CSS_URL__${Buffer.from(id).toString( + 'hex', + )}__"` + ) + } } - } + }, }, + } - async transform(raw, id) { + const transformHook: Plugin['transform'] = { + async handler(raw, id) { if ( !isCSSRequest(id) || commonjsProxyRE.test(id) || @@ -438,6 +442,14 @@ export function cssPlugin(config: ResolvedConfig): Plugin { } }, } + + // for backward compat, make `plugin.transform` a function + // but still keep the `handler` property + // so that we can use `filter` property in the future + plugin.transform = transformHook.handler + ;(plugin.transform as any).handler = transformHook.handler + + return plugin } const createStyleContentMap = () => { @@ -565,124 +577,130 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { codeSplitEmitQueue = createSerialPromiseQueue() }, - async transform(css, id) { - if ( - !isCSSRequest(id) || - commonjsProxyRE.test(id) || - SPECIAL_QUERY_RE.test(id) - ) { - return - } + transform: { + async handler(css, id) { + if ( + !isCSSRequest(id) || + commonjsProxyRE.test(id) || + SPECIAL_QUERY_RE.test(id) + ) { + return + } - css = stripBomTag(css) + css = stripBomTag(css) - // cache css compile result to map - // and then use the cache replace inline-style-flag - // when `generateBundle` in vite:build-html plugin and devHtmlHook - const inlineCSS = inlineCSSRE.test(id) - const isHTMLProxy = htmlProxyRE.test(id) - if (inlineCSS && isHTMLProxy) { - if (styleAttrRE.test(id)) { - css = css.replace(/"/g, '"') - } - const index = htmlProxyIndexRE.exec(id)?.[1] - if (index == null) { - throw new Error(`HTML proxy index in "${id}" not found`) + // cache css compile result to map + // and then use the cache replace inline-style-flag + // when `generateBundle` in vite:build-html plugin and devHtmlHook + const inlineCSS = inlineCSSRE.test(id) + const isHTMLProxy = htmlProxyRE.test(id) + if (inlineCSS && isHTMLProxy) { + if (styleAttrRE.test(id)) { + css = css.replace(/"/g, '"') + } + const index = htmlProxyIndexRE.exec(id)?.[1] + if (index == null) { + throw new Error(`HTML proxy index in "${id}" not found`) + } + addToHTMLProxyTransformResult( + `${getHash(cleanUrl(id))}_${Number.parseInt(index)}`, + css, + ) + return `export default ''` } - addToHTMLProxyTransformResult( - `${getHash(cleanUrl(id))}_${Number.parseInt(index)}`, - css, - ) - return `export default ''` - } - const inlined = inlineRE.test(id) - const modules = cssModulesCache.get(config)!.get(id) - - // #6984, #7552 - // `foo.module.css` => modulesCode - // `foo.module.css?inline` => cssContent - const modulesCode = - modules && - !inlined && - dataToEsm(modules, { namedExports: true, preferConst: true }) - - if (config.command === 'serve') { - const getContentWithSourcemap = async (content: string) => { - if (config.css.devSourcemap) { - const sourcemap = this.getCombinedSourcemap() - if (sourcemap.mappings) { - await injectSourcesContent(sourcemap, cleanUrl(id), config.logger) + const inlined = inlineRE.test(id) + const modules = cssModulesCache.get(config)!.get(id) + + // #6984, #7552 + // `foo.module.css` => modulesCode + // `foo.module.css?inline` => cssContent + const modulesCode = + modules && + !inlined && + dataToEsm(modules, { namedExports: true, preferConst: true }) + + if (config.command === 'serve') { + const getContentWithSourcemap = async (content: string) => { + if (config.css.devSourcemap) { + const sourcemap = this.getCombinedSourcemap() + if (sourcemap.mappings) { + await injectSourcesContent( + sourcemap, + cleanUrl(id), + config.logger, + ) + } + return getCodeWithSourcemap('css', content, sourcemap) } - return getCodeWithSourcemap('css', content, sourcemap) + return content } - return content - } - if (isDirectCSSRequest(id)) { - return null - } - if (inlined) { - return `export default ${JSON.stringify(css)}` - } - if (this.environment.config.consumer === 'server') { - return modulesCode || 'export {}' - } + if (isDirectCSSRequest(id)) { + return null + } + if (inlined) { + return `export default ${JSON.stringify(css)}` + } + if (this.environment.config.consumer === 'server') { + return modulesCode || 'export {}' + } - const cssContent = await getContentWithSourcemap(css) - const code = [ - `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( - path.posix.join(config.base, CLIENT_PUBLIC_PATH), - )}`, - `const __vite__id = ${JSON.stringify(id)}`, - `const __vite__css = ${JSON.stringify(cssContent)}`, - `__vite__updateStyle(__vite__id, __vite__css)`, - // css modules exports change on edit so it can't self accept - `${modulesCode || 'import.meta.hot.accept()'}`, - `import.meta.hot.prune(() => __vite__removeStyle(__vite__id))`, - ].join('\n') - return { code, map: { mappings: '' } } - } + const cssContent = await getContentWithSourcemap(css) + const code = [ + `import { updateStyle as __vite__updateStyle, removeStyle as __vite__removeStyle } from ${JSON.stringify( + path.posix.join(config.base, CLIENT_PUBLIC_PATH), + )}`, + `const __vite__id = ${JSON.stringify(id)}`, + `const __vite__css = ${JSON.stringify(cssContent)}`, + `__vite__updateStyle(__vite__id, __vite__css)`, + // css modules exports change on edit so it can't self accept + `${modulesCode || 'import.meta.hot.accept()'}`, + `import.meta.hot.prune(() => __vite__removeStyle(__vite__id))`, + ].join('\n') + return { code, map: { mappings: '' } } + } - // build CSS handling ---------------------------------------------------- - - const cssScopeTo = - // NOTE: `this.getModuleInfo` can be undefined when the plugin is called directly - // adding `?.` temporary to avoid unocss from breaking - // TODO: remove `?.` after `this.getModuleInfo` in Vite 7 - ( - this.getModuleInfo?.(id)?.meta?.vite as - | CustomPluginOptionsVite - | undefined - )?.cssScopeTo - - // record css - if (!inlined) { - styles.putContent(id, css, cssScopeTo) - } + // build CSS handling ---------------------------------------------------- + + const cssScopeTo = + // NOTE: `this.getModuleInfo` can be undefined when the plugin is called directly + // adding `?.` temporary to avoid unocss from breaking + // TODO: remove `?.` after `this.getModuleInfo` in Vite 7 + ( + this.getModuleInfo?.(id)?.meta?.vite as + | CustomPluginOptionsVite + | undefined + )?.cssScopeTo + + // record css + if (!inlined) { + styles.putContent(id, css, cssScopeTo) + } - let code: string - if (modulesCode) { - code = modulesCode - } else if (inlined) { - let content = css - if (config.build.cssMinify) { - content = await minifyCSS(content, config, true) + let code: string + if (modulesCode) { + code = modulesCode + } else if (inlined) { + let content = css + if (config.build.cssMinify) { + content = await minifyCSS(content, config, true) + } + code = `export default ${JSON.stringify(content)}` + } else { + // empty module when it's not a CSS module nor `?inline` + code = '' } - code = `export default ${JSON.stringify(content)}` - } else { - // empty module when it's not a CSS module nor `?inline` - code = '' - } - return { - code, - map: { mappings: '' }, - // avoid the css module from being tree-shaken so that we can retrieve - // it in renderChunk() - moduleSideEffects: - modulesCode || inlined || cssScopeTo ? false : 'no-treeshake', - } + return { + code, + map: { mappings: '' }, + // avoid the css module from being tree-shaken so that we can retrieve + // it in renderChunk() + moduleSideEffects: + modulesCode || inlined || cssScopeTo ? false : 'no-treeshake', + } + }, }, async renderChunk(code, chunk, opts) { @@ -1113,63 +1131,65 @@ export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:css-analysis', - async transform(_, id) { - if ( - !isCSSRequest(id) || - commonjsProxyRE.test(id) || - SPECIAL_QUERY_RE.test(id) - ) { - return - } + transform: { + async handler(_, id) { + if ( + !isCSSRequest(id) || + commonjsProxyRE.test(id) || + SPECIAL_QUERY_RE.test(id) + ) { + return + } - const { moduleGraph } = this.environment as DevEnvironment - const thisModule = moduleGraph.getModuleById(id) - - // Handle CSS @import dependency HMR and other added modules via this.addWatchFile. - // JS-related HMR is handled in the import-analysis plugin. - if (thisModule) { - // CSS modules cannot self-accept since it exports values - const isSelfAccepting = - !cssModulesCache.get(config)?.get(id) && - !inlineRE.test(id) && - !htmlProxyRE.test(id) - // attached by pluginContainer.addWatchFile - const pluginImports = (this as unknown as TransformPluginContext) - ._addedImports - if (pluginImports) { - // record deps in the module graph so edits to @import css can trigger - // main import to hot update - const depModules = new Set() - for (const file of pluginImports) { - if (isCSSRequest(file)) { - depModules.add(moduleGraph.createFileOnlyEntry(file)) - } else { - const url = await fileToDevUrl( - this.environment, - file, - /* skipBase */ true, - ) - if (url.startsWith('data:')) { + const { moduleGraph } = this.environment as DevEnvironment + const thisModule = moduleGraph.getModuleById(id) + + // Handle CSS @import dependency HMR and other added modules via this.addWatchFile. + // JS-related HMR is handled in the import-analysis plugin. + if (thisModule) { + // CSS modules cannot self-accept since it exports values + const isSelfAccepting = + !cssModulesCache.get(config)?.get(id) && + !inlineRE.test(id) && + !htmlProxyRE.test(id) + // attached by pluginContainer.addWatchFile + const pluginImports = (this as unknown as TransformPluginContext) + ._addedImports + if (pluginImports) { + // record deps in the module graph so edits to @import css can trigger + // main import to hot update + const depModules = new Set() + for (const file of pluginImports) { + if (isCSSRequest(file)) { depModules.add(moduleGraph.createFileOnlyEntry(file)) } else { - depModules.add(await moduleGraph.ensureEntryFromUrl(url)) + const url = await fileToDevUrl( + this.environment, + file, + /* skipBase */ true, + ) + if (url.startsWith('data:')) { + depModules.add(moduleGraph.createFileOnlyEntry(file)) + } else { + depModules.add(await moduleGraph.ensureEntryFromUrl(url)) + } } } + moduleGraph.updateModuleInfo( + thisModule, + depModules, + null, + // The root CSS proxy module is self-accepting and should not + // have an explicit accept list + new Set(), + null, + isSelfAccepting, + ) + } else { + thisModule.isSelfAccepting = isSelfAccepting } - moduleGraph.updateModuleInfo( - thisModule, - depModules, - null, - // The root CSS proxy module is self-accepting and should not - // have an explicit accept list - new Set(), - null, - isSelfAccepting, - ) - } else { - thisModule.isSelfAccepting = isSelfAccepting } - } + }, }, } } diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 36c9b57dbfeb6c..228825a244256e 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -114,69 +114,72 @@ export function definePlugin(config: ResolvedConfig): Plugin { return { name: 'vite:define', - async transform(code, id) { - if (this.environment.config.consumer === 'client' && !isBuild) { - // for dev we inject actual global defines in the vite client to - // avoid the transform cost. see the `clientInjection` and - // `importAnalysis` plugin. - return - } + transform: { + async handler(code, id) { + if (this.environment.config.consumer === 'client' && !isBuild) { + // for dev we inject actual global defines in the vite client to + // avoid the transform cost. see the `clientInjection` and + // `importAnalysis` plugin. + return + } - if ( - // exclude html, css and static assets for performance - isHTMLRequest(id) || - isCSSRequest(id) || - isNonJsRequest(id) || - config.assetsInclude(id) - ) { - return - } + if ( + // exclude html, css and static assets for performance + isHTMLRequest(id) || + isCSSRequest(id) || + isNonJsRequest(id) || + config.assetsInclude(id) + ) { + return + } - let [define, pattern, importMetaEnvVal] = getPattern(this.environment) - if (!pattern) return + let [define, pattern, importMetaEnvVal] = getPattern(this.environment) + if (!pattern) return - // Check if our code needs any replacements before running esbuild - pattern.lastIndex = 0 - if (!pattern.test(code)) return + // Check if our code needs any replacements before running esbuild + pattern.lastIndex = 0 + if (!pattern.test(code)) return - const hasDefineImportMetaEnv = 'import.meta.env' in define - let marker = importMetaEnvMarker + const hasDefineImportMetaEnv = 'import.meta.env' in define + let marker = importMetaEnvMarker - if (hasDefineImportMetaEnv && code.includes(marker)) { - // append a number to the marker until it's unique, to avoid if there is a - // marker already in the code - let i = 1 - do { - marker = importMetaEnvMarker + i++ - } while (code.includes(marker)) + if (hasDefineImportMetaEnv && code.includes(marker)) { + // append a number to the marker until it's unique, to avoid if there is a + // marker already in the code + let i = 1 + do { + marker = importMetaEnvMarker + i++ + } while (code.includes(marker)) - if (marker !== importMetaEnvMarker) { - define = { ...define, 'import.meta.env': marker } + if (marker !== importMetaEnvMarker) { + define = { ...define, 'import.meta.env': marker } + } } - } - - const result = await replaceDefine(this.environment, code, id, define) - - if (hasDefineImportMetaEnv) { - // Replace `import.meta.env.*` with undefined - result.code = result.code.replaceAll( - getImportMetaEnvKeyRe(marker), - (m) => 'undefined'.padEnd(m.length), - ) - // If there's bare `import.meta.env` references, prepend the banner - if (result.code.includes(marker)) { - result.code = `const ${marker} = ${importMetaEnvVal};\n` + result.code - - if (result.map) { - const map = JSON.parse(result.map) - map.mappings = ';' + map.mappings - result.map = map + const result = await replaceDefine(this.environment, code, id, define) + + if (hasDefineImportMetaEnv) { + // Replace `import.meta.env.*` with undefined + result.code = result.code.replaceAll( + getImportMetaEnvKeyRe(marker), + (m) => 'undefined'.padEnd(m.length), + ) + + // If there's bare `import.meta.env` references, prepend the banner + if (result.code.includes(marker)) { + result.code = + `const ${marker} = ${importMetaEnvVal};\n` + result.code + + if (result.map) { + const map = JSON.parse(result.map) + map.mappings = ';' + map.mappings + result.map = map + } } } - } - return result + return result + }, }, } } diff --git a/packages/vite/src/node/plugins/dynamicImportVars.ts b/packages/vite/src/node/plugins/dynamicImportVars.ts index 589b67a0aee864..1c16e7c7697218 100644 --- a/packages/vite/src/node/plugins/dynamicImportVars.ts +++ b/packages/vite/src/node/plugins/dynamicImportVars.ts @@ -180,101 +180,107 @@ export function dynamicImportVarsPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:dynamic-import-vars', - resolveId(id) { - if (id === dynamicImportHelperId) { - return id - } + resolveId: { + handler(id) { + if (id === dynamicImportHelperId) { + return id + } + }, }, - load(id) { - if (id === dynamicImportHelperId) { - return 'export default ' + dynamicImportHelper.toString() - } + load: { + handler(id) { + if (id === dynamicImportHelperId) { + return `export default ${dynamicImportHelper.toString()}` + } + }, }, - async transform(source, importer) { - const { environment } = this - if ( - !getFilter(this)(importer) || - importer === CLIENT_ENTRY || - !hasDynamicImportRE.test(source) - ) { - return - } + transform: { + async handler(source, importer) { + const { environment } = this + if ( + !getFilter(this)(importer) || + importer === CLIENT_ENTRY || + !hasDynamicImportRE.test(source) + ) { + return + } - await init + await init - let imports: readonly ImportSpecifier[] = [] - try { - imports = parseImports(source)[0] - } catch { - // ignore as it might not be a JS file, the subsequent plugins will catch the error - return null - } - - if (!imports.length) { - return null - } + let imports: readonly ImportSpecifier[] = [] + try { + imports = parseImports(source)[0] + } catch { + // ignore as it might not be a JS file, the subsequent plugins will catch the error + return null + } - let s: MagicString | undefined - let needDynamicImportHelper = false + if (!imports.length) { + return null + } - for (let index = 0; index < imports.length; index++) { - const { - s: start, - e: end, - ss: expStart, - se: expEnd, - d: dynamicIndex, - } = imports[index] + let s: MagicString | undefined + let needDynamicImportHelper = false - if (dynamicIndex === -1 || source[start] !== '`') { - continue - } + for (let index = 0; index < imports.length; index++) { + const { + s: start, + e: end, + ss: expStart, + se: expEnd, + d: dynamicIndex, + } = imports[index] - if (hasViteIgnoreRE.test(source.slice(expStart, expEnd))) { - continue - } + if (dynamicIndex === -1 || source[start] !== '`') { + continue + } - s ||= new MagicString(source) - let result - try { - result = await transformDynamicImport( - source.slice(start, end), - importer, - (id, importer) => resolve(environment, id, importer), - config.root, - ) - } catch (error) { - if (environment.config.build.dynamicImportVarsOptions.warnOnError) { - this.warn(error) - } else { - this.error(error) + if (hasViteIgnoreRE.test(source.slice(expStart, expEnd))) { + continue } - } - if (!result) { - continue - } + s ||= new MagicString(source) + let result + try { + result = await transformDynamicImport( + source.slice(start, end), + importer, + (id, importer) => resolve(environment, id, importer), + config.root, + ) + } catch (error) { + if (environment.config.build.dynamicImportVarsOptions.warnOnError) { + this.warn(error) + } else { + this.error(error) + } + } - const { rawPattern, glob } = result + if (!result) { + continue + } - needDynamicImportHelper = true - s.overwrite( - expStart, - expEnd, - `__variableDynamicImportRuntimeHelper(${glob}, \`${rawPattern}\`, ${rawPattern.split('/').length})`, - ) - } + const { rawPattern, glob } = result - if (s) { - if (needDynamicImportHelper) { - s.prepend( - `import __variableDynamicImportRuntimeHelper from "${dynamicImportHelperId}";`, + needDynamicImportHelper = true + s.overwrite( + expStart, + expEnd, + `__variableDynamicImportRuntimeHelper(${glob}, \`${rawPattern}\`, ${rawPattern.split('/').length})`, ) } - return transformStableResult(s, importer, config) - } + + if (s) { + if (needDynamicImportHelper) { + s.prepend( + `import __variableDynamicImportRuntimeHelper from "${dynamicImportHelperId}";`, + ) + } + return transformStableResult(s, importer, config) + } + }, }, } } diff --git a/packages/vite/src/node/plugins/html.ts b/packages/vite/src/node/plugins/html.ts index 19ae3f5e489ae0..f0dd47e0823e87 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -95,26 +95,30 @@ export function htmlInlineProxyPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:html-inline-proxy', - resolveId(id) { - if (isHTMLProxy(id)) { - return id - } + resolveId: { + handler(id) { + if (isHTMLProxy(id)) { + return id + } + }, }, - load(id) { - const proxyMatch = htmlProxyRE.exec(id) - if (proxyMatch) { - const index = Number(proxyMatch[1]) - const file = cleanUrl(id) - const url = file.replace(normalizePath(config.root), '') - const result = htmlProxyMap.get(config)!.get(url)?.[index] - if (result) { - // set moduleSideEffects to keep the module even if `treeshake.moduleSideEffects=false` is set - return { ...result, moduleSideEffects: true } - } else { - throw new Error(`No matching HTML proxy module found from ${id}`) + load: { + handler(id) { + const proxyMatch = htmlProxyRE.exec(id) + if (proxyMatch) { + const index = Number(proxyMatch[1]) + const file = cleanUrl(id) + const url = file.replace(normalizePath(config.root), '') + const result = htmlProxyMap.get(config)!.get(url)?.[index] + if (result) { + // set moduleSideEffects to keep the module even if `treeshake.moduleSideEffects=false` is set + return { ...result, moduleSideEffects: true } + } else { + throw new Error(`No matching HTML proxy module found from ${id}`) + } } - } + }, }, } } @@ -345,380 +349,395 @@ export function buildHtmlPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:build-html', - async transform(html, id) { - if (id.endsWith('.html')) { - id = normalizePath(id) - const relativeUrlPath = normalizePath(path.relative(config.root, id)) - const publicPath = `/${relativeUrlPath}` - const publicBase = getBaseInHTML(relativeUrlPath, config) - - const publicToRelative = (filename: string) => publicBase + filename - const toOutputPublicFilePath = (url: string) => - toOutputFilePathInHtml( - url.slice(1), - 'public', - relativeUrlPath, - 'html', - config, - publicToRelative, - ) - // Determines true start position for the node, either the < character - // position, or the newline at the end of the previous line's node. - const nodeStartWithLeadingWhitespace = ( - node: DefaultTreeAdapterMap['node'], - ) => { - const startOffset = node.sourceCodeLocation!.startOffset - if (startOffset === 0) return 0 - - // Gets the offset for the start of the line including the - // newline trailing the previous node - const lineStartOffset = - startOffset - node.sourceCodeLocation!.startCol - - // - // - // - // Here we want to target the newline at the end of the previous line - // as the start position for our target. - // - // - // - // - // However, if there is content between our target node start and the - // previous newline, we cannot strip it out without risking content deletion. - let isLineEmpty = false - try { - const line = s.slice(Math.max(0, lineStartOffset), startOffset) - isLineEmpty = !line.trim() - } catch { - // magic-string may throw if there's some content removed in the sliced string, - // which we ignore and assume the line is not empty - } - - return isLineEmpty ? lineStartOffset : startOffset - } - - // pre-transform - html = await applyHtmlTransforms(html, preHooks, { - path: publicPath, - filename: id, - }) - - let js = '' - const s = new MagicString(html) - const scriptUrls: ScriptAssetsUrl[] = [] - const styleUrls: ScriptAssetsUrl[] = [] - let inlineModuleIndex = -1 - - let everyScriptIsAsync = true - let someScriptsAreAsync = false - let someScriptsAreDefer = false - - const assetUrlsPromises: Promise[] = [] - - // for each encountered asset url, rewrite original html so that it - // references the post-build location, ignoring empty attributes and - // attributes that directly reference named output. - const namedOutput = Object.keys(config.build.rollupOptions.input || {}) - const processAssetUrl = async (url: string, shouldInline?: boolean) => { - if ( - url !== '' && // Empty attribute - !namedOutput.includes(url) && // Direct reference to named output - !namedOutput.includes(removeLeadingSlash(url)) // Allow for absolute references as named output can't be an absolute path - ) { + transform: { + async handler(html, id) { + if (id.endsWith('.html')) { + id = normalizePath(id) + const relativeUrlPath = normalizePath(path.relative(config.root, id)) + const publicPath = `/${relativeUrlPath}` + const publicBase = getBaseInHTML(relativeUrlPath, config) + + const publicToRelative = (filename: string) => publicBase + filename + const toOutputPublicFilePath = (url: string) => + toOutputFilePathInHtml( + url.slice(1), + 'public', + relativeUrlPath, + 'html', + config, + publicToRelative, + ) + // Determines true start position for the node, either the < character + // position, or the newline at the end of the previous line's node. + const nodeStartWithLeadingWhitespace = ( + node: DefaultTreeAdapterMap['node'], + ) => { + const startOffset = node.sourceCodeLocation!.startOffset + if (startOffset === 0) return 0 + + // Gets the offset for the start of the line including the + // newline trailing the previous node + const lineStartOffset = + startOffset - node.sourceCodeLocation!.startCol + + // + // + // + // Here we want to target the newline at the end of the previous line + // as the start position for our target. + // + // + // + // + // However, if there is content between our target node start and the + // previous newline, we cannot strip it out without risking content deletion. + let isLineEmpty = false try { - return await urlToBuiltUrl(this, url, id, shouldInline) - } catch (e) { - if (e.code !== 'ENOENT') { - throw e - } + const line = s.slice(Math.max(0, lineStartOffset), startOffset) + isLineEmpty = !line.trim() + } catch { + // magic-string may throw if there's some content removed in the sliced string, + // which we ignore and assume the line is not empty } + + return isLineEmpty ? lineStartOffset : startOffset } - return url - } - const setModuleSideEffectPromises: Promise[] = [] - await traverseHtml(html, id, (node) => { - if (!nodeIsElement(node)) { - return + // pre-transform + html = await applyHtmlTransforms(html, preHooks, { + path: publicPath, + filename: id, + }) + + let js = '' + const s = new MagicString(html) + const scriptUrls: ScriptAssetsUrl[] = [] + const styleUrls: ScriptAssetsUrl[] = [] + let inlineModuleIndex = -1 + + let everyScriptIsAsync = true + let someScriptsAreAsync = false + let someScriptsAreDefer = false + + const assetUrlsPromises: Promise[] = [] + + // for each encountered asset url, rewrite original html so that it + // references the post-build location, ignoring empty attributes and + // attributes that directly reference named output. + const namedOutput = Object.keys( + config.build.rollupOptions.input || {}, + ) + const processAssetUrl = async ( + url: string, + shouldInline?: boolean, + ) => { + if ( + url !== '' && // Empty attribute + !namedOutput.includes(url) && // Direct reference to named output + !namedOutput.includes(removeLeadingSlash(url)) // Allow for absolute references as named output can't be an absolute path + ) { + try { + return await urlToBuiltUrl(this, url, id, shouldInline) + } catch (e) { + if (e.code !== 'ENOENT') { + throw e + } + } + } + return url } - let shouldRemove = false + const setModuleSideEffectPromises: Promise[] = [] + await traverseHtml(html, id, (node) => { + if (!nodeIsElement(node)) { + return + } - // script tags - if (node.nodeName === 'script') { - const { src, srcSourceCodeLocation, isModule, isAsync, isIgnored } = - getScriptInfo(node) + let shouldRemove = false + + // script tags + if (node.nodeName === 'script') { + const { + src, + srcSourceCodeLocation, + isModule, + isAsync, + isIgnored, + } = getScriptInfo(node) + + if (isIgnored) { + removeViteIgnoreAttr(s, node.sourceCodeLocation!) + } else { + const url = src && src.value + const isPublicFile = !!(url && checkPublicFile(url, config)) + if (isPublicFile) { + // referencing public dir url, prefix with base + overwriteAttrValue( + s, + srcSourceCodeLocation!, + partialEncodeURIPath(toOutputPublicFilePath(url)), + ) + } - if (isIgnored) { - removeViteIgnoreAttr(s, node.sourceCodeLocation!) - } else { - const url = src && src.value - const isPublicFile = !!(url && checkPublicFile(url, config)) - if (isPublicFile) { - // referencing public dir url, prefix with base - overwriteAttrValue( - s, - srcSourceCodeLocation!, - partialEncodeURIPath(toOutputPublicFilePath(url)), - ) - } + if (isModule) { + inlineModuleIndex++ + if (url && !isExcludedUrl(url) && !isPublicFile) { + setModuleSideEffectPromises.push( + this.resolve(url, id).then((resolved) => { + if (!resolved) { + return Promise.reject( + new Error(`Failed to resolve ${url} from ${id}`), + ) + } + // set moduleSideEffects to keep the module even if `treeshake.moduleSideEffects=false` is set + const moduleInfo = this.getModuleInfo(resolved.id) + if (moduleInfo) { + moduleInfo.moduleSideEffects = true + } else if (!resolved.external) { + return this.load(resolved).then((mod) => { + mod.moduleSideEffects = true + }) + } + }), + ) + // + const filePath = id.replace(normalizePath(config.root), '') + addToHTMLProxyCache(config, filePath, inlineModuleIndex, { + code: contents, + }) + js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"` + shouldRemove = true + } - if (isModule) { - inlineModuleIndex++ - if (url && !isExcludedUrl(url) && !isPublicFile) { - setModuleSideEffectPromises.push( - this.resolve(url, id).then((resolved) => { - if (!resolved) { - return Promise.reject( - new Error(`Failed to resolve ${url} from ${id}`), - ) - } - // set moduleSideEffects to keep the module even if `treeshake.moduleSideEffects=false` is set - const moduleInfo = this.getModuleInfo(resolved.id) - if (moduleInfo) { - moduleInfo.moduleSideEffects = true - } else if (!resolved.external) { - return this.load(resolved).then((mod) => { - mod.moduleSideEffects = true - }) - } - }), - ) - // - const filePath = id.replace(normalizePath(config.root), '') - addToHTMLProxyCache(config, filePath, inlineModuleIndex, { - code: contents, - }) - js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"` - shouldRemove = true - } - - everyScriptIsAsync &&= isAsync - someScriptsAreAsync ||= isAsync - someScriptsAreDefer ||= !isAsync - } else if (url && !isPublicFile) { - if (!isExcludedUrl(url)) { - config.logger.warn( - ` asset - for (const { start, end, url } of scriptUrls) { - if (checkPublicFile(url, config)) { - s.update( - start, - end, - partialEncodeURIPath(toOutputPublicFilePath(url)), - ) - } else if (!isExcludedUrl(url)) { - s.update( - start, - end, - partialEncodeURIPath(await urlToBuiltUrl(this, url, id)), - ) + // emit asset + for (const { start, end, url } of scriptUrls) { + if (checkPublicFile(url, config)) { + s.update( + start, + end, + partialEncodeURIPath(toOutputPublicFilePath(url)), + ) + } else if (!isExcludedUrl(url)) { + s.update( + start, + end, + partialEncodeURIPath(await urlToBuiltUrl(this, url, id)), + ) + } } - } - // ignore if its url can't be resolved - const resolvedStyleUrls = await Promise.all( - styleUrls.map(async (styleUrl) => ({ - ...styleUrl, - resolved: await this.resolve(styleUrl.url, id), - })), - ) - for (const { start, end, url, resolved } of resolvedStyleUrls) { - if (resolved == null) { - config.logger.warnOnce( - `\n${url} doesn't exist at build time, it will remain unchanged to be resolved at runtime`, - ) - const importExpression = `\nimport ${JSON.stringify(url)}` - js = js.replace(importExpression, '') - } else { - s.remove(start, end) + // ignore if its url can't be resolved + const resolvedStyleUrls = await Promise.all( + styleUrls.map(async (styleUrl) => ({ + ...styleUrl, + resolved: await this.resolve(styleUrl.url, id), + })), + ) + for (const { start, end, url, resolved } of resolvedStyleUrls) { + if (resolved == null) { + config.logger.warnOnce( + `\n${url} doesn't exist at build time, it will remain unchanged to be resolved at runtime`, + ) + const importExpression = `\nimport ${JSON.stringify(url)}` + js = js.replace(importExpression, '') + } else { + s.remove(start, end) + } } - } - processedHtml(this).set(id, s.toString()) + processedHtml(this).set(id, s.toString()) - // inject module preload polyfill only when configured and needed - const { modulePreload } = this.environment.config.build - if ( - modulePreload !== false && - modulePreload.polyfill && - (someScriptsAreAsync || someScriptsAreDefer) - ) { - js = `import "${modulePreloadPolyfillId}";\n${js}` - } + // inject module preload polyfill only when configured and needed + const { modulePreload } = this.environment.config.build + if ( + modulePreload !== false && + modulePreload.polyfill && + (someScriptsAreAsync || someScriptsAreDefer) + ) { + js = `import "${modulePreloadPolyfillId}";\n${js}` + } - await Promise.all(setModuleSideEffectPromises) + await Promise.all(setModuleSideEffectPromises) - // Force rollup to keep this module from being shared between other entry points. - // If the resulting chunk is empty, it will be removed in generateBundle. - return { code: js, moduleSideEffects: 'no-treeshake' } - } + // Force rollup to keep this module from being shared between other entry points. + // If the resulting chunk is empty, it will be removed in generateBundle. + return { code: js, moduleSideEffects: 'no-treeshake' } + } + }, }, async generateBundle(options, bundle) { diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 2bdf2c7d66079c..338ffd562f86b3 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -180,200 +180,208 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:build-import-analysis', - resolveId(id) { - if (id === preloadHelperId) { - return id - } + resolveId: { + handler(id) { + if (id === preloadHelperId) { + return id + } + }, }, - load(id) { - if (id === preloadHelperId) { - const { modulePreload } = this.environment.config.build - - const scriptRel = - modulePreload && modulePreload.polyfill - ? `'modulepreload'` - : `/* @__PURE__ */ (${detectScriptRel.toString()})()` - - // There are two different cases for the preload list format in __vitePreload - // - // __vitePreload(() => import(asyncChunk), [ ...deps... ]) - // - // This is maintained to keep backwards compatibility as some users developed plugins - // using regex over this list to workaround the fact that module preload wasn't - // configurable. - const assetsURL = - renderBuiltUrl || isRelativeBase - ? // If `experimental.renderBuiltUrl` is used, the dependencies might be relative to the current chunk. - // If relative base is used, the dependencies are relative to the current chunk. - // The importerUrl is passed as third parameter to __vitePreload in this case - `function(dep, importerUrl) { return new URL(dep, importerUrl).href }` - : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base - // is appended inside __vitePreload too. - `function(dep) { return ${JSON.stringify(config.base)}+dep }` - const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` - return { code: preloadCode, moduleSideEffects: false } - } + load: { + handler(id) { + if (id === preloadHelperId) { + const { modulePreload } = this.environment.config.build + + const scriptRel = + modulePreload && modulePreload.polyfill + ? `'modulepreload'` + : `/* @__PURE__ */ (${detectScriptRel.toString()})()` + + // There are two different cases for the preload list format in __vitePreload + // + // __vitePreload(() => import(asyncChunk), [ ...deps... ]) + // + // This is maintained to keep backwards compatibility as some users developed plugins + // using regex over this list to workaround the fact that module preload wasn't + // configurable. + const assetsURL = + renderBuiltUrl || isRelativeBase + ? // If `experimental.renderBuiltUrl` is used, the dependencies might be relative to the current chunk. + // If relative base is used, the dependencies are relative to the current chunk. + // The importerUrl is passed as third parameter to __vitePreload in this case + `function(dep, importerUrl) { return new URL(dep, importerUrl).href }` + : // If the base isn't relative, then the deps are relative to the projects `outDir` and the base + // is appended inside __vitePreload too. + `function(dep) { return ${JSON.stringify(config.base)}+dep }` + const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` + return { code: preloadCode, moduleSideEffects: false } + } + }, }, - async transform(source, importer) { - if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) { - return - } + transform: { + async handler(source, importer) { + if (isInNodeModules(importer) && !dynamicImportPrefixRE.test(source)) { + return + } - await init - - let imports: readonly ImportSpecifier[] = [] - try { - imports = parseImports(source)[0] - } catch (_e: unknown) { - const e = _e as EsModuleLexerParseError - const { message, showCodeFrame } = createParseErrorInfo( - importer, - source, - ) - this.error(message, showCodeFrame ? e.idx : undefined) - } + await init - if (!imports.length) { - return null - } + let imports: readonly ImportSpecifier[] = [] + try { + imports = parseImports(source)[0] + } catch (_e: unknown) { + const e = _e as EsModuleLexerParseError + const { message, showCodeFrame } = createParseErrorInfo( + importer, + source, + ) + this.error(message, showCodeFrame ? e.idx : undefined) + } - const insertPreload = getInsertPreload(this.environment) - // when wrapping dynamic imports with a preload helper, Rollup is unable to analyze the - // accessed variables for treeshaking. This below tries to match common accessed syntax - // to "copy" it over to the dynamic import wrapped by the preload helper. - const dynamicImports: Record< - number, - { declaration?: string; names?: string } - > = {} - - if (insertPreload) { - let match - while ((match = dynamicImportTreeshakenRE.exec(source))) { - /* handle `const {foo} = await import('foo')` - * - * match[1]: `const {foo} = await import('foo')` - * match[2]: `{foo}` - * import end: `const {foo} = await import('foo')_` - * ^ - */ - if (match[1]) { - dynamicImports[dynamicImportTreeshakenRE.lastIndex] = { - declaration: `const ${match[2]}`, - names: match[2]?.trim(), + if (!imports.length) { + return null + } + + const insertPreload = getInsertPreload(this.environment) + // when wrapping dynamic imports with a preload helper, Rollup is unable to analyze the + // accessed variables for treeshaking. This below tries to match common accessed syntax + // to "copy" it over to the dynamic import wrapped by the preload helper. + const dynamicImports: Record< + number, + { declaration?: string; names?: string } + > = {} + + if (insertPreload) { + let match + while ((match = dynamicImportTreeshakenRE.exec(source))) { + /* handle `const {foo} = await import('foo')` + * + * match[1]: `const {foo} = await import('foo')` + * match[2]: `{foo}` + * import end: `const {foo} = await import('foo')_` + * ^ + */ + if (match[1]) { + dynamicImports[dynamicImportTreeshakenRE.lastIndex] = { + declaration: `const ${match[2]}`, + names: match[2]?.trim(), + } + continue } - continue - } - /* handle `(await import('foo')).foo` - * - * match[3]: `(await import('foo')).foo` - * match[4]: `.foo` - * import end: `(await import('foo'))` - * ^ - */ - if (match[3]) { - let names = /\.([^.?]+)/.exec(match[4])?.[1] || '' - // avoid `default` keyword error - if (names === 'default') { - names = 'default: __vite_default__' + /* handle `(await import('foo')).foo` + * + * match[3]: `(await import('foo')).foo` + * match[4]: `.foo` + * import end: `(await import('foo'))` + * ^ + */ + if (match[3]) { + let names = /\.([^.?]+)/.exec(match[4])?.[1] || '' + // avoid `default` keyword error + if (names === 'default') { + names = 'default: __vite_default__' + } + dynamicImports[ + dynamicImportTreeshakenRE.lastIndex - match[4]?.length - 1 + ] = { declaration: `const {${names}}`, names: `{ ${names} }` } + continue } + + /* handle `import('foo').then(({foo})=>{})` + * + * match[5]: `.then(({foo})` + * match[6]: `foo` + * import end: `import('foo').` + * ^ + */ + const names = match[6]?.trim() dynamicImports[ - dynamicImportTreeshakenRE.lastIndex - match[4]?.length - 1 + dynamicImportTreeshakenRE.lastIndex - match[5]?.length ] = { declaration: `const {${names}}`, names: `{ ${names} }` } - continue } - - /* handle `import('foo').then(({foo})=>{})` - * - * match[5]: `.then(({foo})` - * match[6]: `foo` - * import end: `import('foo').` - * ^ - */ - const names = match[6]?.trim() - dynamicImports[ - dynamicImportTreeshakenRE.lastIndex - match[5]?.length - ] = { declaration: `const {${names}}`, names: `{ ${names} }` } } - } - let s: MagicString | undefined - const str = () => s || (s = new MagicString(source)) - let needPreloadHelper = false - - for (let index = 0; index < imports.length; index++) { - const { - s: start, - e: end, - ss: expStart, - se: expEnd, - d: dynamicIndex, - a: attributeIndex, - } = imports[index] - - const isDynamicImport = dynamicIndex > -1 - - // strip import attributes as we can process them ourselves - if (!isDynamicImport && attributeIndex > -1) { - str().remove(end + 1, expEnd) + let s: MagicString | undefined + const str = () => s || (s = new MagicString(source)) + let needPreloadHelper = false + + for (let index = 0; index < imports.length; index++) { + const { + s: start, + e: end, + ss: expStart, + se: expEnd, + d: dynamicIndex, + a: attributeIndex, + } = imports[index] + + const isDynamicImport = dynamicIndex > -1 + + // strip import attributes as we can process them ourselves + if (!isDynamicImport && attributeIndex > -1) { + str().remove(end + 1, expEnd) + } + + if ( + isDynamicImport && + insertPreload && + // Only preload static urls + (source[start] === '"' || + source[start] === "'" || + source[start] === '`') + ) { + needPreloadHelper = true + const { declaration, names } = dynamicImports[expEnd] || {} + if (names) { + /* transform `const {foo} = await import('foo')` + * to `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)` + * + * transform `import('foo').then(({foo})=>{})` + * to `__vitePreload(async () => { const {foo} = await import('foo');return { foo }},...).then(({foo})=>{})` + * + * transform `(await import('foo')).foo` + * to `__vitePreload(async () => { const {foo} = (await import('foo')).foo; return { foo }},...)).foo` + */ + str().prependLeft( + expStart, + `${preloadMethod}(async () => { ${declaration} = await `, + ) + str().appendRight(expEnd, `;return ${names}}`) + } else { + str().prependLeft(expStart, `${preloadMethod}(() => `) + } + + str().appendRight( + expEnd, + `,${isModernFlag}?${preloadMarker}:void 0${ + renderBuiltUrl || isRelativeBase ? ',import.meta.url' : '' + })`, + ) + } } if ( - isDynamicImport && + needPreloadHelper && insertPreload && - // Only preload static urls - (source[start] === '"' || - source[start] === "'" || - source[start] === '`') + !source.includes(`const ${preloadMethod} =`) ) { - needPreloadHelper = true - const { declaration, names } = dynamicImports[expEnd] || {} - if (names) { - /* transform `const {foo} = await import('foo')` - * to `const {foo} = await __vitePreload(async () => { const {foo} = await import('foo');return {foo}}, ...)` - * - * transform `import('foo').then(({foo})=>{})` - * to `__vitePreload(async () => { const {foo} = await import('foo');return { foo }},...).then(({foo})=>{})` - * - * transform `(await import('foo')).foo` - * to `__vitePreload(async () => { const {foo} = (await import('foo')).foo; return { foo }},...)).foo` - */ - str().prependLeft( - expStart, - `${preloadMethod}(async () => { ${declaration} = await `, - ) - str().appendRight(expEnd, `;return ${names}}`) - } else { - str().prependLeft(expStart, `${preloadMethod}(() => `) - } - - str().appendRight( - expEnd, - `,${isModernFlag}?${preloadMarker}:void 0${ - renderBuiltUrl || isRelativeBase ? ',import.meta.url' : '' - })`, + str().prepend( + `import { ${preloadMethod} } from "${preloadHelperId}";`, ) } - } - if ( - needPreloadHelper && - insertPreload && - !source.includes(`const ${preloadMethod} =`) - ) { - str().prepend(`import { ${preloadMethod} } from "${preloadHelperId}";`) - } - - if (s) { - return { - code: s.toString(), - map: this.environment.config.build.sourcemap - ? s.generateMap({ hires: 'boundary' }) - : null, + if (s) { + return { + code: s.toString(), + map: this.environment.config.build.sourcemap + ? s.generateMap({ hires: 'boundary' }) + : null, + } } - } + }, }, renderChunk(code, _, { format }) { diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 43534d7d82bca5..2ebd0257e19197 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -51,44 +51,46 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { buildStart() { importGlobMaps.clear() }, - async transform(code, id) { - if (!code.includes('import.meta.glob')) return - const result = await transformGlobImport( - code, - id, - config.root, - (im, _, options) => - this.resolve(im, id, options).then((i) => i?.id || im), - config.experimental.importGlobRestoreExtension, - config.logger, - ) - if (result) { - const allGlobs = result.matches.map((i) => i.globsResolved) - if (!importGlobMaps.has(this.environment)) { - importGlobMaps.set(this.environment, new Map()) - } - - const globMatchers = allGlobs.map((globs) => { - const affirmed: string[] = [] - const negated: string[] = [] - for (const glob of globs) { - ;(glob[0] === '!' ? negated : affirmed).push(glob) - } - const affirmedMatcher = picomatch(affirmed) - const negatedMatcher = picomatch(negated) - - return (file: string) => { - // (glob1 || glob2) && !(glob3 || glob4)... - return ( - (affirmed.length === 0 || affirmedMatcher(file)) && - !(negated.length > 0 && negatedMatcher(file)) - ) + transform: { + async handler(code, id) { + if (!code.includes('import.meta.glob')) return + const result = await transformGlobImport( + code, + id, + config.root, + (im, _, options) => + this.resolve(im, id, options).then((i) => i?.id || im), + config.experimental.importGlobRestoreExtension, + config.logger, + ) + if (result) { + const allGlobs = result.matches.map((i) => i.globsResolved) + if (!importGlobMaps.has(this.environment)) { + importGlobMaps.set(this.environment, new Map()) } - }) - importGlobMaps.get(this.environment)!.set(id, globMatchers) - return transformStableResult(result.s, id, config) - } + const globMatchers = allGlobs.map((globs) => { + const affirmed: string[] = [] + const negated: string[] = [] + for (const glob of globs) { + ;(glob[0] === '!' ? negated : affirmed).push(glob) + } + const affirmedMatcher = picomatch(affirmed) + const negatedMatcher = picomatch(negated) + + return (file: string) => { + // (glob1 || glob2) && !(glob3 || glob4)... + return ( + (affirmed.length === 0 || affirmedMatcher(file)) && + !(negated.length > 0 && negatedMatcher(file)) + ) + } + }) + importGlobMaps.get(this.environment)!.set(id, globMatchers) + + return transformStableResult(result.s, id, config) + } + }, }, hotUpdate({ type, file, modules: oldModules }) { if (type === 'update') return diff --git a/packages/vite/src/node/plugins/json.ts b/packages/vite/src/node/plugins/json.ts index ced3852703da5f..a516851cb03ace 100644 --- a/packages/vite/src/node/plugins/json.ts +++ b/packages/vite/src/node/plugins/json.ts @@ -44,78 +44,80 @@ export function jsonPlugin( return { name: 'vite:json', - transform(json, id) { - if (!jsonExtRE.test(id)) return null - if (SPECIAL_QUERY_RE.test(id)) return null - - if (inlineRE.test(id) || noInlineRE.test(id)) { - this.warn( - `\n` + - `Using ?inline or ?no-inline for JSON imports will have no effect.\n` + - `Please use ?url&inline or ?url&no-inline to control JSON file inlining behavior.\n`, - ) - } - - json = stripBomTag(json) - - try { - if (options.stringify !== false) { - if (options.namedExports && jsonObjRE.test(json)) { - const parsed = JSON.parse(json) - const keys = Object.keys(parsed) - - let code = '' - let defaultObjectCode = '{\n' - for (const key of keys) { - if (key === makeLegalIdentifier(key)) { - code += `export const ${key} = ${serializeValue(parsed[key])};\n` - defaultObjectCode += ` ${key},\n` - } else { - defaultObjectCode += ` ${JSON.stringify(key)}: ${serializeValue(parsed[key])},\n` + transform: { + handler(json, id) { + if (!jsonExtRE.test(id)) return null + if (SPECIAL_QUERY_RE.test(id)) return null + + if (inlineRE.test(id) || noInlineRE.test(id)) { + this.warn( + `\n` + + `Using ?inline or ?no-inline for JSON imports will have no effect.\n` + + `Please use ?url&inline or ?url&no-inline to control JSON file inlining behavior.\n`, + ) + } + + json = stripBomTag(json) + + try { + if (options.stringify !== false) { + if (options.namedExports && jsonObjRE.test(json)) { + const parsed = JSON.parse(json) + const keys = Object.keys(parsed) + + let code = '' + let defaultObjectCode = '{\n' + for (const key of keys) { + if (key === makeLegalIdentifier(key)) { + code += `export const ${key} = ${serializeValue(parsed[key])};\n` + defaultObjectCode += ` ${key},\n` + } else { + defaultObjectCode += ` ${JSON.stringify(key)}: ${serializeValue(parsed[key])},\n` + } } - } - defaultObjectCode += '}' + defaultObjectCode += '}' - code += `export default ${defaultObjectCode};\n` - return { - code, - map: { mappings: '' }, + code += `export default ${defaultObjectCode};\n` + return { + code, + map: { mappings: '' }, + } } - } - if ( - options.stringify === true || - // use 10kB as a threshold for 'auto' - // https://v8.dev/blog/cost-of-javascript-2019#:~:text=A%20good%20rule%20of%20thumb%20is%20to%20apply%20this%20technique%20for%20objects%20of%2010%20kB%20or%20larger - json.length > 10 * 1000 - ) { - // during build, parse then double-stringify to remove all - // unnecessary whitespaces to reduce bundle size. - if (isBuild) { - json = JSON.stringify(JSON.parse(json)) - } + if ( + options.stringify === true || + // use 10kB as a threshold for 'auto' + // https://v8.dev/blog/cost-of-javascript-2019#:~:text=A%20good%20rule%20of%20thumb%20is%20to%20apply%20this%20technique%20for%20objects%20of%2010%20kB%20or%20larger + json.length > 10 * 1000 + ) { + // during build, parse then double-stringify to remove all + // unnecessary whitespaces to reduce bundle size. + if (isBuild) { + json = JSON.stringify(JSON.parse(json)) + } - return { - code: `export default /* #__PURE__ */ JSON.parse(${JSON.stringify(json)})`, - map: { mappings: '' }, + return { + code: `export default /* #__PURE__ */ JSON.parse(${JSON.stringify(json)})`, + map: { mappings: '' }, + } } } - } - return { - code: dataToEsm(JSON.parse(json), { - preferConst: true, - namedExports: options.namedExports, - }), - map: { mappings: '' }, + return { + code: dataToEsm(JSON.parse(json), { + preferConst: true, + namedExports: options.namedExports, + }), + map: { mappings: '' }, + } + } catch (e) { + const position = extractJsonErrorPosition(e.message, json.length) + const msg = position + ? `, invalid JSON syntax found at position ${position}` + : `.` + this.error(`Failed to parse JSON file` + msg, position) } - } catch (e) { - const position = extractJsonErrorPosition(e.message, json.length) - const msg = position - ? `, invalid JSON syntax found at position ${position}` - : `.` - this.error(`Failed to parse JSON file` + msg, position) - } + }, }, } } diff --git a/packages/vite/src/node/plugins/loadFallback.ts b/packages/vite/src/node/plugins/loadFallback.ts index f221ce56bdd2fb..b3671c7615843a 100644 --- a/packages/vite/src/node/plugins/loadFallback.ts +++ b/packages/vite/src/node/plugins/loadFallback.ts @@ -8,17 +8,19 @@ import type { Plugin } from '../plugin' export function buildLoadFallbackPlugin(): Plugin { return { name: 'vite:load-fallback', - async load(id) { - try { - const cleanedId = cleanUrl(id) - const content = await fsp.readFile(cleanedId, 'utf-8') - this.addWatchFile(cleanedId) - return content - } catch { - const content = await fsp.readFile(id, 'utf-8') - this.addWatchFile(id) - return content - } + load: { + async handler(id) { + try { + const cleanedId = cleanUrl(id) + const content = await fsp.readFile(cleanedId, 'utf-8') + this.addWatchFile(cleanedId) + return content + } catch { + const content = await fsp.readFile(id, 'utf-8') + this.addWatchFile(id) + return content + } + }, }, } } diff --git a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts index 8bf5e6abce4b28..e662ddf7dd0857 100644 --- a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts +++ b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts @@ -10,25 +10,29 @@ export function modulePreloadPolyfillPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:modulepreload-polyfill', - resolveId(id) { - if (id === modulePreloadPolyfillId) { - return resolvedModulePreloadPolyfillId - } - }, - load(id) { - if (id === resolvedModulePreloadPolyfillId) { - // `isModernFlag` is only available during build since it is resolved by `vite:build-import-analysis` - if ( - config.command !== 'build' || - this.environment.config.consumer !== 'client' - ) { - return '' + resolveId: { + handler(id) { + if (id === modulePreloadPolyfillId) { + return resolvedModulePreloadPolyfillId } - if (!polyfillString) { - polyfillString = `${isModernFlag}&&(${polyfill.toString()}());` + }, + }, + load: { + handler(id) { + if (id === resolvedModulePreloadPolyfillId) { + // `isModernFlag` is only available during build since it is resolved by `vite:build-import-analysis` + if ( + config.command !== 'build' || + this.environment.config.consumer !== 'client' + ) { + return '' + } + if (!polyfillString) { + polyfillString = `${isModernFlag}&&(${polyfill.toString()}());` + } + return { code: polyfillString, moduleSideEffects: true } } - return { code: polyfillString, moduleSideEffects: true } - } + }, }, } } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index d33bdee36cf462..7780fa6a0343d5 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -472,28 +472,30 @@ export function resolvePlugin( debug?.(`[fallthrough] ${colors.dim(id)}`) }, - load(id) { - if (id.startsWith(browserExternalId)) { - if (isProduction) { - return `export default {}` - } else { - id = id.slice(browserExternalId.length + 1) - return `\ -export default new Proxy({}, { - get(_, key) { - throw new Error(\`Module "${id}" has been externalized for browser compatibility. Cannot access "${id}.\${key}" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`) - } -})` + load: { + handler(id) { + if (id.startsWith(browserExternalId)) { + if (isProduction) { + return `export default {}` + } else { + id = id.slice(browserExternalId.length + 1) + return `\ + export default new Proxy({}, { + get(_, key) { + throw new Error(\`Module "${id}" has been externalized for browser compatibility. Cannot access "${id}.\${key}" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`) + } + })` + } } - } - if (id.startsWith(optionalPeerDepId)) { - if (isProduction) { - return `export default {}` - } else { - const [, peerDep, parentDep] = id.split(':') - return `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)` + if (id.startsWith(optionalPeerDepId)) { + if (isProduction) { + return `export default {}` + } else { + const [, peerDep, parentDep] = id.split(':') + return `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)` + } } - } + }, }, } } diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index ea0b45a4c68a51..2cca0b8d09d94d 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -50,27 +50,31 @@ export const wasmHelperPlugin = (): Plugin => { return { name: 'vite:wasm-helper', - resolveId(id) { - if (id === wasmHelperId) { - return id - } + resolveId: { + handler(id) { + if (id === wasmHelperId) { + return id + } + }, }, - async load(id) { - if (id === wasmHelperId) { - return `export default ${wasmHelperCode}` - } + load: { + async handler(id) { + if (id === wasmHelperId) { + return `export default ${wasmHelperCode}` + } - if (!id.endsWith('.wasm?init')) { - return - } + if (!id.endsWith('.wasm?init')) { + return + } - const url = await fileToUrl(this, id) + const url = await fileToUrl(this, id) - return ` -import initWasm from "${wasmHelperId}" -export default opts => initWasm(opts, ${JSON.stringify(url)}) -` + return ` + import initWasm from "${wasmHelperId}" + export default opts => initWasm(opts, ${JSON.stringify(url)}) + ` + }, }, } } @@ -79,17 +83,19 @@ export const wasmFallbackPlugin = (): Plugin => { return { name: 'vite:wasm-fallback', - async load(id) { - if (!id.endsWith('.wasm')) { - return - } + load: { + handler(id) { + if (!id.endsWith('.wasm')) { + return + } - throw new Error( - '"ESM integration proposal for Wasm" is not supported currently. ' + - 'Use vite-plugin-wasm or other community plugins to handle this. ' + - 'Alternatively, you can use `.wasm?init` or `.wasm?url`. ' + - 'See https://vite.dev/guide/features.html#webassembly for more details.', - ) + throw new Error( + '"ESM integration proposal for Wasm" is not supported currently. ' + + 'Use vite-plugin-wasm or other community plugins to handle this. ' + + 'Alternatively, you can use `.wasm?init` or `.wasm?url`. ' + + 'See https://vite.dev/guide/features.html#webassembly for more details.', + ) + }, }, } } diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 15dd48879b7479..f621d3946dea98 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -249,10 +249,12 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { }) }, - load(id) { - if (isBuild && workerOrSharedWorkerRE.test(id)) { - return '' - } + load: { + handler(id) { + if (isBuild && workerOrSharedWorkerRE.test(id)) { + return '' + } + }, }, shouldTransformCachedModule({ id }) { @@ -261,144 +263,146 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { } }, - async transform(raw, id) { - const workerFileMatch = workerFileRE.exec(id) - if (workerFileMatch) { - // if import worker by worker constructor will have query.type - // other type will be import worker by esm - const workerType = workerFileMatch[1] as WorkerType - let injectEnv = '' - - const scriptPath = JSON.stringify( - path.posix.join(config.base, ENV_PUBLIC_PATH), - ) + transform: { + async handler(raw, id) { + const workerFileMatch = workerFileRE.exec(id) + if (workerFileMatch) { + // if import worker by worker constructor will have query.type + // other type will be import worker by esm + const workerType = workerFileMatch[1] as WorkerType + let injectEnv = '' + + const scriptPath = JSON.stringify( + path.posix.join(config.base, ENV_PUBLIC_PATH), + ) - if (workerType === 'classic') { - injectEnv = `importScripts(${scriptPath})\n` - } else if (workerType === 'module') { - injectEnv = `import ${scriptPath}\n` - } else if (workerType === 'ignore') { - if (isBuild) { - injectEnv = '' - } else { - // dynamic worker type we can't know how import the env - // so we copy /@vite/env code of server transform result into file header - const environment = this.environment - const moduleGraph = - environment.mode === 'dev' ? environment.moduleGraph : undefined - const module = moduleGraph?.getModuleById(ENV_ENTRY) - injectEnv = module?.transformResult?.code || '' + if (workerType === 'classic') { + injectEnv = `importScripts(${scriptPath})\n` + } else if (workerType === 'module') { + injectEnv = `import ${scriptPath}\n` + } else if (workerType === 'ignore') { + if (isBuild) { + injectEnv = '' + } else { + // dynamic worker type we can't know how import the env + // so we copy /@vite/env code of server transform result into file header + const environment = this.environment + const moduleGraph = + environment.mode === 'dev' ? environment.moduleGraph : undefined + const module = moduleGraph?.getModuleById(ENV_ENTRY) + injectEnv = module?.transformResult?.code || '' + } } - } - if (injectEnv) { - const s = new MagicString(raw) - s.prepend(injectEnv + ';\n') - return { - code: s.toString(), - map: s.generateMap({ hires: 'boundary' }), + if (injectEnv) { + const s = new MagicString(raw) + s.prepend(injectEnv + ';\n') + return { + code: s.toString(), + map: s.generateMap({ hires: 'boundary' }), + } } + return } - return - } - const workerMatch = workerOrSharedWorkerRE.exec(id) - if (!workerMatch) return - - const { format } = config.worker - const workerConstructor = - workerMatch[1] === 'sharedworker' ? 'SharedWorker' : 'Worker' - const workerType = isBuild - ? format === 'es' - ? 'module' - : 'classic' - : 'module' - const workerTypeOption = `{ - ${workerType === 'module' ? `type: "module",` : ''} - name: options?.name - }` - - let urlCode: string - if (isBuild) { - if (isWorker && config.bundleChain.at(-1) === cleanUrl(id)) { - urlCode = 'self.location.href' - } else if (inlineRE.test(id)) { - const chunk = await bundleWorkerEntry(config, id) - const jsContent = `const jsContent = ${JSON.stringify(chunk.code)};` - - const code = - // Using blob URL for SharedWorker results in multiple instances of a same worker - workerConstructor === 'Worker' - ? `${jsContent} - const blob = typeof self !== "undefined" && self.Blob && new Blob([${ - workerType === 'classic' - ? '' - : // `URL` is always available, in `Worker[type="module"]` - `'URL.revokeObjectURL(import.meta.url);',` - }jsContent], { type: "text/javascript;charset=utf-8" }); - export default function WorkerWrapper(options) { - let objURL; - try { - objURL = blob && (self.URL || self.webkitURL).createObjectURL(blob); - if (!objURL) throw '' - const worker = new ${workerConstructor}(objURL, ${workerTypeOption}); - worker.addEventListener("error", () => { - (self.URL || self.webkitURL).revokeObjectURL(objURL); - }); - return worker; - } catch(e) { + const workerMatch = workerOrSharedWorkerRE.exec(id) + if (!workerMatch) return + + const { format } = config.worker + const workerConstructor = + workerMatch[1] === 'sharedworker' ? 'SharedWorker' : 'Worker' + const workerType = isBuild + ? format === 'es' + ? 'module' + : 'classic' + : 'module' + const workerTypeOption = `{ + ${workerType === 'module' ? `type: "module",` : ''} + name: options?.name + }` + + let urlCode: string + if (isBuild) { + if (isWorker && config.bundleChain.at(-1) === cleanUrl(id)) { + urlCode = 'self.location.href' + } else if (inlineRE.test(id)) { + const chunk = await bundleWorkerEntry(config, id) + const jsContent = `const jsContent = ${JSON.stringify(chunk.code)};` + + const code = + // Using blob URL for SharedWorker results in multiple instances of a same worker + workerConstructor === 'Worker' + ? `${jsContent} + const blob = typeof self !== "undefined" && self.Blob && new Blob([${ + workerType === 'classic' + ? '' + : // `URL` is always available, in `Worker[type="module"]` + `'URL.revokeObjectURL(import.meta.url);',` + }jsContent], { type: "text/javascript;charset=utf-8" }); + export default function WorkerWrapper(options) { + let objURL; + try { + objURL = blob && (self.URL || self.webkitURL).createObjectURL(blob); + if (!objURL) throw '' + const worker = new ${workerConstructor}(objURL, ${workerTypeOption}); + worker.addEventListener("error", () => { + (self.URL || self.webkitURL).revokeObjectURL(objURL); + }); + return worker; + } catch(e) { + return new ${workerConstructor}( + 'data:text/javascript;charset=utf-8,' + encodeURIComponent(jsContent), + ${workerTypeOption} + ); + }${ + // For module workers, we should not revoke the URL until the worker runs, + // otherwise the worker fails to run + workerType === 'classic' + ? ` finally { + objURL && (self.URL || self.webkitURL).revokeObjectURL(objURL); + }` + : '' + } + }` + : `${jsContent} + export default function WorkerWrapper(options) { return new ${workerConstructor}( 'data:text/javascript;charset=utf-8,' + encodeURIComponent(jsContent), ${workerTypeOption} ); - }${ - // For module workers, we should not revoke the URL until the worker runs, - // otherwise the worker fails to run - workerType === 'classic' - ? ` finally { - objURL && (self.URL || self.webkitURL).revokeObjectURL(objURL); - }` - : '' } - }` - : `${jsContent} - export default function WorkerWrapper(options) { - return new ${workerConstructor}( - 'data:text/javascript;charset=utf-8,' + encodeURIComponent(jsContent), - ${workerTypeOption} - ); + ` + + return { + code, + // Empty sourcemap to suppress Rollup warning + map: { mappings: '' }, + } + } else { + urlCode = JSON.stringify(await workerFileToUrl(config, id)) } - ` + } else { + let url = await fileToUrl(this, cleanUrl(id)) + url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`) + urlCode = JSON.stringify(url) + } + if (urlRE.test(id)) { return { - code, - // Empty sourcemap to suppress Rollup warning - map: { mappings: '' }, + code: `export default ${urlCode}`, + map: { mappings: '' }, // Empty sourcemap to suppress Rollup warning } - } else { - urlCode = JSON.stringify(await workerFileToUrl(config, id)) } - } else { - let url = await fileToUrl(this, cleanUrl(id)) - url = injectQuery(url, `${WORKER_FILE_ID}&type=${workerType}`) - urlCode = JSON.stringify(url) - } - if (urlRE.test(id)) { return { - code: `export default ${urlCode}`, + code: `export default function WorkerWrapper(options) { + return new ${workerConstructor}( + ${urlCode}, + ${workerTypeOption} + ); + }`, map: { mappings: '' }, // Empty sourcemap to suppress Rollup warning } - } - - return { - code: `export default function WorkerWrapper(options) { - return new ${workerConstructor}( - ${urlCode}, - ${workerTypeOption} - ); - }`, - map: { mappings: '' }, // Empty sourcemap to suppress Rollup warning - } + }, }, renderChunk(code, chunk, outputOptions) { diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 222491bb72bf36..c8c98520b0987d 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -218,79 +218,81 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { } }, - async transform(code, id) { - if (isIncludeWorkerImportMetaUrl(code)) { - let s: MagicString | undefined - const cleanString = stripLiteral(code) - const workerImportMetaUrlRE = - /\bnew\s+(?:Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/dg - - let match: RegExpExecArray | null - while ((match = workerImportMetaUrlRE.exec(cleanString))) { - const [[, endIndex], [expStart, expEnd], [urlStart, urlEnd]] = - match.indices! - - const rawUrl = code.slice(urlStart, urlEnd) - - // potential dynamic template string - if (rawUrl[0] === '`' && rawUrl.includes('${')) { - this.error( - `\`new URL(url, import.meta.url)\` is not supported in dynamic template string.`, - expStart, - ) - } + transform: { + async handler(code, id) { + if (isIncludeWorkerImportMetaUrl(code)) { + let s: MagicString | undefined + const cleanString = stripLiteral(code) + const workerImportMetaUrlRE = + /\bnew\s+(?:Worker|SharedWorker)\s*\(\s*(new\s+URL\s*\(\s*('[^']+'|"[^"]+"|`[^`]+`)\s*,\s*import\.meta\.url\s*\))/dg + + let match: RegExpExecArray | null + while ((match = workerImportMetaUrlRE.exec(cleanString))) { + const [[, endIndex], [expStart, expEnd], [urlStart, urlEnd]] = + match.indices! + + const rawUrl = code.slice(urlStart, urlEnd) + + // potential dynamic template string + if (rawUrl[0] === '`' && rawUrl.includes('${')) { + this.error( + `\`new URL(url, import.meta.url)\` is not supported in dynamic template string.`, + expStart, + ) + } - s ||= new MagicString(code) - const workerType = await getWorkerType(code, cleanString, endIndex) - const url = rawUrl.slice(1, -1) - let file: string | undefined - if (url[0] === '.') { - file = path.resolve(path.dirname(id), url) - file = slash(tryFsResolve(file, fsResolveOptions) ?? file) - } else { - workerResolver ??= createBackCompatIdResolver(config, { - extensions: [], - tryIndex: false, - preferRelative: true, - }) - file = await workerResolver(this.environment, url, id) - file ??= - url[0] === '/' - ? slash(path.join(config.publicDir, url)) - : slash(path.resolve(path.dirname(id), url)) - } + s ||= new MagicString(code) + const workerType = await getWorkerType(code, cleanString, endIndex) + const url = rawUrl.slice(1, -1) + let file: string | undefined + if (url[0] === '.') { + file = path.resolve(path.dirname(id), url) + file = slash(tryFsResolve(file, fsResolveOptions) ?? file) + } else { + workerResolver ??= createBackCompatIdResolver(config, { + extensions: [], + tryIndex: false, + preferRelative: true, + }) + file = await workerResolver(this.environment, url, id) + file ??= + url[0] === '/' + ? slash(path.join(config.publicDir, url)) + : slash(path.resolve(path.dirname(id), url)) + } - if ( - isBuild && - config.isWorker && - config.bundleChain.at(-1) === cleanUrl(file) - ) { - s.update(expStart, expEnd, 'self.location.href') - } else { - let builtUrl: string - if (isBuild) { - builtUrl = await workerFileToUrl(config, file) + if ( + isBuild && + config.isWorker && + config.bundleChain.at(-1) === cleanUrl(file) + ) { + s.update(expStart, expEnd, 'self.location.href') } else { - builtUrl = await fileToUrl(this, cleanUrl(file)) - builtUrl = injectQuery( - builtUrl, - `${WORKER_FILE_ID}&type=${workerType}`, + let builtUrl: string + if (isBuild) { + builtUrl = await workerFileToUrl(config, file) + } else { + builtUrl = await fileToUrl(this, cleanUrl(file)) + builtUrl = injectQuery( + builtUrl, + `${WORKER_FILE_ID}&type=${workerType}`, + ) + } + s.update( + expStart, + expEnd, + `new URL(/* @vite-ignore */ ${JSON.stringify(builtUrl)}, import.meta.url)`, ) } - s.update( - expStart, - expEnd, - `new URL(/* @vite-ignore */ ${JSON.stringify(builtUrl)}, import.meta.url)`, - ) } - } - if (s) { - return transformStableResult(s, id, config) - } + if (s) { + return transformStableResult(s, id, config) + } - return null - } + return null + } + }, }, } }