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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
37 changes: 37 additions & 0 deletions packages/babel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 11 additions & 0 deletions packages/babel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,28 @@
},
"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",
"vite": "^8.0.0-beta.15"
},
"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
}
Expand Down
52 changes: 52 additions & 0 deletions packages/babel/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> = {
'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 = {
Expand Down
21 changes: 21 additions & 0 deletions packages/babel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ import { calculatePluginFilters } from './filter.ts'
import type { ResolvedConfig, Plugin as VitePlugin } from 'vite'

async function babelPlugin(rawOptions: PluginOptions): Promise<Plugin> {
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<string | undefined, ReturnType<typeof createBabelOptionsConverter>>()

Expand Down Expand Up @@ -88,9 +99,19 @@ async function babelPlugin(rawOptions: PluginOptions): Promise<Plugin> {
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(
Expand Down
13 changes: 12 additions & 1 deletion packages/babel/src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ export interface InnerTransformOptions extends Pick<
}

export interface PluginOptions extends Omit<InnerTransformOptions, 'include' | 'exclude'> {
/**
* 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)(?:$|\?)/`
Expand Down Expand Up @@ -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]),
Expand Down
134 changes: 134 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading