From a3076027b3cb99b385d4fb2a7b9d456fafc231ec Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Thu, 6 Mar 2025 15:54:25 +0900 Subject: [PATCH 01/25] refactor: `[hookName].handler` in plugins --- .../plugins/assetImportMetaUrl.spec.ts | 4 +- .../src/node/__tests__/plugins/define.spec.ts | 4 +- .../src/node/__tests__/plugins/json.spec.ts | 3 +- .../plugins/workerImportMetaUrl.spec.ts | 4 +- packages/vite/src/node/plugins/asset.ts | 96 +-- .../src/node/plugins/assetImportMetaUrl.ts | 204 ++--- packages/vite/src/node/plugins/css.ts | 378 ++++----- packages/vite/src/node/plugins/define.ts | 107 +-- .../src/node/plugins/dynamicImportVars.ts | 160 ++-- packages/vite/src/node/plugins/html.ts | 723 +++++++++--------- .../src/node/plugins/importAnalysisBuild.ts | 348 +++++---- .../vite/src/node/plugins/importMetaGlob.ts | 74 +- packages/vite/src/node/plugins/json.ts | 128 ++-- .../vite/src/node/plugins/loadFallback.ts | 24 +- .../src/node/plugins/modulePreloadPolyfill.ts | 38 +- packages/vite/src/node/plugins/resolve.ts | 42 +- packages/vite/src/node/plugins/wasm.ts | 36 +- packages/vite/src/node/plugins/worker.ts | 250 +++--- .../src/node/plugins/workerImportMetaUrl.ts | 132 ++-- 19 files changed, 1420 insertions(+), 1335 deletions(-) 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 45a2709758cd45..0fed6bca0ad827 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) { @@ -1120,63 +1138,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..73af00cb5e7acc 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)}) + ` + }, }, } } diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 5cebce2343811f..e581aeda9613d4 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -261,10 +261,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 }) { @@ -273,144 +275,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 + } + }, }, } } From 0bcacd2d25d2c773463b6a2fbca5a97d83e6f872 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 14:39:17 +0900 Subject: [PATCH 02/25] feat: implement hook filters --- .../src/node/__tests__/plugins/index.spec.ts | 162 +++++++++++ .../__tests__/plugins/pluginFilter.spec.ts | 268 ++++++++++++++++++ packages/vite/src/node/build.ts | 40 ++- packages/vite/src/node/plugin.ts | 10 +- packages/vite/src/node/plugins/index.ts | 60 ++++ .../vite/src/node/plugins/pluginFilter.ts | 135 +++++++++ packages/vite/src/node/plugins/worker.ts | 4 +- .../vite/src/node/server/pluginContainer.ts | 16 +- 8 files changed, 681 insertions(+), 14 deletions(-) create mode 100644 packages/vite/src/node/__tests__/plugins/index.spec.ts create mode 100644 packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts create mode 100644 packages/vite/src/node/plugins/pluginFilter.ts diff --git a/packages/vite/src/node/__tests__/plugins/index.spec.ts b/packages/vite/src/node/__tests__/plugins/index.spec.ts new file mode 100644 index 00000000000000..4e1c1cc4353c6d --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/index.spec.ts @@ -0,0 +1,162 @@ +import { afterAll, describe, expect, test, vi } from 'vitest' +import { type InlineConfig, type Plugin, build, createServer } from '../..' + +const getConfigWithPlugin = ( + plugins: Plugin[], + input?: string[], +): InlineConfig => { + return { + configFile: false, + server: { middlewareMode: true, ws: false }, + optimizeDeps: { noDiscovery: true, include: [] }, + build: { rollupOptions: { input } }, + plugins, + logLevel: 'silent', + } +} + +describe('hook filter with plugin container', async () => { + const resolveId = vi.fn() + const load = vi.fn() + const transformWithId = vi.fn() + const transformWithCode = vi.fn() + const any = expect.toSatisfy(() => true) // anything including undefined and null + const config = getConfigWithPlugin([ + { + name: 'test', + resolveId: { + filter: { id: '*.js' }, + handler: resolveId, + }, + load: { + filter: { id: '*.js' }, + handler: load, + }, + transform: { + filter: { id: '*.js' }, + handler: transformWithId, + }, + }, + { + name: 'test2', + transform: { + filter: { code: 'import.meta' }, + handler: transformWithCode, + }, + }, + ]) + const server = await createServer(config) + afterAll(async () => { + await server.close() + }) + const pluginContainer = server.environments.ssr.pluginContainer + + test('resolveId', async () => { + await pluginContainer.resolveId('foo.js') + await pluginContainer.resolveId('foo.ts') + expect(resolveId).toHaveBeenCalledTimes(1) + expect(resolveId).toHaveBeenCalledWith('foo.js', any, any) + }) + + test('load', async () => { + await pluginContainer.load('foo.js') + await pluginContainer.load('foo.ts') + expect(load).toHaveBeenCalledTimes(1) + expect(load).toHaveBeenCalledWith('foo.js', any) + }) + + test('transform', async () => { + await server.environments.ssr.moduleGraph.ensureEntryFromUrl('foo.js') + await server.environments.ssr.moduleGraph.ensureEntryFromUrl('foo.ts') + + await pluginContainer.transform('import_meta', 'foo.js') + await pluginContainer.transform('import.meta', 'foo.ts') + expect(transformWithId).toHaveBeenCalledTimes(1) + expect(transformWithId).toHaveBeenCalledWith( + expect.stringContaining('import_meta'), + 'foo.js', + any, + ) + expect(transformWithCode).toHaveBeenCalledTimes(1) + expect(transformWithCode).toHaveBeenCalledWith( + expect.stringContaining('import.meta'), + 'foo.ts', + any, + ) + }) +}) + +describe('hook filter with build', async () => { + const resolveId = vi.fn() + const load = vi.fn() + const transformWithId = vi.fn() + const transformWithCode = vi.fn() + const any = expect.anything() + const config = getConfigWithPlugin( + [ + { + name: 'test', + resolveId: { + filter: { id: '*.js' }, + handler: resolveId, + }, + load: { + filter: { id: '*.js' }, + handler: load, + }, + transform: { + filter: { id: '*.js' }, + handler: transformWithId, + }, + }, + { + name: 'test2', + transform: { + filter: { code: 'import.meta' }, + handler: transformWithCode, + }, + }, + { + name: 'resolver', + resolveId(id) { + return id + }, + load(id) { + if (id === 'foo.js') { + return 'import "foo.ts"\n' + 'import_meta' + } + if (id === 'foo.ts') { + return 'import.meta' + } + }, + }, + ], + ['foo.js', 'foo.ts'], + ) + await build(config) + + test('resolveId', async () => { + expect(resolveId).toHaveBeenCalledTimes(1) + expect(resolveId).toHaveBeenCalledWith('foo.js', undefined, any) + }) + + test('load', async () => { + expect(load).toHaveBeenCalledTimes(1) + expect(load).toHaveBeenCalledWith('foo.js', any) + }) + + test('transform', async () => { + expect(transformWithId).toHaveBeenCalledTimes(1) + expect(transformWithId).toHaveBeenCalledWith( + expect.stringContaining('import_meta'), + 'foo.js', + any, + ) + expect(transformWithCode).toHaveBeenCalledTimes(1) + expect(transformWithCode).toHaveBeenCalledWith( + expect.stringContaining('import.meta'), + 'foo.ts', + any, + ) + }) +}) diff --git a/packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts b/packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts new file mode 100644 index 00000000000000..3368f69a62a1f3 --- /dev/null +++ b/packages/vite/src/node/__tests__/plugins/pluginFilter.spec.ts @@ -0,0 +1,268 @@ +import util from 'node:util' +import { describe, expect, test } from 'vitest' +import { + FALLBACK_FALSE, + FALLBACK_TRUE, + createCodeFilter, + createFilterForTransform, + createIdFilter, +} from '../../plugins/pluginFilter' + +describe('createIdFilter', () => { + const filters = [ + { inputFilter: undefined, cases: undefined }, + { + inputFilter: 'foo.js', + cases: [ + { id: 'foo.js', expected: true }, + { id: 'foo.ts', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: ['foo.js'], + cases: [ + { id: 'foo.js', expected: true }, + { id: 'foo.ts', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: 'foo.js' }, + cases: [ + { id: 'foo.js', expected: true }, + { id: 'foo.ts', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: '*.js' }, + cases: [ + { id: 'foo.js', expected: true }, + { id: 'foo.ts', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: /\.js$/ }, + cases: [ + { id: 'foo.js', expected: true }, + { id: 'foo.ts', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: [/\.js$/] }, + cases: [ + { id: 'foo.js', expected: true }, + { id: 'foo.ts', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { exclude: 'foo.js' }, + cases: [ + { id: 'foo.js', expected: false }, + { id: 'foo.ts', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { exclude: '*.js' }, + cases: [ + { id: 'foo.js', expected: false }, + { id: 'foo.ts', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { exclude: /\.js$/ }, + cases: [ + { id: 'foo.js', expected: false }, + { id: 'foo.ts', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { exclude: [/\.js$/] }, + cases: [ + { id: 'foo.js', expected: false }, + { id: 'foo.ts', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { include: 'foo.js', exclude: 'bar.js' }, + cases: [ + { id: 'foo.js', expected: true }, + { id: 'bar.js', expected: false }, + { id: 'baz.js', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: '*.js', exclude: 'foo.*' }, + cases: [ + { id: 'foo.js', expected: false }, // exclude has higher priority + { id: 'bar.js', expected: true }, + { id: 'foo.ts', expected: false }, + ], + }, + ] + + for (const filter of filters) { + test(`${util.inspect(filter.inputFilter)}`, () => { + const idFilter = createIdFilter(filter.inputFilter) + if (!filter.cases) { + expect(idFilter).toBeUndefined() + return + } + expect(idFilter).not.toBeUndefined() + + for (const testCase of filter.cases) { + const { id, expected } = testCase + expect(idFilter!(id), id).toBe(expected) + } + }) + } +}) + +describe('createCodeFilter', () => { + const filters = [ + { inputFilter: undefined, cases: undefined }, + { + inputFilter: 'import.meta', + cases: [ + { code: 'import.meta', expected: true }, + { code: 'import_meta', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: ['import.meta'], + cases: [ + { code: 'import.meta', expected: true }, + { code: 'import_meta', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: 'import.meta' }, + cases: [ + { code: 'import.meta', expected: true }, + { code: 'import_meta', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: /import\.\w+/ }, + cases: [ + { code: 'import.meta', expected: true }, + { code: 'import_meta', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: [/import\.\w+/] }, + cases: [ + { code: 'import.meta', expected: true }, + { code: 'import_meta', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { exclude: 'import.meta' }, + cases: [ + { code: 'import.meta', expected: false }, + { code: 'import_meta', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { exclude: /import\.\w+/ }, + cases: [ + { code: 'import.meta', expected: false }, + { code: 'import_meta', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { exclude: [/import\.\w+/] }, + cases: [ + { code: 'import.meta', expected: false }, + { code: 'import_meta', expected: FALLBACK_TRUE }, + ], + }, + { + inputFilter: { include: 'import.meta', exclude: 'import_meta' }, + cases: [ + { code: 'import.meta', expected: true }, + { code: 'import_meta', expected: false }, + { code: 'importmeta', expected: FALLBACK_FALSE }, + ], + }, + { + inputFilter: { include: /import\.\w+/, exclude: /\w+\.meta/ }, + cases: [ + { code: 'import.meta', expected: false }, // exclude has higher priority + { code: 'import.foo', expected: true }, + { code: 'foo.meta', expected: false }, + ], + }, + ] + + for (const filter of filters) { + test(`${util.inspect(filter.inputFilter)}`, () => { + const codeFilter = createCodeFilter(filter.inputFilter) + if (!filter.cases) { + expect(codeFilter).toBeUndefined() + return + } + expect(codeFilter).not.toBeUndefined() + + for (const testCase of filter.cases) { + const { code, expected } = testCase + expect(codeFilter!(code), code).toBe(expected) + } + }) + } +}) + +describe('createFilterForTransform', () => { + const filters = [ + { inputFilter: [undefined, undefined], cases: undefined }, + { + inputFilter: ['*.js', undefined], + cases: [ + { id: 'foo.js', code: 'foo', expected: true }, + { id: 'foo.ts', code: 'foo', expected: false }, + ], + }, + { + inputFilter: [undefined, 'import.meta'], + cases: [ + { id: 'foo.js', code: 'import.meta', expected: true }, + { id: 'foo.js', code: 'import_meta', expected: false }, + ], + }, + { + inputFilter: [{ exclude: '*.js' }, 'import.meta'], + cases: [ + { id: 'foo.js', code: 'import.meta', expected: false }, + { id: 'foo.js', code: 'import_meta', expected: false }, + { id: 'foo.ts', code: 'import.meta', expected: true }, + { id: 'foo.ts', code: 'import_meta', expected: false }, + ], + }, + { + inputFilter: [{ include: 'foo.ts', exclude: '*.js' }, 'import.meta'], + cases: [ + { id: 'foo.js', code: 'import.meta', expected: false }, + { id: 'foo.js', code: 'import_meta', expected: false }, + { id: 'foo.ts', code: 'import.meta', expected: true }, + { id: 'foo.ts', code: 'import_meta', expected: true }, + ], + }, + ] + + for (const filter of filters) { + test(`${util.inspect(filter.inputFilter)}`, () => { + const [idFilter, codeFilter] = filter.inputFilter + const filterForTransform = createFilterForTransform(idFilter, codeFilter) + if (!filter.cases) { + expect(filterForTransform).toBeUndefined() + return + } + expect(filterForTransform).not.toBeUndefined() + + for (const testCase of filter.cases) { + const { id, code, expected } = testCase + expect(filterForTransform!(id, code), util.inspect({ id, code })).toBe( + expected, + ) + } + }) + } +}) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 7f1b7418ed18c2..1728b646e117f0 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -77,6 +77,10 @@ import { } from './baseEnvironment' import type { MinimalPluginContext, Plugin, PluginContext } from './plugin' import type { RollupPluginHooks } from './typeUtils' +import { + createFilterForTransform, + createIdFilter, +} from './plugins/pluginFilter' export interface BuildEnvironmentOptions { /** @@ -584,7 +588,7 @@ async function buildEnvironment( // inject environment and ssr arg to plugin load/transform hooks const plugins = environment.plugins.map((p) => - injectEnvironmentToHooks(environment, p), + injectEnvironmentAndFilterToHooks(environment, p), ) const rollupOptions: RollupOptions = { @@ -1128,7 +1132,7 @@ function isExternal(id: string, test: string | RegExp) { } } -export function injectEnvironmentToHooks( +export function injectEnvironmentAndFilterToHooks( environment: BuildEnvironment, plugin: Plugin, ): Plugin { @@ -1139,13 +1143,13 @@ export function injectEnvironmentToHooks( for (const hook of Object.keys(clone) as RollupPluginHooks[]) { switch (hook) { case 'resolveId': - clone[hook] = wrapEnvironmentResolveId(environment, resolveId) + clone[hook] = wrapEnvironmentAndFilterResolveId(environment, resolveId) break case 'load': - clone[hook] = wrapEnvironmentLoad(environment, load) + clone[hook] = wrapEnvironmentAndFilterLoad(environment, load) break case 'transform': - clone[hook] = wrapEnvironmentTransform(environment, transform) + clone[hook] = wrapEnvironmentAndFilterTransform(environment, transform) break default: if (ROLLUP_HOOKS.includes(hook)) { @@ -1158,14 +1162,20 @@ export function injectEnvironmentToHooks( return clone } -function wrapEnvironmentResolveId( +function wrapEnvironmentAndFilterResolveId( environment: BuildEnvironment, hook?: Plugin['resolveId'], ): Plugin['resolveId'] { if (!hook) return + const rawIdFilter = typeof hook === 'object' ? hook.filter?.id : undefined + const idFilter = rawIdFilter ? createIdFilter(rawIdFilter) : undefined + const fn = getHookHandler(hook) const handler: Plugin['resolveId'] = function (id, importer, options) { + if (idFilter && !idFilter(id)) { + return + } return fn.call( injectEnvironmentInContext(this, environment), id, @@ -1184,14 +1194,20 @@ function wrapEnvironmentResolveId( } } -function wrapEnvironmentLoad( +function wrapEnvironmentAndFilterLoad( environment: BuildEnvironment, hook?: Plugin['load'], ): Plugin['load'] { if (!hook) return + const rawIdFilter = typeof hook === 'object' ? hook.filter?.id : undefined + const idFilter = rawIdFilter ? createIdFilter(rawIdFilter) : undefined + const fn = getHookHandler(hook) const handler: Plugin['load'] = function (id, ...args) { + if (idFilter && !idFilter(id)) { + return + } return fn.call( injectEnvironmentInContext(this, environment), id, @@ -1209,14 +1225,22 @@ function wrapEnvironmentLoad( } } -function wrapEnvironmentTransform( +function wrapEnvironmentAndFilterTransform( environment: BuildEnvironment, hook?: Plugin['transform'], ): Plugin['transform'] { if (!hook) return + const filters = typeof hook === 'object' ? hook.filter : undefined + const filter = filters + ? createFilterForTransform(filters.id, filters.code) + : undefined + const fn = getHookHandler(hook) const handler: Plugin['transform'] = function (code, importer, ...args) { + if (filter && !filter(importer, code)) { + return + } return fn.call( injectEnvironmentInContext(this, environment), code, diff --git a/packages/vite/src/node/plugin.ts b/packages/vite/src/node/plugin.ts index aeb83d37094327..481bd1c362715b 100644 --- a/packages/vite/src/node/plugin.ts +++ b/packages/vite/src/node/plugin.ts @@ -25,6 +25,7 @@ import type { Environment } from './environment' import type { PartialEnvironment } from './baseEnvironment' import type { PreviewServerHook } from './preview' import { arraify, asyncFlatten } from './utils' +import type { StringFilter } from './plugins/pluginFilter' /** * Vite plugins extends the Rollup plugin interface with a few extra @@ -139,7 +140,8 @@ export interface Plugin extends RollupPlugin { scan?: boolean isEntry: boolean }, - ) => Promise | ResolveIdResult + ) => Promise | ResolveIdResult, + { filter?: { id?: StringFilter } } > load?: ObjectHook< ( @@ -152,7 +154,8 @@ export interface Plugin extends RollupPlugin { */ html?: boolean }, - ) => Promise | LoadResult + ) => Promise | LoadResult, + { filter?: { id?: StringFilter } } > transform?: ObjectHook< ( @@ -162,7 +165,8 @@ export interface Plugin extends RollupPlugin { options?: { ssr?: boolean }, - ) => Promise | TransformResult + ) => Promise | TransformResult, + { filter?: { id?: StringFilter; code?: StringFilter } } > /** * Opt-in this plugin into the shared plugins pipeline. diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index ac76d610c960a9..9e08fdfc36060a 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -22,6 +22,12 @@ import { assetImportMetaUrlPlugin } from './assetImportMetaUrl' import { metadataPlugin } from './metadata' import { dynamicImportVarsPlugin } from './dynamicImportVars' import { importGlobPlugin } from './importMetaGlob' +import { + type PluginFilterWithFallback, + type TransformHookFilter, + createFilterForTransform, + createIdFilter, +} from './pluginFilter' export async function resolvePlugins( config: ResolvedConfig, @@ -158,6 +164,60 @@ export function getHookHandler>( return (typeof hook === 'object' ? hook.handler : hook) as HookHandler } +type FilterForPluginValue = { + resolveId?: PluginFilterWithFallback | undefined + load?: PluginFilterWithFallback | undefined + transform?: TransformHookFilter | undefined +} +const filterForPlugin = new WeakMap() + +export function getCachedFilterForPlugin< + H extends 'resolveId' | 'load' | 'transform', +>(plugin: Plugin, hookName: H): FilterForPluginValue[H] | undefined { + let filters = filterForPlugin.get(plugin) + if (filters && hookName in filters) { + return filters[hookName] + } + + if (!filters) { + filters = {} + filterForPlugin.set(plugin, filters) + } + + let filter: PluginFilterWithFallback | TransformHookFilter | undefined + switch (hookName) { + case 'resolveId': { + const rawFilter = + typeof plugin.resolveId === 'object' + ? plugin.resolveId.filter?.id + : undefined + filters.resolveId = createIdFilter(rawFilter) + filter = filters.resolveId + break + } + case 'load': { + const rawFilter = + typeof plugin.load === 'object' ? plugin.load.filter?.id : undefined + filters.load = createIdFilter(rawFilter) + filter = filters.load + break + } + case 'transform': { + const rawFilters = + typeof plugin.transform === 'object' + ? plugin.transform.filter + : undefined + filters.transform = createFilterForTransform( + rawFilters?.id, + rawFilters?.code, + ) + filter = filters.transform + break + } + } + return filter as FilterForPluginValue[H] | undefined +} + // Same as `@rollup/plugin-alias` default resolver, but we attach additional meta // if we can't resolve to something, which will error in `importAnalysis` export const viteAliasCustomResolver: ResolverFunction = async function ( diff --git a/packages/vite/src/node/plugins/pluginFilter.ts b/packages/vite/src/node/plugins/pluginFilter.ts new file mode 100644 index 00000000000000..51155c6fcd75bc --- /dev/null +++ b/packages/vite/src/node/plugins/pluginFilter.ts @@ -0,0 +1,135 @@ +import path from 'node:path' +import picomatch from 'picomatch' +import { arraify } from '../utils' + +export const FALLBACK_TRUE = 1 +export const FALLBACK_FALSE = 0 +type FallbackValues = typeof FALLBACK_TRUE | typeof FALLBACK_FALSE + +export type PluginFilter = (input: string) => boolean +export type PluginFilterWithFallback = ( + input: string, +) => boolean | FallbackValues +export type TransformHookFilter = (id: string, code: string) => boolean + +export type StringFilter = + | string + | RegExp + | Array + | { + include?: string | RegExp | Array + exclude?: string | RegExp | Array + } + +type NormalizedStringFilter = { + include?: Array + exclude?: Array +} + +function patternToIdFilter(pattern: string | RegExp): PluginFilter { + if (pattern instanceof RegExp) { + return (id: string) => { + const result = pattern.test(id) + pattern.lastIndex = 0 + return result + } + } + return picomatch(pattern, { dot: true }) +} + +function patternToCodeFilter(pattern: string | RegExp): PluginFilter { + if (pattern instanceof RegExp) { + return (code: string) => pattern.test(code) + } + return (code: string) => code.includes(pattern) +} + +function createFilter( + exclude: Array | undefined, + include: Array | undefined, +): PluginFilterWithFallback | undefined { + if (!exclude && !include) { + return + } + + return (input) => { + if (exclude?.some((filter) => filter(input))) { + return false + } + if (include?.some((filter) => filter(input))) { + return true + } + return !!include && include.length > 0 ? FALLBACK_FALSE : FALLBACK_TRUE + } +} + +function normalizeFilter(filter: StringFilter): NormalizedStringFilter { + if (typeof filter === 'string' || filter instanceof RegExp) { + return { + include: [filter], + } + } + if (Array.isArray(filter)) { + return { + include: arraify(filter), + } + } + return { + include: filter.include ? arraify(filter.include) : undefined, + exclude: filter.exclude ? arraify(filter.exclude) : undefined, + } +} + +export function createIdFilter( + filter: StringFilter | undefined, +): PluginFilterWithFallback | undefined { + if (!filter) return + const { exclude, include } = normalizeFilter(filter) + const excludeFilter = exclude?.map(patternToIdFilter) + const includeFilter = include?.map(patternToIdFilter) + const f = createFilter(excludeFilter, includeFilter) + const cwd = process.cwd() + return f + ? (id) => { + const normalizedId = path.relative(cwd, id) + return f(normalizedId) + } + : undefined +} + +export function createCodeFilter( + filter: StringFilter | undefined, +): PluginFilterWithFallback | undefined { + if (!filter) return + const { exclude, include } = normalizeFilter(filter) + const excludeFilter = exclude?.map(patternToCodeFilter) + const includeFilter = include?.map(patternToCodeFilter) + return createFilter(excludeFilter, includeFilter) +} + +export function createFilterForTransform( + idFilter: StringFilter | undefined, + codeFilter: StringFilter | undefined, +): TransformHookFilter | undefined { + if (!idFilter && !codeFilter) return + const idFilterFn = createIdFilter(idFilter) + const codeFilterFn = createCodeFilter(codeFilter) + return (id, code) => { + let fallback = true + if (idFilterFn) { + const idResult = idFilterFn(id) + if (typeof idResult === 'boolean') { + return idResult + } + fallback &&= !!idResult + } + if (codeFilterFn) { + const codeResult = codeFilterFn(code) + if (typeof codeResult === 'boolean') { + return codeResult + } + fallback &&= !!codeResult + } + return fallback + } +} diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index e581aeda9613d4..26ddd101ecc973 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -15,7 +15,7 @@ import { import { BuildEnvironment, createToImportMetaURLBasedRelativeRuntime, - injectEnvironmentToHooks, + injectEnvironmentAndFilterToHooks, onRollupLog, toOutputFilePathInJS, } from '../build' @@ -95,7 +95,7 @@ async function bundleWorkerEntry( ...rollupOptions, input, plugins: workerEnvironment.plugins.map((p) => - injectEnvironmentToHooks(workerEnvironment, p), + injectEnvironmentAndFilterToHooks(workerEnvironment, p), ), onLog(level, log) { onRollupLog(level, log, workerEnvironment) diff --git a/packages/vite/src/node/server/pluginContainer.ts b/packages/vite/src/node/server/pluginContainer.ts index 7f03ccd7fb3108..b76c7a63482b24 100644 --- a/packages/vite/src/node/server/pluginContainer.ts +++ b/packages/vite/src/node/server/pluginContainer.ts @@ -78,7 +78,11 @@ import { timeFrom, } from '../utils' import { FS_PREFIX } from '../constants' -import { createPluginHookUtils, getHookHandler } from '../plugins' +import { + createPluginHookUtils, + getCachedFilterForPlugin, + getHookHandler, +} from '../plugins' import { cleanUrl, unwrapId } from '../../shared/utils' import type { PluginHookUtils } from '../config' import type { Environment } from '../environment' @@ -376,6 +380,9 @@ class EnvironmentPluginContainer { throwClosedServerError() if (mergedSkip?.has(plugin)) continue + const filter = getCachedFilterForPlugin(plugin, 'resolveId') + if (filter && !filter(rawId)) continue + ctx._plugin = plugin const pluginResolveStart = debugPluginResolve ? performance.now() : 0 @@ -436,6 +443,10 @@ class EnvironmentPluginContainer { for (const plugin of this.getSortedPlugins('load')) { if (this._closed && this.environment.config.dev.recoverable) throwClosedServerError() + + const filter = getCachedFilterForPlugin(plugin, 'load') + if (filter && !filter(id)) continue + ctx._plugin = plugin const handler = getHookHandler(plugin.load) const result = await this.handleHookPromise( @@ -471,6 +482,9 @@ class EnvironmentPluginContainer { if (this._closed && this.environment.config.dev.recoverable) throwClosedServerError() + const filter = getCachedFilterForPlugin(plugin, 'transform') + if (filter && !filter(id, code)) continue + ctx._updateActiveInfo(plugin, id, code) const start = debugPluginTransform ? performance.now() : 0 let result: TransformResult | string | undefined From bc4995f89f73060e2a68b6affcc6f6f53d8e3493 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:33:15 +0900 Subject: [PATCH 03/25] chore: add rolldown as dependency --- packages/vite/package.json | 1 + pnpm-lock.yaml | 200 +++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) diff --git a/packages/vite/package.json b/packages/vite/package.json index e72ef590e54cf3..e3a3872b052b75 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -87,6 +87,7 @@ "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", + "rolldown": "1.0.0-beta.3-commit.62fba31", "rollup": "^4.30.1" }, "optionalDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a1d25b244cd79..b4bbf8949db627 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -229,6 +229,9 @@ importers: postcss: specifier: ^8.5.3 version: 8.5.3 + rolldown: + specifier: 1.0.0-beta.3-commit.62fba31 + version: 1.0.0-beta.3-commit.62fba31(typescript@5.7.3) rollup: specifier: ^4.30.1 version: 4.34.8 @@ -2346,6 +2349,15 @@ packages: search-insights: optional: true + '@emnapi/core@1.3.1': + resolution: {integrity: sha512-pVGjBIt1Y6gg3EJN8jTcfpP/+uuRksIo055oE/OBkDNcjZqVbfkWCksG1Jp4yZnj3iKWyWX8fdG/j6UDYPbFog==} + + '@emnapi/runtime@1.3.1': + resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + + '@emnapi/wasi-threads@1.0.1': + resolution: {integrity: sha512-iIBu7mwkq4UQGeMEM8bLwNK962nXdhodeScX4slfQnRhEMMzvYivHhutCIk8uojvmASXXPC2WNEjwxFWk72Oqw==} + '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} @@ -2764,6 +2776,9 @@ packages: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true + '@napi-rs/wasm-runtime@0.2.7': + resolution: {integrity: sha512-5yximcFK5FNompXfJFoWanu5l8v1hNGqNHh9du1xETp9HWk/B/PzvchX55WYOPaIeNglG8++68AAiauBAtbnzw==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2776,6 +2791,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@oxc-project/types@0.53.0': + resolution: {integrity: sha512-8JXXVoHnRLcl6kDBboSfAmAkKeb6PSvSc5qSJxiOFzFx0ZCLAbUDmuwR2hkBnY7kQS3LmNXaONq1BFAmwTyeZw==} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -2873,6 +2891,66 @@ packages: resolution: {integrity: sha512-TvCl79Y8v18ZhFGd5mjO1kYPovSBq3+4LVCi5Nfl1JI8fS8i8kXbgQFGwBJRXczim8GlW8c2LMBKTtExYXOy/A==} engines: {node: '>=18'} + '@rolldown/binding-darwin-arm64@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-6gQv9wb2sZlYGNdSmGRCfb3MoqdHYCZMhZGydLztGpDxnLd46TJmoX8yM5/PXbQg3PtAq/Mehv0WXhcMrrPe8A==} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-BywVC+5mQKBdoQqWDVUUUj7xQMGBJYfUTJ+utNcrg4CawF6WJuLNO7o5O9RnjwIOXmbOQReD7tX/bSL1bFvZVg==} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-Wm7BoNbuyvBsj/KrSEMGtGPLtAIKt0ArO94Cgi2Dwye/AYF9krBg49MAyy5FUE2FAA3hu1beJQwdKfRPivJJmA==} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-pnUWq/f5Rmr6IsAYjm/JfNu5g3Z7k1HenR4P5ApVnsllrcd0NopfSOsL9NBH2xuTAKQ5xNx0syugywjYvtSXDQ==} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-dJT9EnMiLNKeGALEzvM04yBwRPjFT+UB23qG/XIpm0v2yxbXyttCEnjAY8qGrjh9P6LtlO/9muzOq2nfJF3h2Q==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-KSONRVQO3gEIq9HDH7uDC8eYwjhIn7HOcGeA7bq3memP/xxLDPJBCygvY8L/F4ojP5yrjseq9SQfgsPZfLxLcQ==} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-skvZRdyPdd9IIQf43eMCCxW9av+WKCnRZ+KXP23b4iY5kN0VqsED0SSNprfQJO58SnlB+9F0g58WbAXT4UY8fA==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-pwCtm6BtZ0y507figQ0wSZFgVP8a6wB+USz6Ht5i7k5fnH8zm9x7xLfXhk3eAp3ZpGOzkRk0kiQUicEMitBF9Q==} + cpu: [x64] + os: [linux] + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-idPTXJzhej0E2GQ01chSsISuGSA/fXhF7fI8kxHTq6IcYM+AOR4rjeKfCI0P9Ia56owKbLx2QypT8lIcjevPIw==} + engines: {node: '>=14.21.3'} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-WDFunJCD9RYGRodmqlc+zpqlaDzVoQBWmPdVolTpqHc1KPcQgKCCqbOYq8ISKIw6w8JvwsifkqWB1oZINnM+kQ==} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-nx/u5tu3Py0CBVwMopJKUNDKCnutEzghw1z/BuXufIzHK8Ik97r8NcRGm89HgUInly+vTVClmz/tOMhHeTtwGQ==} + cpu: [ia32] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.3-commit.62fba31': + resolution: {integrity: sha512-G+Seww/f7MKr7A7oNbNjn3q1Lz/OybPchVwx+m9jLoMgQH8UZBEDtvVL2M8ZVE8lccYIGfSW6xqSYo7pDz4omg==} + cpu: [x64] + os: [win32] + '@rollup/plugin-alias@5.1.1': resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} engines: {node: '>=14.0.0'} @@ -3159,6 +3237,9 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@tybys/wasm-util@0.9.0': + resolution: {integrity: sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw==} + '@type-challenges/utils@0.1.1': resolution: {integrity: sha512-A7ljYfBM+FLw+NDyuYvGBJiCEV9c0lPWEAdzfOAkb3JFqfLl0Iv/WhWMMARHiRKlmmiD1g8gz/507yVvHdQUYA==} @@ -3373,6 +3454,11 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@valibot/to-json-schema@1.0.0-rc.0': + resolution: {integrity: sha512-F3WDgnPzcDs9Y8qZwU9qfPnEJBQ6lCMCFjI7VsMjAza6yAixGr4cZ50gOy6zniSCk49GkFvq2a6cBKfZjTpyOw==} + peerDependencies: + valibot: ^1.0.0 || ^1.0.0-beta.5 || ^1.0.0-rc + '@vitejs/longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@file:playground/optimize-deps/longfilename': resolution: {directory: playground/optimize-deps/longfilename, type: directory} @@ -6422,6 +6508,15 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true + rolldown@1.0.0-beta.3-commit.62fba31: + resolution: {integrity: sha512-p7wDEDL4xw72C/bQ/ChB4pPgMAB/Sp+I2H1pHrj0rDBEERukXAcVnPyC2HEXLfiy+sKbJ2dTl2feLkDdHu5Frg==} + hasBin: true + peerDependencies: + '@oxc-project/runtime': 0.53.0 + peerDependenciesMeta: + '@oxc-project/runtime': + optional: true + rollup-plugin-dts@6.1.1: resolution: {integrity: sha512-aSHRcJ6KG2IHIioYlvAOcEq6U99sVtqDDKVhnwt70rW6tsz3tv5OSjEiWcgzfsHdLyGXZ/3b/7b/+Za3Y6r1XA==} engines: {node: '>=16'} @@ -7152,6 +7247,14 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + valibot@1.0.0-rc.3: + resolution: {integrity: sha512-LT0REa7Iqx4QGcaHLiTiTkcmJqJ9QdpOy89HALFFBJgejTS64GQFRIbDF7e4f6pauQbo/myfKGmWXCLhMeM6+g==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + validate-npm-package-license@3.0.4: resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==} @@ -8182,6 +8285,22 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@emnapi/core@1.3.1': + dependencies: + '@emnapi/wasi-threads': 1.0.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.3.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.0.1': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.24.2': optional: true @@ -8479,6 +8598,13 @@ snapshots: - encoding - supports-color + '@napi-rs/wasm-runtime@0.2.7': + dependencies: + '@emnapi/core': 1.3.1 + '@emnapi/runtime': 1.3.1 + '@tybys/wasm-util': 0.9.0 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -8491,6 +8617,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.0 + '@oxc-project/types@0.53.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -8561,6 +8689,44 @@ snapshots: '@publint/pack@0.1.1': {} + '@rolldown/binding-darwin-arm64@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-beta.3-commit.62fba31': + dependencies: + '@napi-rs/wasm-runtime': 0.2.7 + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.3-commit.62fba31': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.3-commit.62fba31': + optional: true + '@rollup/plugin-alias@5.1.1(rollup@4.34.8)': optionalDependencies: rollup: 4.34.8 @@ -8828,6 +8994,11 @@ snapshots: '@trysound/sax@0.2.0': {} + '@tybys/wasm-util@0.9.0': + dependencies: + tslib: 2.8.1 + optional: true + '@type-challenges/utils@0.1.1': {} '@types/babel__core@7.20.5': @@ -9097,6 +9268,10 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@valibot/to-json-schema@1.0.0-rc.0(valibot@1.0.0-rc.3(typescript@5.7.3))': + dependencies: + valibot: 1.0.0-rc.3(typescript@5.7.3) + '@vitejs/longfilename-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa@file:playground/optimize-deps/longfilename': {} '@vitejs/plugin-vue@5.2.1(vite@packages+vite)(vue@3.5.13(typescript@5.7.3))': @@ -12353,6 +12528,27 @@ snapshots: dependencies: glob: 7.2.3 + rolldown@1.0.0-beta.3-commit.62fba31(typescript@5.7.3): + dependencies: + '@oxc-project/types': 0.53.0 + '@valibot/to-json-schema': 1.0.0-rc.0(valibot@1.0.0-rc.3(typescript@5.7.3)) + valibot: 1.0.0-rc.3(typescript@5.7.3) + optionalDependencies: + '@rolldown/binding-darwin-arm64': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-darwin-x64': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-freebsd-x64': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.3-commit.62fba31 + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.3-commit.62fba31 + transitivePeerDependencies: + - typescript + rollup-plugin-dts@6.1.1(rollup@4.34.8)(typescript@5.7.3): dependencies: magic-string: 0.30.17 @@ -13124,6 +13320,10 @@ snapshots: utils-merge@1.0.1: {} + valibot@1.0.0-rc.3(typescript@5.7.3): + optionalDependencies: + typescript: 5.7.3 + validate-npm-package-license@3.0.4: dependencies: spdx-correct: 3.2.0 From 5a160a57ce75e83cb11f672454cf278e3ccacd4b Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 17:33:43 +0900 Subject: [PATCH 04/25] feat: use rolldown in the dep optimizer Co-authored-by: underfin --- packages/vite/rollup.config.ts | 1 + packages/vite/rollup.dts.config.ts | 1 + packages/vite/src/node/config.ts | 1 + packages/vite/src/node/optimizer/index.ts | 318 ++++----- ...buildDepPlugin.ts => rolldownDepPlugin.ts} | 301 ++++---- packages/vite/src/node/optimizer/scan.ts | 666 ++++++++---------- .../vite/src/node/plugins/importAnalysis.ts | 12 +- playground/optimize-deps/vite.config.js | 27 +- 8 files changed, 641 insertions(+), 686 deletions(-) rename packages/vite/src/node/optimizer/{esbuildDepPlugin.ts => rolldownDepPlugin.ts} (50%) diff --git a/packages/vite/rollup.config.ts b/packages/vite/rollup.config.ts index bec0eabdd65d38..5c21548649169f 100644 --- a/packages/vite/rollup.config.ts +++ b/packages/vite/rollup.config.ts @@ -111,6 +111,7 @@ const nodeConfig = defineConfig({ /^vite\//, 'fsevents', 'rollup/parseAst', + 'rolldown/experimental', /^tsx\//, /^#/, ...Object.keys(pkg.dependencies), diff --git a/packages/vite/rollup.dts.config.ts b/packages/vite/rollup.dts.config.ts index d909755844ac1a..2a1727bc1b3fe3 100644 --- a/packages/vite/rollup.dts.config.ts +++ b/packages/vite/rollup.dts.config.ts @@ -17,6 +17,7 @@ const external = [ /^node:*/, /^vite\//, 'rollup/parseAst', + 'rolldown/experimental', ...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies), ...Object.keys(pkg.devDependencies), diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 45c81fc9172aeb..c7953efc0b5b46 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -723,6 +723,7 @@ export const configDefaults = Object.freeze({ exclude: [], needsInterop: [], // esbuildOptions + rollupOptions: {}, /** @experimental */ extensions: [], /** @deprecated @experimental */ diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 1951c4ce92d86b..63c28eae967ac0 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -4,12 +4,19 @@ import path from 'node:path' import { promisify } from 'node:util' import { performance } from 'node:perf_hooks' import colors from 'picocolors' -import type { BuildContext, BuildOptions as EsbuildBuildOptions } from 'esbuild' -import esbuild, { build } from 'esbuild' +import type { BuildOptions as EsbuildBuildOptions } from 'esbuild' import { init, parse } from 'es-module-lexer' import { isDynamicPattern } from 'tinyglobby' +import { + type RolldownOptions, + type RolldownOutput, + type OutputOptions as RolldownOutputOptions, + rolldown, +} from 'rolldown' import type { ResolvedConfig } from '../config' import { + arraify, + asyncFlatten, createDebugger, flattenId, getHash, @@ -21,21 +28,20 @@ import { tryStatSync, unique, } from '../utils' -import { - defaultEsbuildSupported, - transformWithEsbuild, -} from '../plugins/esbuild' -import { ESBUILD_MODULES_TARGET, METADATA_FILENAME } from '../constants' +import { transformWithEsbuild } from '../plugins/esbuild' +import { METADATA_FILENAME } from '../constants' import { isWindows } from '../../shared/utils' import type { Environment } from '../environment' -import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' import { ScanEnvironment, scanImports } from './scan' import { createOptimizeDepsIncludeResolver, expandGlobIds } from './resolve' +import { + rolldownCjsExternalPlugin, + rolldownDepPlugin, +} from './rolldownDepPlugin' const debug = createDebugger('vite:deps') const jsExtensionRE = /\.js$/i -const jsMapExtensionRE = /\.js\.map$/i export type ExportsData = { hasModuleSyntax: boolean @@ -103,6 +109,12 @@ export interface DepOptimizationConfig { | 'outExtension' | 'metafile' > + rollupOptions?: Omit & { + output?: Omit< + RolldownOutputOptions, + 'format' | 'sourcemap' | 'dir' | 'banner' + > + } /** * List of file extensions that can be optimized. A corresponding esbuild * plugin must exist to handle the specific extension. @@ -201,6 +213,7 @@ export interface OptimizedDepInfo { * data used both to define if interop is needed and when pre-bundling */ exportsData?: Promise + isDynamicEntry?: boolean } export interface DepOptimizationMetadata { @@ -614,7 +627,7 @@ export function runOptimizeDeps( const start = performance.now() - const preparedRun = prepareEsbuildOptimizerRun( + const preparedRun = prepareRolldownOptimizerRun( environment, depsInfo, processingCacheDir, @@ -622,64 +635,49 @@ export function runOptimizeDeps( ) const runResult = preparedRun.then(({ context, idToExports }) => { - function disposeContext() { - return context?.dispose().catch((e) => { - environment.logger.error('Failed to dispose esbuild context', { - error: e, - }) - }) - } if (!context || optimizerContext.cancelled) { - disposeContext() return cancelledResult } return context - .rebuild() + .build() .then((result) => { - const meta = result.metafile! + const depForEntryFileName: Record = {} + for (const dep of Object.values(depsInfo)) { + const entryFileName = flattenId(dep.id) + '.js' + depForEntryFileName[entryFileName] = dep + } - // the paths in `meta.outputs` are relative to `process.cwd()` - const processingCacheDirOutputPath = path.relative( - process.cwd(), - processingCacheDir, - ) + for (const chunk of result.output) { + if (chunk.type !== 'chunk') continue - for (const id in depsInfo) { - const output = esbuildOutputFromId( - meta.outputs, - id, - processingCacheDir, - ) - - const { exportsData, ...info } = depsInfo[id] - addOptimizedDepInfo(metadata, 'optimized', { - ...info, - // We only need to hash the output.imports in to check for stability, but adding the hash - // and file path gives us a unique hash that may be useful for other things in the future - fileHash: getHash( - metadata.hash + - depsInfo[id].file + - JSON.stringify(output.imports), - ), - browserHash: metadata.browserHash, - // After bundling we have more information and can warn the user about legacy packages - // that require manual configuration - needsInterop: needsInterop( - environment, + if (chunk.isEntry) { + const { exportsData, file, id, ...info } = + depForEntryFileName[chunk.fileName] + addOptimizedDepInfo(metadata, 'optimized', { id, - idToExports[id], - output, - ), - }) - } - - for (const o of Object.keys(meta.outputs)) { - if (!jsMapExtensionRE.test(o)) { - const id = path - .relative(processingCacheDirOutputPath, o) - .replace(jsExtensionRE, '') - const file = getOptimizedDepPath(environment, id) + file, + ...info, + // We only need to hash the chunk.imports in to check for stability, but adding the hash + // and file path gives us a unique hash that may be useful for other things in the future + fileHash: getHash( + metadata.hash + file + JSON.stringify(chunk.imports), + ), + browserHash: metadata.browserHash, + // After bundling we have more information and can warn the user about legacy packages + // that require manual configuration + needsInterop: needsInterop( + environment, + id, + idToExports[id], + chunk, + ), + }) + } else { + const id = chunk.fileName.replace(jsExtensionRE, '') + const file = normalizePath( + path.resolve(getDepsCacheDir(environment), chunk.fileName), + ) if ( !findOptimizedDepInfoInRecord( metadata.optimized, @@ -691,27 +689,9 @@ export function runOptimizeDeps( file, needsInterop: false, browserHash: metadata.browserHash, + isDynamicEntry: chunk.isDynamicEntry, }) } - } else { - // workaround Firefox warning by removing blank source map reference - // https://github.com/evanw/esbuild/issues/3945 - const output = meta.outputs[o] - // filter by exact bytes of an empty source map - if (output.bytes === 93) { - const jsMapPath = path.resolve(o) - const jsPath = jsMapPath.slice(0, -4) - if (fs.existsSync(jsPath) && fs.existsSync(jsMapPath)) { - const map = JSON.parse(fs.readFileSync(jsMapPath, 'utf-8')) - if (map.sources.length === 0) { - const js = fs.readFileSync(jsPath, 'utf-8') - fs.writeFileSync( - jsPath, - js.slice(0, js.lastIndexOf('//# sourceMappingURL=')), - ) - } - } - } } } @@ -721,18 +701,14 @@ export function runOptimizeDeps( return successfulResult }) - .catch((e) => { if (e.errors && e.message.includes('The build was canceled')) { - // esbuild logs an error when cancelling, but this is expected so + // an error happens when cancelling, but this is expected so // return an empty result instead return cancelledResult } throw e }) - .finally(() => { - return disposeContext() - }) }) runResult.catch(() => { @@ -743,20 +719,20 @@ export function runOptimizeDeps( async cancel() { optimizerContext.cancelled = true const { context } = await preparedRun - await context?.cancel() + context?.cancel() cleanUp() }, result: runResult, } } -async function prepareEsbuildOptimizerRun( +async function prepareRolldownOptimizerRun( environment: Environment, depsInfo: Record, processingCacheDir: string, optimizerContext: { cancelled: boolean }, ): Promise<{ - context?: BuildContext + context?: { build: () => Promise; cancel: () => void } idToExports: Record }> { // esbuild generates nested directory output with lowest common ancestor base @@ -770,21 +746,19 @@ async function prepareEsbuildOptimizerRun( const { optimizeDeps } = environment.config - const { plugins: pluginsFromConfig = [], ...esbuildOptions } = - optimizeDeps.esbuildOptions ?? {} + const { plugins: pluginsFromConfig = [], ...rollupOptions } = + optimizeDeps.rollupOptions ?? {} + let jsxLoader = false await Promise.all( Object.keys(depsInfo).map(async (id) => { const src = depsInfo[id].src! const exportsData = await (depsInfo[id].exportsData ?? extractExportsData(environment, src)) - if (exportsData.jsxLoader && !esbuildOptions.loader?.['.js']) { + if (exportsData.jsxLoader) { // Ensure that optimization won't fail by defaulting '.js' to the JSX parser. // This is useful for packages such as Gatsby. - esbuildOptions.loader = { - '.js': 'jsx', - ...esbuildOptions.loader, - } + jsxLoader = true } const flatId = flattenId(id) flatIdDeps[flatId] = src @@ -803,7 +777,7 @@ async function prepareEsbuildOptimizerRun( } const platform = - optimizeDeps.esbuildOptions?.platform ?? + optimizeDeps.rollupOptions?.platform ?? // We generally don't want to use platform 'neutral', as esbuild has custom handling // when the platform is 'node' or 'browser' that can't be emulated by using mainFields // and conditions @@ -814,43 +788,57 @@ async function prepareEsbuildOptimizerRun( const external = [...(optimizeDeps.exclude ?? [])] - const plugins = [...pluginsFromConfig] + const plugins = await asyncFlatten(arraify(pluginsFromConfig)) if (external.length) { - plugins.push(esbuildCjsExternalPlugin(external, platform)) + plugins.push(rolldownCjsExternalPlugin(external, platform)) } - plugins.push(esbuildDepPlugin(environment, flatIdDeps, external)) - - const context = await esbuild.context({ - absWorkingDir: process.cwd(), - entryPoints: Object.keys(flatIdDeps), - bundle: true, - platform, - define, - format: 'esm', - // See https://github.com/evanw/esbuild/issues/1921#issuecomment-1152991694 - banner: - platform === 'node' - ? { - js: `import { createRequire } from 'module';const require = createRequire(import.meta.url);`, - } - : undefined, - target: ESBUILD_MODULES_TARGET, - external, - logLevel: 'error', - splitting: true, - sourcemap: true, - outdir: processingCacheDir, - ignoreAnnotations: true, - metafile: true, - plugins, - charset: 'utf8', - ...esbuildOptions, - supported: { - ...defaultEsbuildSupported, - ...esbuildOptions.supported, - }, - }) - return { context, idToExports } + plugins.push(...rolldownDepPlugin(environment, flatIdDeps, external)) + + let canceled = false + async function build() { + const bundle = await rolldown({ + ...rollupOptions, + // TODO: pass target + input: flatIdDeps, + logLevel: 'warn', + plugins, + define, + platform, + resolve: { + extensions: ['.tsx', '.ts', '.jsx', '.js', '.css', '.json'], + ...rollupOptions.resolve, + }, + // TODO: remove this and enable rolldown's CSS support later + moduleTypes: { + '.css': 'js', + ...rollupOptions.moduleTypes, + ...(jsxLoader ? { '.js': 'jsx' } : {}), + }, + }) + if (canceled) { + await bundle.close() + throw new Error('The build was canceled') + } + const result = await bundle.write({ + ...rollupOptions.output, + format: 'esm', + sourcemap: true, + dir: processingCacheDir, + entryFileNames: '[name].js', + banner: + platform === 'node' + ? `import { createRequire } from 'module';const require = createRequire(import.meta.url);` + : undefined, + }) + await bundle.close() + return result + } + + function cancel() { + canceled = true + } + + return { context: { build, cancel }, idToExports } } export async function addManuallyIncludedOptimizeDeps( @@ -1047,19 +1035,23 @@ function stringifyDepsOptimizerMetadata( browserHash, optimized: Object.fromEntries( Object.values(optimized).map( - ({ id, src, file, fileHash, needsInterop }) => [ + ({ id, src, file, fileHash, needsInterop, isDynamicEntry }) => [ id, { src, file, fileHash, needsInterop, + isDynamicEntry, }, ], ), ), chunks: Object.fromEntries( - Object.values(chunks).map(({ id, file }) => [id, { file }]), + Object.values(chunks).map(({ id, file, isDynamicEntry }) => [ + id, + { file, isDynamicEntry }, + ]), ), }, (key: string, value: string) => { @@ -1074,29 +1066,6 @@ function stringifyDepsOptimizerMetadata( ) } -function esbuildOutputFromId( - outputs: Record, - id: string, - cacheDirOutputPath: string, -): any { - const cwd = process.cwd() - const flatId = flattenId(id) + '.js' - const normalizedOutputPath = normalizePath( - path.relative(cwd, path.join(cacheDirOutputPath, flatId)), - ) - const output = outputs[normalizedOutputPath] - if (output) { - return output - } - // If the root dir was symlinked, esbuild could return output keys as `../cwd/` - // Normalize keys to support this case too - for (const [key, value] of Object.entries(outputs)) { - if (normalizePath(path.relative(cwd, key)) === normalizedOutputPath) { - return value - } - } -} - export async function extractExportsData( environment: Environment, filePath: string, @@ -1105,18 +1074,38 @@ export async function extractExportsData( const { optimizeDeps } = environment.config - const esbuildOptions = optimizeDeps.esbuildOptions ?? {} + const rollupOptions = optimizeDeps.rollupOptions ?? {} if (optimizeDeps.extensions?.some((ext) => filePath.endsWith(ext))) { // For custom supported extensions, build the entry file to transform it into JS, // and then parse with es-module-lexer. Note that the `bundle` option is not `true`, // so only the entry file is being transformed. - const result = await build({ - ...esbuildOptions, - entryPoints: [filePath], - write: false, + const { plugins: pluginsFromConfig = [], ...remainingRollupOptions } = + rollupOptions + const plugins = await asyncFlatten(arraify(pluginsFromConfig)) + plugins.unshift({ + name: 'externalize', + resolveId(id, importer) { + if (importer !== undefined) { + return { id, external: true } + } + }, + }) + const build = await rolldown({ + ...remainingRollupOptions, + plugins, + input: [filePath], + // TODO: remove this and enable rolldown's CSS support later + moduleTypes: { + '.css': 'js', + ...remainingRollupOptions.moduleTypes, + }, + }) + const result = await build.generate({ + ...rollupOptions.output, format: 'esm', + sourcemap: false, }) - const [, exports, , hasModuleSyntax] = parse(result.outputFiles[0].text) + const [, exports, , hasModuleSyntax] = parse(result.output[0].code) return { hasModuleSyntax, exports: exports.map((e) => e.n), @@ -1130,14 +1119,17 @@ export async function extractExportsData( try { parseResult = parse(entryContent) } catch { - const loader = esbuildOptions.loader?.[path.extname(filePath)] || 'jsx' + const lang = rollupOptions.moduleTypes?.[path.extname(filePath)] || 'jsx' debug?.( - `Unable to parse: ${filePath}.\n Trying again with a ${loader} transform.`, + `Unable to parse: ${filePath}.\n Trying again with a ${lang} transform.`, ) + if (lang !== 'jsx' && lang !== 'tsx' && lang !== 'ts') { + throw new Error(`Unable to parse : ${filePath}.`) + } const transformed = await transformWithEsbuild( entryContent, filePath, - { loader }, + { loader: lang }, undefined, environment.config, ) diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/rolldownDepPlugin.ts similarity index 50% rename from packages/vite/src/node/optimizer/esbuildDepPlugin.ts rename to packages/vite/src/node/optimizer/rolldownDepPlugin.ts index 7b065cbb0a8ea0..97f07349c6f13a 100644 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ b/packages/vite/src/node/optimizer/rolldownDepPlugin.ts @@ -1,5 +1,5 @@ import path from 'node:path' -import type { ImportKind, Plugin } from 'esbuild' +import type { ImportKind, Plugin, RolldownPlugin } from 'rolldown' import { JS_TYPES_RE, KNOWN_ASSET_TYPES } from '../constants' import type { PackageCache } from '../packages' import { @@ -14,6 +14,7 @@ import { browserExternalId, optionalPeerDepId } from '../plugins/resolve' import { isCSSRequest, isModuleCSSRequest } from '../plugins/css' import type { Environment } from '../environment' import { createBackCompatIdResolver } from '../idResolver' +import { isWindows } from '../../shared/utils' const externalWithConversionNamespace = 'vite:dep-pre-bundle:external-conversion' @@ -47,11 +48,14 @@ const externalTypes = [ ...KNOWN_ASSET_TYPES, ] -export function esbuildDepPlugin( +const optionalPeerDepNamespace = 'optional-peer-dep:' +const browserExternalNamespace = 'browser-external:' + +export function rolldownDepPlugin( environment: Environment, qualified: Record, external: string[], -): Plugin { +): RolldownPlugin[] { const { isProduction } = environment.config const { extensions } = environment.config.optimizeDeps @@ -85,16 +89,16 @@ export function esbuildDepPlugin( const resolve = ( id: string, - importer: string, + importer: string | undefined, kind: ImportKind, resolveDir?: string, ): Promise => { - let _importer: string + let _importer: string | undefined // explicit resolveDir - this is passed only during yarn pnp resolve for // entries if (resolveDir) { _importer = normalizePath(path.join(resolveDir, '*')) - } else { + } else if (importer) { // map importer ids to file paths for correct resolution _importer = importer in qualified ? qualified[importer] : importer } @@ -105,14 +109,12 @@ export function esbuildDepPlugin( const resolveResult = (id: string, resolved: string) => { if (resolved.startsWith(browserExternalId)) { return { - path: id, - namespace: 'browser-external', + id: browserExternalNamespace + id, } } if (resolved.startsWith(optionalPeerDepId)) { return { - path: resolved, - namespace: 'optional-peer-dep', + id: optionalPeerDepNamespace + resolved, } } if (isBuiltin(environment.config.resolve.builtins, resolved)) { @@ -120,37 +122,41 @@ export function esbuildDepPlugin( } if (isExternalUrl(resolved)) { return { - path: resolved, + id: resolved, external: true, } } return { - path: path.resolve(resolved), + id: path.resolve(resolved), } } - return { - name: 'vite:dep-pre-bundle', - setup(build) { - // clear package cache when esbuild is finished - build.onEnd(() => { - esmPackageCache.clear() - cjsPackageCache.clear() - }) + const allExternalTypesReg = new RegExp( + `\\.(` + allExternalTypes.join('|') + `)(\\?.*)?$`, + ) + function resolveEntry(id: string) { + const flatId = flattenId(id) + if (flatId in qualified) { + return { + id: qualified[flatId], + } + } + } + + return [ + { + name: 'vite:dep-pre-bundle-assets', // externalize assets and commonly known non-js file types // See #8459 for more details about this require-import conversion - build.onResolve( - { - filter: new RegExp( - `\\.(` + allExternalTypes.join('|') + `)(\\?.*)?$`, - ), - }, - async ({ path: id, importer, kind }) => { + resolveId: { + filter: { id: allExternalTypesReg }, + async handler(id, importer, options) { + const kind = options.kind // if the prefix exist, it is already converted to `import`, so set `external: true` if (id.startsWith(convertedExternalPrefix)) { return { - path: id.slice(convertedExternalPrefix.length), + id: id.slice(convertedExternalPrefix.length), external: true, } } @@ -162,7 +168,8 @@ export function esbuildDepPlugin( // or package name (e.g. import "some-package.pdf") if (JS_TYPES_RE.test(resolved)) { return { - path: resolved, + // normalize to \\ on windows for esbuild/rolldown behavior difference: https://github.com/sapphi-red-repros/rolldown-esbuild-path-normalization + id: isWindows ? resolved.replaceAll('/', '\\') : resolved, external: false, } } @@ -170,54 +177,55 @@ export function esbuildDepPlugin( if (kind === 'require-call') { // here it is not set to `external: true` to convert `require` to `import` return { - path: resolved, - namespace: externalWithConversionNamespace, + id: externalWithConversionNamespace + resolved, } } return { - path: resolved, + id: resolved, external: true, } } }, - ) - build.onLoad( - { filter: /./, namespace: externalWithConversionNamespace }, - (args) => { + }, + load: { + filter: { + id: new RegExp(`^${externalWithConversionNamespace}`), + }, + handler(id) { + const path = id.slice(externalWithConversionNamespace.length) // import itself with prefix (this is the actual part of require-import conversion) - const modulePath = `"${convertedExternalPrefix}${args.path}"` + const modulePath = `"${convertedExternalPrefix}${path}"` return { - contents: - isCSSRequest(args.path) && !isModuleCSSRequest(args.path) + code: + isCSSRequest(path) && !isModuleCSSRequest(path) ? `import ${modulePath};` : `export { default } from ${modulePath};` + `export * from ${modulePath};`, - loader: 'js', } }, - ) - - function resolveEntry(id: string) { - const flatId = flattenId(id) - if (flatId in qualified) { - return { - path: qualified[flatId], - } - } - } + }, + }, + { + name: 'vite:dep-pre-bundle', + // clear package cache when build is finished + buildEnd() { + esmPackageCache.clear() + cjsPackageCache.clear() + }, + resolveId: { + filter: { id: /^[\w@][^:]/ }, + async handler(id, importer, options) { + const kind = options.kind - build.onResolve( - { filter: /^[\w@][^:]/ }, - async ({ path: id, importer, kind }) => { if (moduleListContains(external, id)) { return { - path: id, + id: id, external: true, } } - // ensure esbuild uses our resolved entries - let entry: { path: string } | undefined + // ensure rolldown uses our resolved entries + let entry: { id: string } | undefined // if this is an entry, return entry namespace resolve result if (!importer) { if ((entry = resolveEntry(id))) return entry @@ -234,114 +242,125 @@ export function esbuildDepPlugin( return resolveResult(id, resolved) } }, - ) - - build.onLoad( - { filter: /.*/, namespace: 'browser-external' }, - ({ path }) => { - if (isProduction) { - return { - contents: 'module.exports = {}', - } - } else { - return { - // Return in CJS to intercept named imports. Use `Object.create` to - // create the Proxy in the prototype to workaround esbuild issue. Why? - // - // In short, esbuild cjs->esm flow: - // 1. Create empty object using `Object.create(Object.getPrototypeOf(module.exports))`. - // 2. Assign props of `module.exports` to the object. - // 3. Return object for ESM use. - // - // If we do `module.exports = new Proxy({}, {})`, step 1 returns empty object, - // step 2 does nothing as there's no props for `module.exports`. The final object - // is just an empty object. - // - // Creating the Proxy in the prototype satisfies step 1 immediately, which means - // the returned object is a Proxy that we can intercept. - // - // Note: Skip keys that are accessed by esbuild and browser devtools. - contents: `\ -module.exports = Object.create(new Proxy({}, { - get(_, key) { - if ( - key !== '__esModule' && - key !== '__proto__' && - key !== 'constructor' && - key !== 'splice' - ) { - console.warn(\`Module "${path}" has been externalized for browser compatibility. Cannot access "${path}.\${key}" in client code. See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`) - } - } -}))`, + }, + load: { + filter: { + id: [ + new RegExp(`^${browserExternalNamespace}`), + new RegExp(`^${optionalPeerDepNamespace}`), + ], + }, + handler(id) { + if (id.startsWith(browserExternalNamespace)) { + const path = id.slice(browserExternalNamespace.length) + if (isProduction) { + return { + code: 'module.exports = {}', + } + } else { + return { + // Return in CJS to intercept named imports. Use `Object.create` to + // create the Proxy in the prototype to workaround esbuild issue. Why? + // + // In short, esbuild cjs->esm flow: + // 1. Create empty object using `Object.create(Object.getPrototypeOf(module.exports))`. + // 2. Assign props of `module.exports` to the object. + // 3. Return object for ESM use. + // + // If we do `module.exports = new Proxy({}, {})`, step 1 returns empty object, + // step 2 does nothing as there's no props for `module.exports`. The final object + // is just an empty object. + // + // Creating the Proxy in the prototype satisfies step 1 immediately, which means + // the returned object is a Proxy that we can intercept. + // + // Note: Skip keys that are accessed by esbuild and browser devtools. + code: `\ + module.exports = Object.create(new Proxy({}, { + get(_, key) { + if ( + key !== '__esModule' && + key !== '__proto__' && + key !== 'constructor' && + key !== 'splice' + ) { + console.warn(\`Module "${path}" has been externalized for browser compatibility. Cannot access "${path}.\${key}" in client code. See http://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\`) + } + } + }))`, + } } } - }, - ) - build.onLoad( - { filter: /.*/, namespace: 'optional-peer-dep' }, - ({ path }) => { - if (isProduction) { - return { - contents: 'module.exports = {}', - } - } else { - const [, peerDep, parentDep] = path.split(':') - return { - contents: `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)`, + if (id.startsWith(optionalPeerDepNamespace)) { + if (isProduction) { + return { + code: 'module.exports = {}', + } + } else { + const path = id.slice(externalWithConversionNamespace.length) + const [, peerDep, parentDep] = path.split(':') + return { + code: `throw new Error(\`Could not resolve "${peerDep}" imported by "${parentDep}". Is it installed?\`)`, + } } } }, - ) + }, }, - } + ] } const matchesEntireLine = (text: string) => `^${escapeRegex(text)}$` -// esbuild doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized +// rolldown (and esbuild) doesn't transpile `require('foo')` into `import` statements if 'foo' is externalized // https://github.com/evanw/esbuild/issues/566#issuecomment-735551834 -export function esbuildCjsExternalPlugin( +export function rolldownCjsExternalPlugin( externals: string[], platform: 'node' | 'browser' | 'neutral', ): Plugin { + const filter = new RegExp(externals.map(matchesEntireLine).join('|')) + return { name: 'cjs-external', - setup(build) { - const filter = new RegExp(externals.map(matchesEntireLine).join('|')) - - build.onResolve({ filter: new RegExp(`^${nonFacadePrefix}`) }, (args) => { - return { - path: args.path.slice(nonFacadePrefix.length), - external: true, + resolveId: { + filter: { id: [new RegExp(`^${nonFacadePrefix}`), filter] }, + handler(id, _importer, options) { + if (id.startsWith(nonFacadePrefix)) { + return { + id: id.slice(nonFacadePrefix.length), + external: true, + } } - }) - build.onResolve({ filter }, (args) => { - // preserve `require` for node because it's more accurate than converting it to import - if (args.kind === 'require-call' && platform !== 'node') { + if (filter.test(id)) { + const kind = options.kind + // preserve `require` for node because it's more accurate than converting it to import + if (kind === 'require-call' && platform !== 'node') { + return { + id: cjsExternalFacadeNamespace + id, + } + } + return { - path: args.path, - namespace: cjsExternalFacadeNamespace, + id, + external: true, } } - - return { - path: args.path, - external: true, + }, + }, + load: { + filter: { id: [new RegExp(`^${cjsExternalFacadeNamespace}`)] }, + handler(id) { + if (id.startsWith(cjsExternalFacadeNamespace)) { + return { + code: + `import * as m from ${JSON.stringify( + nonFacadePrefix + id.slice(cjsExternalFacadeNamespace.length), + )};` + `module.exports = m;`, + } } - }) - - build.onLoad( - { filter: /.*/, namespace: cjsExternalFacadeNamespace }, - (args) => ({ - contents: - `import * as m from ${JSON.stringify( - nonFacadePrefix + args.path, - )};` + `module.exports = m;`, - }), - ) + }, }, } } diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index e46413321822d1..ed9162aff3797d 100644 --- a/packages/vite/src/node/optimizer/scan.ts +++ b/packages/vite/src/node/optimizer/scan.ts @@ -2,15 +2,8 @@ import fs from 'node:fs' import fsp from 'node:fs/promises' import path from 'node:path' import { performance } from 'node:perf_hooks' -import type { - BuildContext, - Loader, - OnLoadArgs, - OnLoadResult, - Plugin, -} from 'esbuild' -import esbuild, { formatMessages, transform } from 'esbuild' -import type { PartialResolvedId } from 'rollup' +import { scan, transform } from 'rolldown/experimental' +import type { PartialResolvedId, Plugin } from 'rolldown' import colors from 'picocolors' import { glob, isDynamicPattern } from 'tinyglobby' import { @@ -21,6 +14,7 @@ import { } from '../constants' import { arraify, + asyncFlatten, createDebugger, dataUrlRE, externalRE, @@ -41,7 +35,6 @@ import { BaseEnvironment } from '../baseEnvironment' import type { DevEnvironment } from '../server/environment' import { transformGlobImport } from '../plugins/importMetaGlob' import { cleanUrl } from '../../shared/utils' -import { loadTsconfigJsonForFile } from '../plugins/esbuild' export class ScanEnvironment extends BaseEnvironment { mode = 'scan' as const @@ -108,7 +101,7 @@ export function devToScanEnvironment( const debug = createDebugger('vite:deps') -const htmlTypesRE = /\.(html|vue|svelte|astro|imba)$/ +const htmlTypesRE = /\.(?:html|vue|svelte|astro|imba)$/ // A simple regex to detect import sources. This is only used on // - const filePath = id.replace(normalizePath(config.root), '') - addToHTMLProxyCache(config, filePath, inlineModuleIndex, { - code: contents, - }) - js += `\nimport "${id}?html-proxy&index=${inlineModuleIndex}.js"` - shouldRemove = true - } + // script tags + if (node.nodeName === 'script') { + const { src, srcSourceCodeLocation, isModule, isAsync, isIgnored } = + getScriptInfo(node) - everyScriptIsAsync &&= isAsync - someScriptsAreAsync ||= isAsync - someScriptsAreDefer ||= !isAsync - } else if (url && !isPublicFile) { - if (!isExcludedUrl(url)) { - config.logger.warn( - ` + 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)), - ) - } + if (shouldRemove) { + // remove the script tag from the html. we are going to inject new + // ones in the end. + s.remove( + nodeStartWithLeadingWhitespace(node), + node.sourceCodeLocation!.endOffset, + ) } + }) - // 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), - })), + isAsyncScriptMap.get(config)!.set(id, everyScriptIsAsync) + + if (someScriptsAreAsync && someScriptsAreDefer) { + config.logger.warn( + `\nMixed async and defer script modules in ${id}, output script will fallback to defer. Every script, including inline ones, need to be marked as async for your output script to be async.`, ) - 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()) + await Promise.all(assetUrlsPromises) - // 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}` + // 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)), + ) } + } - await Promise.all(setModuleSideEffectPromises) + // 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()) - // 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' } + // 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) + + // 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' } }, }, diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index 17fdbdb402a7e8..b023d4e51dcdee 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -181,42 +181,44 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:build-import-analysis', resolveId: { + filter: { + id: preloadHelperId, + }, handler(id) { - if (id === preloadHelperId) { - return id - } + return id }, }, 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 } - } + filter: { + id: preloadHelperId, + }, + handler(_id) { + 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 } }, }, diff --git a/packages/vite/src/node/plugins/importMetaGlob.ts b/packages/vite/src/node/plugins/importMetaGlob.ts index 68898ddca0515d..f917225598ed5d 100644 --- a/packages/vite/src/node/plugins/importMetaGlob.ts +++ b/packages/vite/src/node/plugins/importMetaGlob.ts @@ -52,8 +52,10 @@ export function importGlobPlugin(config: ResolvedConfig): Plugin { importGlobMaps.clear() }, transform: { + filter: { + code: 'import.meta.glob', + }, async handler(code, id) { - if (!code.includes('import.meta.glob')) return const result = await transformGlobImport( code, id, diff --git a/packages/vite/src/node/plugins/json.ts b/packages/vite/src/node/plugins/json.ts index 5ce8f4358dc8cb..21664d024318ee 100644 --- a/packages/vite/src/node/plugins/json.ts +++ b/packages/vite/src/node/plugins/json.ts @@ -45,10 +45,10 @@ export function jsonPlugin( name: 'vite:json', transform: { + filter: { + id: { include: jsonExtRE, exclude: SPECIAL_QUERY_RE }, + }, 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` + diff --git a/packages/vite/src/node/plugins/loadFallback.ts b/packages/vite/src/node/plugins/loadFallback.ts index b3671c7615843a..040cc3d3ea6855 100644 --- a/packages/vite/src/node/plugins/loadFallback.ts +++ b/packages/vite/src/node/plugins/loadFallback.ts @@ -9,6 +9,12 @@ export function buildLoadFallbackPlugin(): Plugin { return { name: 'vite:load-fallback', load: { + filter: { + id: { + include: /\?|#/, + exclude: /^data:/, + }, + }, async handler(id) { try { const cleanedId = cleanUrl(id) diff --git a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts index e662ddf7dd0857..998a93dac16563 100644 --- a/packages/vite/src/node/plugins/modulePreloadPolyfill.ts +++ b/packages/vite/src/node/plugins/modulePreloadPolyfill.ts @@ -11,27 +11,25 @@ export function modulePreloadPolyfillPlugin(config: ResolvedConfig): Plugin { return { name: 'vite:modulepreload-polyfill', resolveId: { - handler(id) { - if (id === modulePreloadPolyfillId) { - return resolvedModulePreloadPolyfillId - } + filter: { id: modulePreloadPolyfillId }, + handler(_id) { + return resolvedModulePreloadPolyfillId }, }, 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 } + filter: { id: resolvedModulePreloadPolyfillId }, + handler(_id) { + // `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 } }, }, } diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 364a75d94e24dd..703104bcab3a67 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -477,6 +477,12 @@ export function resolvePlugin( }, load: { + filter: { + id: [ + new RegExp(`^${browserExternalId}`), + new RegExp(`^${optionalPeerDepId}`), + ], + }, handler(id) { if (id.startsWith(browserExternalId)) { if (isBuild) { diff --git a/packages/vite/src/node/plugins/wasm.ts b/packages/vite/src/node/plugins/wasm.ts index 73af00cb5e7acc..ad3fdb67cfef14 100644 --- a/packages/vite/src/node/plugins/wasm.ts +++ b/packages/vite/src/node/plugins/wasm.ts @@ -51,23 +51,19 @@ export const wasmHelperPlugin = (): Plugin => { name: 'vite:wasm-helper', resolveId: { + filter: { id: wasmHelperId }, handler(id) { - if (id === wasmHelperId) { - return id - } + return id }, }, load: { + filter: { id: [wasmHelperId, /\.wasm\?init$/] }, async handler(id) { if (id === wasmHelperId) { return `export default ${wasmHelperCode}` } - if (!id.endsWith('.wasm?init')) { - return - } - const url = await fileToUrl(this, id) return ` @@ -83,17 +79,16 @@ export const wasmFallbackPlugin = (): Plugin => { return { name: 'vite:wasm-fallback', - async load(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.', - ) + load: { + filter: { id: /\.wasm$/ }, + handler(_id) { + 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 969db75c8b765e..6779cf8e59aaed 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -295,8 +295,9 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { }, load: { - handler(id) { - if (isBuild && workerOrSharedWorkerRE.test(id)) { + filter: { id: workerOrSharedWorkerRE }, + handler(_id) { + if (isBuild) { return '' } }, @@ -309,6 +310,9 @@ export function webWorkerPlugin(config: ResolvedConfig): Plugin { // }, transform: { + filter: { + id: [workerFileRE, workerOrSharedWorkerRE], + }, async handler(raw, id) { const workerFileMatch = workerFileRE.exec(id) if (workerFileMatch) { diff --git a/packages/vite/src/node/plugins/workerImportMetaUrl.ts b/packages/vite/src/node/plugins/workerImportMetaUrl.ts index 9d68e57b6f1dbb..d90f3dc8a1874a 100644 --- a/packages/vite/src/node/plugins/workerImportMetaUrl.ts +++ b/packages/vite/src/node/plugins/workerImportMetaUrl.ts @@ -180,17 +180,6 @@ async function getWorkerType( return 'classic' } -function isIncludeWorkerImportMetaUrl(code: string): boolean { - if ( - (code.includes('new Worker') || code.includes('new SharedWorker')) && - code.includes('new URL') && - code.includes(`import.meta.url`) - ) { - return true - } - return false -} - export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { const isBuild = config.command === 'build' let workerResolver: ResolveIdFn @@ -218,80 +207,81 @@ export function workerImportMetaUrlPlugin(config: ResolvedConfig): Plugin { // }, transform: { + filter: { + code: /new\s+(?:Worker|SharedWorker).+new\s+URL.+import\.meta\.url/s, + }, 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, - ) - } + 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') + 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) } else { - 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, - // NOTE: add `'' +` to opt-out rolldown's transform: https://github.com/rolldown/rolldown/issues/2745 - `new URL(/* @vite-ignore */ ${JSON.stringify(builtUrl)}, '' + import.meta.url)`, + builtUrl = await fileToUrl(this, cleanUrl(file)) + builtUrl = injectQuery( + builtUrl, + `${WORKER_FILE_ID}&type=${workerType}`, ) } + s.update( + expStart, + expEnd, + // NOTE: add `'' +` to opt-out rolldown's transform: https://github.com/rolldown/rolldown/issues/2745 + `new URL(/* @vite-ignore */ ${JSON.stringify(builtUrl)}, '' + import.meta.url)`, + ) } + } - if (s) { - return transformStableResult(s, id, config) - } - - return null + if (s) { + return transformStableResult(s, id, config) } + + return null }, }, } From c8ced295e9f0212e5496f0b63afecd2b5d70269c Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 23:01:15 +0900 Subject: [PATCH 15/25] refactor: skip hook filter inject in build --- packages/vite/src/node/build.ts | 40 +++++------------------- packages/vite/src/node/plugins/worker.ts | 4 +-- 2 files changed, 10 insertions(+), 34 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 7ad9b9a7973916..627e64578ad131 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -81,10 +81,6 @@ import { } from './baseEnvironment' import type { MinimalPluginContext, Plugin, PluginContext } from './plugin' import type { RollupPluginHooks } from './typeUtils' -import { - createFilterForTransform, - createIdFilter, -} from './plugins/pluginFilter' import { buildOxcPlugin } from './plugins/oxc' export interface BuildEnvironmentOptions { @@ -597,7 +593,7 @@ async function buildEnvironment( // inject environment and ssr arg to plugin load/transform hooks const chunkMetadataMap = new Map() const plugins = environment.plugins.map((p) => - injectEnvironmentAndFilterToHooks(environment, chunkMetadataMap, p), + injectEnvironmentToHooks(environment, chunkMetadataMap, p), ) const rollupOptions: RolldownOptions = { @@ -1157,7 +1153,7 @@ function isExternal(id: string, test: string | RegExp) { } } -export function injectEnvironmentAndFilterToHooks( +export function injectEnvironmentToHooks( environment: BuildEnvironment, chunkMetadataMap: Map, plugin: Plugin, @@ -1169,13 +1165,13 @@ export function injectEnvironmentAndFilterToHooks( for (const hook of Object.keys(clone) as RollupPluginHooks[]) { switch (hook) { case 'resolveId': - clone[hook] = wrapEnvironmentAndFilterResolveId(environment, resolveId) + clone[hook] = wrapEnvironmentResolveId(environment, resolveId) break case 'load': - clone[hook] = wrapEnvironmentAndFilterLoad(environment, load) + clone[hook] = wrapEnvironmentLoad(environment, load) break case 'transform': - clone[hook] = wrapEnvironmentAndFilterTransform(environment, transform) + clone[hook] = wrapEnvironmentTransform(environment, transform) break default: if (ROLLUP_HOOKS.includes(hook)) { @@ -1193,20 +1189,14 @@ export function injectEnvironmentAndFilterToHooks( return clone } -function wrapEnvironmentAndFilterResolveId( +function wrapEnvironmentResolveId( environment: BuildEnvironment, hook?: Plugin['resolveId'], ): Plugin['resolveId'] { if (!hook) return - const rawIdFilter = typeof hook === 'object' ? hook.filter?.id : undefined - const idFilter = rawIdFilter ? createIdFilter(rawIdFilter) : undefined - const fn = getHookHandler(hook) const handler: Plugin['resolveId'] = function (id, importer, options) { - if (idFilter && !idFilter(id)) { - return - } return fn.call( injectEnvironmentInContext(this, environment), id, @@ -1225,20 +1215,14 @@ function wrapEnvironmentAndFilterResolveId( } } -function wrapEnvironmentAndFilterLoad( +function wrapEnvironmentLoad( environment: BuildEnvironment, hook?: Plugin['load'], ): Plugin['load'] { if (!hook) return - const rawIdFilter = typeof hook === 'object' ? hook.filter?.id : undefined - const idFilter = rawIdFilter ? createIdFilter(rawIdFilter) : undefined - const fn = getHookHandler(hook) const handler: Plugin['load'] = function (id, ...args) { - if (idFilter && !idFilter(id)) { - return - } return fn.call( injectEnvironmentInContext(this, environment), id, @@ -1256,22 +1240,14 @@ function wrapEnvironmentAndFilterLoad( } } -function wrapEnvironmentAndFilterTransform( +function wrapEnvironmentTransform( environment: BuildEnvironment, hook?: Plugin['transform'], ): Plugin['transform'] { if (!hook) return - const filters = typeof hook === 'object' ? hook.filter : undefined - const filter = filters - ? createFilterForTransform(filters.id, filters.code) - : undefined - const fn = getHookHandler(hook) const handler: Plugin['transform'] = function (code, importer, ...args) { - if (filter && !filter(importer, code)) { - return - } return fn.call( injectEnvironmentInContext(this, environment), code, diff --git a/packages/vite/src/node/plugins/worker.ts b/packages/vite/src/node/plugins/worker.ts index 6779cf8e59aaed..ec07794d1cac4f 100644 --- a/packages/vite/src/node/plugins/worker.ts +++ b/packages/vite/src/node/plugins/worker.ts @@ -17,7 +17,7 @@ import { import { BuildEnvironment, createToImportMetaURLBasedRelativeRuntime, - injectEnvironmentAndFilterToHooks, + injectEnvironmentToHooks, onRollupLog, toOutputFilePathInJS, } from '../build' @@ -98,7 +98,7 @@ async function bundleWorkerEntry( ...rollupOptions, input, plugins: workerEnvironment.plugins.map((p) => - injectEnvironmentAndFilterToHooks(workerEnvironment, chunkMetadataMap, p), + injectEnvironmentToHooks(workerEnvironment, chunkMetadataMap, p), ), onLog(level, log) { onRollupLog(level, log, workerEnvironment) From fe6c8af7d37e1855c2331eba3b7974480fe8115a Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 23:14:24 +0900 Subject: [PATCH 16/25] perf: add `experimental.enableNativePlugin` Co-authored-by: IWANABETHATGUY --- packages/vite/src/node/build.ts | 46 ++- packages/vite/src/node/config.ts | 9 + packages/vite/src/node/idResolver.ts | 41 ++- packages/vite/src/node/plugins/css.ts | 1 + packages/vite/src/node/plugins/define.ts | 13 + .../src/node/plugins/importAnalysisBuild.ts | 95 ++++-- packages/vite/src/node/plugins/index.ts | 115 +++++-- packages/vite/src/node/plugins/resolve.ts | 306 +++++++++++++++++- playground/resolve/__tests__/resolve.spec.ts | 6 +- playground/resolve/browser-field/relative.js | 5 +- playground/resolve/index.html | 18 +- 11 files changed, 562 insertions(+), 93 deletions(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 627e64578ad131..8a1acdb6710e64 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -21,6 +21,10 @@ import type { WarningHandlerWithDefault, // WatcherOptions, } from 'rolldown' +import { + loadFallbackPlugin as nativeLoadFallbackPlugin, + manifestPlugin as nativeManifestPlugin, +} from 'rolldown/experimental' import type { RollupCommonJSOptions } from 'dep-types/commonjs' import type { RollupDynamicImportVarsOptions } from 'dep-types/dynamicImportVars' import type { EsbuildTarget } from 'types/internal/esbuildOptions' @@ -474,10 +478,12 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ pre: Plugin[] post: Plugin[] }> { + const enableNativePlugin = config.experimental.enableNativePlugin return { pre: [ completeSystemWrapPlugin(), - dataURIPlugin(), + // rolldown has builtin support datauri, use a switch to control it for convenience + ...(enableNativePlugin === true ? [] : [dataURIPlugin()]), perEnvironmentPlugin( 'vite:rollup-options-plugins', async (environment) => @@ -491,13 +497,37 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ], post: [ buildImportAnalysisPlugin(config), - buildOxcPlugin(), - ...(config.build.minify === 'esbuild' ? [buildEsbuildPlugin()] : []), + ...(enableNativePlugin !== true + ? [ + buildOxcPlugin(), + ...(config.build.minify === 'esbuild' + ? [buildEsbuildPlugin()] + : []), + ] + : []), terserPlugin(config), ...(!config.isWorker - ? [manifestPlugin(), ssrManifestPlugin(), buildReporterPlugin(config)] + ? [ + config.build.manifest && enableNativePlugin === true + ? perEnvironmentPlugin('native:manifest', (environment) => { + if (!environment.config.build.manifest) return false + + return nativeManifestPlugin({ + root: environment.config.root, + outPath: + environment.config.build.manifest === true + ? '.vite/manifest.json' + : environment.config.build.manifest, + }) + }) + : manifestPlugin(), + ssrManifestPlugin(), + buildReporterPlugin(config), + ] : []), - buildLoadFallbackPlugin(), + enableNativePlugin === true + ? nativeLoadFallbackPlugin() + : buildLoadFallbackPlugin(), ], } } @@ -1160,7 +1190,11 @@ export function injectEnvironmentToHooks( ): Plugin { const { resolveId, load, transform } = plugin - const clone = { ...plugin } + // the plugin can be a class instance (e.g. native plugins) + const clone: Plugin = Object.assign( + Object.create(Object.getPrototypeOf(plugin)), + plugin, + ) for (const hook of Object.keys(clone) as RollupPluginHooks[]) { switch (hook) { diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index cc91ff5baf849f..2f85f2ee2e1363 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -515,6 +515,13 @@ export interface ExperimentalOptions { * @default false */ skipSsrTransform?: boolean + /** + * Enable builtin plugin that written by rust, which is faster than js plugin. + * + * @experimental + * @default false + */ + enableNativePlugin?: boolean | 'resolver' } export interface LegacyOptions { @@ -702,6 +709,7 @@ export const configDefaults = Object.freeze({ renderBuiltUrl: undefined, hmrPartialAccept: false, skipSsrTransform: false, + enableNativePlugin: false, }, future: { removePluginHookHandleHotUpdate: undefined, @@ -1634,6 +1642,7 @@ export async function resolveConfig( experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false, + enableNativePlugin: false, ...config.experimental, }, future: config.future, diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index 7cd0ffdea132d7..6f43238ded87e2 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -3,7 +3,7 @@ import aliasPlugin from '@rollup/plugin-alias' import type { ResolvedConfig } from './config' import type { EnvironmentPluginContainer } from './server/pluginContainer' import { createEnvironmentPluginContainer } from './server/pluginContainer' -import { resolvePlugin } from './plugins/resolve' +import { oxcResolvePlugin, resolvePlugin } from './plugins/resolve' import type { InternalResolveOptions } from './plugins/resolve' import type { Environment } from './environment' import type { PartialEnvironment } from './baseEnvironment' @@ -61,17 +61,34 @@ export function createIdResolver( [ // @ts-expect-error the aliasPlugin uses rollup types aliasPlugin({ entries: environment.config.resolve.alias }), - resolvePlugin({ - root: config.root, - isProduction: config.isProduction, - isBuild: config.command === 'build', - asSrc: true, - preferRelative: false, - tryIndex: true, - ...options, - // Ignore sideEffects and other computations as we only need the id - idOnly: true, - }), + ...(config.experimental.enableNativePlugin + ? oxcResolvePlugin( + { + root: config.root, + isProduction: config.isProduction, + isBuild: config.command === 'build', + asSrc: true, + preferRelative: false, + tryIndex: true, + ...options, + // Ignore sideEffects and other computations as we only need the id + idOnly: true, + }, + environment.config, + ) + : [ + resolvePlugin({ + root: config.root, + isProduction: config.isProduction, + isBuild: config.command === 'build', + asSrc: true, + preferRelative: false, + tryIndex: true, + ...options, + // Ignore sideEffects and other computations as we only need the id + idOnly: true, + }), + ]), ], undefined, false, diff --git a/packages/vite/src/node/plugins/css.ts b/packages/vite/src/node/plugins/css.ts index 0334948086c1fb..98a302af77f031 100644 --- a/packages/vite/src/node/plugins/css.ts +++ b/packages/vite/src/node/plugins/css.ts @@ -1284,6 +1284,7 @@ function createCSSResolvers(config: ResolvedConfig): CSSAtImportResolvers { tryIndex: true, tryPrefix: '_', preferRelative: true, + skipMainField: true, }) sassResolve = async (...args) => { // the modern API calls `canonicalize` with resolved file URLs diff --git a/packages/vite/src/node/plugins/define.ts b/packages/vite/src/node/plugins/define.ts index 074680dc398dc4..a3ea472eab094f 100644 --- a/packages/vite/src/node/plugins/define.ts +++ b/packages/vite/src/node/plugins/define.ts @@ -110,6 +110,19 @@ export function definePlugin(config: ResolvedConfig): Plugin { return pattern } + if (config.experimental.enableNativePlugin === true) { + return { + name: 'vite:define', + options(option) { + const [define, _pattern, importMetaEnvVal] = getPattern( + this.environment, + ) + define['import.meta.env'] = importMetaEnvVal + option.define = define + }, + } + } + return { name: 'vite:define', diff --git a/packages/vite/src/node/plugins/importAnalysisBuild.ts b/packages/vite/src/node/plugins/importAnalysisBuild.ts index b023d4e51dcdee..0460c06e3043de 100644 --- a/packages/vite/src/node/plugins/importAnalysisBuild.ts +++ b/packages/vite/src/node/plugins/importAnalysisBuild.ts @@ -6,6 +6,7 @@ import type { } from 'es-module-lexer' import { init, parse as parseImports } from 'es-module-lexer' import type { SourceMap } from 'rolldown' +import { buildImportAnalysisPlugin as nativeBuildImportAnalysisPlugin } from 'rolldown/experimental' import type { RawSourceMap } from '@ampproject/remapping' import convertSourceMap from 'convert-source-map' import { @@ -14,11 +15,11 @@ import { isInNodeModules, numberToPos, } from '../utils' -import type { Plugin } from '../plugin' +import { type Plugin, perEnvironmentPlugin } from '../plugin' import type { ResolvedConfig } from '../config' import { toOutputFilePathInJS } from '../build' import { genSourceMapUrl } from '../server/sourcemap' -import type { Environment } from '../environment' +import type { PartialEnvironment } from '../baseEnvironment' import { removedPureCssFilesCache } from './css' import { createParseErrorInfo } from './importAnalysis' @@ -166,11 +167,43 @@ function preload( }) } +function getPreloadCode( + environment: PartialEnvironment, + renderBuiltUrlBoolean: boolean, + isRelativeBase: boolean, +) { + const { modulePreload } = 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 = + renderBuiltUrlBoolean || 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(environment.config.base)}+dep }` + const preloadCode = `const scriptRel = ${scriptRel};const assetsURL = ${assetsURL};const seen = {};export const ${preloadMethod} = ${preload.toString()}` + return preloadCode +} + /** * Build only. During serve this is performed as part of ./importAnalysis. */ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { - const getInsertPreload = (environment: Environment) => + const getInsertPreload = (environment: PartialEnvironment) => environment.config.consumer === 'client' && !config.isWorker && !config.build.lib @@ -178,7 +211,7 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { const renderBuiltUrl = config.experimental.renderBuiltUrl const isRelativeBase = config.base === './' || config.base === '' - return { + const plugin: Plugin = { name: 'vite:build-import-analysis', resolveId: { filter: { @@ -194,30 +227,11 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { id: preloadHelperId, }, handler(_id) { - 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()}` + const preloadCode = getPreloadCode( + this.environment, + !!renderBuiltUrl, + isRelativeBase, + ) return { code: preloadCode, moduleSideEffects: false } }, }, @@ -738,4 +752,29 @@ export function buildImportAnalysisPlugin(config: ResolvedConfig): Plugin { } }, } + + if (config.experimental.enableNativePlugin === true) { + delete plugin.transform + delete plugin.resolveId + delete plugin.load + return perEnvironmentPlugin( + 'native:import-analysis-build', + (environment) => { + const preloadCode = getPreloadCode( + environment, + !!renderBuiltUrl, + isRelativeBase, + ) + return nativeBuildImportAnalysisPlugin({ + preloadCode, + insertPreload: getInsertPreload(environment), + // this field looks redundant, put a dummy value for now + optimizeModulePreloadRelativePaths: false, + renderBuiltUrl: !!renderBuiltUrl, + isRelativeBase, + }) + }, + ) + } + return plugin } diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index 286f8aeabe8a73..f3a8e278496733 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -1,10 +1,25 @@ import aliasPlugin, { type ResolverFunction } from '@rollup/plugin-alias' import type { ObjectHook } from 'rolldown' +import { + aliasPlugin as nativeAliasPlugin, + dynamicImportVarsPlugin as nativeDynamicImportVarsPlugin, + importGlobPlugin as nativeImportGlobPlugin, + jsonPlugin as nativeJsonPlugin, + modulePreloadPolyfillPlugin as nativeModulePreloadPolyfillPlugin, + transformPlugin as nativeTransformPlugin, + wasmFallbackPlugin as nativeWasmFallbackPlugin, + wasmHelperPlugin as nativeWasmHelperPlugin, +} from 'rolldown/experimental' import type { PluginHookUtils, ResolvedConfig } from '../config' -import type { HookHandler, Plugin, PluginWithRequiredHook } from '../plugin' +import { + type HookHandler, + type Plugin, + type PluginWithRequiredHook, + perEnvironmentPlugin, +} from '../plugin' import { watchPackageDataPlugin } from '../packages' import { jsonPlugin } from './json' -import { resolvePlugin } from './resolve' +import { oxcResolvePlugin, resolvePlugin } from './resolve' import { optimizedDepsPlugin } from './optimizedDeps' import { importAnalysisPlugin } from './importAnalysis' import { cssAnalysisPlugin, cssPlugin, cssPostPlugin } from './css' @@ -40,50 +55,104 @@ export async function resolvePlugins( ? await (await import('../build')).resolveBuildPlugins(config) : { pre: [], post: [] } const { modulePreload } = config.build + const enableNativePlugin = config.experimental.enableNativePlugin return [ !isBuild ? optimizedDepsPlugin() : null, !isWorker ? watchPackageDataPlugin(config.packageCache) : null, !isBuild ? preAliasPlugin(config) : null, - aliasPlugin({ - // @ts-expect-error aliasPlugin receives rollup types - entries: config.resolve.alias, - customResolver: viteAliasCustomResolver, - }), + enableNativePlugin === true + ? nativeAliasPlugin({ + entries: config.resolve.alias.map((item) => { + return { + find: item.find, + replacement: item.replacement, + } + }), + }) + : aliasPlugin({ + // @ts-expect-error aliasPlugin receives rollup types + entries: config.resolve.alias, + customResolver: viteAliasCustomResolver, + }), ...prePlugins, modulePreload !== false && modulePreload.polyfill - ? modulePreloadPolyfillPlugin(config) + ? enableNativePlugin === true + ? perEnvironmentPlugin( + 'native:modulepreload-polyfill', + (environment) => { + if ( + config.command !== 'build' || + environment.config.consumer !== 'client' + ) + return false + return nativeModulePreloadPolyfillPlugin({ + skip: false, + }) + }, + ) + : modulePreloadPolyfillPlugin(config) : null, - resolvePlugin({ - root: config.root, - isProduction: config.isProduction, - isBuild, - packageCache: config.packageCache, - asSrc: true, - optimizeDeps: true, - externalize: true, - }), + ...(enableNativePlugin + ? oxcResolvePlugin( + { + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + asSrc: true, + optimizeDeps: true, + externalize: true, + }, + isWorker ? { ...config, consumer: 'client' } : undefined, + ) + : [ + resolvePlugin({ + root: config.root, + isProduction: config.isProduction, + isBuild, + packageCache: config.packageCache, + asSrc: true, + optimizeDeps: true, + externalize: true, + }), + ]), htmlInlineProxyPlugin(config), cssPlugin(config), - config.oxc !== false ? oxcPlugin(config) : null, - jsonPlugin(config.json, isBuild), - wasmHelperPlugin(), + config.oxc !== false + ? enableNativePlugin === true + ? nativeTransformPlugin() + : oxcPlugin(config) + : null, + enableNativePlugin === true + ? nativeJsonPlugin({ ...config.json, isBuild }) + : jsonPlugin(config.json, isBuild), + enableNativePlugin === true ? nativeWasmHelperPlugin() : wasmHelperPlugin(), webWorkerPlugin(config), assetPlugin(config), ...normalPlugins, - wasmFallbackPlugin(), + enableNativePlugin === true + ? nativeWasmFallbackPlugin() + : wasmFallbackPlugin(), definePlugin(config), cssPostPlugin(config), isBuild && buildHtmlPlugin(config), workerImportMetaUrlPlugin(config), assetImportMetaUrlPlugin(config), ...buildPlugins.pre, - dynamicImportVarsPlugin(config), - importGlobPlugin(config), + enableNativePlugin === true + ? nativeDynamicImportVarsPlugin() + : dynamicImportVarsPlugin(config), + enableNativePlugin === true + ? nativeImportGlobPlugin({ + root: config.root, + restoreQueryExtension: config.experimental.importGlobRestoreExtension, + }) + : importGlobPlugin(config), ...postPlugins, diff --git a/packages/vite/src/node/plugins/resolve.ts b/packages/vite/src/node/plugins/resolve.ts index 703104bcab3a67..0b13e90acbd58b 100644 --- a/packages/vite/src/node/plugins/resolve.ts +++ b/packages/vite/src/node/plugins/resolve.ts @@ -3,6 +3,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import colors from 'picocolors' import type { PartialResolvedId } from 'rolldown' +import { viteResolvePlugin } from 'rolldown/experimental' import { exports, imports } from 'resolve.exports' import { hasESMSyntax } from 'mlly' import type { Plugin } from '../plugin' @@ -35,7 +36,7 @@ import { } from '../utils' import { optimizedDepInfoFromFile, optimizedDepInfoFromId } from '../optimizer' import type { DepsOptimizer } from '../optimizer' -import type { SSROptions } from '..' +import { type Environment, type SSROptions, perEnvironmentPlugin } from '..' import type { PackageCache, PackageData } from '../packages' import { canExternalizeFile, shouldExternalize } from '../external' import { @@ -51,6 +52,7 @@ import { splitFileAndPostfix, withTrailingSlash, } from '../../shared/utils' +import type { ResolvedConfig, ResolvedEnvironmentOptions } from '../config' const normalizedClientEntry = normalizePath(CLIENT_ENTRY) const normalizedEnvEntry = normalizePath(ENV_ENTRY) @@ -128,6 +130,10 @@ interface ResolvePluginOptions { isFromTsImporter?: boolean // True when resolving during the scan phase to discover dependencies scan?: boolean + /** + * @internal + */ + skipMainField?: boolean /** * Optimize deps during dev, defaults to false // TODO: Review default @@ -179,6 +185,270 @@ export interface ResolvePluginOptionsWithOverrides extends ResolveOptions, ResolvePluginOptions {} +const perEnvironmentOrWorkerPlugin = ( + name: string, + overrideEnvConfig: (ResolvedConfig & ResolvedEnvironmentOptions) | undefined, + f: (env: { + name: string + config: ResolvedConfig & ResolvedEnvironmentOptions + }) => Plugin, +): Plugin => { + if (overrideEnvConfig) { + return f({ name: 'client', config: overrideEnvConfig }) + } + return perEnvironmentPlugin(name, f) +} + +export function oxcResolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, + overrideEnvConfig: (ResolvedConfig & ResolvedEnvironmentOptions) | undefined, +): Plugin[] { + return [ + optimizerResolvePlugin(resolveOptions), + importGlobSubpathImportsResolvePlugin(resolveOptions), + perEnvironmentOrWorkerPlugin( + 'vite:resolve-builtin', + overrideEnvConfig, + (env) => { + const environment = env as Environment + // The resolve plugin is used for createIdResolver and the depsOptimizer should be + // disabled in that case, so deps optimization is opt-in when creating the plugin. + const depsOptimizer = + resolveOptions.optimizeDeps && environment?.mode === 'dev' + ? environment.depsOptimizer + : undefined + + const options: InternalResolveOptions = { + ...environment.config.resolve, + ...resolveOptions, // plugin options + resolve options overrides + } + const noExternal = + Array.isArray(options.noExternal) || options.noExternal === true + ? options.noExternal + : [options.noExternal] + + return viteResolvePlugin({ + resolveOptions: { + isBuild: options.isBuild, + isProduction: options.isProduction, + asSrc: options.asSrc ?? false, + preferRelative: options.preferRelative ?? false, + isRequire: options.isRequire, + root: options.root, + scan: options.scan ?? false, + + mainFields: options.skipMainField + ? options.mainFields + : options.mainFields.concat(['main']), + conditions: options.conditions, + externalConditions: options.externalConditions, + extensions: options.extensions, + tryIndex: options.tryIndex ?? true, + tryPrefix: options.tryPrefix, + preserveSymlinks: options.preserveSymlinks, + }, + environmentConsumer: environment.config.consumer, + environmentName: environment.name, + external: options.external, + noExternal: noExternal, + dedupe: options.dedupe, + finalizeBareSpecifier: !depsOptimizer + ? undefined + : (resolvedId, rawId, importer) => { + // if we reach here, it's a valid dep import that hasn't been optimized. + const isJsType = isOptimizable( + resolvedId, + depsOptimizer.options, + ) + const exclude = depsOptimizer?.options.exclude + + // check for deep import, e.g. "my-lib/foo" + const deepMatch = deepImportRE.exec(rawId) + // package name doesn't include postfixes + // trim them to support importing package with queries (e.g. `import css from 'normalize.css?inline'`) + const pkgId = deepMatch + ? deepMatch[1] || deepMatch[2] + : cleanUrl(rawId) + + const skipOptimization = + depsOptimizer.options.noDiscovery || + !isJsType || + (importer && isInNodeModules(importer)) || + exclude?.includes(pkgId) || + exclude?.includes(rawId) || + SPECIAL_QUERY_RE.test(resolvedId) + + let newId = resolvedId + if (skipOptimization) { + // excluded from optimization + // Inject a version query to npm deps so that the browser + // can cache it without re-validation, but only do so for known js types. + // otherwise we may introduce duplicated modules for externalized files + // from pre-bundled deps. + const versionHash = depsOptimizer!.metadata.browserHash + if (versionHash && isJsType) { + newId = injectQuery(newId, `v=${versionHash}`) + } + } else { + // this is a missing import, queue optimize-deps re-run and + // get a resolved its optimized info + const optimizedInfo = depsOptimizer!.registerMissingImport( + rawId, + newId, + ) + newId = depsOptimizer!.getOptimizedDepId(optimizedInfo) + } + return newId + }, + finalizeOtherSpecifiers: !depsOptimizer + ? undefined + : (resolvedId, rawId) => { + const newResolvedId = ensureVersionQuery( + resolvedId, + rawId, + options, + depsOptimizer, + ) + return newResolvedId === resolvedId ? undefined : newResolvedId + }, + }) + }, + ), + ] +} + +function optimizerResolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, +): Plugin { + const { root, asSrc } = resolveOptions + + return { + name: 'vite:resolve-dev', + apply: 'serve', + resolveId: { + filter: { + id: { + exclude: [ + /^\0/, + /^virtual:/, + // When injected directly in html/client code + /^\/virtual:/, + /^__vite-/, + ], + }, + }, + async handler(id, importer, resolveOpts) { + // The resolve plugin is used for createIdResolver and the depsOptimizer should be + // disabled in that case, so deps optimization is opt-in when creating the plugin. + const depsOptimizer = + resolveOptions.optimizeDeps && this.environment.mode === 'dev' + ? this.environment.depsOptimizer + : undefined + if (!depsOptimizer) { + return + } + + const options: InternalResolveOptions = { + isRequire: resolveOpts.kind === 'require-call', + ...this.environment.config.resolve, + ...resolveOptions, + scan: resolveOpts.scan ?? resolveOptions.scan, + } + options.preferRelative ||= importer?.endsWith('.html') + + // resolve pre-bundled deps requests, these could be resolved by + // tryFileResolve or /fs/ resolution but these files may not yet + // exists if we are in the middle of a deps re-processing + if (asSrc && depsOptimizer.isOptimizedDepUrl(id)) { + const optimizedPath = id.startsWith(FS_PREFIX) + ? fsPathFromId(id) + : normalizePath(path.resolve(root, id.slice(1))) + return optimizedPath + } + + if (!isDataUrl(id) && !isExternalUrl(id)) { + if ( + id[0] === '.' || + (options.preferRelative && startsWithWordCharRE.test(id)) + ) { + const basedir = importer ? path.dirname(importer) : root + const fsPath = path.resolve(basedir, id) + // handle browser field mapping for relative imports + + const normalizedFsPath = normalizePath(fsPath) + + if (depsOptimizer.isOptimizedDepFile(normalizedFsPath)) { + // Optimized files could not yet exist in disk, resolve to the full path + // Inject the current browserHash version if the path doesn't have one + if (!DEP_VERSION_RE.test(normalizedFsPath)) { + const browserHash = optimizedDepInfoFromFile( + depsOptimizer.metadata, + normalizedFsPath, + )?.browserHash + if (browserHash) { + return injectQuery(normalizedFsPath, `v=${browserHash}`) + } + } + return normalizedFsPath + } + } + + // bare package imports, perform node resolve + if (bareImportRE.test(id)) { + let res: string | PartialResolvedId | undefined + if ( + asSrc && + !options.scan && + (res = await tryOptimizedResolve( + depsOptimizer, + id, + importer, + options.preserveSymlinks, + options.packageCache, + )) + ) { + return res + } + } + } + }, + }, + } +} + +function importGlobSubpathImportsResolvePlugin( + resolveOptions: ResolvePluginOptionsWithOverrides, +): Plugin { + const { root } = resolveOptions + + return { + name: 'vite:resolve-import-glob-subpath-imports', + resolveId: { + filter: { + id: { + include: [/^#/], + }, + }, + handler(id, importer, resolveOpts) { + const options: InternalResolveOptions = { + isRequire: resolveOpts.kind === 'require-call', + ...this.environment.config.resolve, + ...resolveOptions, + scan: resolveOpts.scan ?? resolveOptions.scan, + } + options.preferRelative ||= importer?.endsWith('.html') + + if (resolveOpts.custom?.['vite:import-glob']?.isSubImportsPattern) { + const resolvedImports = resolveSubpathImports(id, importer, options) + if (resolvedImports) { + return normalizePath(path.join(root, resolvedImports)) + } + } + }, + }, + } +} + export function resolvePlugin( resolveOptions: ResolvePluginOptionsWithOverrides, ): Plugin { @@ -1033,25 +1303,39 @@ function packageEntryFailure(id: string, details?: string) { throw err } -function resolveExportsOrImports( - pkg: PackageData['data'], - key: string, - options: InternalResolveOptions, - type: 'imports' | 'exports', +function getConditions( + conditions: string[], + isProduction: boolean, + isRequire: boolean | undefined, ) { - const conditions = options.conditions.map((condition) => { + const resolvedConditions = conditions.map((condition) => { if (condition === DEV_PROD_CONDITION) { - return options.isProduction ? 'production' : 'development' + return isProduction ? 'production' : 'development' } return condition }) - if (options.isRequire) { - conditions.push('require') + if (isRequire) { + resolvedConditions.push('require') } else { - conditions.push('import') + resolvedConditions.push('import') } + return resolvedConditions +} + +function resolveExportsOrImports( + pkg: PackageData['data'], + key: string, + options: InternalResolveOptions, + type: 'imports' | 'exports', +) { + const conditions = getConditions( + options.conditions, + options.isProduction, + options.isRequire, + ) + const fn = type === 'imports' ? imports : exports const result = fn(pkg, key, { conditions, unsafe: true }) return result ? result[0] : undefined diff --git a/playground/resolve/__tests__/resolve.spec.ts b/playground/resolve/__tests__/resolve.spec.ts index d5d11f4a7b08ce..a766a19dd50280 100644 --- a/playground/resolve/__tests__/resolve.spec.ts +++ b/playground/resolve/__tests__/resolve.spec.ts @@ -139,11 +139,13 @@ test('Resolve browser field even if module field exists', async () => { expect(await page.textContent('.browser-module1')).toMatch('[success]') }) -test('Resolve module field if browser field is likely UMD or CJS', async () => { +// should not fallback +test.skip('Resolve module field if browser field is likely UMD or CJS', async () => { expect(await page.textContent('.browser-module2')).toMatch('[success]') }) -test('Resolve module field if browser field is likely IIFE', async () => { +// should not fallback +test.skip('Resolve module field if browser field is likely IIFE', async () => { expect(await page.textContent('.browser-module3')).toMatch('[success]') }) diff --git a/playground/resolve/browser-field/relative.js b/playground/resolve/browser-field/relative.js index 660d6be578a728..6b45c5758d37dd 100644 --- a/playground/resolve/browser-field/relative.js +++ b/playground/resolve/browser-field/relative.js @@ -4,7 +4,6 @@ import rb from './no-ext.js' // no substitution import rc from './ext' import rd from './ext.js' import re from './ext-index/index.js' -import rf from './ext-index' -import rg from './no-ext-index/index.js' // no substitution +import rf from './no-ext-index/index.js' // no substitution -export { ra, rb, rc, rd, re, rf, rg } +export { ra, rb, rc, rd, re, rf } diff --git a/playground/resolve/index.html b/playground/resolve/index.html index 1b5cd5ae76a3fd..93918cf3183f4f 100644 --- a/playground/resolve/index.html +++ b/playground/resolve/index.html @@ -322,7 +322,8 @@

