diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index baff016..71e8db4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,8 +71,8 @@ jobs: node-version: 24 cache: 'pnpm' - - name: Override @babel/core to v7 - run: yq -i '.overrides."@babel/core" = "^7.29.0"' pnpm-workspace.yaml + - name: Override Babel related deps to v7 + run: yq -i '.overrides."@babel/core" = "catalog:babel7" | .overrides."@babel/plugin-transform-runtime" = "catalog:babel7" | .overrides."@babel/runtime" = "catalog:babel7"' pnpm-workspace.yaml - name: Install deps run: pnpm install --no-frozen-lockfile diff --git a/packages/babel/README.md b/packages/babel/README.md index df89b83..4bf78f8 100644 --- a/packages/babel/README.md +++ b/packages/babel/README.md @@ -71,6 +71,43 @@ List of Babel plugins to apply. Array of additional configurations that are merged into the current configuration. Use with Babel's `test`/`include`/`exclude` options to conditionally apply overrides. +### `runtimeVersion` + +- **Type:** `string` + +When set, automatically adds [`@babel/plugin-transform-runtime`](https://babeljs.io/docs/babel-plugin-transform-runtime) so that Babel helpers are imported from `@babel/runtime` instead of being inlined into every file. This deduplicates helpers across modules and reduces bundle size. + +The value is the version of `@babel/runtime` that is assumed to be installed. If you are externalizing `@babel/runtime` (for example, you are packaging a library), you should set the version range of `@babel/runtime` in your package.json. If you are bundling `@babel/runtime` for your application, you should set the version of `@babel/runtime` that is installed. + +```bash +pnpm add -D @babel/plugin-transform-runtime @babel/runtime +``` + +```js +import babel from '@rolldown/plugin-babel' + +// if you are externalizing @babel/runtime +import fs from 'node:fs' +import path from 'node:path' +const packageJson = JSON.parse( + fs.readFileSync(path.join(import.meta.dirname, 'package.json'), 'utf8'), +) +const babelRuntimeVersion = packageJson.dependencies['@babel/runtime'] + +// if you are bundling @babel/runtime +import babelRuntimePackageJson from '@babel/runtime/package.json' +const babelRuntimeVersion = babelRuntimePackageJson.version + +export default { + plugins: [ + babel({ + runtimeVersion: babelRuntimeVersion, + plugins: ['@babel/plugin-proposal-decorators'], + }), + ], +} +``` + ### Other Babel options The following [Babel options](https://babeljs.io/docs/options) are forwarded directly: diff --git a/packages/babel/package.json b/packages/babel/package.json index 984005c..31589a7 100644 --- a/packages/babel/package.json +++ b/packages/babel/package.json @@ -34,6 +34,9 @@ }, "devDependencies": { "@babel/core": "^8.0.0-rc.1", + "@babel/plugin-proposal-decorators": "^8.0.0-rc.2", + "@babel/plugin-transform-runtime": "^8.0.0-rc.2", + "@babel/runtime": "^8.0.0-rc.2", "@types/node": "^22.19.11", "@types/picomatch": "^4.0.2", "rolldown": "1.0.0-rc.5", @@ -41,10 +44,18 @@ }, "peerDependencies": { "@babel/core": "^7.29.0 || ^8.0.0-rc.1", + "@babel/plugin-transform-runtime": "^7.29.0 || ^8.0.0-rc.1", + "@babel/runtime": "^7.27.0 || ^8.0.0-rc.1", "rolldown": "^1.0.0-rc.5", "vite": "^8.0.0" }, "peerDependenciesMeta": { + "@babel/plugin-transform-runtime": { + "optional": true + }, + "@babel/runtime": { + "optional": true + }, "vite": { "optional": true } diff --git a/packages/babel/src/index.test.ts b/packages/babel/src/index.test.ts index 8475f24..257df9e 100644 --- a/packages/babel/src/index.test.ts +++ b/packages/babel/src/index.test.ts @@ -571,6 +571,58 @@ test('babel syntax error produces enhanced error message', async () => { `) }) +test('runtimeVersion deduplicates helpers via @babel/runtime', async () => { + const entryCode = ` +import { decorated } from './dep.js' +@decorator +class Entry { method() { return decorated } } +function decorator(target) { return target } +export { Entry } +` + const depCode = ` +@decorator +class Dep { method() { return 42 } } +function decorator(target) { return target } +export const decorated = new Dep() +` + + const files: Record = { + 'entry.js': entryCode, + 'dep.js': depCode, + } + + const babelRuntimeVersion = ( + await import('@babel/runtime/package.json', { with: { type: 'json' } }) + ).default.version + + const bundle = await rolldown({ + input: 'entry.js', + external: [/^@babel\/runtime/], + plugins: [ + { + name: 'virtual', + resolveId(id) { + if (id in files) return id + if (id === './dep.js') return 'dep.js' + }, + load(id) { + if (id in files) return files[id] + }, + }, + babelPlugin({ + runtimeVersion: babelRuntimeVersion, + plugins: [['@babel/plugin-proposal-decorators', { version: '2023-11' }]], + }), + ], + }) + const { output } = await bundle.generate() + const chunk = output.find((o) => o.type === 'chunk') + assert(chunk, 'expected a chunk in output') + + // Helpers should come from @babel/runtime, not be inlined + expect(chunk.code).toContain('@babel/runtime') +}) + describe('optimizeDeps.include', () => { test('collectOptimizeDepsInclude merges from presets and overrides', () => { const topPreset: RolldownBabelPreset = { diff --git a/packages/babel/src/index.ts b/packages/babel/src/index.ts index 63db079..d07d3c7 100644 --- a/packages/babel/src/index.ts +++ b/packages/babel/src/index.ts @@ -13,6 +13,17 @@ import { calculatePluginFilters } from './filter.ts' import type { ResolvedConfig, Plugin as VitePlugin } from 'vite' async function babelPlugin(rawOptions: PluginOptions): Promise { + if (rawOptions.runtimeVersion) { + try { + import.meta.resolve('@babel/plugin-transform-runtime') + } catch (err) { + throw new Error( + `Failed to load @babel/plugin-transform-runtime. Please install it to use the runtime option.`, + { cause: err }, + ) + } + } + let configFilteredOptions: PluginOptions | undefined const envState = new Map>() @@ -88,9 +99,19 @@ async function babelPlugin(rawOptions: PluginOptions): Promise { filename: id, }) if (!loadedOptions || loadedOptions.plugins.length === 0) { + // No plugins to run — @babel/plugin-transform-runtime only affects + // how other plugins' helpers are emitted, so skip it too. return } + if (rawOptions.runtimeVersion) { + loadedOptions.plugins ??= [] + loadedOptions.plugins.push([ + '@babel/plugin-transform-runtime', + { version: rawOptions.runtimeVersion }, + ]) + } + let result: babel.FileResult | null try { result = await babel.transformAsync( diff --git a/packages/babel/src/options.ts b/packages/babel/src/options.ts index cbf1ad0..5b871a5 100644 --- a/packages/babel/src/options.ts +++ b/packages/babel/src/options.ts @@ -37,6 +37,15 @@ export interface InnerTransformOptions extends Pick< } export interface PluginOptions extends Omit { + /** + * When set, automatically adds `@babel/plugin-transform-runtime` so that + * babel helpers are imported from `@babel/runtime` instead of being inlined + * into every file. + * + * Requires `@babel/plugin-transform-runtime` and `@babel/runtime` to be installed. + */ + runtimeVersion?: string + /** * If specified, only files matching the pattern will be processed by babel. * @default `/\.(?:[jt]sx?|[cm][jt]s)(?:$|\?)/` @@ -175,8 +184,10 @@ export function createBabelOptionsConverter(options: ResolvedPluginOptions) { ) return function (ctx: PresetConversionContext): babel.InputOptions { + // Strip plugin-level options that babel doesn't understand + const { runtimeVersion: _, ...babelOptions } = options return { - ...options, + ...babelOptions, presets: options.presets ? filterMap(options.presets, (preset, i) => convertToBabelPresetItem(ctx, preset, presetFilters![i]), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9f4293..bc4edb4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,6 +41,15 @@ importers: '@babel/core': specifier: ^8.0.0-rc.1 version: 8.0.0-rc.2 + '@babel/plugin-proposal-decorators': + specifier: ^8.0.0-rc.2 + version: 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + '@babel/plugin-transform-runtime': + specifier: ^8.0.0-rc.2 + version: 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + '@babel/runtime': + specifier: ^8.0.0-rc.2 + version: 8.0.0-rc.2 '@types/node': specifier: ^22.19.11 version: 22.19.11 @@ -93,14 +102,52 @@ packages: resolution: {integrity: sha512-oCQ1IKPwkzCeJzAPb7Fv8rQ9k5+1sG8mf2uoHiMInPYvkRfrDJxbTIbH51U+jstlkghus0vAi3EBvkfvEsYNLQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-annotate-as-pure@8.0.0-rc.2': + resolution: {integrity: sha512-KbQiXXTEGDdQo6SHaVafQowpPT+Kksqnq20zRY23pZgd63buryBA0dciIHs/04a86SxIsl1Ggvn82cWgdeq5nQ==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-compilation-targets@8.0.0-rc.2': resolution: {integrity: sha512-oMIhKru9gl3mj0eKDyKW6wBDAvyWoZd28d6V/m4JTeeiFsJLfOYnqu+s+cnK4jSo87cg/oj4hsATgkmZ3AzsDQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-create-class-features-plugin@8.0.0-rc.2': + resolution: {integrity: sha512-6pQRz1vSvC28Kbmlns0r+AEsuO0iracJyAfnvtKle7rk737JLtItEOPCo5iW/mb2mgwo2vw9SLpW32r4kAWahw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@babel/core': ^8.0.0-rc.2 + '@babel/helper-globals@8.0.0-rc.2': resolution: {integrity: sha512-Q1AIOaW4EOxkI/8wYJKyLI59gfqTK3imFUfIqxuve0Q3GlOSrOTVmvHU6Gb3Y5GxtoS1hIzhO47k5GkfyGTQEQ==} engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-member-expression-to-functions@8.0.0-rc.2': + resolution: {integrity: sha512-LbzA3y4YgiVC8TlnOucMWLdsag1KPYijUmm9hmaHcU7srGNYfH9Qe6Y5I3FlJ/PjmWEvIKX2MX+NFMuCMrpkHQ==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-module-imports@8.0.0-rc.2': + resolution: {integrity: sha512-9xSWdt/w6bA9sEcdxIDoNvnKZSGcUqeLQSWu0bh69xRV2Skj5vXSYXbFdYE4M8g/stEeNQ/9zSAs2OlSF3ZOOw==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-optimise-call-expression@8.0.0-rc.2': + resolution: {integrity: sha512-y1H1DXTPIR0yi/T9ervuo4f0zD1zCEn7FXnFBFU8DtCr3SMA0oS50jgt1PMCX5hg44RxvgG+dhAwHCP9mneZqg==} + engines: {node: ^20.19.0 || >=22.12.0} + + '@babel/helper-plugin-utils@8.0.0-rc.2': + resolution: {integrity: sha512-APa2p8RHBNGUmNPDYshswXQkS2sMNthL8VZSc9soe5lQfT2RXRXM6TwOLaktQwnNSwdoEy+Xu9q3qMdFrV92sg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@babel/core': ^8.0.0-rc.2 + + '@babel/helper-replace-supers@8.0.0-rc.2': + resolution: {integrity: sha512-m8vNX/op6hcitT/j4HfkBJnzZNSO3EA3iqyl8cxorOKfUJTmFuiUkqJgSck3sfjKyFcE0c3BXoD/xSblUFIj7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@babel/core': ^8.0.0-rc.2 + + '@babel/helper-skip-transparent-expression-wrappers@8.0.0-rc.2': + resolution: {integrity: sha512-NO8HTg+G9V5R7HGkVX5jwanxZcjcj0FZ6TwUFZJmGVOxpWBty+zStju0PTkhah0A/npmRGwJPy8+RCCUtX8Qug==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/helper-string-parser@8.0.0-rc.2': resolution: {integrity: sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -131,6 +178,28 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + '@babel/plugin-proposal-decorators@8.0.0-rc.2': + resolution: {integrity: sha512-og10yZVuwSmy2DPoqMSYMpw7zjQQSyBg+ooR3aGqnn4by21pV0EdA8h2k4j542ckkFnPOhkKZ/EsilJxYizWSg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@babel/core': ^8.0.0-rc.2 + + '@babel/plugin-syntax-decorators@8.0.0-rc.2': + resolution: {integrity: sha512-7vx1Hag1jLUjqsd0LOCB+YRbhK1JnIvM2hQQ3eY3kDVVz/Qno6MMsCWYGlzdQLsZYfOTEF5BBIU6+XJh4WXwmg==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@babel/core': ^8.0.0-rc.2 + + '@babel/plugin-transform-runtime@8.0.0-rc.2': + resolution: {integrity: sha512-cdkw/49F6Pb7/Z98zUIlHgQNCHSFi8jA1NtZvoZu8Ng89RsVTt69H7KORAguy1cP8fmsUGtBrqLe+C3slNnurw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@babel/core': ^8.0.0-rc.2 + + '@babel/runtime@8.0.0-rc.2': + resolution: {integrity: sha512-wI+xpmXzz8HwQflyWnjgWqBNSwSCloGq+f5/8MpWQ9XE+w8PMPl/g8fjh6KHZa4ub5/WAMZKaNTThvRWJnzVYA==} + engines: {node: ^20.19.0 || >=22.12.0} + '@babel/template@8.0.0-rc.2': resolution: {integrity: sha512-INp+KufeQpvU+V+gxR7xoiVzU6sRRQo8oOsCU/sTe0wtJ/Adrfgyet0i19qvXXSeuyiZ9+PV8IF/eEPzyJ527g==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1852,6 +1921,10 @@ snapshots: '@types/jsesc': 2.5.1 jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@8.0.0-rc.2': + dependencies: + '@babel/types': 8.0.0-rc.2 + '@babel/helper-compilation-targets@8.0.0-rc.2': dependencies: '@babel/compat-data': 8.0.0-rc.2 @@ -1860,8 +1933,49 @@ snapshots: lru-cache: 7.18.3 semver: 7.7.4 + '@babel/helper-create-class-features-plugin@8.0.0-rc.2(@babel/core@8.0.0-rc.2)': + dependencies: + '@babel/core': 8.0.0-rc.2 + '@babel/helper-annotate-as-pure': 8.0.0-rc.2 + '@babel/helper-member-expression-to-functions': 8.0.0-rc.2 + '@babel/helper-optimise-call-expression': 8.0.0-rc.2 + '@babel/helper-replace-supers': 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + '@babel/helper-skip-transparent-expression-wrappers': 8.0.0-rc.2 + '@babel/traverse': 8.0.0-rc.2 + semver: 7.7.4 + '@babel/helper-globals@8.0.0-rc.2': {} + '@babel/helper-member-expression-to-functions@8.0.0-rc.2': + dependencies: + '@babel/traverse': 8.0.0-rc.2 + '@babel/types': 8.0.0-rc.2 + + '@babel/helper-module-imports@8.0.0-rc.2': + dependencies: + '@babel/traverse': 8.0.0-rc.2 + '@babel/types': 8.0.0-rc.2 + + '@babel/helper-optimise-call-expression@8.0.0-rc.2': + dependencies: + '@babel/types': 8.0.0-rc.2 + + '@babel/helper-plugin-utils@8.0.0-rc.2(@babel/core@8.0.0-rc.2)': + dependencies: + '@babel/core': 8.0.0-rc.2 + + '@babel/helper-replace-supers@8.0.0-rc.2(@babel/core@8.0.0-rc.2)': + dependencies: + '@babel/core': 8.0.0-rc.2 + '@babel/helper-member-expression-to-functions': 8.0.0-rc.2 + '@babel/helper-optimise-call-expression': 8.0.0-rc.2 + '@babel/traverse': 8.0.0-rc.2 + + '@babel/helper-skip-transparent-expression-wrappers@8.0.0-rc.2': + dependencies: + '@babel/traverse': 8.0.0-rc.2 + '@babel/types': 8.0.0-rc.2 + '@babel/helper-string-parser@8.0.0-rc.2': {} '@babel/helper-validator-identifier@8.0.0-rc.1': {} @@ -1883,6 +1997,26 @@ snapshots: dependencies: '@babel/types': 8.0.0-rc.2 + '@babel/plugin-proposal-decorators@8.0.0-rc.2(@babel/core@8.0.0-rc.2)': + dependencies: + '@babel/core': 8.0.0-rc.2 + '@babel/helper-create-class-features-plugin': 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + '@babel/helper-plugin-utils': 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + '@babel/plugin-syntax-decorators': 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + + '@babel/plugin-syntax-decorators@8.0.0-rc.2(@babel/core@8.0.0-rc.2)': + dependencies: + '@babel/core': 8.0.0-rc.2 + '@babel/helper-plugin-utils': 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + + '@babel/plugin-transform-runtime@8.0.0-rc.2(@babel/core@8.0.0-rc.2)': + dependencies: + '@babel/core': 8.0.0-rc.2 + '@babel/helper-module-imports': 8.0.0-rc.2 + '@babel/helper-plugin-utils': 8.0.0-rc.2(@babel/core@8.0.0-rc.2) + + '@babel/runtime@8.0.0-rc.2': {} + '@babel/template@8.0.0-rc.2': dependencies: '@babel/code-frame': 8.0.0-rc.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 7127a3a..53f6789 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,5 +2,11 @@ packages: - packages/* - scripts +catalogs: + babel7: + '@babel/core': ^7.29.0 + '@babel/plugin-transform-runtime': ^7.29.0 + '@babel/runtime': ^7.28.6 + patchedDependencies: '@vitejs/release-scripts@1.6.0': patches/@vitejs__release-scripts@1.6.0.patch