diff --git a/packages/vite/src/node/__tests__/plugins/css.spec.ts b/packages/vite/src/node/__tests__/plugins/css.spec.ts index 891690e58b6f51..3469961fe45aa7 100644 --- a/packages/vite/src/node/__tests__/plugins/css.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/css.spec.ts @@ -1,6 +1,7 @@ import fs from 'node:fs' import path from 'node:path' import { describe, expect, test, vi } from 'vitest' +import type { Plugin } from 'rolldown' import { resolveConfig } from '../../config' import type { InlineConfig } from '../../config' import { @@ -219,7 +220,7 @@ async function createCssPluginTransform( const config = await resolveConfig(inlineConfig, 'serve') const environment = new PartialEnvironment('client', config) - const { transform, buildStart } = cssPlugin(config) + const { transform, buildStart } = cssPlugin(config) as Plugin // @ts-expect-error buildStart is function await buildStart.call({}) @@ -233,8 +234,8 @@ async function createCssPluginTransform( return { async transform(code: string, id: string) { - // @ts-expect-error transform is function - return await transform.call( + // @ts-expect-error transform.handler is function + return await transform.handler.call( { addWatchFile() { return diff --git a/packages/vite/src/node/plugins/assetImportMetaUrl.ts b/packages/vite/src/node/plugins/assetImportMetaUrl.ts index 385b8184a78c11..c71361184d4f37 100644 --- a/packages/vite/src/node/plugins/assetImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/assetImportMetaUrl.ts @@ -2,7 +2,7 @@ import path from 'node:path' import MagicString from 'magic-string' import { stripLiteral } from 'strip-literal' import { parseAst } from 'rollup/parseAst' -import type { Plugin } from '../plugin' +import type { RolldownPlugin } from 'rolldown' import type { ResolvedConfig } from '../config' import { injectQuery, isParentDirectory, transformStableResult } from '../utils' import { CLIENT_ENTRY } from '../constants' @@ -25,7 +25,9 @@ import { hasViteIgnoreRE } from './importAnalysis' * import.meta.glob('./dir/**.png', { eager: true, import: 'default' })[`./dir/${name}.png`] * ``` */ -export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { +export function assetImportMetaUrlPlugin( + config: ResolvedConfig, +): RolldownPlugin { const { publicDir } = config let assetResolver: ResolveIdFn @@ -40,121 +42,128 @@ export function assetImportMetaUrlPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:asset-import-meta-url', - async transform(code, id) { - const { environment } = this - if ( - environment.config.consumer === 'client' && - 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: { + filter: { + code: { + include: ['new URL', 'import.meta.url'], + }, + }, + async handler(code, id) { + const { environment } = this + if ( + environment.config.consumer === 'client' && + 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 = parseAst(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 - } + // 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 = parseAst(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'), + 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 } - s.update( - startIndex, - endIndex, - `new URL((import.meta.glob(${JSON.stringify( - pattern, - )}, ${JSON.stringify( - globOptions, - )}))[${pureUrl}], import.meta.url)`, - ) - continue } - } - const url = rawUrl.slice(1, -1) - 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(environment, url, id) - file ??= - url[0] === '/' - ? slash(path.join(publicDir, url)) - : slash(path.resolve(path.dirname(id), url)) - } + const url = rawUrl.slice(1, -1) + 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(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 9354f05b1d7b76..98445624a74142 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -11,6 +11,7 @@ import type { OutputAsset, OutputChunk, RenderedChunk, + RolldownPlugin, RollupError, SourceMapInput, } from 'rolldown' @@ -42,7 +43,6 @@ import { SPECIAL_QUERY_RE, } from '../constants' import type { ResolvedConfig } from '../config' -import type { Plugin } from '../plugin' import { checkPublicFile } from '../publicDir' import { arraify, @@ -259,7 +259,7 @@ const cssUrlAssetRE = /__VITE_CSS_URL__([\da-f]+)__/g /** * Plugin applied before user plugins */ -export function cssPlugin(config: ResolvedConfig): Plugin { +export function cssPlugin(config: ResolvedConfig): RolldownPlugin { const isBuild = config.command === 'build' let moduleCache: Map> @@ -299,105 +299,120 @@ export function cssPlugin(config: ResolvedConfig): Plugin { preprocessorWorkerController?.close() }, - async load(id) { - if (!isCSSRequest(id)) return + load: { + filter: { + id: { + include: [CSS_LANGS_RE, urlRE], + }, + }, + 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 { environment } = this - if ( - !isCSSRequest(id) || - commonjsProxyRE.test(id) || - SPECIAL_QUERY_RE.test(id) - ) { - return - } - const resolveUrl = (url: string, importer?: string) => - idResolver(environment, url, importer) - - const urlReplacer: CssUrlReplacer = async (url, importer) => { - const decodedUrl = decodeURI(url) - if (checkPublicFile(decodedUrl, config)) { - if (encodePublicUrlsInCSS(config)) { - return publicFileToBuiltUrl(decodedUrl, config) - } else { - return joinUrlSegments(config.base, decodedUrl) - } - } - const [id, fragment] = decodedUrl.split('#') - let resolved = await resolveUrl(id, importer) - if (resolved) { - if (fragment) resolved += '#' + fragment - return fileToUrl(this, resolved) + transform: { + filter: { + id: { + include: [CSS_LANGS_RE], + exclude: [commonjsProxyRE, SPECIAL_QUERY_RE], + }, + }, + async handler(raw, id) { + const { environment } = this + if ( + !isCSSRequest(id) || + commonjsProxyRE.test(id) || + SPECIAL_QUERY_RE.test(id) + ) { + return } - if (config.command === 'build') { - const isExternal = config.build.rollupOptions.external - ? resolveUserExternal( - config.build.rollupOptions.external, - decodedUrl, // use URL as id since id could not be resolved - id, - false, - ) - : false + const resolveUrl = (url: string, importer?: string) => + idResolver(environment, url, importer) + + const urlReplacer: CssUrlReplacer = async (url, importer) => { + const decodedUrl = decodeURI(url) + if (checkPublicFile(decodedUrl, config)) { + if (encodePublicUrlsInCSS(config)) { + return publicFileToBuiltUrl(decodedUrl, config) + } else { + return joinUrlSegments(config.base, decodedUrl) + } + } + const [id, fragment] = decodedUrl.split('#') + let resolved = await resolveUrl(id, importer) + if (resolved) { + if (fragment) resolved += '#' + fragment + return fileToUrl(this, resolved) + } + if (config.command === 'build') { + const isExternal = config.build.rollupOptions.external + ? resolveUserExternal( + config.build.rollupOptions.external, + decodedUrl, // use URL as id since id could not be resolved + id, + false, + ) + : false - if (!isExternal) { - // #9800 If we cannot resolve the css url, leave a warning. - config.logger.warnOnce( - `\n${decodedUrl} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`, - ) + if (!isExternal) { + // #9800 If we cannot resolve the css url, leave a warning. + config.logger.warnOnce( + `\n${decodedUrl} referenced in ${id} didn't resolve at build time, it will remain unchanged to be resolved at runtime`, + ) + } } + return url } - return url - } - const { - code: css, - modules, - deps, - map, - } = await compileCSS( - environment, - id, - raw, - preprocessorWorkerController!, - urlReplacer, - ) - if (modules) { - moduleCache.set(id, modules) - } + const { + code: css, + modules, + deps, + map, + } = await compileCSS( + environment, + id, + raw, + preprocessorWorkerController!, + urlReplacer, + ) + if (modules) { + moduleCache.set(id, modules) + } - if (deps) { - for (const file of deps) { - this.addWatchFile(file) + if (deps) { + for (const file of deps) { + this.addWatchFile(file) + } } - } - return { - code: css, - map, - } + return { + code: css, + map, + } + }, }, } } @@ -405,7 +420,7 @@ export function cssPlugin(config: ResolvedConfig): Plugin { /** * Plugin applied after user plugins */ -export function cssPostPlugin(config: ResolvedConfig): Plugin { +export function cssPostPlugin(config: ResolvedConfig): RolldownPlugin { // styles initialization in buildStart causes a styling loss in watch const styles: Map = new Map() // queue to emit css serially to guarantee the files are emitted in a deterministic order @@ -453,114 +468,126 @@ 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: { + filter: { + id: { + include: [CSS_LANGS_RE], + exclude: [commonjsProxyRE, SPECIAL_QUERY_RE], + }, + }, + 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 - } - // server only - if (this.environment.config.consumer !== 'client') { - return modulesCode || `export default ${JSON.stringify(css)}` - } - if (inlined) { - return `export default ${JSON.stringify(css)}` - } + if (isDirectCSSRequest(id)) { + return null + } + // server only + if (this.environment.config.consumer !== 'client') { + return modulesCode || `export default ${JSON.stringify(css)}` + } + if (inlined) { + return `export default ${JSON.stringify(css)}` + } - 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 ---------------------------------------------------- + // build CSS handling ---------------------------------------------------- - // record css - if (!inlined) { - styles.set(id, css) - } + // record css + if (!inlined) { + styles.set(id, css) + } - 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 ? 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 ? false : 'no-treeshake', + } + }, }, async renderChunk(code, chunk, opts) { @@ -964,60 +991,68 @@ export function cssPostPlugin(config: ResolvedConfig): Plugin { } } -export function cssAnalysisPlugin(config: ResolvedConfig): Plugin { +export function cssAnalysisPlugin(config: ResolvedConfig): RolldownPlugin { return { name: 'vite:css-analysis', - async transform(_, id) { - if ( - !isCSSRequest(id) || - commonjsProxyRE.test(id) || - SPECIAL_QUERY_RE.test(id) - ) { - return - } + transform: { + filter: { + id: { + include: [CSS_LANGS_RE], + exclude: [commonjsProxyRE, SPECIAL_QUERY_RE], + }, + }, + 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) { - depModules.add( - isCSSRequest(file) - ? moduleGraph.createFileOnlyEntry(file) - : await moduleGraph.ensureEntryFromUrl( - fileToDevUrl(file, config, /* skipBase */ true), - ), + 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) { + depModules.add( + isCSSRequest(file) + ? moduleGraph.createFileOnlyEntry(file) + : await moduleGraph.ensureEntryFromUrl( + fileToDevUrl(file, config, /* skipBase */ true), + ), + ) + } + 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/html.ts b/packages/vite/src/node/plugins/html.ts index 82075b27314b55..e7c6c5f2e0d44c 100644 --- a/packages/vite/src/node/plugins/html.ts +++ b/packages/vite/src/node/plugins/html.ts @@ -3,6 +3,7 @@ import type { OutputAsset, OutputBundle, OutputChunk, + RolldownPlugin, RollupError, SourceMapInput, } from 'rolldown' @@ -86,7 +87,7 @@ export const htmlProxyMap = new WeakMap< // PS: key like `hash(/vite/playground/assets/index.html)_1`) export const htmlProxyResult = new Map() -export function htmlInlineProxyPlugin(config: ResolvedConfig): Plugin { +export function htmlInlineProxyPlugin(config: ResolvedConfig): RolldownPlugin { // Should do this when `constructor` rather than when `buildStart`, // `buildStart` will be triggered multiple times then the cached result will be emptied. // https://github.com/vitejs/vite/issues/6372 @@ -94,25 +95,38 @@ export function htmlInlineProxyPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:html-inline-proxy', - resolveId(id) { - if (isHTMLProxy(id)) { - return id - } + resolveId: { + filter: { + id: { + include: [isHtmlProxyRE], + }, + }, + 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) { - return result - } else { - throw new Error(`No matching HTML proxy module found from ${id}`) + load: { + filter: { + id: { + include: [htmlProxyRE], + }, + }, + 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) { + return result + } else { + throw new Error(`No matching HTML proxy module found from ${id}`) + } } - } + }, }, } } @@ -311,7 +325,7 @@ function handleParseError( /** * Compiles index.html into an entry js module */ -export function buildHtmlPlugin(config: ResolvedConfig): Plugin { +export function buildHtmlPlugin(config: ResolvedConfig): RolldownPlugin { const [preHooks, normalHooks, postHooks] = resolveHtmlTransforms( config.plugins, config.logger, @@ -332,365 +346,375 @@ 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 - } + transform: { + filter: { + id: { + include: [/\.html$/], + }, + }, + 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 { + 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 isLineEmpty ? lineStartOffset : startOffset + } - // pre-transform - html = await applyHtmlTransforms(html, preHooks, { - path: publicPath, - filename: id, - }) + // 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 js = '' + const s = new MagicString(html) + const scriptUrls: ScriptAssetsUrl[] = [] + const styleUrls: ScriptAssetsUrl[] = [] + let inlineModuleIndex = -1 - let everyScriptIsAsync = true - let someScriptsAreAsync = false - let someScriptsAreDefer = false + let everyScriptIsAsync = true + let someScriptsAreAsync = false + let someScriptsAreDefer = false - const assetUrlsPromises: Promise[] = [] + 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 + // 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 - } - - await traverseHtml(html, id, (node) => { - if (!nodeIsElement(node)) { - return + return url } - let shouldRemove = false + await traverseHtml(html, id, (node) => { + if (!nodeIsElement(node)) { + return + } - // script tags - if (node.nodeName === 'script') { - const { src, sourceCodeLocation, isModule, isAsync } = - getScriptInfo(node) + let shouldRemove = false + + // script tags + if (node.nodeName === 'script') { + const { src, sourceCodeLocation, isModule, isAsync } = + getScriptInfo(node) + + const url = src && src.value + const isPublicFile = !!(url && checkPublicFile(url, config)) + if (isPublicFile) { + // referencing public dir url, prefix with base + overwriteAttrValue( + s, + sourceCodeLocation!, + partialEncodeURIPath(toOutputPublicFilePath(url)), + ) + } - const url = src && src.value - const isPublicFile = !!(url && checkPublicFile(url, config)) - if (isPublicFile) { - // referencing public dir url, prefix with base - overwriteAttrValue( - s, - sourceCodeLocation!, - partialEncodeURIPath(toOutputPublicFilePath(url)), - ) - } + if (isModule) { + inlineModuleIndex++ + if (url && !isExcludedUrl(url) && !isPublicFile) { + // + 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) { - // - 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.set(id, s.toString()) + processedHtml.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}` + } - // 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/worker.ts b/packages/vite/src/node/plugins/worker.ts index d1b28ffe5e2cbf..1c41934d674920 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -1,6 +1,6 @@ import path from 'node:path' import MagicString from 'magic-string' -import type { OutputChunk } from 'rolldown' +import type { OutputChunk, RolldownPlugin } from 'rolldown' import type { ResolvedConfig } from '../config' import type { Plugin } from '../plugin' import { ENV_ENTRY, ENV_PUBLIC_PATH } from '../constants' @@ -221,7 +221,7 @@ export function webWorkerPostPlugin(): Plugin { } } -export function webWorkerPlugin(config: ResolvedConfig): Plugin { +export function webWorkerPlugin(config: ResolvedConfig): RolldownPlugin { const isBuild = config.command === 'build' const isWorker = config.isWorker @@ -239,10 +239,17 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { }) }, - load(id) { - if (isBuild && workerOrSharedWorkerRE.test(id)) { - return '' - } + load: { + filter: { + id: { + include: [workerOrSharedWorkerRE], + }, + }, + handler(id) { + if (isBuild && workerOrSharedWorkerRE.test(id)) { + return '' + } + }, }, // shouldTransformCachedModule({ id }) { @@ -251,147 +258,154 @@ 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: { + filter: { + id: { + include: [workerOrSharedWorkerRE, workerFileRE], + }, + }, + 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 encodedJs = `const encodedJs = "${Buffer.from( - chunk.code, - ).toString('base64')}";` - - const code = - // Using blob URL for SharedWorker results in multiple instances of a same worker - workerConstructor === 'Worker' - ? `${encodedJs} - const decodeBase64 = (base64) => Uint8Array.from(atob(base64), c => c.charCodeAt(0)); - const blob = typeof self !== "undefined" && self.Blob && new Blob([${ - workerType === 'classic' - ? '' - : // `URL` is always available, in `Worker[type="module"]` - `'URL.revokeObjectURL(import.meta.url);',` - }decodeBase64(encodedJs)], { 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 encodedJs = `const encodedJs = "${Buffer.from( + chunk.code, + ).toString('base64')}";` + + const code = + // Using blob URL for SharedWorker results in multiple instances of a same worker + workerConstructor === 'Worker' + ? `${encodedJs} + const decodeBase64 = (base64) => Uint8Array.from(atob(base64), c => c.charCodeAt(0)); + const blob = typeof self !== "undefined" && self.Blob && new Blob([${ + workerType === 'classic' + ? '' + : // `URL` is always available, in `Worker[type="module"]` + `'URL.revokeObjectURL(import.meta.url);',` + }decodeBase64(encodedJs)], { 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;base64," + encodedJs, + ${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); + }` + : '' + } + }` + : `${encodedJs} + export default function WorkerWrapper(options) { return new ${workerConstructor}( "data:text/javascript;base64," + encodedJs, ${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); - }` - : '' } - }` - : `${encodedJs} - export default function WorkerWrapper(options) { - return new ${workerConstructor}( - "data:text/javascript;base64," + encodedJs, - ${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 54f83324303399..d1e484acd54891 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -1,9 +1,8 @@ import path from 'node:path' import MagicString from 'magic-string' -import type { RollupError } from 'rolldown' +import type { RolldownPlugin, RollupError } from 'rolldown' import { stripLiteral } from 'strip-literal' import type { ResolvedConfig } from '../config' -import type { Plugin } from '../plugin' import { evalValue, injectQuery, transformStableResult } from '../utils' import { createBackCompatIdResolver } from '../idResolver' import type { ResolveIdFn } from '../idResolver' @@ -104,7 +103,9 @@ function isIncludeWorkerImportMetaUrl(code: string): boolean { return false } -export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { +export function workerImportMetaUrlPlugin( + config: ResolvedConfig, +): RolldownPlugin { const isBuild = config.command === 'build' let workerResolver: ResolveIdFn @@ -126,82 +127,89 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { // } // }, - async transform(code, id) { - if ( - this.environment.config.consumer === 'client' && - 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: { + filter: { + code: { + include: [/(?:new Worker|new SharedWorker)/], + }, + }, + async handler(code, id) { + if ( + this.environment.config.consumer === 'client' && + 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 = 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 = 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 = 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 = 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 + } + }, }, } }