Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/lower-const-let-to-var.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@lynx-js/rspeedy": minor
"@lynx-js/react-rsbuild-plugin": minor
---

Lower `let`/`const` to `var` in the build output for faster QuickJS parsing. The SWC `transform-block-scoping` pass is added to both the background and main-thread layers (on top of the existing target baseline), and rspack `output.environment.const` is set to `false` so bundler-generated runtime code also uses `var`.
11 changes: 10 additions & 1 deletion packages/rspeedy/core/src/plugins/output.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,17 @@ export function pluginOutput(options?: Output): RsbuildPlugin {
name: 'lynx:rsbuild:output',
setup(api) {
api.modifyRsbuildConfig((config, { mergeRsbuildConfig }) => {
// Default bundler-generated runtime / wrapper code to `var` (QuickJS
// parses it faster than `const`/`let`); the SWC `transform-block-scoping`
// pass handles user source separately. Placed first so user-provided
// `tools.rspack.output.environment.const` can opt out.
const lowerToVar: RsbuildConfig = {
tools: { rspack: { output: { environment: { const: false } } } },
}

if (!options) {
return mergeRsbuildConfig(
lowerToVar,
{
output: {
filename: {
Expand All @@ -41,7 +50,7 @@ export function pluginOutput(options?: Output): RsbuildPlugin {
)
}

return mergeRsbuildConfig(config, {
return mergeRsbuildConfig(lowerToVar, config, {
output: {
distPath: Object.assign(
{},
Expand Down
7 changes: 4 additions & 3 deletions packages/rspeedy/core/src/plugins/swc.plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,13 @@ export function pluginSwc(): RsbuildPlugin {
)
}

// Merge any user `env.include` on top of Rspeedy's baseline.
// `targets` stays owned by Rspeedy; other `env` fields from the
// bundler default (e.g. `mode`) are dropped, as before.
config.env = {
...config.env,
targets: ES_ENV_TARGETS,
include: [
// Lower `let`/`const` to `var`; QuickJS parses `var` faster.
// Listing it in `env.exclude` opts out (exclude > include).
'transform-block-scoping',
...getESVersionEnvInclude(getESVersionTarget(isProd)),
...(config.env?.include ?? []),
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ exports[`Plugins - Output > defaults - Production 1`] = `
"chunkLoading": "lynx",
"devtoolFallbackModuleFilenameTemplate": "[relative-resource-path]?[hash]",
"devtoolModuleFilenameTemplate": "[relative-resource-path]",
"environment": {
"const": false,
},
"filename": "static/js/[name].[contenthash:10].js",
"path": "<ROOT>/dist",
"publicPath": "/",
Expand All @@ -23,6 +26,9 @@ exports[`Plugins - Output > defaults 1`] = `
"chunkLoading": "lynx",
"devtoolFallbackModuleFilenameTemplate": "[relative-resource-path]?[hash]",
"devtoolModuleFilenameTemplate": "[relative-resource-path]",
"environment": {
"const": false,
},
"filename": "static/js/[name].js",
"path": "<ROOT>/dist",
"publicPath": "/",
Expand All @@ -38,6 +44,9 @@ exports[`Plugins - Output > output.filename 1`] = `
"chunkLoading": "lynx",
"devtoolFallbackModuleFilenameTemplate": "[relative-resource-path]?[hash]",
"devtoolModuleFilenameTemplate": "[relative-resource-path]",
"environment": {
"const": false,
},
"filename": "static/js/[name].js",
"path": "<ROOT>/dist",
"publicPath": "/",
Expand All @@ -53,6 +62,9 @@ exports[`Plugins - Output > output.filename.js 1`] = `
"chunkLoading": "lynx",
"devtoolFallbackModuleFilenameTemplate": "[relative-resource-path]?[hash]",
"devtoolModuleFilenameTemplate": "[relative-resource-path]",
"environment": {
"const": false,
},
"filename": "static/js/[name].js",
"path": "<ROOT>/dist",
"publicPath": "/",
Expand Down
28 changes: 28 additions & 0 deletions packages/rspeedy/core/test/plugins/output.plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,34 @@ describe('Plugins - Output', () => {
expect(config.output).toMatchSnapshot()
})

test('lowers const/let to var via output.environment', async () => {
const rsbuild = await createStubRspeedy({})

const config = await rsbuild.unwrapConfig({
action: 'build',
})

// Bundler-generated runtime/wrapper code uses `var` (QuickJS parses it
// faster); SWC `transform-block-scoping` handles user source separately.
expect(config.output?.environment?.const).toBe(false)
})

test('user can opt out of const/let lowering', async () => {
const rsbuild = await createStubRspeedy({
tools: {
rspack: {
output: { environment: { const: true } },
},
},
})

const config = await rsbuild.unwrapConfig({
action: 'build',
})

expect(config.output?.environment?.const).toBe(true)
})

test('output.filename', async () => {
const rsbuild = await createStubRspeedy({
output: {
Expand Down
29 changes: 29 additions & 0 deletions packages/rspeedy/core/test/plugins/swc.plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Plugins - SWC', () => {
"detectSyntax": "auto",
"env": {
"include": [
"transform-block-scoping",
"transform-exponentiation-operator",
"transform-async-to-generator",
"transform-async-generator-functions",
Expand Down Expand Up @@ -87,6 +88,7 @@ describe('Plugins - SWC', () => {
"detectSyntax": "auto",
"env": {
"include": [
"transform-block-scoping",
"transform-nullish-coalescing-operator",
"transform-optional-chaining",
"transform-export-namespace-from",
Expand Down Expand Up @@ -142,6 +144,32 @@ describe('Plugins - SWC', () => {
)
})

test('user can opt out of transform-block-scoping via env.exclude', async () => {
const rsbuild = await createStubRspeedy({
mode: 'production',
tools: {
swc: {
env: {
exclude: ['transform-block-scoping'],
},
},
},
})

const config = await rsbuild.unwrapConfig()
const loaderOptions = getLoaderOptions<Rspack.SwcLoaderOptions>(
config,
'builtin:swc-loader',
)

// SWC's `env.exclude` wins over `include`, so forwarding the user's
// exclude opts out of the let/const → var lowering.
expect(loaderOptions?.env?.exclude).toEqual(['transform-block-scoping'])
expect(loaderOptions?.env?.include).toContain(
'transform-async-to-generator',
)
})

test('user-configured env.include is merged onto the baseline', async () => {
const rsbuild = await createStubRspeedy({
mode: 'production',
Expand Down Expand Up @@ -204,6 +232,7 @@ describe('Plugins - SWC', () => {
"detectSyntax": "auto",
"env": {
"include": [
"transform-block-scoping",
"transform-nullish-coalescing-operator",
"transform-optional-chaining",
"transform-export-namespace-from",
Expand Down
6 changes: 5 additions & 1 deletion packages/rspeedy/plugin-react/src/loaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,12 @@ export function applyLoaders(
...swcLoaderOptions,
jsc,
env: {
...swcLoaderOptions.env,
targets: MAIN_THREAD_ENV_TARGETS,
include: MAIN_THREAD_ENV_INCLUDE,
// Lower `let`/`const` to `var`; QuickJS parses `var` faster.
// Spreading `swcLoaderOptions.env` carries `exclude` through,
// so the background opt-out also applies here.
include: ['transform-block-scoping', ...MAIN_THREAD_ENV_INCLUDE],
Comment thread
upupming marked this conversation as resolved.
},
} satisfies Rspack.SwcLoaderOptions,
)
Expand Down
64 changes: 61 additions & 3 deletions packages/rspeedy/plugin-react/test/swc-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ describe('SWC configuration', () => {
"detectSyntax": "auto",
"env": {
"include": [
"transform-block-scoping",
"transform-nullish-coalescing-operator",
"transform-optional-chaining",
"transform-export-namespace-from",
Expand Down Expand Up @@ -275,6 +276,10 @@ describe('SWC configuration', () => {
expect(mainThreadLoaderOptions?.env?.include).not.toContain(
'transform-async-to-generator',
)
// `let`/`const` are lowered to `var` on the main thread too.
expect(mainThreadLoaderOptions?.env?.include).toContain(
'transform-block-scoping',
)
})

test('user-configured jsc.target is rejected', async () => {
Expand Down Expand Up @@ -307,7 +312,10 @@ describe('SWC configuration', () => {
tools: {
swc: {
env: {
include: ['transform-block-scoping'],
// An extra transform that is in neither layer's baseline, so its
// routing is observable (`transform-block-scoping` would not work
// here — it is a default on both layers).
include: ['transform-arrow-functions'],
},
},
},
Expand Down Expand Up @@ -348,7 +356,7 @@ describe('SWC configuration', () => {
},
}, 'builtin:swc-loader')
expect(backgroundLoaderOptions?.env?.include).toContain(
'transform-block-scoping',
'transform-arrow-functions',
)

const mainThreadRule = getLayerRule(swcRule, LAYERS.MAIN_THREAD)
Expand All @@ -371,13 +379,63 @@ describe('SWC configuration', () => {
},
}, 'builtin:swc-loader')
expect(mainThreadLoaderOptions?.env?.include).not.toContain(
'transform-block-scoping',
'transform-arrow-functions',
)
expect(mainThreadLoaderOptions?.env?.include).toContain(
'transform-optional-chaining',
)
})

test('layers - user env.exclude opts both layers out of transform-block-scoping', async () => {
const { pluginReactLynx } = await import('../src/pluginReactLynx.js')
const rsbuild = await createRspeedy({
rspeedyConfig: {
tools: {
swc: {
env: {
exclude: ['transform-block-scoping'],
},
},
},
plugins: [
pluginStubRspeedyAPI(),
pluginReactLynx(),
],
},
})

const [config] = await rsbuild.initConfigs()

const swcRule = config.module.rules.find(
(rule): rule is Rspack.RuleSetRule => {
return rule && rule !== '...'
&& (rule.test as RegExp | undefined)?.toString()
=== SCRIPT_REGEXP.toString()
},
)
assert(swcRule)

// SWC's `env.exclude` wins over `include`, so forwarding the user's
// exclude opts out of the let/const → var lowering on both layers.
const backgroundRule = getLayerRule(swcRule, LAYERS.BACKGROUND)
assert(backgroundRule)
const backgroundLoaderOptions = getLoaderOptions<Rspack.SwcLoaderOptions>({
module: { rules: [backgroundRule] },
}, 'builtin:swc-loader')
expect(backgroundLoaderOptions?.env?.exclude).toEqual([
'transform-block-scoping',
])

const mainThreadRule = getLayerRule(swcRule, LAYERS.MAIN_THREAD)
assert(mainThreadRule)
const mainThreadLoaderOptions = getLoaderOptions<Rspack.SwcLoaderOptions>({
module: { rules: [mainThreadRule] },
}, 'builtin:swc-loader')
expect(mainThreadLoaderOptions?.env?.exclude).toEqual([
'transform-block-scoping',
])
})

test('`include` defaults to all js file if not configured by user', async () => {
const { pluginReactLynx } = await import('../src/pluginReactLynx.js')
const rsbuild = await createRspeedy({
Expand Down
Loading