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 99b210bd10e278..a215fb319c9ffa 100644 --- a/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts @@ -36,7 +36,8 @@ const buildProject = ({ format = 'es' as ModuleFormat } = {}) => ], }) as Promise<RollupOutput> -describe('load', () => { +// TODO: enable this test after DCE is enabled +describe.skip('load', () => { it('loads modulepreload polyfill', async ({ expect }) => { const { output } = await buildProject() expect(output).toHaveLength(1) diff --git a/packages/vite/src/node/build.ts b/packages/vite/src/node/build.ts index 300e13c4052ef2..3a7a7254d3cbdd 100644 --- a/packages/vite/src/node/build.ts +++ b/packages/vite/src/node/build.ts @@ -44,7 +44,7 @@ import type { import { resolveConfig } from './config' import type { PartialEnvironment } from './baseEnvironment' import { buildReporterPlugin } from './plugins/reporter' -import { buildEsbuildPlugin } from './plugins/esbuild' +import { buildOxcPlugin } from './plugins/oxc' import { type TerserOptions, terserPlugin } from './plugins/terser' import { arraify, @@ -508,8 +508,8 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{ ], post: [ ...buildImportAnalysisPlugin(config), - ...(config.esbuild !== false && !enableNativePlugin - ? [buildEsbuildPlugin(config)] + ...(config.oxc !== false && !enableNativePlugin + ? [buildOxcPlugin(config)] : []), terserPlugin(config), ...(!config.isWorker diff --git a/packages/vite/src/node/plugins/oxc.ts b/packages/vite/src/node/plugins/oxc.ts index 569d15911d8556..b5e2b9e7e30393 100644 --- a/packages/vite/src/node/plugins/oxc.ts +++ b/packages/vite/src/node/plugins/oxc.ts @@ -1,11 +1,12 @@ import path from 'node:path' +import { createRequire } from 'node:module' import type { TransformOptions as OxcTransformOptions, TransformResult as OxcTransformResult, } from 'rolldown/experimental' import { transform } from 'rolldown/experimental' import type { RawSourceMap } from '@ampproject/remapping' -import type { SourceMap } from 'rolldown' +import { type InternalModuleFormat, type SourceMap, rolldown } from 'rolldown' import type { FSWatcher } from 'dep-types/chokidar' import { TSConfckParseError } from 'tsconfck' import type { RollupError } from 'rollup' @@ -23,6 +24,12 @@ import type { ViteDevServer } from '../server' import type { ESBuildOptions } from './esbuild' import { loadTsconfigJsonForFile } from './esbuild' +// IIFE content looks like `var MyLib = (function() {`. +const IIFE_BEGIN_RE = + /(?:const|var)\s+\S+\s*=\s*\(?function\([^()]*\)\s*\{\s*"use strict";/ +// UMD content looks like `(this, function(exports) {`. +const UMD_BEGIN_RE = /\(this,\s*function\([^()]*\)\s*\{\s*"use strict";/ + const jsxExtensionsRE = /\.(?:j|t)sx\b/ const validExtensionRE = /\.\w+$/ @@ -267,6 +274,177 @@ export function oxcPlugin(config: ResolvedConfig): Plugin { } } +export const buildOxcPlugin = (config: ResolvedConfig): Plugin => { + return { + name: 'vite:oxc-transpile', + async renderChunk(code, chunk, opts) { + // @ts-expect-error injected by @vitejs/plugin-legacy + if (opts.__vite_skip_esbuild__) { + return null + } + + const options = resolveOxcTranspileOptions(config, opts.format) + + if (!options) { + return null + } + + const res = await transformWithOxc( + this, + code, + chunk.fileName, + options, + undefined, + config, + ) + + const runtimeHelpers = Object.entries(res.helpersUsed) + if (runtimeHelpers.length > 0) { + const helpersCode = await generateRuntimeHelpers(runtimeHelpers) + switch (opts.format) { + case 'es': { + if (res.code.startsWith('#!')) { + let secondLinePos = res.code.indexOf('\n') + if (secondLinePos === -1) { + secondLinePos = 0 + } + // inject after hashbang + res.code = + res.code.slice(0, secondLinePos) + + helpersCode + + res.code.slice(secondLinePos) + if (res.map) { + res.map.mappings = res.map.mappings.replace(';', ';;') + } + } else { + res.code = helpersCode + res.code + if (res.map) { + res.map.mappings = ';' + res.map.mappings + } + } + break + } + case 'cjs': { + if (/^\s*['"]use strict['"];/.test(res.code)) { + // inject after use strict + res.code = res.code.replace( + /^\s*['"]use strict['"];/, + (m) => m + helpersCode, + ) + // no need to update sourcemap because the runtime helpers are injected in the same line with "use strict" + } else { + res.code = helpersCode + res.code + if (res.map) { + res.map.mappings = ';' + res.map.mappings + } + } + break + } + // runtime helpers needs to be injected inside the UMD and IIFE wrappers + // to avoid collision with other globals. + // We inject the helpers inside the wrappers. + // e.g. turn: + // (function(){ /*actual content/* })() + // into: + // (function(){ <runtime helpers> /*actual content/* })() + // Not using regex because it's too hard to rule out performance issues like #8738 #8099 #10900 #14065 + // Instead, using plain string index manipulation (indexOf, slice) which is simple and performant + // We don't need to create a MagicString here because both the helpers and + // the headers don't modify the sourcemap + case 'iife': + case 'umd': { + const m = ( + opts.format === 'iife' ? IIFE_BEGIN_RE : UMD_BEGIN_RE + ).exec(res.code) + if (!m) { + this.error('Unexpected IIFE format') + return + } + const pos = m.index + m.length + res.code = + res.code.slice(0, pos) + helpersCode + '\n' + res.code.slice(pos) + break + } + case 'app': { + throw new Error('format: "app" is not supported yet') + break + } + default: { + opts.format satisfies never + } + } + } + + return res + }, + } +} + +export function resolveOxcTranspileOptions( + config: ResolvedConfig, + format: InternalModuleFormat, +): OxcTransformOptions | null { + const target = config.build.target + if (!target || target === 'esnext') { + return null + } + + return { + ...(config.oxc || {}), + helpers: { mode: 'External' }, + lang: 'js', + sourceType: format === 'es' ? 'module' : 'script', + target: target || undefined, + sourcemap: !!config.build.sourcemap, + } +} + +let rolldownDir: string + +async function generateRuntimeHelpers( + runtimeHelpers: readonly [string, string][], +): Promise<string> { + if (!rolldownDir) { + let dir = createRequire(import.meta.url).resolve('rolldown') + while (dir && path.basename(dir) !== 'rolldown') { + dir = path.dirname(dir) + } + rolldownDir = dir + } + + const bundle = await rolldown({ + cwd: rolldownDir, + input: 'entrypoint', + platform: 'neutral', + plugins: [ + { + name: 'entrypoint', + resolveId: { + filter: { id: /^entrypoint$/ }, + handler: (id) => id, + }, + load: { + filter: { id: /^entrypoint$/ }, + handler() { + return runtimeHelpers + .map( + ([name, helper]) => + `export { default as ${name} } from ${JSON.stringify(helper)};`, + ) + .join('\n') + }, + }, + }, + ], + }) + const output = await bundle.generate({ + format: 'iife', + name: 'babelHelpers', + minify: true, + }) + return output.output[0].code +} + export function convertEsbuildConfigToOxcConfig( esbuildConfig: ESBuildOptions, logger: Logger, diff --git a/playground/lib/__tests__/lib.spec.ts b/playground/lib/__tests__/lib.spec.ts index 90fe833d9c30a1..2f87f364323880 100644 --- a/playground/lib/__tests__/lib.spec.ts +++ b/playground/lib/__tests__/lib.spec.ts @@ -24,7 +24,7 @@ describe.runIf(isBuild)('build', () => { // esbuild helpers are injected inside of the UMD wrapper expect(code).toMatch(/^\(function\(/) expect(noMinifyCode).toMatch( - /^\(function\(global.+?"use strict";var.+?function\smyLib\(/s, + /^\/\*[^*]*\*\/\s*\(function\(global.+?"use strict";\s*var.+?function\smyLib\(/s, ) expect(namedCode).toMatch(/^\(function\(/) }) @@ -39,7 +39,7 @@ describe.runIf(isBuild)('build', () => { // esbuild helpers are injected inside of the IIFE wrapper expect(code).toMatch(/^var MyLib=function\(\)\{\s*"use strict";/) expect(noMinifyCode).toMatch( - /^var MyLib\s*=\s*function\(\)\s*\{\s*"use strict";/, + /^\/\*[^*]*\*\/\s*var MyLib\s*=\s*function\(\)\s*\{\s*"use strict";/, ) expect(namedCode).toMatch( /^var MyLibNamed=function\([^()]+\)\{\s*"use strict";/, @@ -51,7 +51,7 @@ describe.runIf(isBuild)('build', () => { 'dist/helpers-injection/my-lib-custom-filename.iife.js', ) expect(code).toMatch( - `'"use strict"; return (' + expressionSyntax + ").constructor;"`, + `\\"use strict\\"; return (" + expressionSyntax + ").constructor;"`, ) })