utf8-bom-package

import c from '@vitejs/test-resolve-browser-field/ext' import d from '@vitejs/test-resolve-browser-field/ext.js' import e from '@vitejs/test-resolve-browser-field/ext-index/index.js' - import f from '@vitejs/test-resolve-browser-field/ext-index' + // webpack does not support this case, so should be fine + // import f from '@vitejs/test-resolve-browser-field/ext-index' import g from '@vitejs/test-resolve-browser-field/no-ext-index/index.js' // no substitution import h from '@vitejs/test-resolve-browser-field/no-ext?query' import i from '@vitejs/test-resolve-browser-field/bare-import' @@ -334,11 +335,10 @@

utf8-bom-package

rd, re, rf, - rg, } from '@vitejs/test-resolve-browser-field/relative' - const success = [main, a, c, d, e, f, h, i, ra, rc, rd, re, rf] - const noSuccess = [b, g, rb, rg] + const success = [main, a, c, d, e, h, i, ra, rc, rd, re] + const noSuccess = [b, g, rb, rf] if ( [...success, ...noSuccess].filter((text) => text.includes('[success]')) @@ -350,11 +350,13 @@

utf8-bom-package

import browserModule1 from '@vitejs/test-resolve-browser-module-field1' text('.browser-module1', browserModule1) - import browserModule2 from '@vitejs/test-resolve-browser-module-field2' - text('.browser-module2', browserModule2) + // should not fallback + // import browserModule2 from '@vitejs/test-resolve-browser-module-field2' + // text('.browser-module2', browserModule2) - import browserModule3 from '@vitejs/test-resolve-browser-module-field3' - text('.browser-module3', browserModule3) + // should not fallback + // import browserModule3 from '@vitejs/test-resolve-browser-module-field3' + // text('.browser-module3', browserModule3) import { msg as requireButWithModuleFieldMsg } from '@vitejs/test-require-pkg-with-module-field' text('.require-pkg-with-module-field', requireButWithModuleFieldMsg) From 1cff7aad794b9d3d5cde5bfe38a3ddb8390e7c4e Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 23:16:52 +0900 Subject: [PATCH 17/25] feat: add an new option to disable build report Co-Authored-By: IWANABETHATGUY --- packages/vite/src/node/build.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 8a1acdb6710e64..62fd1e56022d2c 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -287,6 +287,7 @@ export interface BuildEnvironmentOptions { name: string, config: ResolvedConfig, ) => Promise | BuildEnvironment + enableBuildReport?: boolean } export type BuildOptions = BuildEnvironmentOptions @@ -395,6 +396,7 @@ export const buildEnvironmentOptionsDefaults = Object.freeze({ chunkSizeWarningLimit: 500, watch: null, // createEnvironment + enableBuildReport: true, }) export function resolveBuildEnvironmentOptions( @@ -522,7 +524,9 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ }) : manifestPlugin(), ssrManifestPlugin(), - buildReporterPlugin(config), + ...(config.build.enableBuildReport + ? [buildReporterPlugin(config)] + : []), ] : []), enableNativePlugin === true From 3839d949c62367fffadc45c90693889e0d126532 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 23:23:02 +0900 Subject: [PATCH 18/25] fix: buffer full reload messages --- packages/vite/src/node/server/ws.ts | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/packages/vite/src/node/server/ws.ts b/packages/vite/src/node/server/ws.ts index 3fd6edea26d96c..583a68457c8a2b 100644 --- a/packages/vite/src/node/server/ws.ts +++ b/packages/vite/src/node/server/ws.ts @@ -10,7 +10,11 @@ import colors from 'picocolors' import type { WebSocket as WebSocketRaw } from 'ws' import { WebSocketServer as WebSocketServerRaw_ } from 'ws' import type { WebSocket as WebSocketTypes } from 'dep-types/ws' -import type { ErrorPayload, HotPayload } from 'types/hmrPayload' +import type { + ErrorPayload, + FullReloadPayload, + HotPayload, +} from 'types/hmrPayload' import type { InferCustomEventPayload } from 'types/customEvent' import type { ResolvedConfig } from '..' import { isObject } from '../utils' @@ -297,9 +301,9 @@ export function createWebSocketServer( }) }) socket.send(JSON.stringify({ type: 'connected' })) - if (bufferedError) { - socket.send(JSON.stringify(bufferedError)) - bufferedError = null + if (bufferedMessage) { + socket.send(JSON.stringify(bufferedMessage)) + bufferedMessage = null } }) @@ -345,13 +349,18 @@ export function createWebSocketServer( // sends the error payload before the client connection is established. // If we have no open clients, buffer the error and send it to the next // connected client. - let bufferedError: ErrorPayload | null = null + // The same thing may happen when the optimizer runs fast enough to + // finish the bundling before the client connects. + let bufferedMessage: ErrorPayload | FullReloadPayload | null = null const normalizedHotChannel = normalizeHotChannel( { send(payload) { - if (payload.type === 'error' && !wss.clients.size) { - bufferedError = payload + if ( + (payload.type === 'error' || payload.type === 'full-reload') && + !wss.clients.size + ) { + bufferedMessage = payload return } From 3e81f9e4bebea2af4e2ec08bb1b1f4a17e7ea2aa Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 7 Mar 2025 23:24:57 +0900 Subject: [PATCH 19/25] chore: enable continuous release Co-Authored-By: Evan You --- .github/workflows/preview-release.yml | 6 +++--- packages/vite/package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/preview-release.yml b/.github/workflows/preview-release.yml index 021bedd60ea786..2cbfe1aac8e357 100644 --- a/.github/workflows/preview-release.yml +++ b/.github/workflows/preview-release.yml @@ -10,14 +10,14 @@ permissions: on: push: branches: - - main + - rolldown-vite pull_request: types: [opened, synchronize, labeled] jobs: preview: if: > - github.repository == 'vitejs/vite' && + github.repository == 'vitejs/rolldown-vite' && (github.event_name == 'push' || (github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'trigger: preview'))) runs-on: ubuntu-latest @@ -35,4 +35,4 @@ jobs: working-directory: ./packages/vite run: pnpm build - - run: pnpm dlx pkg-pr-new@0.0 publish --compact --pnpm ./packages/vite + - run: pnpm dlx pkg-pr-new@0.0 publish --pnpm ./packages/vite diff --git a/packages/vite/package.json b/packages/vite/package.json index 35325046e116ca..a44b28b6b6ba79 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -4,7 +4,7 @@ "type": "module", "license": "MIT", "author": "Evan You", - "description": "Native-ESM powered web dev build tool", + "description": "Vite on Rolldown preview", "bin": { "vite": "bin/vite.js" }, From aa038096d34dc5f6996bdf09e2043c871203ae72 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa Date: Mon, 10 Mar 2025 17:38:22 +0900 Subject: [PATCH 20/25] fix: remove duplicate `createRequire` in optimizer (#86) --- packages/vite/src/node/optimizer/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index 8d1bfa89776170..4a4ceb555acb0e 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -813,10 +813,6 @@ async function prepareRolldownOptimizerRun( sourcemap: true, dir: processingCacheDir, entryFileNames: '[name].js', - banner: - platform === 'node' - ? `import { createRequire } from 'module';const require = createRequire(import.meta.url);` - : undefined, }) await bundle.close() return result From 078a640b8268d3a1eae4d4b33c6e45aae263f7ed Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:37:16 +0900 Subject: [PATCH 21/25] chore: bump rolldown --- packages/vite/package.json | 6 +- pnpm-lock.yaml | 132 ++++++++++++++++++------------------- 2 files changed, 69 insertions(+), 69 deletions(-) diff --git a/packages/vite/package.json b/packages/vite/package.json index a44b28b6b6ba79..77a5280dbb6fa1 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -85,10 +85,10 @@ }, "//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!", "dependencies": { - "@oxc-project/runtime": "^0.53.0", + "@oxc-project/runtime": "^0.58.1", "lightningcss": "^1.29.1", "postcss": "^8.5.3", - "rolldown": "1.0.0-beta.3-commit.62fba31" + "rolldown": "1.0.0-beta.4-commit.308f68b" }, "optionalDependencies": { "fsevents": "~2.3.3" @@ -97,7 +97,7 @@ "@ampproject/remapping": "^2.3.0", "@babel/parser": "^7.26.9", "@jridgewell/trace-mapping": "^0.3.25", - "@oxc-project/types": "^0.53.0", + "@oxc-project/types": "^0.58.1", "@polka/compression": "^1.0.0-next.25", "@rollup/plugin-alias": "^5.1.1", "@rollup/plugin-commonjs": "^28.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d33ed6b28745f9..9f90ace8b9e8f0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -224,8 +224,8 @@ importers: packages/vite: dependencies: '@oxc-project/runtime': - specifier: ^0.53.0 - version: 0.53.0 + specifier: ^0.58.1 + version: 0.58.1 lightningcss: specifier: ^1.29.1 version: 1.29.1 @@ -233,8 +233,8 @@ importers: specifier: ^8.5.3 version: 8.5.3 rolldown: - specifier: 1.0.0-beta.3-commit.62fba31 - version: 1.0.0-beta.3-commit.62fba31(@oxc-project/runtime@0.53.0)(typescript@5.7.3) + specifier: 1.0.0-beta.4-commit.308f68b + version: 1.0.0-beta.4-commit.308f68b(@oxc-project/runtime@0.58.1)(typescript@5.7.3) devDependencies: '@ampproject/remapping': specifier: ^2.3.0 @@ -246,8 +246,8 @@ importers: specifier: ^0.3.25 version: 0.3.25 '@oxc-project/types': - specifier: ^0.53.0 - version: 0.53.0 + specifier: ^0.58.1 + version: 0.58.1 '@polka/compression': specifier: ^1.0.0-next.25 version: 1.0.0-next.25 @@ -2797,12 +2797,12 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/runtime@0.53.0': - resolution: {integrity: sha512-iyZ//fKb3glRZyV6kWo04QjNkXKSj9B61M74Y0Gpqfi+vWAv2nBHdUa5ZiN8fDIobzmPtOu8F4bTj/CdK3w5bg==} + '@oxc-project/runtime@0.58.1': + resolution: {integrity: sha512-xKuEaTrsaLSuYTItOBcEmTQRAsFKq9e30iiexIZQRSzDOJPi0oDbyNjKjIxasTZ/WuLduj8tkweXifX/i/7A/g==} engines: {node: '>=6.9.0'} - '@oxc-project/types@0.53.0': - resolution: {integrity: sha512-8JXXVoHnRLcl6kDBboSfAmAkKeb6PSvSc5qSJxiOFzFx0ZCLAbUDmuwR2hkBnY7kQS3LmNXaONq1BFAmwTyeZw==} + '@oxc-project/types@0.58.1': + resolution: {integrity: sha512-/412rL5TIAsZJ428FvFsZCKYsnnKsABv9Z7xZmdtUylGT+qiN240wHU++HdHwYj2j1A5SeScB4O4t8EjjcPlUw==} '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} @@ -2901,63 +2901,63 @@ packages: resolution: {integrity: sha512-TvCl79Y8v18ZhFGd5mjO1kYPovSBq3+4LVCi5Nfl1JI8fS8i8kXbgQFGwBJRXczim8GlW8c2LMBKTtExYXOy/A==} engines: {node: '>=18'} - '@rolldown/binding-darwin-arm64@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-6gQv9wb2sZlYGNdSmGRCfb3MoqdHYCZMhZGydLztGpDxnLd46TJmoX8yM5/PXbQg3PtAq/Mehv0WXhcMrrPe8A==} + '@rolldown/binding-darwin-arm64@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-KyaoooVseQkmkNjSE9o+501FuFjZPXxUlK7sq8gUH8kK3/2Cmv2sehxedH1ePr+rE27p4ZMa1t8zklnkxyhKXw==} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-BywVC+5mQKBdoQqWDVUUUj7xQMGBJYfUTJ+utNcrg4CawF6WJuLNO7o5O9RnjwIOXmbOQReD7tX/bSL1bFvZVg==} + '@rolldown/binding-darwin-x64@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-9mWHUSIvvI8f+cHeV44UEUzS3LeNC0HgP4EUujoaAsGMJRHen6awFTev2VjTgqEIn6zE9c04TWdwmRaO2iQ2oA==} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-Wm7BoNbuyvBsj/KrSEMGtGPLtAIKt0ArO94Cgi2Dwye/AYF9krBg49MAyy5FUE2FAA3hu1beJQwdKfRPivJJmA==} + '@rolldown/binding-freebsd-x64@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-jmv2TsefLqVsshxN2IWDpnr9Q6Z0qBJ1zyuyCh5UYLDSdUQ3mc5+WZZ8mokAmhxRpKDGRFnM1JMregd8x2PgrA==} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-pnUWq/f5Rmr6IsAYjm/JfNu5g3Z7k1HenR4P5ApVnsllrcd0NopfSOsL9NBH2xuTAKQ5xNx0syugywjYvtSXDQ==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-GlhV+geKejgIdXJiMFjkDUqrwebHX32z26ibiwgup03MyRNiE1wsZFxu60JseVHzh7ltpTdQeybzuNDdKeV2rg==} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-dJT9EnMiLNKeGALEzvM04yBwRPjFT+UB23qG/XIpm0v2yxbXyttCEnjAY8qGrjh9P6LtlO/9muzOq2nfJF3h2Q==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-vZFuO0SeXqLEg0jgyAnUEUuDi74vAUZ45UCzCejvtjWKkllqywkUiAMRgvDmkJvdLk8tCvTfVE5CX2Zh9qjasg==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-KSONRVQO3gEIq9HDH7uDC8eYwjhIn7HOcGeA7bq3memP/xxLDPJBCygvY8L/F4ojP5yrjseq9SQfgsPZfLxLcQ==} + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-nx8Q4vuaViSW/1vPBdf3L2lBQTMzoSArsHzGySUCx+Dc7XWBOc5Cxds31j2QXYcfNkrtVD44GN5xK424RW10FQ==} cpu: [arm64] os: [linux] - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-skvZRdyPdd9IIQf43eMCCxW9av+WKCnRZ+KXP23b4iY5kN0VqsED0SSNprfQJO58SnlB+9F0g58WbAXT4UY8fA==} + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-cFnsUf4JuJRYnjway2dI6M5paJobX2w6a+imlQcIMvF4O+wH1sdkiyOx3YZ/ShOH0Ht9gnYk3/Vzzj/IlpjCjw==} cpu: [x64] os: [linux] - '@rolldown/binding-linux-x64-musl@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-pwCtm6BtZ0y507figQ0wSZFgVP8a6wB+USz6Ht5i7k5fnH8zm9x7xLfXhk3eAp3ZpGOzkRk0kiQUicEMitBF9Q==} + '@rolldown/binding-linux-x64-musl@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-l6yGSXHw1iTwD2E6VdzT8LV77x8W0iK7I0D4B1E2qSSIUNZ8O/OajKcZC74pgwwhrmUgB+AvYz/V2hog2nS4qQ==} cpu: [x64] os: [linux] - '@rolldown/binding-wasm32-wasi@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-idPTXJzhej0E2GQ01chSsISuGSA/fXhF7fI8kxHTq6IcYM+AOR4rjeKfCI0P9Ia56owKbLx2QypT8lIcjevPIw==} + '@rolldown/binding-wasm32-wasi@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-wiUTvwgMdMQHyJoxMCFJRn6XSf8sCiuTUNs1gnbcONtNbwX2N65QXbUFuLGTntVtjj8iBxxEBMVfMjbifT7PCA==} engines: {node: '>=14.21.3'} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-WDFunJCD9RYGRodmqlc+zpqlaDzVoQBWmPdVolTpqHc1KPcQgKCCqbOYq8ISKIw6w8JvwsifkqWB1oZINnM+kQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-0J9RVmE1ZqwkzFLuzk1KLI6AvWpsjm6zVIVBCt2nf6+Mhb4kbxCJ8jHuHULXHe/a4y+AwC511VCPYKNh9u7Amg==} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-nx/u5tu3Py0CBVwMopJKUNDKCnutEzghw1z/BuXufIzHK8Ik97r8NcRGm89HgUInly+vTVClmz/tOMhHeTtwGQ==} + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-AhQHKFDl1mnK/xb9O2PUtv27S/+0vv/UgTpU+zNCqNWMviNeGXkZaHvZSl8nv67sHl2nHvnjy/2JnPCNuLxfTQ==} cpu: [ia32] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.3-commit.62fba31': - resolution: {integrity: sha512-G+Seww/f7MKr7A7oNbNjn3q1Lz/OybPchVwx+m9jLoMgQH8UZBEDtvVL2M8ZVE8lccYIGfSW6xqSYo7pDz4omg==} + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.4-commit.308f68b': + resolution: {integrity: sha512-xBXe2usvk57PkiNcG9MnPpHnxUhB5FMcIB3K35oOMoLlkDSNlkS5L7csdxO01LheX7oLWQAw6GdAHftTUYfeeA==} cpu: [x64] os: [win32] @@ -6518,11 +6518,11 @@ packages: deprecated: Rimraf versions prior to v4 are no longer supported hasBin: true - rolldown@1.0.0-beta.3-commit.62fba31: - resolution: {integrity: sha512-p7wDEDL4xw72C/bQ/ChB4pPgMAB/Sp+I2H1pHrj0rDBEERukXAcVnPyC2HEXLfiy+sKbJ2dTl2feLkDdHu5Frg==} + rolldown@1.0.0-beta.4-commit.308f68b: + resolution: {integrity: sha512-IMysQJF9/23Qy0QybVffX6pg0e6HIs55+y6AIKtWqooqKh4jlhx0qyRYC6enWPnPf6sU4Xz4R+mxF1hjD4sGgQ==} hasBin: true peerDependencies: - '@oxc-project/runtime': 0.53.0 + '@oxc-project/runtime': 0.58.1 peerDependenciesMeta: '@oxc-project/runtime': optional: true @@ -8627,9 +8627,9 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.0 - '@oxc-project/runtime@0.53.0': {} + '@oxc-project/runtime@0.58.1': {} - '@oxc-project/types@0.53.0': {} + '@oxc-project/types@0.58.1': {} '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -8701,42 +8701,42 @@ snapshots: '@publint/pack@0.1.1': {} - '@rolldown/binding-darwin-arm64@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-darwin-arm64@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-darwin-x64@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-darwin-x64@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-freebsd-x64@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-linux-arm64-gnu@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-linux-arm64-musl@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-linux-x64-gnu@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-linux-x64-musl@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-wasm32-wasi@1.0.0-beta.4-commit.308f68b': dependencies: '@napi-rs/wasm-runtime': 0.2.7 optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-win32-arm64-msvc@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-win32-ia32-msvc@1.0.0-beta.4-commit.308f68b': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-beta.3-commit.62fba31': + '@rolldown/binding-win32-x64-msvc@1.0.0-beta.4-commit.308f68b': optional: true '@rollup/plugin-alias@5.1.1(rollup@4.34.8)': @@ -12540,25 +12540,25 @@ snapshots: dependencies: glob: 7.2.3 - rolldown@1.0.0-beta.3-commit.62fba31(@oxc-project/runtime@0.53.0)(typescript@5.7.3): + rolldown@1.0.0-beta.4-commit.308f68b(@oxc-project/runtime@0.58.1)(typescript@5.7.3): dependencies: - '@oxc-project/types': 0.53.0 + '@oxc-project/types': 0.58.1 '@valibot/to-json-schema': 1.0.0-rc.0(valibot@1.0.0-rc.3(typescript@5.7.3)) valibot: 1.0.0-rc.3(typescript@5.7.3) optionalDependencies: - '@oxc-project/runtime': 0.53.0 - '@rolldown/binding-darwin-arm64': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-darwin-x64': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-freebsd-x64': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-linux-x64-musl': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-wasm32-wasi': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.3-commit.62fba31 - '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.3-commit.62fba31 + '@oxc-project/runtime': 0.58.1 + '@rolldown/binding-darwin-arm64': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-darwin-x64': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-freebsd-x64': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-linux-arm64-gnu': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-linux-arm64-musl': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-linux-x64-gnu': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-linux-x64-musl': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-wasm32-wasi': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-win32-arm64-msvc': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-win32-ia32-msvc': 1.0.0-beta.4-commit.308f68b + '@rolldown/binding-win32-x64-msvc': 1.0.0-beta.4-commit.308f68b transitivePeerDependencies: - typescript From ea36743bd458ffceff4dbe80dd4d82954fec7b8d Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 10 Mar 2025 18:46:37 +0900 Subject: [PATCH 22/25] chore: set `ROLLDOWN_OPTIONS_VALIDATION=loose` for vitepress --- docs/.vitepress/config.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 8957174f9022dc..51ed396ba837e8 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -7,6 +7,9 @@ import { } from 'vitepress-plugin-group-icons' import { buildEnd } from './buildEnd.config' +// NOTE: allow additional options to be passed to rolldown for now +process.env.ROLLDOWN_OPTIONS_VALIDATION = 'loose' + const ogDescription = 'Next Generation Frontend Tooling' const ogImage = 'https://vite.dev/og-image.jpg' const ogTitle = 'Vite' From a974a4ad0d14e0d30faa2f792037b980227b10e1 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 17 Mar 2025 12:56:19 +0900 Subject: [PATCH 23/25] fix(optimizer): externalize as absolute --- .../vite/src/node/optimizer/rolldownDepPlugin.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/vite/src/node/optimizer/rolldownDepPlugin.ts b/packages/vite/src/node/optimizer/rolldownDepPlugin.ts index 97f07349c6f13a..78f29a404c3343 100644 --- a/packages/vite/src/node/optimizer/rolldownDepPlugin.ts +++ b/packages/vite/src/node/optimizer/rolldownDepPlugin.ts @@ -123,7 +123,7 @@ export function rolldownDepPlugin( if (isExternalUrl(resolved)) { return { id: resolved, - external: true, + external: 'absolute', } } return { @@ -157,7 +157,7 @@ export function rolldownDepPlugin( if (id.startsWith(convertedExternalPrefix)) { return { id: id.slice(convertedExternalPrefix.length), - external: true, + external: 'absolute', } } @@ -182,7 +182,7 @@ export function rolldownDepPlugin( } return { id: resolved, - external: true, + external: 'absolute', } } }, @@ -220,7 +220,7 @@ export function rolldownDepPlugin( if (moduleListContains(external, id)) { return { id: id, - external: true, + external: 'absolute', } } @@ -329,7 +329,7 @@ export function rolldownCjsExternalPlugin( if (id.startsWith(nonFacadePrefix)) { return { id: id.slice(nonFacadePrefix.length), - external: true, + external: 'absolute', } } @@ -344,7 +344,7 @@ export function rolldownCjsExternalPlugin( return { id, - external: true, + external: 'absolute', } } }, From fa33494473d41956fa16ae441d9a4be98bd192d0 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Mon, 17 Mar 2025 15:52:37 +0900 Subject: [PATCH 24/25] ci: update workflows --- .github/workflows/issue-close-require.yml | 2 +- .github/workflows/issue-labeled.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/release-tag.yml | 4 ++-- .github/workflows/semantic-pull-request.yml | 2 +- packages/vite/package.json | 2 +- packages/vite/src/node/__tests__/config.spec.ts | 2 +- scripts/publishCI.ts | 2 +- scripts/release.ts | 7 ++++--- 9 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/issue-close-require.yml b/.github/workflows/issue-close-require.yml index 5347951bb52086..c70bc43e7c6957 100644 --- a/.github/workflows/issue-close-require.yml +++ b/.github/workflows/issue-close-require.yml @@ -6,7 +6,7 @@ on: jobs: close-issues: - if: github.repository == 'vitejs/vite' + if: github.repository == 'vitejs/rolldown-vite' runs-on: ubuntu-latest steps: - name: needs reproduction diff --git a/.github/workflows/issue-labeled.yml b/.github/workflows/issue-labeled.yml index db719cc3d467d9..46b266a09afbbe 100644 --- a/.github/workflows/issue-labeled.yml +++ b/.github/workflows/issue-labeled.yml @@ -6,7 +6,7 @@ on: jobs: reply-labeled: - if: github.repository == 'vitejs/vite' + if: github.repository == 'vitejs/rolldown-vite' runs-on: ubuntu-latest steps: - name: contribution welcome diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 009da3ac72a57c..caf5a96315be7b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -10,7 +10,7 @@ on: jobs: publish: # prevents this action from running on forks - if: github.repository == 'vitejs/vite' + if: github.repository == 'vitejs/rolldown-vite' runs-on: ubuntu-latest permissions: contents: read diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index ef12c0b8836dfb..ba5b59cbdb0067 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -11,7 +11,7 @@ on: jobs: release: - if: github.repository == 'vitejs/vite' + if: github.repository == 'vitejs/rolldown-vite' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -45,4 +45,4 @@ jobs: with: tag_name: ${{ github.ref }} body: | - Please refer to [CHANGELOG.md](https://github.com/vitejs/vite/blob/${{ github.ref_name }}/packages/${{ steps.tag.outputs.pkgName }}/CHANGELOG.md) for details. + Please refer to [CHANGELOG.md](https://github.com/vitejs/rolldown-vite/blob/${{ github.ref_name }}/packages/${{ steps.tag.outputs.pkgName }}/CHANGELOG.md) for details. diff --git a/.github/workflows/semantic-pull-request.yml b/.github/workflows/semantic-pull-request.yml index d9ed5cd34a561f..0a1149374c4228 100644 --- a/.github/workflows/semantic-pull-request.yml +++ b/.github/workflows/semantic-pull-request.yml @@ -9,7 +9,7 @@ on: jobs: main: - if: github.repository == 'vitejs/vite' + if: github.repository == 'vitejs/rolldown-vite' runs-on: ubuntu-latest name: Semantic Pull Request steps: diff --git a/packages/vite/package.json b/packages/vite/package.json index 77a5280dbb6fa1..7381f24cfcffe2 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,5 +1,5 @@ { - "name": "vite", + "name": "rolldown-vite", "version": "6.2.1", "type": "module", "license": "MIT", diff --git a/packages/vite/src/node/__tests__/config.spec.ts b/packages/vite/src/node/__tests__/config.spec.ts index 72636d3a095783..deee2231c98555 100644 --- a/packages/vite/src/node/__tests__/config.spec.ts +++ b/packages/vite/src/node/__tests__/config.spec.ts @@ -754,7 +754,7 @@ describe('loadConfigFromFile', () => { ))! expect(config).toMatchInlineSnapshot(` { - "jsonValue": "vite", + "jsonValue": "rolldown-vite", } `) }, diff --git a/scripts/publishCI.ts b/scripts/publishCI.ts index 7668807bd14ef0..f2316ec3091b2d 100644 --- a/scripts/publishCI.ts +++ b/scripts/publishCI.ts @@ -5,4 +5,4 @@ import { publish } from '@vitejs/release-scripts' const tag = process.argv.slice(2)[0] ?? '' const provenance = !tag.includes('@') -publish({ defaultPackage: 'vite', provenance, packageManager: 'pnpm' }) +publish({ defaultPackage: 'rolldown-vite', provenance, packageManager: 'pnpm' }) diff --git a/scripts/release.ts b/scripts/release.ts index 5cff4816336824..648ac126401201 100644 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -5,9 +5,9 @@ import extendCommitHash from './extendCommitHash' release({ repo: 'vite', - packages: ['vite', 'create-vite', 'plugin-legacy'], + packages: ['rolldown-vite', 'create-vite', 'plugin-legacy'], toTag: (pkg, version) => - pkg === 'vite' ? `v${version}` : `${pkg}@${version}`, + pkg === 'rolldown-vite' ? `v${version}` : `${pkg}@${version}`, logChangelog: (pkg) => logRecentCommits(pkg), generateChangelog: async (pkgName) => { if (pkgName === 'create-vite') await updateTemplateVersions() @@ -23,7 +23,8 @@ release({ '--commit-path', '.', ] - if (pkgName !== 'vite') changelogArgs.push('--lerna-package', pkgName) + if (pkgName !== 'rolldown-vite') + changelogArgs.push('--lerna-package', pkgName) await run('npx', changelogArgs, { cwd: `packages/${pkgName}` }) // conventional-changelog generates links with short commit hashes, extend them to full hashes extendCommitHash(`packages/${pkgName}/CHANGELOG.md`) From 46b3f762a3b7d62c7e845ee732feec4b435cb48e Mon Sep 17 00:00:00 2001 From: Alexander Date: Wed, 19 Mar 2025 08:49:08 +0100 Subject: [PATCH 25/25] chore: update readme --- README.md | 44 +++++++++++--------------------------------- 1 file changed, 11 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 205c4701a52e4f..da52a8b4ee7e3c 100644 --- a/README.md +++ b/README.md @@ -1,50 +1,28 @@

- + Vite logo + Rolldown logo


- npm package - node compatibility - build status - Start new PR in StackBlitz Codeflow + npm package + node compatibility + build status + Start new PR in StackBlitz Codeflow discord chat


-# Vite ⚡ +# rolldown-vite ⚡ -> Next Generation Frontend Tooling +> Vite with Rolldown as bundler. Temporary package. -- 💡 Instant Server Start -- ⚡️ Lightning Fast HMR -- 🛠️ Rich Features -- 📦 Optimized Build -- 🔩 Universal Plugin Interface -- 🔑 Fully Typed APIs +`rolldown-vite` is a fork of Vite that uses [Rolldown](https://github.com/rolldown-rs/rolldown) instead of Rollup & esbuild. Rolldown is a Rust-based JavaScript bundler designed for maximum performance. -Vite (French word for "quick", pronounced [`/vit/`](https://cdn.jsdelivr.net/gh/vitejs/vite@main/docs/public/vite.mp3), like "veet") is a new breed of frontend build tooling that significantly improves the frontend development experience. It consists of two major parts: +It can be used as a drop-in replacement to test out Rolldown with Vite. Eventually, this package is not needed anymore as all changes will be merged into the main Vite repository. -- A dev server that serves your source files over [native ES modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules), with [rich built-in features](https://vite.dev/guide/features.html) and astonishingly fast [Hot Module Replacement (HMR)](https://vite.dev/guide/features.html#hot-module-replacement). - -- A [build command](https://vite.dev/guide/build.html) that bundles your code with [Rollup](https://rollupjs.org), pre-configured to output highly optimized static assets for production. - -In addition, Vite is highly extensible via its [Plugin API](https://vite.dev/guide/api-plugin.html) and [JavaScript API](https://vite.dev/guide/api-javascript.html) with full typing support. - -[Read the Docs to Learn More](https://vite.dev). - -## Packages - -| Package | Version (click for changelogs) | -| ----------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------- | -| [vite](packages/vite) | [![vite version](https://img.shields.io/npm/v/vite.svg?label=%20)](packages/vite/CHANGELOG.md) | -| [@vitejs/plugin-legacy](packages/plugin-legacy) | [![plugin-legacy version](https://img.shields.io/npm/v/@vitejs/plugin-legacy.svg?label=%20)](packages/plugin-legacy/CHANGELOG.md) | -| [create-vite](packages/create-vite) | [![create-vite version](https://img.shields.io/npm/v/create-vite.svg?label=%20)](packages/create-vite/CHANGELOG.md) | - -## Contribution - -See [Contributing Guide](CONTRIBUTING.md). +[Read the Docs to Learn More](https://vite.dev/guide/rolldown.html). ## License