Skip to content

Commit 7c66129

Browse files
authored
feat: use oxc for lowering (#77)
* feat: use oxc for lowering * chore: skip modulepreload polyfill test for now
1 parent f328a00 commit 7c66129

File tree

4 files changed

+187
-8
lines changed

4 files changed

+187
-8
lines changed

packages/vite/src/node/__tests__/plugins/modulePreloadPolyfill/modulePreloadPolyfill.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ const buildProject = ({ format = 'es' as ModuleFormat } = {}) =>
3636
],
3737
}) as Promise<RollupOutput>
3838

39-
describe('load', () => {
39+
// TODO: enable this test after DCE is enabled
40+
describe.skip('load', () => {
4041
it('loads modulepreload polyfill', async ({ expect }) => {
4142
const { output } = await buildProject()
4243
expect(output).toHaveLength(1)

packages/vite/src/node/build.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import type {
4444
import { resolveConfig } from './config'
4545
import type { PartialEnvironment } from './baseEnvironment'
4646
import { buildReporterPlugin } from './plugins/reporter'
47-
import { buildEsbuildPlugin } from './plugins/esbuild'
47+
import { buildOxcPlugin } from './plugins/oxc'
4848
import { type TerserOptions, terserPlugin } from './plugins/terser'
4949
import {
5050
arraify,
@@ -508,8 +508,8 @@ export async function resolveBuildPlugins(config: ResolvedConfig): Promise<{
508508
],
509509
post: [
510510
...buildImportAnalysisPlugin(config),
511-
...(config.esbuild !== false && !enableNativePlugin
512-
? [buildEsbuildPlugin(config)]
511+
...(config.oxc !== false && !enableNativePlugin
512+
? [buildOxcPlugin(config)]
513513
: []),
514514
terserPlugin(config),
515515
...(!config.isWorker

packages/vite/src/node/plugins/oxc.ts

Lines changed: 179 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import path from 'node:path'
2+
import { createRequire } from 'node:module'
23
import type {
34
TransformOptions as OxcTransformOptions,
45
TransformResult as OxcTransformResult,
56
} from 'rolldown/experimental'
67
import { transform } from 'rolldown/experimental'
78
import type { RawSourceMap } from '@ampproject/remapping'
8-
import type { SourceMap } from 'rolldown'
9+
import { type InternalModuleFormat, type SourceMap, rolldown } from 'rolldown'
910
import type { FSWatcher } from 'dep-types/chokidar'
1011
import { TSConfckParseError } from 'tsconfck'
1112
import type { RollupError } from 'rollup'
@@ -23,6 +24,12 @@ import type { ViteDevServer } from '../server'
2324
import type { ESBuildOptions } from './esbuild'
2425
import { loadTsconfigJsonForFile } from './esbuild'
2526

27+
// IIFE content looks like `var MyLib = (function() {`.
28+
const IIFE_BEGIN_RE =
29+
/(?:const|var)\s+\S+\s*=\s*\(?function\([^()]*\)\s*\{\s*"use strict";/
30+
// UMD content looks like `(this, function(exports) {`.
31+
const UMD_BEGIN_RE = /\(this,\s*function\([^()]*\)\s*\{\s*"use strict";/
32+
2633
const jsxExtensionsRE = /\.(?:j|t)sx\b/
2734
const validExtensionRE = /\.\w+$/
2835

@@ -267,6 +274,177 @@ export function oxcPlugin(config: ResolvedConfig): Plugin {
267274
}
268275
}
269276

277+
export const buildOxcPlugin = (config: ResolvedConfig): Plugin => {
278+
return {
279+
name: 'vite:oxc-transpile',
280+
async renderChunk(code, chunk, opts) {
281+
// @ts-expect-error injected by @vitejs/plugin-legacy
282+
if (opts.__vite_skip_esbuild__) {
283+
return null
284+
}
285+
286+
const options = resolveOxcTranspileOptions(config, opts.format)
287+
288+
if (!options) {
289+
return null
290+
}
291+
292+
const res = await transformWithOxc(
293+
this,
294+
code,
295+
chunk.fileName,
296+
options,
297+
undefined,
298+
config,
299+
)
300+
301+
const runtimeHelpers = Object.entries(res.helpersUsed)
302+
if (runtimeHelpers.length > 0) {
303+
const helpersCode = await generateRuntimeHelpers(runtimeHelpers)
304+
switch (opts.format) {
305+
case 'es': {
306+
if (res.code.startsWith('#!')) {
307+
let secondLinePos = res.code.indexOf('\n')
308+
if (secondLinePos === -1) {
309+
secondLinePos = 0
310+
}
311+
// inject after hashbang
312+
res.code =
313+
res.code.slice(0, secondLinePos) +
314+
helpersCode +
315+
res.code.slice(secondLinePos)
316+
if (res.map) {
317+
res.map.mappings = res.map.mappings.replace(';', ';;')
318+
}
319+
} else {
320+
res.code = helpersCode + res.code
321+
if (res.map) {
322+
res.map.mappings = ';' + res.map.mappings
323+
}
324+
}
325+
break
326+
}
327+
case 'cjs': {
328+
if (/^\s*['"]use strict['"];/.test(res.code)) {
329+
// inject after use strict
330+
res.code = res.code.replace(
331+
/^\s*['"]use strict['"];/,
332+
(m) => m + helpersCode,
333+
)
334+
// no need to update sourcemap because the runtime helpers are injected in the same line with "use strict"
335+
} else {
336+
res.code = helpersCode + res.code
337+
if (res.map) {
338+
res.map.mappings = ';' + res.map.mappings
339+
}
340+
}
341+
break
342+
}
343+
// runtime helpers needs to be injected inside the UMD and IIFE wrappers
344+
// to avoid collision with other globals.
345+
// We inject the helpers inside the wrappers.
346+
// e.g. turn:
347+
// (function(){ /*actual content/* })()
348+
// into:
349+
// (function(){ <runtime helpers> /*actual content/* })()
350+
// Not using regex because it's too hard to rule out performance issues like #8738 #8099 #10900 #14065
351+
// Instead, using plain string index manipulation (indexOf, slice) which is simple and performant
352+
// We don't need to create a MagicString here because both the helpers and
353+
// the headers don't modify the sourcemap
354+
case 'iife':
355+
case 'umd': {
356+
const m = (
357+
opts.format === 'iife' ? IIFE_BEGIN_RE : UMD_BEGIN_RE
358+
).exec(res.code)
359+
if (!m) {
360+
this.error('Unexpected IIFE format')
361+
return
362+
}
363+
const pos = m.index + m.length
364+
res.code =
365+
res.code.slice(0, pos) + helpersCode + '\n' + res.code.slice(pos)
366+
break
367+
}
368+
case 'app': {
369+
throw new Error('format: "app" is not supported yet')
370+
break
371+
}
372+
default: {
373+
opts.format satisfies never
374+
}
375+
}
376+
}
377+
378+
return res
379+
},
380+
}
381+
}
382+
383+
export function resolveOxcTranspileOptions(
384+
config: ResolvedConfig,
385+
format: InternalModuleFormat,
386+
): OxcTransformOptions | null {
387+
const target = config.build.target
388+
if (!target || target === 'esnext') {
389+
return null
390+
}
391+
392+
return {
393+
...(config.oxc || {}),
394+
helpers: { mode: 'External' },
395+
lang: 'js',
396+
sourceType: format === 'es' ? 'module' : 'script',
397+
target: target || undefined,
398+
sourcemap: !!config.build.sourcemap,
399+
}
400+
}
401+
402+
let rolldownDir: string
403+
404+
async function generateRuntimeHelpers(
405+
runtimeHelpers: readonly [string, string][],
406+
): Promise<string> {
407+
if (!rolldownDir) {
408+
let dir = createRequire(import.meta.url).resolve('rolldown')
409+
while (dir && path.basename(dir) !== 'rolldown') {
410+
dir = path.dirname(dir)
411+
}
412+
rolldownDir = dir
413+
}
414+
415+
const bundle = await rolldown({
416+
cwd: rolldownDir,
417+
input: 'entrypoint',
418+
platform: 'neutral',
419+
plugins: [
420+
{
421+
name: 'entrypoint',
422+
resolveId: {
423+
filter: { id: /^entrypoint$/ },
424+
handler: (id) => id,
425+
},
426+
load: {
427+
filter: { id: /^entrypoint$/ },
428+
handler() {
429+
return runtimeHelpers
430+
.map(
431+
([name, helper]) =>
432+
`export { default as ${name} } from ${JSON.stringify(helper)};`,
433+
)
434+
.join('\n')
435+
},
436+
},
437+
},
438+
],
439+
})
440+
const output = await bundle.generate({
441+
format: 'iife',
442+
name: 'babelHelpers',
443+
minify: true,
444+
})
445+
return output.output[0].code
446+
}
447+
270448
export function convertEsbuildConfigToOxcConfig(
271449
esbuildConfig: ESBuildOptions,
272450
logger: Logger,

playground/lib/__tests__/lib.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ describe.runIf(isBuild)('build', () => {
2424
// esbuild helpers are injected inside of the UMD wrapper
2525
expect(code).toMatch(/^\(function\(/)
2626
expect(noMinifyCode).toMatch(
27-
/^\(function\(global.+?"use strict";var.+?function\smyLib\(/s,
27+
/^\/\*[^*]*\*\/\s*\(function\(global.+?"use strict";\s*var.+?function\smyLib\(/s,
2828
)
2929
expect(namedCode).toMatch(/^\(function\(/)
3030
})
@@ -39,7 +39,7 @@ describe.runIf(isBuild)('build', () => {
3939
// esbuild helpers are injected inside of the IIFE wrapper
4040
expect(code).toMatch(/^var MyLib=function\(\)\{\s*"use strict";/)
4141
expect(noMinifyCode).toMatch(
42-
/^var MyLib\s*=\s*function\(\)\s*\{\s*"use strict";/,
42+
/^\/\*[^*]*\*\/\s*var MyLib\s*=\s*function\(\)\s*\{\s*"use strict";/,
4343
)
4444
expect(namedCode).toMatch(
4545
/^var MyLibNamed=function\([^()]+\)\{\s*"use strict";/,
@@ -51,7 +51,7 @@ describe.runIf(isBuild)('build', () => {
5151
'dist/helpers-injection/my-lib-custom-filename.iife.js',
5252
)
5353
expect(code).toMatch(
54-
`'"use strict"; return (' + expressionSyntax + ").constructor;"`,
54+
`\\"use strict\\"; return (" + expressionSyntax + ").constructor;"`,
5555
)
5656
})
5757

0 commit comments

Comments
 (0)