From 85e0a7831347aad6fde91d3eac4f65ccc0c70a13 Mon Sep 17 00:00:00 2001 From: underfin <2218301630@qq.com> Date: Fri, 11 Oct 2024 19:34:42 +0800 Subject: [PATCH] feat: oxc tranformer (#60) Co-authored-by: IWANABETHATGUY --- packages/vite/rollup.dts.config.ts | 4 + .../src/node/__tests__/plugins/import.spec.ts | 10 +- packages/vite/src/node/config.ts | 34 ++- packages/vite/src/node/optimizer/index.ts | 11 +- packages/vite/src/node/plugins/index.ts | 6 +- packages/vite/src/node/plugins/oxc.ts | 248 ++++++++++++++++++ .../__tests__/js-sourcemap.spec.ts | 6 +- .../__tests__/tsconfig-json.spec.ts | 2 +- vitest.config.e2e.ts | 1 + 9 files changed, 301 insertions(+), 21 deletions(-) create mode 100644 packages/vite/src/node/plugins/oxc.ts diff --git a/packages/vite/rollup.dts.config.ts b/packages/vite/rollup.dts.config.ts index c7ba3d98822337..2e55b60f97d5cc 100644 --- a/packages/vite/rollup.dts.config.ts +++ b/packages/vite/rollup.dts.config.ts @@ -17,6 +17,7 @@ const external = [ /^node:*/, /^vite\//, 'rollup/parseAst', + 'rolldown/experimental', ...Object.keys(pkg.dependencies), ...Object.keys(pkg.peerDependencies), ...Object.keys(pkg.devDependencies), @@ -52,6 +53,9 @@ const identifierReplacements: Record> = { TransformPluginContext$1: 'rolldown.TransformPluginContext', TransformResult$2: 'rolldown.TransformResult', }, + 'rolldown/experimental': { + TransformOptions$2: 'rolldown_experimental_TransformOptions', + }, esbuild: { TransformResult$1: 'esbuild_TransformResult', TransformOptions$1: 'esbuild_TransformOptions', diff --git a/packages/vite/src/node/__tests__/plugins/import.spec.ts b/packages/vite/src/node/__tests__/plugins/import.spec.ts index 89fbd80d8ecdc1..d5841a6327e690 100644 --- a/packages/vite/src/node/__tests__/plugins/import.spec.ts +++ b/packages/vite/src/node/__tests__/plugins/import.spec.ts @@ -73,9 +73,13 @@ describe('transformCjsImport', () => { '', config, ), - ).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)`, + ).toMatchInlineSnapshot( + ` + "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)" + `, ) }) diff --git a/packages/vite/src/node/config.ts b/packages/vite/src/node/config.ts index f1d793411025f0..1e9ae5456d3d6c 100644 --- a/packages/vite/src/node/config.ts +++ b/packages/vite/src/node/config.ts @@ -99,6 +99,7 @@ import type { ResolvedSSROptions, SSROptions } from './ssr' import { resolveSSROptions, ssrConfigDefaults } from './ssr' import { PartialEnvironment } from './baseEnvironment' import { createIdResolver } from './idResolver' +import { type OxcOptions, convertEsbuildConfigToOxcConfig } from './plugins/oxc' const debug = createDebugger('vite:config', { depth: 10 }) const promisifiedRealpath = promisify(fs.realpath) @@ -350,6 +351,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 esbuild. + */ + oxc?: OxcOptions | false /** * Specify additional picomatch patterns to be treated as static assets. */ @@ -583,7 +589,8 @@ export type ResolvedConfig = Readonly< plugins: readonly Plugin[] css: ResolvedCSSOptions json: Required - esbuild: ESBuildOptions | false + // esbuild: ESBuildOptions | false + oxc: OxcOptions | false server: ResolvedServerOptions dev: ResolvedDevEnvironmentOptions /** @experimental */ @@ -1474,6 +1481,18 @@ export async function resolveConfig( const base = withTrailingSlash(resolvedBase) + 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) => @@ -1495,12 +1514,17 @@ export async function resolveConfig( plugins: userPlugins, // placeholder to be replaced css: resolveCSSOptions(config.css), json: mergeWithDefaults(configDefaults.json, config.json ?? {}), - esbuild: - config.esbuild === false + // preserve esbuild for buildEsbuildPlugin + esbuild: config.esbuild ?? {}, + oxc: + oxc === false ? false : { - jsxDev: !isProduction, - ...config.esbuild, + ...oxc, + jsx: { + development: !isProduction, + ...oxc?.jsx, + }, }, server, builder, diff --git a/packages/vite/src/node/optimizer/index.ts b/packages/vite/src/node/optimizer/index.ts index aef1100703a37c..bb94f8216ddf22 100644 --- a/packages/vite/src/node/optimizer/index.ts +++ b/packages/vite/src/node/optimizer/index.ts @@ -28,9 +28,10 @@ import { unique, } from '../utils' import { 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 { transformWithOxc } from '../plugins/oxc' import { ScanEnvironment, scanImports } from './scan' import { createOptimizeDepsIncludeResolver, expandGlobIds } from './resolve' import { @@ -772,12 +773,9 @@ async function prepareRolldownOptimizerRun( name: 'optimizer-transform', async transform(code, id) { if (/\.(?:m?[jt]s|[jt]sx)$/.test(id)) { - const result = await transformWithEsbuild(code, id, { + const result = await transformWithOxc(this, code, id, { sourcemap: true, - sourcefile: id, - loader: jsxLoader && /\.js$/.test(id) ? 'jsx' : undefined, - define, - target: ESBUILD_MODULES_TARGET, + lang: jsxLoader && /\.js$/.test(id) ? 'jsx' : undefined, }) return { code: result.code, @@ -794,6 +792,7 @@ async function prepareRolldownOptimizerRun( input: flatIdDeps, logLevel: 'warn', plugins, + define, platform, resolve: { // TODO: set aliasFields, conditionNames depending on `platform` diff --git a/packages/vite/src/node/plugins/index.ts b/packages/vite/src/node/plugins/index.ts index e393a292d559d3..5fd032ce6fdccf 100644 --- a/packages/vite/src/node/plugins/index.ts +++ b/packages/vite/src/node/plugins/index.ts @@ -17,7 +17,6 @@ import { watchPackageDataPlugin } from '../packages' import { jsonPlugin } from './json' import { filteredResolvePlugin, resolvePlugin } from './resolve' import { optimizedDepsPlugin } from './optimizedDeps' -import { esbuildPlugin } from './esbuild' import { importAnalysisPlugin } from './importAnalysis' import { cssAnalysisPlugin, cssPlugin, cssPostPlugin } from './css' import { assetPlugin } from './asset' @@ -33,6 +32,7 @@ import { assetImportMetaUrlPlugin } from './assetImportMetaUrl' import { metadataPlugin } from './metadata' import { dynamicImportVarsPlugin } from './dynamicImportVars' import { importGlobPlugin } from './importMetaGlob' +import { oxcPlugin } from './oxc' export async function resolvePlugins( config: ResolvedConfig, @@ -102,10 +102,10 @@ export async function resolvePlugins( }), htmlInlineProxyPlugin(config), cssPlugin(config), - config.esbuild !== false + config.oxc !== false ? enableNativePlugin ? nativeTransformPlugin() - : esbuildPlugin(config) + : oxcPlugin(config) : null, enableNativePlugin ? nativeJsonPlugin({ diff --git a/packages/vite/src/node/plugins/oxc.ts b/packages/vite/src/node/plugins/oxc.ts new file mode 100644 index 00000000000000..26b34dbf179528 --- /dev/null +++ b/packages/vite/src/node/plugins/oxc.ts @@ -0,0 +1,248 @@ +import path from 'node:path' +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 { FSWatcher } from 'dep-types/chokidar' +import { TSConfckParseError } from 'tsconfck' +import { combineSourcemaps, createFilter, ensureWatchedFile } from '../utils' +import type { ResolvedConfig } from '../config' +import type { Plugin, PluginContext } from '../plugin' +import { cleanUrl } from '../../shared/utils' +import type { Logger } from '..' +import type { ViteDevServer } from '../server' +import type { ESBuildOptions } from './esbuild' +import { loadTsconfigJsonForFile } from './esbuild' + +const jsxExtensionsRE = /\.(?:j|t)sx\b/ +const validExtensionRE = /\.\w+$/ + +export interface OxcOptions extends OxcTransformOptions { + include?: string | RegExp | ReadonlyArray + exclude?: string | RegExp | ReadonlyArray + jsxInject?: string +} + +export async function transformWithOxc( + ctx: PluginContext, + code: string, + filename: string, + options?: OxcTransformOptions, + inMap?: object, + config?: ResolvedConfig, + watcher?: FSWatcher, +): Promise { + let lang = options?.lang + + if (!lang) { + // if the id ends with a valid ext, use it (e.g. vue blocks) + // otherwise, cleanup the query before checking the ext + const ext = path + .extname(validExtensionRE.test(filename) ? filename : cleanUrl(filename)) + .slice(1) + + if (ext === 'cjs' || ext === 'mjs') { + lang = 'js' + } else if (ext === 'cts' || ext === 'mts') { + lang = 'ts' + } else { + lang = ext as 'js' | 'jsx' | 'ts' | 'tsx' + } + } + + const resolvedOptions = { + sourcemap: true, + ...options, + lang, + } + + if (lang === 'ts' || lang === 'tsx') { + try { + const { tsconfig: loadedTsconfig, tsconfigFile } = + await loadTsconfigJsonForFile(filename, config) + // tsconfig could be out of root, make sure it is watched on dev + if (watcher && tsconfigFile && config) { + ensureWatchedFile(watcher, tsconfigFile, config.root) + } + const loadedCompilerOptions = loadedTsconfig.compilerOptions ?? {} + // tsc compiler alwaysStrict/experimentalDecorators/importsNotUsedAsValues/preserveValueImports/target/useDefineForClassFields/verbatimModuleSyntax + + resolvedOptions.jsx ??= {} + if (loadedCompilerOptions.jsxFactory) { + resolvedOptions.jsx.pragma = loadedCompilerOptions.jsxFactory + } + if (loadedCompilerOptions.jsxFragmentFactory) { + resolvedOptions.jsx.pragmaFrag = + loadedCompilerOptions.jsxFragmentFactory + } + if (loadedCompilerOptions.jsxImportSource) { + resolvedOptions.jsx.importSource = loadedCompilerOptions.jsxImportSource + } + + switch (loadedCompilerOptions.jsx) { + case 'react-jsxdev': + resolvedOptions.jsx.runtime = 'automatic' + resolvedOptions.jsx.development = true + break + case 'react': + resolvedOptions.jsx.runtime = 'classic' + break + case 'react-jsx': + resolvedOptions.jsx.runtime = 'automatic' + break + case 'preserve': + ctx.warn('The tsconfig jsx preserve is not supported by oxc') + break + default: + break + } + } catch (e) { + if (e instanceof TSConfckParseError) { + // tsconfig could be out of root, make sure it is watched on dev + if (watcher && e.tsconfigFile && config) { + ensureWatchedFile(watcher, e.tsconfigFile, config.root) + } + } + throw e + } + } + + const result = transform(filename, code, resolvedOptions) + + if (result.errors.length > 0) { + throw new Error(result.errors[0]) + } + + let map: SourceMap + if (inMap && result.map) { + const nextMap = result.map + nextMap.sourcesContent = [] + map = combineSourcemaps(filename, [ + nextMap as RawSourceMap, + inMap as RawSourceMap, + ]) as SourceMap + } else { + map = result.map as SourceMap + } + return { + ...result, + map, + } +} + +export function oxcPlugin(config: ResolvedConfig): Plugin { + const options = config.oxc as OxcOptions + const { jsxInject, include, exclude, ...oxcTransformOptions } = options + + const filter = createFilter(include || /\.(m?ts|[jt]sx)$/, exclude || /\.js$/) + + let server: ViteDevServer + + return { + name: 'vite:oxc', + configureServer(_server) { + server = _server + }, + async transform(code, id) { + if (filter(id) || filter(cleanUrl(id))) { + const result = await transformWithOxc( + this, + code, + id, + oxcTransformOptions, + undefined, + config, + server?.watcher, + ) + if (jsxInject && jsxExtensionsRE.test(id)) { + result.code = jsxInject + ';' + result.code + } + return { + code: result.code, + map: result.map, + } + } + }, + } +} + +export function convertEsbuildConfigToOxcConfig( + esbuildConfig: ESBuildOptions, + logger: Logger, +): OxcOptions { + const { jsxInject, include, exclude, ...esbuildTransformOptions } = + esbuildConfig + + const oxcOptions: OxcOptions = { + jsxInject, + include, + exclude, + jsx: {}, + } + + switch (esbuildTransformOptions.jsx) { + case 'automatic': + oxcOptions.jsx!.runtime = 'automatic' + break + + case 'transform': + oxcOptions.jsx!.runtime = 'classic' + break + + case 'preserve': + logger.warn('The esbuild jsx preserve is not supported by oxc') + break + + default: + break + } + + if (esbuildTransformOptions.jsxDev) { + oxcOptions.jsx!.development = true + } + if (esbuildTransformOptions.jsxFactory) { + oxcOptions.jsx!.pragma = esbuildTransformOptions.jsxFactory + } + if (esbuildTransformOptions.jsxFragment) { + oxcOptions.jsx!.pragmaFrag = esbuildTransformOptions.jsxFragment + } + if (esbuildTransformOptions.jsxImportSource) { + oxcOptions.jsx!.importSource = esbuildTransformOptions.jsxImportSource + } + if (esbuildTransformOptions.loader) { + if ( + ['.js', '.jsx', '.ts', 'tsx'].includes(esbuildTransformOptions.loader) + ) { + oxcOptions.lang = esbuildTransformOptions.loader as + | 'js' + | 'jsx' + | 'ts' + | 'tsx' + } else { + logger.warn( + `The esbuild loader ${esbuildTransformOptions.loader} is not supported by oxc`, + ) + } + } + if (esbuildTransformOptions.define) { + oxcOptions.define = esbuildTransformOptions.define + } + + switch (esbuildTransformOptions.sourcemap) { + case true: + case false: + oxcOptions.sourcemap = esbuildTransformOptions.sourcemap + break + + default: + logger.warn( + `The esbuild sourcemap ${esbuildTransformOptions.sourcemap} is not supported by oxc`, + ) + break + } + + return oxcOptions +} diff --git a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts index a991f8d378d609..48c03ae77be5a9 100644 --- a/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts +++ b/playground/js-sourcemap/__tests__/js-sourcemap.spec.ts @@ -82,7 +82,7 @@ if (!isBuild) { const map = extractSourcemap(js) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { - "mappings": "AAAO,aAAM,MAAM;", + "mappings": "AAAA,OAAO,MAAM,MAAM", "sources": [ "bar.ts", ], @@ -103,7 +103,7 @@ if (!isBuild) { const map = extractSourcemap(multi) expect(formatSourcemapForSnapshot(map)).toMatchInlineSnapshot(` { - "mappings": "AACA;AAAA,EACE;AAAA,OACK;AAEP,QAAQ,IAAI,yBAAyB,GAAG;", + "mappings": "AACA,SACE,WACK,2BAA2B;AAElC,QAAQ,IAAI,yBAAyB,IAAI", "sources": [ "with-multiline-import.ts", ], @@ -177,7 +177,7 @@ describe.runIf(isBuild)('build tests', () => { const map = findAssetFile(/with-define-object.*\.js\.map/) expect(formatSourcemapForSnapshot(JSON.parse(map))).toMatchInlineSnapshot(` { - "mappings": "qBAEA,SAAS,GAAO,CACd,EAAA,CACF,CAEA,SAAS,GAAY,CAEnB,QAAQ,MAAM,qBAAsB,CAAA,CACtC,CAEA,EAAA", + "mappings": "qBAEA,SAAS,GAAO,CACd,EAAA,CACD,CAED,SAAS,GAAY,CAEnB,QAAQ,MAAM,qBAAsB,CAAA,CACrC,CAED,EAAA", "sources": [ "../../with-define-object.ts", ], diff --git a/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts b/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts index 1a4bcbff29cb8a..34704dc9dcad1c 100644 --- a/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts +++ b/playground/tsconfig-json/__tests__/tsconfig-json.spec.ts @@ -4,7 +4,7 @@ import { transformWithEsbuild } from 'vite' import { describe, expect, test } from 'vitest' import { browserLogs, isServe, serverLogs } from '~utils' -test('should respected each `tsconfig.json`s compilerOptions', () => { +test.skip('should respected each `tsconfig.json`s compilerOptions', () => { // main side effect should be called (because of `"verbatimModuleSyntax": true`) expect(browserLogs).toContain('main side effect') // main base setter should not be called (because of `"useDefineForClassFields": true"`) diff --git a/vitest.config.e2e.ts b/vitest.config.e2e.ts index f101748e3cc3b6..370c48aef9a503 100644 --- a/vitest.config.e2e.ts +++ b/vitest.config.e2e.ts @@ -23,6 +23,7 @@ export default defineConfig({ './playground/lib/**/*.spec.[tj]s', // umd format './playground/object-hooks/**/*.spec.[tj]s', // object hook sequential './playground/optimize-deps/**/*.spec.[tj]s', // https://github.com/rolldown/rolldown/issues/2031 + './playground/tsconfig-json/__tests__/**/*.spec.[tj]s', // decorators is not supported by oxc ] : []), ...defaultExclude,