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/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/.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/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' diff --git a/docs/_data/blog.data.ts b/docs/_data/blog.data.ts index 39d45ec2b2b1a2..9cd7af169babac 100644 --- a/docs/_data/blog.data.ts +++ b/docs/_data/blog.data.ts @@ -9,8 +9,8 @@ interface Post { } } -declare const data: Post[] -export { data } +// NOTE: https://github.com/rolldown/rolldown/issues/3048, https://github.com/oxc-project/oxc/issues/7951 +export declare const data: Post[] export default createContentLoader('blog/*.md', { // excerpt: true, diff --git a/eslint.config.js b/eslint.config.js index 6fc2d08520070a..2d3227f560f32d 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -91,6 +91,7 @@ export default tseslint.config( { allowModules: [ 'vite', + 'esbuild', 'less', 'sass', 'sass-embedded', diff --git a/packages/plugin-legacy/package.json b/packages/plugin-legacy/package.json index eea6933df17ce0..03b589605ff4af 100644 --- a/packages/plugin-legacy/package.json +++ b/packages/plugin-legacy/package.json @@ -23,8 +23,8 @@ } }, "scripts": { - "dev": "unbuild --stub", - "build": "unbuild && pnpm run patch-cjs", + "//dev": "unbuild --stub", + "//build": "unbuild && pnpm run patch-cjs", "patch-cjs": "tsx ../../scripts/patchCJS.ts", "prepublishOnly": "npm run build" }, diff --git a/packages/vite/LICENSE.md b/packages/vite/LICENSE.md index d9c9262f393d36..fcad1f842cb4cc 100644 --- a/packages/vite/LICENSE.md +++ b/packages/vite/LICENSE.md @@ -360,15 +360,11 @@ Repository: lukeed/polka --------------------------------------- -## @rollup/plugin-alias, @rollup/plugin-commonjs, @rollup/plugin-dynamic-import-vars, @rollup/pluginutils +## @rollup/plugin-alias, @rollup/plugin-dynamic-import-vars, @rollup/pluginutils License: MIT By: Johannes Stein Repository: rollup/plugins -License: MIT -By: Rich Harris -Repository: rollup/plugins - License: MIT By: LarsDenBakker Repository: rollup/plugins @@ -584,38 +580,6 @@ Repository: git+https://github.com/paulmillr/chokidar.git --------------------------------------- -## commondir, shell-quote -License: MIT -By: James Halliday -Repositories: http://github.com/substack/node-commondir.git, http://github.com/ljharb/shell-quote.git - -> The MIT License -> -> Copyright (c) 2013 James Halliday (mail@substack.net) -> -> Permission is hereby granted, free of charge, -> to any person obtaining a copy of this software and -> associated documentation files (the "Software"), to -> deal in the Software without restriction, including -> without limitation the rights to use, copy, modify, -> merge, publish, distribute, sublicense, and/or sell -> copies of the Software, and to permit persons to whom -> the Software is furnished to do so, -> subject to the following conditions: -> -> The above copyright notice and this permission notice -> shall be included in all copies or substantial portions of the Software. -> -> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -> OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR -> ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - ---------------------------------------- - ## connect License: MIT By: TJ Holowaychuk, Douglas Christopher Wilson, Jonathan Ong, Tim Caswell @@ -1298,13 +1262,6 @@ Repository: micromatch/is-glob --------------------------------------- -## is-reference -License: MIT -By: Rich Harris -Repository: git+https://github.com/Rich-Harris/is-reference.git - ---------------------------------------- - ## isexe, which License: ISC By: Isaac Z. Schlueter @@ -2147,6 +2104,38 @@ Repository: kevva/shebang-command --------------------------------------- +## shell-quote +License: MIT +By: James Halliday +Repository: http://github.com/ljharb/shell-quote.git + +> The MIT License +> +> Copyright (c) 2013 James Halliday (mail@substack.net) +> +> Permission is hereby granted, free of charge, +> to any person obtaining a copy of this software and +> associated documentation files (the "Software"), to +> deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, +> merge, publish, distribute, sublicense, and/or sell +> copies of the Software, and to permit persons to whom +> the Software is furnished to do so, +> subject to the following conditions: +> +> The above copyright notice and this permission notice +> shall be included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +> OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +> ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--------------------------------------- + ## sirv License: MIT By: Luke Edwards diff --git a/packages/vite/index.cjs b/packages/vite/index.cjs index 823b11bc167e97..36d87b5d98cddd 100644 --- a/packages/vite/index.cjs +++ b/packages/vite/index.cjs @@ -15,6 +15,7 @@ const asyncFunctions = [ 'createServer', 'preview', 'transformWithEsbuild', + 'transformWithOxc', 'resolveConfig', 'optimizeDeps', 'formatPostcssSourceMap', diff --git a/packages/vite/package.json b/packages/vite/package.json index e72ef590e54cf3..7381f24cfcffe2 100644 --- a/packages/vite/package.json +++ b/packages/vite/package.json @@ -1,10 +1,10 @@ { - "name": "vite", + "name": "rolldown-vite", "version": "6.2.1", "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" }, @@ -85,9 +85,10 @@ }, "//": "READ CONTRIBUTING.md to understand what to put under deps vs. devDeps!", "dependencies": { - "esbuild": "^0.25.0", + "@oxc-project/runtime": "^0.58.1", + "lightningcss": "^1.29.1", "postcss": "^8.5.3", - "rollup": "^4.30.1" + "rolldown": "1.0.0-beta.4-commit.308f68b" }, "optionalDependencies": { "fsevents": "~2.3.3" @@ -96,6 +97,7 @@ "@ampproject/remapping": "^2.3.0", "@babel/parser": "^7.26.9", "@jridgewell/trace-mapping": "^0.3.25", + "@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", @@ -117,12 +119,12 @@ "dotenv": "^16.4.7", "dotenv-expand": "^12.0.1", "es-module-lexer": "^1.6.0", + "esbuild": "^0.25.0", "escape-html": "^1.0.3", "estree-walker": "^3.0.3", "etag": "^1.8.1", "http-proxy": "^1.18.1", "launch-editor-middleware": "^2.10.0", - "lightningcss": "^1.29.1", "magic-string": "^0.30.17", "mlly": "^1.7.4", "mrmime": "^2.0.1", @@ -137,6 +139,7 @@ "postcss-load-config": "^6.0.1", "postcss-modules": "^6.0.1", "resolve.exports": "^2.0.3", + "rollup": "^4.30.1", "rollup-plugin-dts": "^6.1.1", "rollup-plugin-esbuild": "^6.2.1", "rollup-plugin-license": "^3.6.0", @@ -155,9 +158,9 @@ }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "esbuild": "^0.25.0", "jiti": ">=1.21.0", "less": "*", - "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", @@ -170,6 +173,9 @@ "@types/node": { "optional": true }, + "esbuild": { + "optional": true + }, "jiti": { "optional": true }, @@ -188,9 +194,6 @@ "sugarss": { "optional": true }, - "lightningcss": { - "optional": true - }, "terser": { "optional": true }, diff --git a/packages/vite/rollup.config.ts b/packages/vite/rollup.config.ts index bec0eabdd65d38..971a32e4e0f44f 100644 --- a/packages/vite/rollup.config.ts +++ b/packages/vite/rollup.config.ts @@ -110,7 +110,8 @@ const nodeConfig = defineConfig({ external: [ /^vite\//, 'fsevents', - 'rollup/parseAst', + 'rolldown/parseAst', + 'rolldown/experimental', /^tsx\//, /^#/, ...Object.keys(pkg.dependencies), @@ -190,7 +191,7 @@ const moduleRunnerConfig = defineConfig({ external: [ 'fsevents', 'lightningcss', - 'rollup/parseAst', + 'rolldown/parseAst', ...Object.keys(pkg.dependencies), ], plugins: [ diff --git a/packages/vite/rollup.dts.config.ts b/packages/vite/rollup.dts.config.ts index d909755844ac1a..7aa9674e07cf70 100644 --- a/packages/vite/rollup.dts.config.ts +++ b/packages/vite/rollup.dts.config.ts @@ -16,7 +16,8 @@ const pkg = JSON.parse( const external = [ /^node:*/, /^vite\//, - 'rollup/parseAst', + 'rolldown/parseAst', + 'rolldown/experimental', ...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies), ...Object.keys(pkg.devDependencies), @@ -46,17 +47,16 @@ const identifierWithTrailingDollarRE = /\b(\w+)\$\d+\b/g * the module that imports the identifer as a named import alias */ const identifierReplacements: Record> = { - rollup: { - Plugin$1: 'rollup.Plugin', - PluginContext$1: 'rollup.PluginContext', - MinimalPluginContext$1: 'rollup.MinimalPluginContext', - TransformPluginContext$1: 'rollup.TransformPluginContext', - TransformResult$1: 'rollup.TransformResult', + rolldown: { + Plugin$1: 'rolldown.Plugin', + PluginContext$1: 'rolldown.PluginContext', + MinimalPluginContext$1: 'rolldown.MinimalPluginContext', + TransformPluginContext$1: 'rolldown.TransformPluginContext', + TransformResult$1: 'rolldown.TransformResult', }, - esbuild: { - TransformResult$2: 'esbuild_TransformResult', - TransformOptions$1: 'esbuild_TransformOptions', - BuildOptions$1: 'esbuild_BuildOptions', + 'rolldown/experimental': { + TransformOptions$1: 'rolldown_experimental_TransformOptions', + TransformResult$2: 'rolldown_experimental_TransformResult', }, 'node:https': { Server$1: 'HttpsServer', diff --git a/packages/vite/src/node/__tests__/build.spec.ts b/packages/vite/src/node/__tests__/build.spec.ts index f36811178cc238..6d4bb109f565e3 100644 --- a/packages/vite/src/node/__tests__/build.spec.ts +++ b/packages/vite/src/node/__tests__/build.spec.ts @@ -8,10 +8,10 @@ import type { LogLevel, OutputChunk, OutputOptions, + RolldownOptions, + RolldownOutput, RollupLog, - RollupOptions, - RollupOutput, -} from 'rollup' +} from 'rolldown' import type { LibraryFormats, LibraryOptions } from '../build' import { build, @@ -62,7 +62,7 @@ describe('build', () => { }, }, ], - })) as RollupOutput + })) as RolldownOutput } const result = await Promise.all([ buildProject('red'), @@ -121,7 +121,7 @@ describe('build', () => { }, }, ], - })) as RollupOutput + })) as RolldownOutput } const result = await Promise.all([ buildProject('yellow'), @@ -131,13 +131,13 @@ describe('build', () => { { "changed": [ "index", - "_foo", "_bar", + "_foo", "_baz.css", ], "unchanged": [ - "_foo.css", "_bar.css", + "_foo.css", "undefined", ], } @@ -198,7 +198,7 @@ describe('build', () => { }, }, ], - })) as RollupOutput + })) as RolldownOutput const foo = esBundle.output.find( (chunk) => chunk.type === 'chunk' && chunk.isEntry, @@ -245,7 +245,7 @@ describe('build', () => { }, }, ], - })) as RollupOutput[] + })) as RolldownOutput[] const foo = esBundle.output.find( (chunk) => chunk.fileName === 'foo.js', @@ -756,7 +756,7 @@ describe('resolveBuildOutputs', () => { }, }) const result = await builder.build(builder.environments.ssr) - expect((result as RollupOutput).output[0].code).not.toContain('preload') + expect((result as RolldownOutput).output[0].code).not.toContain('preload') }) test('ssr custom', async () => { @@ -777,7 +777,7 @@ describe('resolveBuildOutputs', () => { }, }) const result = await builder.build(builder.environments.custom) - expect((result as RollupOutput).output[0].code).not.toContain('preload') + expect((result as RolldownOutput).output[0].code).not.toContain('preload') }) }) @@ -858,14 +858,14 @@ test.for([true, false])( const custom1 = await builder.build(builder.environments.custom1) const custom2 = await builder.build(builder.environments.custom2) expect( - ([client, ssr, custom1, custom2] as RollupOutput[]).map( + ([client, ssr, custom1, custom2] as RolldownOutput[]).map( (o) => o.output[0].code.split('\n').length, ), - ).toEqual([2, 5, 2, 5]) + ).toEqual([1, 5, 1, 5]) }, ) -test('adjust worker build error for worker.format', async () => { +test.skip('adjust worker build error for worker.format', async () => { try { await build({ root: resolve(__dirname, 'fixtures/worker-dynamic'), @@ -886,7 +886,8 @@ test('adjust worker build error for worker.format', async () => { expect.unreachable() }) -describe('onRollupLog', () => { +// rolldown does not append plugin name to the message automatically +describe.skip('onRollupLog', () => { const pluginName = 'rollup-plugin-test' const msgInfo = 'This is the INFO message.' const msgWarn = 'This is the WARN message.' @@ -894,7 +895,7 @@ describe('onRollupLog', () => { level: LogLevel | 'error', message: string | RollupLog, logger: Logger, - options?: Pick, + options?: Pick, ) => { await build({ root: resolve(__dirname, 'packages/build-project'), @@ -1059,7 +1060,7 @@ test('watch rebuild manifest', async (ctx) => { }, }) - function getManifestKeys(output: RollupOutput) { + function getManifestKeys(output: RolldownOutput) { return Object.keys( JSON.parse( (output.output.find((o) => o.fileName === '.vite/manifest.json') as any) @@ -1069,7 +1070,7 @@ test('watch rebuild manifest', async (ctx) => { } const result = await builder.build(builder.environments.client) - expect(getManifestKeys(result as RollupOutput)).toMatchInlineSnapshot(` + expect(getManifestKeys(result as RolldownOutput)).toMatchInlineSnapshot(` [ "dep.js", "entry.js", @@ -1087,7 +1088,7 @@ test('watch rebuild manifest', async (ctx) => { }) const result2 = await builder.build(builder.environments.client) - expect(getManifestKeys(result2 as RollupOutput)).toMatchInlineSnapshot(` + expect(getManifestKeys(result2 as RolldownOutput)).toMatchInlineSnapshot(` [ "entry.js", ] @@ -1099,8 +1100,8 @@ test('watch rebuild manifest', async (ctx) => { * ensure that the chunk code is the same. if not, the chunk hash should have changed. */ function assertOutputHashContentChange( - output1: RollupOutput, - output2: RollupOutput, + output1: RolldownOutput, + output2: RolldownOutput, ) { for (const chunk of output1.output) { if (chunk.type === 'chunk') { @@ -1117,7 +1118,10 @@ function assertOutputHashContentChange( } } -function getOutputHashChanges(output1: RollupOutput, output2: RollupOutput) { +function getOutputHashChanges( + output1: RolldownOutput, + output2: RolldownOutput, +) { const map1 = Object.fromEntries( output1.output.map((o) => [o.name, o.fileName]), ) 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/packages/vite/src/node/__tests__/environment.spec.ts b/packages/vite/src/node/__tests__/environment.spec.ts index 75b12136c5cd2d..aa4ae65dbb62fe 100644 --- a/packages/vite/src/node/__tests__/environment.spec.ts +++ b/packages/vite/src/node/__tests__/environment.spec.ts @@ -1,6 +1,6 @@ import path from 'node:path' import { describe, expect, onTestFinished, test } from 'vitest' -import type { RollupOutput } from 'rollup' +import type { RolldownOutput } from 'rolldown' import { createServer } from '../server' import type { InlineConfig } from '../config' import { createBuilder } from '../build' @@ -167,7 +167,7 @@ describe('custom environment conditions', () => { const results: Record = {} for (const key of ['ssr', 'worker', 'custom1', 'custom1_2']) { const output = await builder.build(builder.environments[key]) - const chunk = (output as RollupOutput).output[0] + const chunk = (output as RolldownOutput).output[0] const mod = await import( path.join( import.meta.dirname, 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..915d36e167c787 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', @@ -60,7 +60,7 @@ describe('definePlugin', () => { // assert that the default behavior is to replace import.meta.hot with undefined const transform = await createDefinePluginTransform() expect(await transform('const hot = import.meta.hot;')).toBe( - 'const hot = void 0;\n', + 'const hot = undefined;\n', ) // assert that we can specify a user define to preserve import.meta.hot const overrideTransform = await createDefinePluginTransform({ diff --git a/packages/vite/src/node/__tests__/plugins/import.spec.ts b/packages/vite/src/node/__tests__/plugins/import.spec.ts index 89fbd80d8ecdc1..c43bfed53115a8 100644 --- a/packages/vite/src/node/__tests__/plugins/import.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/import.spec.ts @@ -75,7 +75,10 @@ describe('transformCjsImport', () => { ), ).toBe( 'import __vite__cjsImport0_react from "./node_modules/.vite/deps/react.js"; ' + - `const react = ((m) => m?.__esModule ? m : { ...typeof m === "object" && !Array.isArray(m) || typeof m === "function" ? m : {}, default: m })(__vite__cjsImport0_react)`, + 'const react = ((m) => m?.__esModule ? m : {\n' + + '\t...typeof m === "object" && !Array.isArray(m) || typeof m === "function" ? m : {},\n' + + '\tdefault: m\n' + + '})(__vite__cjsImport0_react)', ) }) 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/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/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap index d00d19e409978c..1ebddb4c84b6c2 100644 --- a/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap +++ b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/__snapshots__/modulePreloadPolyfill.spec.ts.snap @@ -7,41 +7,33 @@ exports[`load > doesn't load modulepreload polyfill when format is cjs 1`] = ` exports[`load > loads modulepreload polyfill 1`] = ` "(function polyfill() { - const relList = document.createElement("link").relList; - if (relList && relList.supports && relList.supports("modulepreload")) { - return; - } - for (const link of document.querySelectorAll('link[rel="modulepreload"]')) { - processPreload(link); - } - new MutationObserver((mutations) => { - for (const mutation of mutations) { - if (mutation.type !== "childList") { - continue; - } - for (const node of mutation.addedNodes) { - if (node.tagName === "LINK" && node.rel === "modulepreload") - processPreload(node); - } - } - }).observe(document, { childList: true, subtree: true }); - function getFetchOpts(link) { - const fetchOpts = {}; - if (link.integrity) fetchOpts.integrity = link.integrity; - if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy; - if (link.crossOrigin === "use-credentials") - fetchOpts.credentials = "include"; - else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; - else fetchOpts.credentials = "same-origin"; - return fetchOpts; - } - function processPreload(link) { - if (link.ep) - return; - link.ep = true; - const fetchOpts = getFetchOpts(link); - fetch(link.href, fetchOpts); - } + const relList = document.createElement("link").relList; + if (relList && relList.supports && relList.supports("modulepreload")) return; + for (const link of document.querySelectorAll("link[rel=\\"modulepreload\\"]")) processPreload(link); + new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== "childList") continue; + for (const node of mutation.addedNodes) if (node.tagName === "LINK" && node.rel === "modulepreload") processPreload(node); + } + }).observe(document, { + childList: true, + subtree: true + }); + function getFetchOpts(link) { + const fetchOpts = {}; + if (link.integrity) fetchOpts.integrity = link.integrity; + if (link.referrerPolicy) fetchOpts.referrerPolicy = link.referrerPolicy; + if (link.crossOrigin === "use-credentials") fetchOpts.credentials = "include"; + else if (link.crossOrigin === "anonymous") fetchOpts.credentials = "omit"; + else fetchOpts.credentials = "same-origin"; + return fetchOpts; + } + function processPreload(link) { + if (link.ep) return; + link.ep = true; + const fetchOpts = getFetchOpts(link); + fetch(link.href, fetchOpts); + } })(); " `; diff --git a/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts index 3b24fbd5203baa..6eb0c1598b113d 100644 --- a/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts @@ -1,5 +1,5 @@ import { describe, it } from 'vitest' -import type { ModuleFormat, RollupOutput } from 'rollup' +import type { ModuleFormat, RolldownOutput } from 'rolldown' import { build } from '../../../build' import { modulePreloadPolyfillId } from '../../../plugins/modulePreloadPolyfill' @@ -34,7 +34,7 @@ const buildProject = ({ format = 'es' as ModuleFormat } = {}) => }, }, ], - }) as Promise + }) as Promise describe('load', () => { it('loads modulepreload polyfill', async ({ expect }) => { 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/__tests__/plugins/workerImportMetaUrl.spec.ts b/packages/vite/src/node/__tests__/plugins/workerImportMetaUrl.spec.ts index 559b51a8d51cbd..a6d60a84b345f3 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', @@ -27,7 +27,7 @@ describe('workerImportMetaUrlPlugin', async () => { expect( await transform('new Worker(new URL("./worker.js", import.meta.url))'), ).toMatchInlineSnapshot( - `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`, + `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", '' + import.meta.url))"`, ) }) @@ -37,7 +37,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'new SharedWorker(new URL("./worker.js", import.meta.url))', ), ).toMatchInlineSnapshot( - `"new SharedWorker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url))"`, + `"new SharedWorker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", '' + import.meta.url))"`, ) }) @@ -47,7 +47,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'new Worker(new URL("./worker.js", import.meta.url), { type: "module", name: "worker1" })', ), ).toMatchInlineSnapshot( - `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { type: "module", name: "worker1" })"`, + `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), { type: "module", name: "worker1" })"`, ) }) @@ -57,7 +57,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'new Worker(new URL("./worker.js", import.meta.url), { "type": "module", "name": "worker1" })', ), ).toMatchInlineSnapshot( - `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { "type": "module", "name": "worker1" })"`, + `"new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), { "type": "module", "name": "worker1" })"`, ) }) @@ -67,7 +67,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id })', ), ).toMatchInlineSnapshot( - `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url), { name: "worker" + id })"`, + `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", '' + import.meta.url), { name: "worker" + id })"`, ) }) @@ -77,7 +77,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: `worker-${id}` })', ), ).toMatchInlineSnapshot( - `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", import.meta.url), { name: \`worker-\${id}\` })"`, + `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=classic", '' + import.meta.url), { name: \`worker-\${id}\` })"`, ) }) @@ -87,7 +87,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: "worker" + id, type: "module" })', ), ).toMatchInlineSnapshot( - `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: "worker" + id, type: "module" })"`, + `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), { name: "worker" + id, type: "module" })"`, ) }) @@ -97,7 +97,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'const id = 1; new Worker(new URL("./worker.js", import.meta.url), { name: `worker-${id}`, type: "module" })', ), ).toMatchInlineSnapshot( - `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: \`worker-\${id}\`, type: "module" })"`, + `"const id = 1; new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), { name: \`worker-\${id}\`, type: "module" })"`, ) }) @@ -107,7 +107,7 @@ describe('workerImportMetaUrlPlugin', async () => { 'const worker = new Worker(new URL("./worker.js", import.meta.url), { name: genName(), type: "module"})', ), ).toMatchInlineSnapshot( - `"const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { name: genName(), type: "module"})"`, + `"const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), { name: genName(), type: "module"})"`, ) }) @@ -122,15 +122,17 @@ const worker = new Worker(new URL("./worker.js", import.meta.url), { worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data))) `), - ).toMatchInlineSnapshot(`" -const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), { - name: genName(), - type: "module", - }, -) + ).toMatchInlineSnapshot(` + " + const worker = new Worker(new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), { + name: genName(), + type: "module", + }, + ) -worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data))) -"`) + worker.addEventListener('message', (ev) => text('.simple-worker-url', JSON.stringify(ev.data))) + " + `) }) test('trailing comma', async () => { @@ -143,14 +145,16 @@ new Worker( }, // }, ) `), - ).toMatchInlineSnapshot(`" -new Worker( - new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", import.meta.url), - { - type: 'module' - }, // }, -) -"`) + ).toMatchInlineSnapshot(` + " + new Worker( + new URL(/* @vite-ignore */ "/worker.js?worker_file&type=module", '' + import.meta.url), + { + type: 'module' + }, // }, + ) + " + `) }) test('throws an error when non-static worker options are provided', async () => { @@ -207,14 +211,14 @@ new Worker( `(() => { new Worker(new URL('./worker', import.meta.url)); repro({ test: "foo", }); })();`, ), ).toMatchInlineSnapshot( - `"(() => { new Worker(new URL(/* @vite-ignore */ "/worker?worker_file&type=classic", import.meta.url)); repro({ test: "foo", }); })();"`, + `"(() => { new Worker(new URL(/* @vite-ignore */ "/worker?worker_file&type=classic", '' + import.meta.url)); repro({ test: "foo", }); })();"`, ) expect( await transform( `repro(new Worker(new URL('./worker', import.meta.url)), { type: "module" })`, ), ).toMatchInlineSnapshot( - `"repro(new Worker(new URL(/* @vite-ignore */ "/worker?worker_file&type=classic", import.meta.url)), { type: "module" })"`, + `"repro(new Worker(new URL(/* @vite-ignore */ "/worker?worker_file&type=classic", '' + import.meta.url)), { type: "module" })"`, ) }) }) diff --git a/packages/vite/src/node/__tests_dts__/plugin.ts b/packages/vite/src/node/__tests_dts__/plugin.ts index 5b4ebeb82895c8..d8f5523edafef8 100644 --- a/packages/vite/src/node/__tests_dts__/plugin.ts +++ b/packages/vite/src/node/__tests_dts__/plugin.ts @@ -1,7 +1,7 @@ /** * This is a development only file for testing types. */ -import type { Plugin as RollupPlugin } from 'rollup' +import type { Plugin as RollupPlugin } from 'rolldown' import type { Equal, ExpectExtends, ExpectTrue } from '@type-challenges/utils' import type { Plugin, PluginContextExtension } from '../plugin' import type { ROLLUP_HOOKS } from '../constants' diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 7f1b7418ed18c2..62fd1e56022d2c 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -8,20 +8,27 @@ import type { LogLevel, LogOrStringHandler, ModuleFormat, + OutputBundle, + OutputChunk, OutputOptions, - RollupBuild, + RenderedChunk, + RolldownBuild, + RolldownOptions, + RolldownOutput, RollupError, RollupLog, - RollupOptions, - RollupOutput, - RollupWatcher, + // RollupWatcher, WarningHandlerWithDefault, - WatcherOptions, -} from 'rollup' -import commonjsPlugin from '@rollup/plugin-commonjs' + // 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 { TransformOptions } from 'esbuild' +import type { EsbuildTarget } from 'types/internal/esbuildOptions' +import type { ChunkMetadata } from 'types/metadata' import { withTrailingSlash } from '../shared/utils' import { DEFAULT_ASSETS_INLINE_LIMIT, @@ -52,6 +59,7 @@ import { mergeWithDefaults, normalizePath, partialEncodeURIPath, + unique, } from './utils' import { perEnvironmentPlugin, resolveEnvironmentPlugins } from './plugin' import { manifestPlugin } from './plugins/manifest' @@ -64,7 +72,7 @@ import { findNearestMainPackageData, findNearestPackageData } from './packages' import type { PackageCache } from './packages' import { getResolvedOutDirs, - resolveChokidarOptions, + // resolveChokidarOptions, resolveEmptyOutDir, } from './watch' import { completeSystemWrapPlugin } from './plugins/completeSystemWrap' @@ -77,6 +85,7 @@ import { } from './baseEnvironment' import type { MinimalPluginContext, Plugin, PluginContext } from './plugin' import type { RollupPluginHooks } from './typeUtils' +import { buildOxcPlugin } from './plugins/oxc' export interface BuildEnvironmentOptions { /** @@ -95,7 +104,7 @@ export interface BuildEnvironmentOptions { * https://esbuild.github.io/content-types/#javascript for more details. * @default 'modules' */ - target?: 'modules' | TransformOptions['target'] | false + target?: 'modules' | EsbuildTarget | false /** * whether to inject module preload polyfill. * Note: does not apply to library mode. @@ -148,13 +157,13 @@ export interface BuildEnvironmentOptions { * doesn't support the #RGBA syntax. * @default target */ - cssTarget?: TransformOptions['target'] | false + cssTarget?: EsbuildTarget | false /** * Override CSS minification specifically instead of defaulting to `build.minify`, * so you can configure minification for JS and CSS separately. - * @default 'esbuild' + * @default 'lightningcss' */ - cssMinify?: boolean | 'esbuild' | 'lightningcss' + cssMinify?: boolean | 'lightningcss' | 'esbuild' /** * If `true`, a separate sourcemap file will be created. If 'inline', the * sourcemap will be appended to the resulting output file as data URI. @@ -165,10 +174,10 @@ export interface BuildEnvironmentOptions { sourcemap?: boolean | 'inline' | 'hidden' /** * Set to `false` to disable minification, or specify the minifier to use. - * Available options are 'terser' or 'esbuild'. - * @default 'esbuild' + * Available options are 'oxc' or 'terser' or 'esbuild'. + * @default 'oxc' */ - minify?: boolean | 'terser' | 'esbuild' + minify?: boolean | 'oxc' | 'terser' | 'esbuild' /** * Options for terser * https://terser.org/docs/api-reference#minify-options @@ -181,7 +190,7 @@ export interface BuildEnvironmentOptions { * Will be merged with internal rollup options. * https://rollupjs.org/configuration-options/ */ - rollupOptions?: RollupOptions + rollupOptions?: RolldownOptions /** * Options to pass on to `@rollup/plugin-commonjs` */ @@ -270,7 +279,7 @@ export interface BuildEnvironmentOptions { * https://rollupjs.org/configuration-options/#watch * @default null */ - watch?: WatcherOptions | null + // watch?: WatcherOptions | null /** * create the Build Environment instance */ @@ -278,6 +287,7 @@ export interface BuildEnvironmentOptions { name: string, config: ResolvedConfig, ) => Promise | BuildEnvironment + enableBuildReport?: boolean } export type BuildOptions = BuildEnvironmentOptions @@ -311,7 +321,7 @@ export interface LibraryOptions { cssFileName?: string } -export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' | 'system' +export type LibraryFormats = 'es' | 'cjs' | 'umd' | 'iife' // | 'system' export interface ModulePreloadOptions { /** @@ -386,6 +396,7 @@ export const buildEnvironmentOptionsDefaults = Object.freeze({ chunkSizeWarningLimit: 500, watch: null, // createEnvironment + enableBuildReport: true, }) export function resolveBuildEnvironmentOptions( @@ -412,7 +423,10 @@ export function resolveBuildEnvironmentOptions( { ...buildEnvironmentOptionsDefaults, cssCodeSplit: !raw.lib, - minify: consumer === 'server' ? false : 'esbuild', + minify: consumer === 'server' ? false : 'oxc', + rollupOptions: { + platform: consumer === 'server' ? 'node' : 'browser', + }, ssr: consumer === 'server', emitAssets: consumer === 'client', createEnvironment: (name, config) => new BuildEnvironment(name, config), @@ -424,12 +438,17 @@ export function resolveBuildEnvironmentOptions( if (merged.target === 'modules') { merged.target = ESBUILD_MODULES_TARGET } + // dedupe target + if (Array.isArray(merged.target)) { + // esbuild allowed duplicate targets but oxc does not + merged.target = unique(merged.target) + } // normalize false string into actual false if ((merged.minify as string) === 'false') { merged.minify = false } else if (merged.minify === true) { - merged.minify = 'esbuild' + merged.minify = 'oxc' } const defaultModulePreload = { @@ -440,7 +459,8 @@ export function resolveBuildEnvironmentOptions( ...merged, cssTarget: merged.cssTarget ?? merged.target, cssMinify: - merged.cssMinify ?? (consumer === 'server' ? 'esbuild' : !!merged.minify), + merged.cssMinify ?? + (consumer === 'server' ? 'lightningcss' : !!merged.minify), // Resolve to false | object modulePreload: merged.modulePreload === false @@ -460,17 +480,12 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ pre: Plugin[] post: Plugin[] }> { + const enableNativePlugin = config.experimental.enableNativePlugin return { pre: [ completeSystemWrapPlugin(), - perEnvironmentPlugin('commonjs', (environment) => { - const { commonjsOptions } = environment.config.build - const usePluginCommonjs = - !Array.isArray(commonjsOptions.include) || - commonjsOptions.include.length !== 0 - return usePluginCommonjs ? commonjsPlugin(commonjsOptions) : false - }), - dataURIPlugin(), + // rolldown has builtin support datauri, use a switch to control it for convenience + ...(enableNativePlugin === true ? [] : [dataURIPlugin()]), perEnvironmentPlugin( 'vite:rollup-options-plugins', async (environment) => @@ -484,12 +499,39 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ], post: [ buildImportAnalysisPlugin(config), - 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(), + ...(config.build.enableBuildReport + ? [buildReporterPlugin(config)] + : []), + ] : []), - buildLoadFallbackPlugin(), + enableNativePlugin === true + ? nativeLoadFallbackPlugin() + : buildLoadFallbackPlugin(), ], } } @@ -500,7 +542,7 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ */ export async function build( inlineConfig: InlineConfig = {}, -): Promise { +): Promise { const builder = await createBuilder(inlineConfig, true) const environment = Object.values(builder.environments)[0] if (!environment) throw new Error('No environment found') @@ -528,7 +570,7 @@ function resolveConfigToBuild( **/ async function buildEnvironment( environment: BuildEnvironment, -): Promise { +): Promise { const { root, packageCache } = environment.config const options = environment.config.build const libOptions = options.lib @@ -583,17 +625,18 @@ async function buildEnvironment( const outDir = resolve(options.outDir) // inject environment and ssr arg to plugin load/transform hooks + const chunkMetadataMap = new Map() const plugins = environment.plugins.map((p) => - injectEnvironmentToHooks(environment, p), + injectEnvironmentToHooks(environment, chunkMetadataMap, p), ) - const rollupOptions: RollupOptions = { - preserveEntrySignatures: ssr - ? 'allow-extension' - : libOptions - ? 'strict' - : false, - cache: options.watch ? undefined : false, + const rollupOptions: RolldownOptions = { + // preserveEntrySignatures: ssr + // ? 'allow-extension' + // : libOptions + // ? 'strict' + // : false, + // cache: options.watch ? undefined : false, ...options.rollupOptions, output: options.rollupOptions.output, input, @@ -602,6 +645,16 @@ async function buildEnvironment( onLog(level, log) { onRollupLog(level, log, environment) }, + define: { + ...options.rollupOptions.define, + // disable builtin process.env.NODE_ENV replacement as it is handled by the define plugin + 'process.env.NODE_ENV': 'process.env.NODE_ENV', + }, + // TODO: remove this and enable rolldown's CSS support later + moduleTypes: { + ...options.rollupOptions.moduleTypes, + '.css': 'js', + }, } /** @@ -663,17 +716,17 @@ async function buildEnvironment( } } - const outputBuildError = (e: RollupError) => { - enhanceRollupError(e) - clearLine() - logger.error(e.message, { error: e }) - } + // const outputBuildError = (e: RollupError) => { + // enhanceRollupError(e) + // clearLine() + // logger.error(e.message, { error: e }) + // } const isSsrTargetWebworkerEnvironment = environment.name === 'ssr' && environment.getTopLevelConfig().ssr?.target === 'webworker' - let bundle: RollupBuild | undefined + let bundle: RolldownBuild | undefined let startTime: number | undefined try { const buildOutputOptions = (output: OutputOptions = {}): OutputOptions => { @@ -715,11 +768,11 @@ async function buildEnvironment( exports: 'auto', sourcemap: options.sourcemap, name: libOptions ? libOptions.name : undefined, - hoistTransitiveImports: libOptions ? false : undefined, + // hoistTransitiveImports: libOptions ? false : undefined, // es2015 enables `generatedCode.symbols` // - #764 add `Symbol.toStringTag` when build es module into cjs chunk // - #1048 add `Symbol.toStringTag` for module default export - generatedCode: 'es2015', + // generatedCode: 'es2015', entryFileNames: ssr ? `[name].${jsExt}` : libOptions @@ -744,6 +797,12 @@ async function buildEnvironment( output.format === 'iife' || (isSsrTargetWebworkerEnvironment && (typeof input === 'string' || Object.keys(input).length === 1)), + minify: + options.minify === 'oxc' + ? true + : options.minify === false + ? 'dce-only' + : false, ...output, } } @@ -777,53 +836,53 @@ async function buildEnvironment( ) // watch file changes with rollup - if (options.watch) { - logger.info(colors.cyan(`\nwatching for file changes...`)) - - const resolvedChokidarOptions = resolveChokidarOptions( - options.watch.chokidar, - resolvedOutDirs, - emptyOutDir, - environment.config.cacheDir, - ) - - const { watch } = await import('rollup') - const watcher = watch({ - ...rollupOptions, - output: normalizedOutputs, - watch: { - ...options.watch, - chokidar: resolvedChokidarOptions, - }, - }) - - watcher.on('event', (event) => { - if (event.code === 'BUNDLE_START') { - logger.info(colors.cyan(`\nbuild started...`)) - if (options.write) { - prepareOutDir(resolvedOutDirs, emptyOutDir, environment) - } - } else if (event.code === 'BUNDLE_END') { - event.result.close() - logger.info(colors.cyan(`built in ${event.duration}ms.`)) - } else if (event.code === 'ERROR') { - outputBuildError(event.error) - } - }) - - return watcher - } - - // write or generate files with rollup - const { rollup } = await import('rollup') + // if (options.watch) { + // logger.info(colors.cyan(`\nwatching for file changes...`)) + + // const resolvedChokidarOptions = resolveChokidarOptions( + // options.watch.chokidar, + // resolvedOutDirs, + // emptyOutDir, + // environment.config.cacheDir, + // ) + + // const { watch } = await import('rollup') + // const watcher = watch({ + // ...rollupOptions, + // output: normalizedOutputs, + // watch: { + // ...options.watch, + // chokidar: resolvedChokidarOptions, + // }, + // }) + + // watcher.on('event', (event) => { + // if (event.code === 'BUNDLE_START') { + // logger.info(colors.cyan(`\nbuild started...`)) + // if (options.write) { + // prepareOutDir(resolvedOutDirs, emptyOutDir, environment) + // } + // } else if (event.code === 'BUNDLE_END') { + // event.result.close() + // logger.info(colors.cyan(`built in ${event.duration}ms.`)) + // } else if (event.code === 'ERROR') { + // outputBuildError(event.error) + // } + // }) + + // return watcher + // } + + // write or generate files with rolldown + const { rolldown } = await import('rolldown') startTime = Date.now() - bundle = await rollup(rollupOptions) + bundle = await rolldown(rollupOptions) if (options.write) { prepareOutDir(resolvedOutDirs, emptyOutDir, environment) } - const res: RollupOutput[] = [] + const res: RolldownOutput[] = [] for (const output of normalizedOutputs) { res.push(await bundle[options.write ? 'write' : 'generate'](output)) } @@ -1130,11 +1189,16 @@ function isExternal(id: string, test: string | RegExp) { export function injectEnvironmentToHooks( environment: BuildEnvironment, + chunkMetadataMap: Map, plugin: Plugin, ): 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) { @@ -1149,7 +1213,12 @@ export function injectEnvironmentToHooks( break default: if (ROLLUP_HOOKS.includes(hook)) { - ;(clone as any)[hook] = wrapEnvironmentHook(environment, clone[hook]) + ;(clone as any)[hook] = wrapEnvironmentHook( + environment, + chunkMetadataMap, + plugin, + hook, + ) } break } @@ -1237,8 +1306,11 @@ function wrapEnvironmentTransform( function wrapEnvironmentHook( environment: BuildEnvironment, - hook?: Plugin[HookName], + chunkMetadataMap: Map, + plugin: Plugin, + hookName: HookName, ): Plugin[HookName] { + const hook = plugin[hookName] if (!hook) return const fn = getHookHandler(hook) @@ -1248,6 +1320,20 @@ function wrapEnvironmentHook( this: PluginContext, ...args: any[] ) { + if (hookName === 'renderChunk') { + injectChunkMetadata(chunkMetadataMap, args[1]) + } + if (hookName === 'augmentChunkHash') { + injectChunkMetadata(chunkMetadataMap, args[0]) + } + if (hookName === 'generateBundle') { + const bundle = args[1] as OutputBundle + for (const chunk of Object.values(bundle)) { + if (chunk.type === 'chunk') { + injectChunkMetadata(chunkMetadataMap, chunk) + } + } + } return fn.call(injectEnvironmentInContext(this, environment), ...args) } @@ -1261,6 +1347,30 @@ function wrapEnvironmentHook( } } +function injectChunkMetadata( + chunkMetadataMap: Map, + chunk: RenderedChunk | OutputChunk, +) { + const key = + 'preliminaryFileName' in chunk ? chunk.preliminaryFileName : chunk.fileName + if (!chunkMetadataMap.has(key)) { + chunkMetadataMap.set(key, { + importedAssets: new Set(), + importedCss: new Set(), + // NOTE: adding this as a workaround for now ideally we'd want to remove this workaround + // use shared `chunk.modules` object + // to allow mutation on js side plugins + __modules: chunk.modules, + }) + } + chunk.viteMetadata = chunkMetadataMap.get(key) + Object.defineProperty(chunk, 'modules', { + get() { + return chunk.viteMetadata!.__modules + }, + }) +} + function injectEnvironmentInContext( context: Context, environment: BuildEnvironment, @@ -1318,12 +1428,12 @@ const relativeUrlMechanisms: Record< InternalModuleFormat, (relativePath: string) => string > = { - amd: (relativePath) => { - if (relativePath[0] !== '.') relativePath = './' + relativePath - return getResolveUrl( - `require.toUrl('${escapeId(relativePath)}'), document.baseURI`, - ) - }, + // amd: (relativePath) => { + // if (relativePath[0] !== '.') relativePath = './' + relativePath + // return getResolveUrl( + // `require.toUrl('${escapeId(relativePath)}'), document.baseURI`, + // ) + // }, cjs: (relativePath) => `(typeof document === 'undefined' ? ${getFileUrlFromRelativePath( relativePath, @@ -1334,14 +1444,17 @@ const relativeUrlMechanisms: Record< ), iife: (relativePath) => getRelativeUrlFromDocument(relativePath), // NOTE: make sure rollup generate `module` params - system: (relativePath) => - getResolveUrl( - `'${escapeId(partialEncodeURIPath(relativePath))}', module.meta.url`, - ), + // system: (relativePath) => + // getResolveUrl( + // `'${escapeId(partialEncodeURIPath(relativePath))}', module.meta.url`, + // ), umd: (relativePath) => `(typeof document === 'undefined' && typeof location === 'undefined' ? ${getFileUrlFromRelativePath( relativePath, )} : ${getRelativeUrlFromDocument(relativePath, true)})`, + // FIXME: how to handle this? + app: (relativePath) => + `new Error('Cannot resolve ${relativePath} in output format "app".')`, } /* end of copy */ @@ -1501,7 +1614,7 @@ export interface ViteBuilder { buildApp(): Promise build( environment: BuildEnvironment, - ): Promise + ): Promise } export interface BuilderOptions { diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index 45c81fc9172aeb..2f85f2ee2e1363 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -8,9 +8,8 @@ import { createRequire } from 'node:module' import crypto from 'node:crypto' import colors from 'picocolors' import type { Alias, AliasOptions } from 'dep-types/alias' -import type { RollupOptions } from 'rollup' import picomatch from 'picomatch' -import { build } from 'esbuild' +import { type OutputChunk, type RolldownOptions, rolldown } from 'rolldown' import type { AnymatchFn } from '../types/anymatch' import { withTrailingSlash } from '../shared/utils' import { @@ -103,6 +102,8 @@ import { PartialEnvironment } from './baseEnvironment' import { createIdResolver } from './idResolver' import { runnerImport } from './ssr/runnerImport' import { getAdditionalAllowedHosts } from './server/middlewares/hostCheck' +import { convertEsbuildPluginToRolldownPlugin } from './optimizer/pluginConverter' +import { type OxcOptions, convertEsbuildConfigToOxcConfig } from './plugins/oxc' const debug = createDebugger('vite:config', { depth: 10 }) const promisifiedRealpath = promisify(fs.realpath) @@ -354,6 +355,11 @@ export interface UserConfig extends DefaultEnvironmentOptions { * Or set to `false` to disable esbuild. */ esbuild?: ESBuildOptions | false + /** + * Transform options to pass to esbuild. + * Or set to `false` to disable OXC. + */ + oxc?: OxcOptions | false /** * Specify additional picomatch patterns to be treated as static assets. */ @@ -433,7 +439,7 @@ export interface UserConfig extends DefaultEnvironmentOptions { * Rollup options to build worker bundle */ rollupOptions?: Omit< - RollupOptions, + RolldownOptions, 'plugins' | 'input' | 'onwarn' | 'preserveEntrySignatures' > } @@ -509,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 { @@ -540,7 +553,7 @@ export interface LegacyOptions { export interface ResolvedWorkerOptions { format: 'es' | 'iife' plugins: (bundleChain: string[]) => Promise - rollupOptions: RollupOptions + rollupOptions: RolldownOptions } export interface InlineConfig extends UserConfig { @@ -596,6 +609,7 @@ export interface ResolvedConfig css: ResolvedCSSOptions json: Required esbuild: ESBuildOptions | false + oxc: OxcOptions | false server: ResolvedServerOptions dev: ResolvedDevEnvironmentOptions /** @experimental */ @@ -695,6 +709,7 @@ export const configDefaults = Object.freeze({ renderBuiltUrl: undefined, hmrPartialAccept: false, skipSsrTransform: false, + enableNativePlugin: false, }, future: { removePluginHookHandleHotUpdate: undefined, @@ -723,6 +738,7 @@ export const configDefaults = Object.freeze({ exclude: [], needsInterop: [], // esbuildOptions + rollupOptions: {}, /** @experimental */ extensions: [], /** @deprecated @experimental */ @@ -810,6 +826,7 @@ function resolveEnvironmentOptions( resolve.preserveSymlinks, forceOptimizeDeps, consumer, + logger, ), dev: resolveDevEnvironmentOptions( options.dev, @@ -990,7 +1007,133 @@ function resolveDepOptimizationOptions( preserveSymlinks: boolean, forceOptimizeDeps: boolean | undefined, consumer: 'client' | 'server' | undefined, + logger: Logger, ): DepOptimizationOptions { + if ( + optimizeDeps?.esbuildOptions && + Object.keys(optimizeDeps.esbuildOptions).length > 0 + ) { + logger.warn( + colors.yellow( + `You have set \`optimizeDeps.esbuildOptions\` but this options is now deprecated. ` + + `Vite now uses Rolldown to optimize the dependencies. ` + + `Please use \`optimizeDeps.rollupOptions\` instead.`, + ), + ) + + optimizeDeps.rollupOptions ??= {} + optimizeDeps.rollupOptions.resolve ??= {} + optimizeDeps.rollupOptions.output ??= {} + + const setResolveOptions = < + T extends keyof Exclude, + >( + key: T, + value: Exclude[T], + ) => { + if ( + value !== undefined && + optimizeDeps.rollupOptions!.resolve![key] === undefined + ) { + optimizeDeps.rollupOptions!.resolve![key] = value + } + } + + if ( + optimizeDeps.esbuildOptions.minify !== undefined && + optimizeDeps.rollupOptions.output.minify === undefined + ) { + optimizeDeps.rollupOptions.output.minify = + optimizeDeps.esbuildOptions.minify + } + if ( + optimizeDeps.esbuildOptions.treeShaking !== undefined && + optimizeDeps.rollupOptions.treeshake === undefined + ) { + optimizeDeps.rollupOptions.treeshake = + optimizeDeps.esbuildOptions.treeShaking + } + if ( + optimizeDeps.esbuildOptions.define !== undefined && + optimizeDeps.rollupOptions.define === undefined + ) { + optimizeDeps.rollupOptions.define = optimizeDeps.esbuildOptions.define + } + if (optimizeDeps.esbuildOptions.loader !== undefined) { + const loader = optimizeDeps.esbuildOptions.loader + optimizeDeps.rollupOptions.moduleTypes ??= {} + for (const [key, value] of Object.entries(loader)) { + if ( + optimizeDeps.rollupOptions.moduleTypes[key] === undefined && + value !== 'copy' && + value !== 'css' && + value !== 'default' && + value !== 'file' && + value !== 'local-css' + ) { + optimizeDeps.rollupOptions.moduleTypes[key] = value + } + } + } + setResolveOptions('symlinks', optimizeDeps.esbuildOptions.preserveSymlinks) + setResolveOptions( + 'extensions', + optimizeDeps.esbuildOptions.resolveExtensions, + ) + setResolveOptions('mainFields', optimizeDeps.esbuildOptions.mainFields) + setResolveOptions('conditionNames', optimizeDeps.esbuildOptions.conditions) + if ( + optimizeDeps.esbuildOptions.keepNames !== undefined && + optimizeDeps.rollupOptions.keepNames === undefined + ) { + optimizeDeps.rollupOptions.keepNames = + optimizeDeps.esbuildOptions.keepNames + } + + if ( + optimizeDeps.esbuildOptions.platform !== undefined && + optimizeDeps.rollupOptions.platform === undefined + ) { + optimizeDeps.rollupOptions.platform = optimizeDeps.esbuildOptions.platform + } + + // NOTE: the following options cannot be converted + // - legalComments + // - target, supported (Vite used to transpile down to `ESBUILD_MODULES_TARGET`) + // - ignoreAnnotations + // - jsx, jsxFactory, jsxFragment, jsxImportSource, jsxDev, jsxSideEffects + // - tsconfigRaw, tsconfig + + // NOTE: the following options can be converted but probably not worth it + // - sourceRoot + // - sourcesContent (`output.sourcemapExcludeSources` is not supported by rolldown) + // - drop + // - dropLabels + // - mangleProps, reserveProps, mangleQuoted, mangleCache + // - minifyWhitespace, minifyIdentifiers, minifySyntax + // - lineLimit + // - charset + // - pure (`treeshake.manualPureFunctions` is not supported by rolldown) + // - alias (it probably does not work the same with `resolve.alias`) + // - inject + // - banner, footer + // - nodePaths + + // NOTE: the following options does not make sense to set / convert it + // - globalName (we only use ESM format) + // - color + // - logLimit + // - logOverride + // - splitting + // - outbase + // - packages (this should not be set) + // - allowOverwrite + // - publicPath (`file` loader is not supported by rolldown) + // - entryNames, chunkNames, assetNames (Vite does not support changing these options) + // - stdin + // - absWorkingDir + } + return mergeWithDefaults( { ...configDefaults.optimizeDeps, @@ -1005,6 +1148,21 @@ function resolveDepOptimizationOptions( ) } +function applyDepOptimizationOptionCompat(resolvedConfig: ResolvedConfig) { + if ( + resolvedConfig.optimizeDeps.esbuildOptions?.plugins && + resolvedConfig.optimizeDeps.esbuildOptions.plugins.length > 0 + ) { + resolvedConfig.optimizeDeps.rollupOptions ??= {} + resolvedConfig.optimizeDeps.rollupOptions.plugins ||= [] + ;(resolvedConfig.optimizeDeps.rollupOptions.plugins as any[]).push( + ...resolvedConfig.optimizeDeps.esbuildOptions.plugins.map((plugin) => + convertEsbuildPluginToRolldownPlugin(plugin), + ), + ) + } +} + export async function resolveConfig( inlineConfig: InlineConfig, command: 'build' | 'serve', @@ -1410,6 +1568,17 @@ export async function resolveConfig( const preview = resolvePreviewOptions(config.preview, server) + let oxc: OxcOptions | false | undefined = config.oxc + if (config.esbuild) { + if (config.oxc) { + logger.warn( + `Found esbuild and oxc options, will use oxc and ignore esbuild at transformer.`, + ) + } else { + oxc = convertEsbuildConfigToOxcConfig(config.esbuild, logger) + } + } + resolved = { configFile: configFile ? normalizePath(configFile) : undefined, configFileDependencies: configFileDependencies.map((name) => @@ -1431,6 +1600,7 @@ export async function resolveConfig( plugins: userPlugins, // placeholder to be replaced css: resolveCSSOptions(config.css), json: mergeWithDefaults(configDefaults.json, config.json ?? {}), + // preserve esbuild for buildEsbuildPlugin esbuild: config.esbuild === false ? false @@ -1438,6 +1608,19 @@ export async function resolveConfig( jsxDev: !isProduction, ...config.esbuild, }, + oxc: + oxc === false + ? false + : { + ...oxc, + jsx: + typeof oxc?.jsx === 'string' + ? oxc.jsx + : { + development: oxc?.jsx?.development ?? !isProduction, + ...oxc?.jsx, + }, + }, server, builder, preview, @@ -1459,6 +1642,7 @@ export async function resolveConfig( experimental: { importGlobRestoreExtension: false, hmrPartialAccept: false, + enableNativePlugin: false, ...config.experimental, }, future: config.future, @@ -1568,6 +1752,8 @@ export async function resolveConfig( resolved.build.ssrEmitAssets || resolved.build.emitAssets } + applyDepOptimizationOptionCompat(resolved) + debug?.(`using resolved config: %O`, { ...resolved, plugins: resolved.plugins.map((p) => p.name), @@ -1823,19 +2009,14 @@ async function bundleConfigFile( const dirnameVarName = '__vite_injected_original_dirname' const filenameVarName = '__vite_injected_original_filename' const importMetaUrlVarName = '__vite_injected_original_import_meta_url' - const result = await build({ - absWorkingDir: process.cwd(), - entryPoints: [fileName], - write: false, - target: [`node${process.versions.node}`], + + const bundle = await rolldown({ + input: fileName, + // target: [`node${process.versions.node}`], platform: 'node', - bundle: true, - format: isESM ? 'esm' : 'cjs', - mainFields: ['main'], - sourcemap: 'inline', - // the last slash is needed to make the path correct - sourceRoot: path.dirname(fileName) + path.sep, - metafile: true, + resolve: { + mainFields: ['main'], + }, define: { __dirname: dirnameVarName, __filename: filenameVarName, @@ -1843,48 +2024,45 @@ async function bundleConfigFile( 'import.meta.dirname': dirnameVarName, 'import.meta.filename': filenameVarName, }, + // disable treeshake to include files that is not sideeffectful to `moduleIds` + treeshake: false, plugins: [ - { - name: 'externalize-deps', - setup(build) { - const packageCache = new Map() - const resolveByViteResolver = ( - id: string, - importer: string, - isRequire: boolean, - ) => { - return tryNodeResolve(id, importer, { - root: path.dirname(fileName), - isBuild: true, - isProduction: true, - preferRelative: false, - tryIndex: true, - mainFields: [], - conditions: [ - 'node', - ...(isModuleSyncConditionEnabled ? ['module-sync'] : []), - ], - externalConditions: [], - external: [], - noExternal: [], - dedupe: [], - extensions: configDefaults.resolve.extensions, - preserveSymlinks: false, - packageCache, - isRequire, - builtins: nodeLikeBuiltins, - })?.id - } - - // externalize bare imports - build.onResolve( - { filter: /^[^.#].*/ }, - async ({ path: id, importer, kind }) => { - if ( - kind === 'entry-point' || - path.isAbsolute(id) || - isNodeBuiltin(id) - ) { + (() => { + const packageCache = new Map() + const resolveByViteResolver = ( + id: string, + importer: string, + isRequire: boolean, + ) => { + return tryNodeResolve(id, importer, { + root: path.dirname(fileName), + isBuild: true, + isProduction: true, + preferRelative: false, + tryIndex: true, + mainFields: [], + conditions: [ + 'node', + ...(isModuleSyncConditionEnabled ? ['module-sync'] : []), + ], + externalConditions: [], + external: [], + noExternal: [], + dedupe: [], + extensions: configDefaults.resolve.extensions, + preserveSymlinks: false, + packageCache, + isRequire, + builtins: nodeLikeBuiltins, + })?.id + } + + return { + name: 'externalize-deps', + resolveId: { + filter: { id: /^[^.#].*/ }, + async handler(id, importer, { kind }) { + if (!importer || path.isAbsolute(id) || isNodeBuiltin(id)) { return } @@ -1892,7 +2070,7 @@ async function bundleConfigFile( // non-node built-in, which esbuild doesn't know how to handle. In that case, we // externalize it so the non-node runtime handles it instead. if (isNodeLikeBuiltin(id)) { - return { external: true } + return { id, external: true } } const isImport = isESM || kind === 'dynamic-import' @@ -1919,44 +2097,83 @@ async function bundleConfigFile( } throw e } + if (!idFsPath) return + // always no-externalize json files as rolldown does not support import attributes + if (idFsPath.endsWith('.json')) { + return idFsPath + } + if (idFsPath && isImport) { idFsPath = pathToFileURL(idFsPath).href } - return { - path: idFsPath, - external: true, - } + return { id: idFsPath, external: true } }, - ) - }, - }, + }, + } + })(), { name: 'inject-file-scope-variables', - setup(build) { - build.onLoad({ filter: /\.[cm]?[jt]s$/ }, async (args) => { - const contents = await fsp.readFile(args.path, 'utf-8') + transform: { + filter: { id: /\.[cm]?[jt]s$/ }, + async handler(code, id) { const injectValues = - `const ${dirnameVarName} = ${JSON.stringify( - path.dirname(args.path), - )};` + - `const ${filenameVarName} = ${JSON.stringify(args.path)};` + + `const ${dirnameVarName} = ${JSON.stringify(path.dirname(id))};` + + `const ${filenameVarName} = ${JSON.stringify(id)};` + `const ${importMetaUrlVarName} = ${JSON.stringify( - pathToFileURL(args.path).href, + pathToFileURL(id).href, )};` - - return { - loader: args.path.endsWith('ts') ? 'ts' : 'js', - contents: injectValues + contents, - } - }) + return { code: injectValues + code, map: null } + }, }, }, ], }) - const { text } = result.outputFiles[0] + const result = await bundle.generate({ + format: isESM ? 'esm' : 'cjs', + sourcemap: 'inline', + sourcemapPathTransform(relative) { + return path.resolve(fileName, relative) + }, + }) + await bundle.close() + + const entryChunk = result.output.find( + (chunk): chunk is OutputChunk => chunk.type === 'chunk' && chunk.isEntry, + )! + const bundleChunks = Object.fromEntries( + result.output.flatMap((c) => (c.type === 'chunk' ? [[c.fileName, c]] : [])), + ) + + const allModules = new Set() + collectAllModules(bundleChunks, entryChunk.fileName, allModules) + allModules.delete(fileName) + return { - code: text, - dependencies: Object.keys(result.metafile.inputs), + code: entryChunk.code, + dependencies: [...allModules], + } +} + +function collectAllModules( + bundle: Record, + fileName: string, + allModules: Set, + analyzedModules = new Set(), +) { + if (analyzedModules.has(fileName)) return + analyzedModules.add(fileName) + + const chunk = bundle[fileName]! + for (const mod of chunk.moduleIds) { + allModules.add(mod) + } + for (const i of chunk.imports) { + analyzedModules.add(i) + collectAllModules(bundle, i, allModules, analyzedModules) + } + for (const i of chunk.dynamicImports) { + analyzedModules.add(i) + collectAllModules(bundle, i, allModules, analyzedModules) } } diff --git a/packages/vite/src/node/constants.ts b/packages/vite/src/node/constants.ts index 0ddc510c74a5b8..b9d4278d96d672 100644 --- a/packages/vite/src/node/constants.ts +++ b/packages/vite/src/node/constants.ts @@ -19,10 +19,10 @@ export const ROLLUP_HOOKS = [ 'banner', 'footer', 'augmentChunkHash', - 'outputOptions', - 'renderDynamicImport', - 'resolveFileUrl', - 'resolveImportMeta', + // 'outputOptions', + // 'renderDynamicImport', + // 'resolveFileUrl', + // 'resolveImportMeta', 'intro', 'outro', 'closeBundle', @@ -32,7 +32,7 @@ export const ROLLUP_HOOKS = [ 'watchChange', 'resolveDynamicImport', 'resolveId', - 'shouldTransformCachedModule', + // 'shouldTransformCachedModule', 'transform', 'onLog', ] satisfies RollupPluginHooks[] diff --git a/packages/vite/src/node/idResolver.ts b/packages/vite/src/node/idResolver.ts index 7860d568fbf419..6f43238ded87e2 100644 --- a/packages/vite/src/node/idResolver.ts +++ b/packages/vite/src/node/idResolver.ts @@ -1,9 +1,9 @@ -import type { PartialResolvedId } from 'rollup' +import type { PartialResolvedId } from 'rolldown' 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' @@ -59,18 +59,36 @@ export function createIdResolver( pluginContainer = await createEnvironmentPluginContainer( environment as Environment, [ + // @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, @@ -93,6 +111,7 @@ export function createIdResolver( if (!pluginContainer) { pluginContainer = await createEnvironmentPluginContainer( environment as Environment, + // @ts-expect-error the aliasPlugin uses rollup types [aliasPlugin({ entries: environment.config.resolve.alias })], undefined, false, diff --git a/packages/vite/src/node/index.ts b/packages/vite/src/node/index.ts index ee5e4e9c820d28..5682088eb98acc 100644 --- a/packages/vite/src/node/index.ts +++ b/packages/vite/src/node/index.ts @@ -1,7 +1,7 @@ -import type * as Rollup from 'rollup' +import type * as Rollup from 'rolldown' export type { Rollup } -export { parseAst, parseAstAsync } from 'rollup/parseAst' +export { parseAst, parseAstAsync } from 'rolldown/parseAst' export { defineConfig, loadConfigFromFile, @@ -19,6 +19,7 @@ export { createIdResolver } from './idResolver' export { formatPostcssSourceMap, preprocessCSS } from './plugins/css' export { transformWithEsbuild } from './plugins/esbuild' +export { transformWithOxc } from './plugins/oxc' export { buildErrorMessage } from './server/middlewares/error' export { @@ -137,8 +138,8 @@ export type { StylusPreprocessorOptions, } from './plugins/css' export type { JsonOptions } from './plugins/json' -export type { TransformOptions as EsbuildTransformOptions } from 'esbuild' -export type { ESBuildOptions, ESBuildTransformResult } from './plugins/esbuild' +export type { EsbuildTransformOptions } from 'types/internal/esbuildOptions' +export type { OxcOptions } from './plugins/oxc' export type { Manifest, ManifestChunk } from './plugins/manifest' export type { ResolveOptions, InternalResolveOptions } from './plugins/resolve' export type { SplitVendorChunkCache } from './plugins/splitVendorChunk' diff --git a/packages/vite/src/node/logger.ts b/packages/vite/src/node/logger.ts index 8bfa027c61fcd2..b55d9351f1df6b 100644 --- a/packages/vite/src/node/logger.ts +++ b/packages/vite/src/node/logger.ts @@ -2,7 +2,7 @@ import readline from 'node:readline' import colors from 'picocolors' -import type { RollupError } from 'rollup' +import type { RollupError } from 'rolldown' import type { ResolvedServerUrls } from './server' export type LogType = 'error' | 'warn' | 'info' diff --git a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts b/packages/vite/src/node/optimizer/esbuildDepPlugin.ts deleted file mode 100644 index 7b065cbb0a8ea0..00000000000000 --- a/packages/vite/src/node/optimizer/esbuildDepPlugin.ts +++ /dev/null @@ -1,347 +0,0 @@ -import path from 'node:path' -import type { ImportKind, Plugin } from 'esbuild' -import { JS_TYPES_RE, KNOWN_ASSET_TYPES } from '../constants' -import type { PackageCache } from '../packages' -import { - escapeRegex, - flattenId, - isBuiltin, - isExternalUrl, - moduleListContains, - normalizePath, -} from '../utils' -import { browserExternalId, optionalPeerDepId } from '../plugins/resolve' -import { isCSSRequest, isModuleCSSRequest } from '../plugins/css' -import type { Environment } from '../environment' -import { createBackCompatIdResolver } from '../idResolver' - -const externalWithConversionNamespace = - 'vite:dep-pre-bundle:external-conversion' -const convertedExternalPrefix = 'vite-dep-pre-bundle-external:' - -const cjsExternalFacadeNamespace = 'vite:cjs-external-facade' -const nonFacadePrefix = 'vite-cjs-external-facade:' - -const externalTypes = [ - 'css', - // supported pre-processor types - 'less', - 'sass', - 'scss', - 'styl', - 'stylus', - 'pcss', - 'postcss', - // wasm - 'wasm', - // known SFC types - 'vue', - 'svelte', - 'marko', - 'astro', - 'imba', - // JSX/TSX may be configured to be compiled differently from how esbuild - // handles it by default, so exclude them as well - 'jsx', - 'tsx', - ...KNOWN_ASSET_TYPES, -] - -export function esbuildDepPlugin( - environment: Environment, - qualified: Record, - external: string[], -): Plugin { - const { isProduction } = environment.config - const { extensions } = environment.config.optimizeDeps - - // remove optimizable extensions from `externalTypes` list - const allExternalTypes = extensions - ? externalTypes.filter((type) => !extensions.includes('.' + type)) - : externalTypes - - // use separate package cache for optimizer as it caches paths around node_modules - // and it's unlikely for the core Vite process to traverse into node_modules again - const esmPackageCache: PackageCache = new Map() - const cjsPackageCache: PackageCache = new Map() - - // default resolver which prefers ESM - const _resolve = createBackCompatIdResolver(environment.getTopLevelConfig(), { - asSrc: false, - scan: true, - packageCache: esmPackageCache, - }) - - // cjs resolver that prefers Node - const _resolveRequire = createBackCompatIdResolver( - environment.getTopLevelConfig(), - { - asSrc: false, - isRequire: true, - scan: true, - packageCache: cjsPackageCache, - }, - ) - - const resolve = ( - id: string, - importer: string, - kind: ImportKind, - resolveDir?: string, - ): Promise => { - let _importer: string - // explicit resolveDir - this is passed only during yarn pnp resolve for - // entries - if (resolveDir) { - _importer = normalizePath(path.join(resolveDir, '*')) - } else { - // map importer ids to file paths for correct resolution - _importer = importer in qualified ? qualified[importer] : importer - } - const resolver = kind.startsWith('require') ? _resolveRequire : _resolve - return resolver(environment, id, _importer) - } - - const resolveResult = (id: string, resolved: string) => { - if (resolved.startsWith(browserExternalId)) { - return { - path: id, - namespace: 'browser-external', - } - } - if (resolved.startsWith(optionalPeerDepId)) { - return { - path: resolved, - namespace: 'optional-peer-dep', - } - } - if (isBuiltin(environment.config.resolve.builtins, resolved)) { - return - } - if (isExternalUrl(resolved)) { - return { - path: resolved, - external: true, - } - } - return { - path: path.resolve(resolved), - } - } - - return { - name: 'vite:dep-pre-bundle', - setup(build) { - // clear package cache when esbuild is finished - build.onEnd(() => { - esmPackageCache.clear() - cjsPackageCache.clear() - }) - - // 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 }) => { - // if the prefix exist, it is already converted to `import`, so set `external: true` - if (id.startsWith(convertedExternalPrefix)) { - return { - path: id.slice(convertedExternalPrefix.length), - external: true, - } - } - - const resolved = await resolve(id, importer, kind) - if (resolved) { - // `resolved` can be javascript even when `id` matches `allExternalTypes` - // due to cjs resolution (e.g. require("./test.pdf") for "./test.pdf.js") - // or package name (e.g. import "some-package.pdf") - if (JS_TYPES_RE.test(resolved)) { - return { - path: resolved, - external: false, - } - } - - if (kind === 'require-call') { - // here it is not set to `external: true` to convert `require` to `import` - return { - path: resolved, - namespace: externalWithConversionNamespace, - } - } - return { - path: resolved, - external: true, - } - } - }, - ) - build.onLoad( - { filter: /./, namespace: externalWithConversionNamespace }, - (args) => { - // import itself with prefix (this is the actual part of require-import conversion) - const modulePath = `"${convertedExternalPrefix}${args.path}"` - return { - contents: - isCSSRequest(args.path) && !isModuleCSSRequest(args.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], - } - } - } - - build.onResolve( - { filter: /^[\w@][^:]/ }, - async ({ path: id, importer, kind }) => { - if (moduleListContains(external, id)) { - return { - path: id, - external: true, - } - } - - // ensure esbuild uses our resolved entries - let entry: { path: string } | undefined - // if this is an entry, return entry namespace resolve result - if (!importer) { - if ((entry = resolveEntry(id))) return entry - // check if this is aliased to an entry - also return entry namespace - const aliased = await _resolve(environment, id, undefined, true) - if (aliased && (entry = resolveEntry(aliased))) { - return entry - } - } - - // use vite's own resolver - const resolved = await resolve(id, importer, kind) - if (resolved) { - 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.\`) - } - } -}))`, - } - } - }, - ) - - 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?\`)`, - } - } - }, - ) - }, - } -} - -const matchesEntireLine = (text: string) => `^${escapeRegex(text)}$` - -// 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( - externals: string[], - platform: 'node' | 'browser' | 'neutral', -): Plugin { - 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, - } - }) - - 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') { - return { - path: args.path, - namespace: cjsExternalFacadeNamespace, - } - } - - return { - path: args.path, - external: true, - } - }) - - 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/index.ts b/packages/vite/src/node/optimizer/index.ts index 1951c4ce92d86b..4a4ceb555acb0e 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 { DepsOptimizerEsbuildOptions } from 'types/internal/esbuildOptions' 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 { METADATA_FILENAME } from '../constants' import { isWindows } from '../../shared/utils' import type { Environment } from '../environment' -import { esbuildCjsExternalPlugin, esbuildDepPlugin } from './esbuildDepPlugin' +import { transformWithOxc } from '../plugins/oxc' 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 @@ -90,19 +96,13 @@ export interface DepOptimizationConfig { * * https://esbuild.github.io/api */ - esbuildOptions?: Omit< - EsbuildBuildOptions, - | 'bundle' - | 'entryPoints' - | 'external' - | 'write' - | 'watch' - | 'outdir' - | 'outfile' - | 'outbase' - | 'outExtension' - | 'metafile' - > + esbuildOptions?: DepsOptimizerEsbuildOptions + 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 +201,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 +615,7 @@ export function runOptimizeDeps( const start = performance.now() - const preparedRun = prepareEsbuildOptimizerRun( + const preparedRun = prepareRolldownOptimizerRun( environment, depsInfo, processingCacheDir, @@ -622,64 +623,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 +677,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 +689,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 +707,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 +734,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 +765,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 +776,53 @@ 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', + }) + await bundle.close() + return result + } + + function cancel() { + canceled = true + } + + return { context: { build, cancel }, idToExports } } export async function addManuallyIncludedOptimizeDeps( @@ -1047,19 +1019,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 +1050,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 +1058,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 +1103,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.`, ) - const transformed = await transformWithEsbuild( + if (lang !== 'jsx' && lang !== 'tsx' && lang !== 'ts') { + throw new Error(`Unable to parse : ${filePath}.`) + } + const transformed = await transformWithOxc( entryContent, filePath, - { loader }, + { lang }, undefined, environment.config, ) diff --git a/packages/vite/src/node/optimizer/pluginConverter.ts b/packages/vite/src/node/optimizer/pluginConverter.ts new file mode 100644 index 00000000000000..fbec09de4c6f39 --- /dev/null +++ b/packages/vite/src/node/optimizer/pluginConverter.ts @@ -0,0 +1,291 @@ +import { dirname } from 'node:path' +import type * as esbuild from 'esbuild' +import type { + ImportKind, + LoadResult, + PluginContext, + ResolveIdResult, + Plugin as RolldownPlugin, + RolldownPluginOption, +} from 'rolldown' + +type MaybePromise = T | Promise +type EsbuildOnResolveCallback = ( + args: esbuild.OnResolveArgs, +) => MaybePromise +type EsbuildOnLoadCallback = ( + args: esbuild.OnLoadArgs, +) => MaybePromise +type ResolveIdHandler = ( + this: PluginContext, + id: string, + importer: string | undefined, + opts: { kind: ImportKind }, +) => MaybePromise +type LoadHandler = (this: PluginContext, id: string) => MaybePromise + +export function convertEsbuildPluginToRolldownPlugin( + esbuildPlugin: esbuild.Plugin, +): RolldownPlugin { + const onStartCallbacks: Array<() => void> = [] + const onEndCallbacks: Array<(buildResult: esbuild.BuildResult) => void> = [] + const onDisposeCallbacks: Array<() => void> = [] + let resolveIdHandlers: ResolveIdHandler[] + let loadHandlers: LoadHandler[] + + let isSetupDone = false + const setup = async ( + plugins: RolldownPluginOption[], + platform: 'browser' | 'node' | 'neutral', + ) => { + const onResolveCallbacks: Array< + [options: esbuild.OnResolveOptions, callback: EsbuildOnResolveCallback] + > = [] + const onLoadCallbacks: Array< + [options: esbuild.OnLoadOptions, callback: EsbuildOnLoadCallback] + > = [] + + const pluginBuild: esbuild.PluginBuild = { + initialOptions: new Proxy( + { + platform, + plugins: + plugins?.flatMap((p) => + p && 'name' in p + ? [ + { + name: p.name, + // eslint-disable-next-line @typescript-eslint/no-empty-function + setup() {}, + }, + ] + : [], + ) ?? [], + }, + { + get(target, p, _receiver) { + if (p in target) return (target as any)[p] + throw new Error('Not implemented') + }, + }, + ) as esbuild.BuildOptions, + resolve() { + throw new Error('Not implemented') + }, + onStart(callback) { + onStartCallbacks.push(callback) + }, + onEnd(callback) { + onEndCallbacks.push(callback) + }, + onResolve(options, callback) { + onResolveCallbacks.push([options, callback]) + }, + onLoad(options, callback) { + onLoadCallbacks.push([options, callback]) + }, + onDispose(callback) { + onDisposeCallbacks.push(callback) + }, + get esbuild(): esbuild.PluginBuild['esbuild'] { + throw new Error('Not implemented') + }, + set esbuild(_: unknown) { + throw new Error('Not implemented') + }, + } + + await esbuildPlugin.setup(pluginBuild) + + resolveIdHandlers = onResolveCallbacks.map(([options, callback]) => + createResolveIdHandler(options, callback), + ) + loadHandlers = onLoadCallbacks.map(([options, callback]) => + createLoadHandler(options, callback), + ) + isSetupDone = true + } + + return { + name: esbuildPlugin.name, + async options(inputOptions) { + await setup( + inputOptions.plugins as RolldownPluginOption[], + inputOptions.platform ?? 'node', + ) + }, + async buildStart(inputOptions) { + // options hook is not called for scanner + if (!isSetupDone) { + // inputOptions.plugins is not available for buildStart hook + // put a dummy plugin to tell that this is a scan + await setup( + [{ name: 'vite:dep-scan' }], + inputOptions.platform ?? 'node', + ) + } + + for (const cb of onStartCallbacks) { + cb() + } + }, + generateBundle() { + const buildResult = new Proxy( + {}, + { + get(_target, _prop) { + throw new Error('Not implemented') + }, + }, + ) as esbuild.BuildResult + for (const cb of onEndCallbacks) { + cb(buildResult) + } + }, + async resolveId(id, importer, opts) { + for (const handler of resolveIdHandlers) { + const result = await handler.call(this, id, importer, opts) + if (result) { + return result + } + } + }, + async load(id) { + for (const handler of loadHandlers) { + const result = await handler.call(this, id) + if (result) { + return result + } + } + }, + closeBundle() { + if (!this.meta.watchMode) { + for (const cb of onDisposeCallbacks) { + cb() + } + } + }, + closeWatcher() { + for (const cb of onDisposeCallbacks) { + cb() + } + }, + } +} + +function createResolveIdHandler( + options: esbuild.OnResolveOptions, + callback: EsbuildOnResolveCallback, +): ResolveIdHandler { + return async function (id, importer, opts) { + const [importerWithoutNamespace, importerNamespace] = + idToPathAndNamespace(importer) + if ( + options.namespace !== undefined && + options.namespace !== importerNamespace + ) { + return + } + if (options.filter !== undefined && !options.filter.test(id)) { + return + } + + const result = await callback({ + path: id, + importer: importerWithoutNamespace ?? '', + namespace: importerNamespace, + resolveDir: dirname(importerWithoutNamespace ?? ''), + kind: + importerWithoutNamespace === undefined + ? 'entry-point' + : opts.kind === 'import' + ? 'import-statement' + : opts.kind, + pluginData: {}, + with: {}, + }) + if (!result) return + if (result.errors && result.errors.length > 0) { + throw new AggregateError(result.errors) + } + if ( + (result.warnings && result.warnings.length > 0) || + (result.watchDirs && result.watchDirs.length > 0) || + !result.path + ) { + throw new Error('not implemented') + } + for (const file of result.watchFiles ?? []) { + this.addWatchFile(file) + } + + return { + id: result.namespace ? `${result.namespace}:${result.path}` : result.path, + external: result.external, + moduleSideEffects: result.sideEffects, + } + } +} + +function createLoadHandler( + options: esbuild.OnLoadOptions, + callback: EsbuildOnLoadCallback, +): LoadHandler { + const textDecoder = new TextDecoder() + return async function (id) { + const [idWithoutNamespace, idNamespace] = idToPathAndNamespace(id) + if (options.namespace !== undefined && options.namespace !== idNamespace) { + return + } + if (options.filter !== undefined && !options.filter.test(id)) { + return + } + + const result = await callback.call(this, { + path: idWithoutNamespace, + namespace: idNamespace, + suffix: '', + pluginData: {}, + with: {}, + }) + if (!result) return + if (result.errors && result.errors.length > 0) { + throw new AggregateError(result.errors) + } + if ( + (result.warnings && result.warnings.length > 0) || + (result.watchDirs && result.watchDirs.length > 0) || + !result.contents + ) { + throw new Error('not implemented') + } + for (const file of result.watchFiles ?? []) { + this.addWatchFile(file) + } + + return { + code: + typeof result.contents === 'string' + ? result.contents + : textDecoder.decode(result.contents), + moduleType: result.loader, + } + } +} + +function idToPathAndNamespace(id: string): [path: string, namespace: string] +function idToPathAndNamespace( + id: string | undefined, +): [path: string | undefined, namespace: string] +function idToPathAndNamespace( + id: string | undefined, +): [path: string | undefined, namespace: string] { + if (id === undefined) return [undefined, 'file'] + + const namespaceIndex = id.indexOf(':') + if (namespaceIndex >= 0) { + return [id.slice(namespaceIndex + 1), id.slice(0, namespaceIndex)] + } else { + return [id, 'file'] + } +} diff --git a/packages/vite/src/node/optimizer/rolldownDepPlugin.ts b/packages/vite/src/node/optimizer/rolldownDepPlugin.ts new file mode 100644 index 00000000000000..78f29a404c3343 --- /dev/null +++ b/packages/vite/src/node/optimizer/rolldownDepPlugin.ts @@ -0,0 +1,366 @@ +import path from 'node:path' +import type { ImportKind, Plugin, RolldownPlugin } from 'rolldown' +import { JS_TYPES_RE, KNOWN_ASSET_TYPES } from '../constants' +import type { PackageCache } from '../packages' +import { + escapeRegex, + flattenId, + isBuiltin, + isExternalUrl, + moduleListContains, + normalizePath, +} from '../utils' +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' +const convertedExternalPrefix = 'vite-dep-pre-bundle-external:' + +const cjsExternalFacadeNamespace = 'vite:cjs-external-facade' +const nonFacadePrefix = 'vite-cjs-external-facade:' + +const externalTypes = [ + 'css', + // supported pre-processor types + 'less', + 'sass', + 'scss', + 'styl', + 'stylus', + 'pcss', + 'postcss', + // wasm + 'wasm', + // known SFC types + 'vue', + 'svelte', + 'marko', + 'astro', + 'imba', + // JSX/TSX may be configured to be compiled differently from how esbuild + // handles it by default, so exclude them as well + 'jsx', + 'tsx', + ...KNOWN_ASSET_TYPES, +] + +const optionalPeerDepNamespace = 'optional-peer-dep:' +const browserExternalNamespace = 'browser-external:' + +export function rolldownDepPlugin( + environment: Environment, + qualified: Record, + external: string[], +): RolldownPlugin[] { + const { isProduction } = environment.config + const { extensions } = environment.config.optimizeDeps + + // remove optimizable extensions from `externalTypes` list + const allExternalTypes = extensions + ? externalTypes.filter((type) => !extensions.includes('.' + type)) + : externalTypes + + // use separate package cache for optimizer as it caches paths around node_modules + // and it's unlikely for the core Vite process to traverse into node_modules again + const esmPackageCache: PackageCache = new Map() + const cjsPackageCache: PackageCache = new Map() + + // default resolver which prefers ESM + const _resolve = createBackCompatIdResolver(environment.getTopLevelConfig(), { + asSrc: false, + scan: true, + packageCache: esmPackageCache, + }) + + // cjs resolver that prefers Node + const _resolveRequire = createBackCompatIdResolver( + environment.getTopLevelConfig(), + { + asSrc: false, + isRequire: true, + scan: true, + packageCache: cjsPackageCache, + }, + ) + + const resolve = ( + id: string, + importer: string | undefined, + kind: ImportKind, + resolveDir?: string, + ): Promise => { + let _importer: string | undefined + // explicit resolveDir - this is passed only during yarn pnp resolve for + // entries + if (resolveDir) { + _importer = normalizePath(path.join(resolveDir, '*')) + } else if (importer) { + // map importer ids to file paths for correct resolution + _importer = importer in qualified ? qualified[importer] : importer + } + const resolver = kind.startsWith('require') ? _resolveRequire : _resolve + return resolver(environment, id, _importer) + } + + const resolveResult = (id: string, resolved: string) => { + if (resolved.startsWith(browserExternalId)) { + return { + id: browserExternalNamespace + id, + } + } + if (resolved.startsWith(optionalPeerDepId)) { + return { + id: optionalPeerDepNamespace + resolved, + } + } + if (isBuiltin(environment.config.resolve.builtins, resolved)) { + return + } + if (isExternalUrl(resolved)) { + return { + id: resolved, + external: 'absolute', + } + } + return { + id: path.resolve(resolved), + } + } + + 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 + 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 { + id: id.slice(convertedExternalPrefix.length), + external: 'absolute', + } + } + + const resolved = await resolve(id, importer, kind) + if (resolved) { + // `resolved` can be javascript even when `id` matches `allExternalTypes` + // due to cjs resolution (e.g. require("./test.pdf") for "./test.pdf.js") + // or package name (e.g. import "some-package.pdf") + if (JS_TYPES_RE.test(resolved)) { + return { + // 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, + } + } + + if (kind === 'require-call') { + // here it is not set to `external: true` to convert `require` to `import` + return { + id: externalWithConversionNamespace + resolved, + } + } + return { + id: resolved, + external: 'absolute', + } + } + }, + }, + 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}${path}"` + return { + code: + isCSSRequest(path) && !isModuleCSSRequest(path) + ? `import ${modulePath};` + : `export { default } from ${modulePath};` + + `export * from ${modulePath};`, + } + }, + }, + }, + { + 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 + + if (moduleListContains(external, id)) { + return { + id: id, + external: 'absolute', + } + } + + // 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 + // check if this is aliased to an entry - also return entry namespace + const aliased = await _resolve(environment, id, undefined, true) + if (aliased && (entry = resolveEntry(aliased))) { + return entry + } + } + + // use vite's own resolver + const resolved = await resolve(id, importer, kind) + if (resolved) { + return resolveResult(id, resolved) + } + }, + }, + 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.\`) + } + } + }))`, + } + } + } + + 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)}$` + +// 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 rolldownCjsExternalPlugin( + externals: string[], + platform: 'node' | 'browser' | 'neutral', +): Plugin { + const filter = new RegExp(externals.map(matchesEntireLine).join('|')) + + return { + name: 'cjs-external', + resolveId: { + filter: { id: [new RegExp(`^${nonFacadePrefix}`), filter] }, + handler(id, _importer, options) { + if (id.startsWith(nonFacadePrefix)) { + return { + id: id.slice(nonFacadePrefix.length), + external: 'absolute', + } + } + + 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 { + id, + external: 'absolute', + } + } + }, + }, + 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;`, + } + } + }, + }, + } +} diff --git a/packages/vite/src/node/optimizer/scan.ts b/packages/vite/src/node/optimizer/scan.ts index e46413321822d1..401abf7da7cfcf 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 //