From bad9ed1d7e2f65327f276024ca4eb7e0c24ef201 Mon Sep 17 00:00:00 2001 From: sapphi-red <49056869+sapphi-red@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:03:36 +0900 Subject: [PATCH] feat(babel): allow preset to specify optimize deps --- packages/babel/README.md | 18 ++++++++ packages/babel/src/index.test.ts | 69 +++++++++++++++++++++++++++- packages/babel/src/index.ts | 7 +++ packages/babel/src/options.ts | 17 +++++++ packages/babel/src/rolldownPreset.ts | 3 ++ 5 files changed, 113 insertions(+), 1 deletion(-) diff --git a/packages/babel/README.md b/packages/babel/README.md index f702dcb..503c466 100644 --- a/packages/babel/README.md +++ b/packages/babel/README.md @@ -177,6 +177,24 @@ defineRolldownBabelPreset({ When running without Vite (pure Rolldown), `applyToEnvironmentHook` is ignored. +### `optimizeDeps` + +A preset can declare dependencies that should be pre-bundled by Vite's dependency optimizer. The plugin automatically merges these into `optimizeDeps.include` in the Vite config. + +```js +defineRolldownBabelPreset({ + preset: ['@babel/preset-react'], + rolldown: { + filter: { id: /\.[jt]sx$/ }, + optimizeDeps: { + include: ['react', 'react-dom'], + }, + }, +}) +``` + +When running without Vite (pure Rolldown), `optimizeDeps` is ignored. + ### How preset filters work Preset filters operate at two levels: diff --git a/packages/babel/src/index.test.ts b/packages/babel/src/index.test.ts index 37143b1..86b2d77 100644 --- a/packages/babel/src/index.test.ts +++ b/packages/babel/src/index.test.ts @@ -4,7 +4,7 @@ import * as babel from '@babel/core' import { rolldown, type OutputChunk } from 'rolldown' import { build as viteBuild, createBuilder, type Rollup } from 'vite' import path from 'node:path' -import type { PluginOptions } from './options.ts' +import { collectOptimizeDepsInclude, type PluginOptions } from './options.ts' import type { RolldownBabelPreset } from './rolldownPreset.ts' test('plugin works', async () => { @@ -570,6 +570,73 @@ test('babel syntax error produces enhanced error message', async () => { `) }) +describe('optimizeDeps.include', () => { + test('collectOptimizeDepsInclude merges from presets and overrides', () => { + const topPreset: RolldownBabelPreset = { + preset: '@babel/preset-react', + rolldown: { + optimizeDeps: { include: ['react'] }, + }, + } + const overridePreset: RolldownBabelPreset = { + preset: '@babel/preset-react', + rolldown: { + optimizeDeps: { include: ['react-dom'] }, + }, + } + const result = collectOptimizeDepsInclude({ + presets: [topPreset], + overrides: [{ presets: [overridePreset] }], + }) + expect(result).toEqual(['react', 'react-dom']) + }) + + test('collectOptimizeDepsInclude skips plain babel presets', () => { + const result = collectOptimizeDepsInclude({ + presets: ['@babel/preset-env'], + }) + expect(result).toEqual([]) + }) + + test('collectOptimizeDepsInclude returns empty for no optimizeDeps', () => { + const preset: RolldownBabelPreset = { + preset: '@babel/preset-react', + rolldown: {}, + } + const result = collectOptimizeDepsInclude({ presets: [preset] }) + expect(result).toEqual([]) + }) + + test('config hook returns optimizeDeps.include via Vite build', async () => { + const preset: RolldownBabelPreset = { + preset: (): babel.InputOptions => ({ plugins: [] }), + rolldown: { + optimizeDeps: { include: ['react', 'react-dom'] }, + }, + } + let receivedOptimizeDeps: any + await viteBuild({ + configFile: false, + logLevel: 'silent', + plugins: [ + { + name: 'capture-config', + configResolved(config) { + receivedOptimizeDeps = config.optimizeDeps + }, + }, + babelPlugin({ presets: [preset] }), + ], + build: { + write: false, + minify: false, + rollupOptions: { input: 'foo.js' }, + }, + }).catch(() => {}) + expect(receivedOptimizeDeps?.include).toEqual(expect.arrayContaining(['react', 'react-dom'])) + }) +}) + async function buildWithVite( filename: string, code: string, diff --git a/packages/babel/src/index.ts b/packages/babel/src/index.ts index bf26450..a030d0f 100644 --- a/packages/babel/src/index.ts +++ b/packages/babel/src/index.ts @@ -1,5 +1,6 @@ import { Plugin, type HookFilter, type SourceMapInput } from 'rolldown' import { + collectOptimizeDepsInclude, createBabelOptionsConverter, filterPresetsWithConfigResolved, filterPresetsWithEnvironment, @@ -19,6 +20,12 @@ async function babelPlugin(rawOptions: PluginOptions): Promise { name: '@rolldown/plugin-babel', // this plugin should run before TS, JSX, TSX transformations are done enforce: 'pre', + config() { + const include = collectOptimizeDepsInclude(rawOptions) + if (include.length > 0) { + return { optimizeDeps: { include } } + } + }, configResolved(config: ResolvedConfig) { configFilteredOptions = filterPresetsWithConfigResolved(rawOptions, config) const resolved = resolveOptions(configFilteredOptions) diff --git a/packages/babel/src/options.ts b/packages/babel/src/options.ts index 7a95d54..8bec7d7 100644 --- a/packages/babel/src/options.ts +++ b/packages/babel/src/options.ts @@ -147,6 +147,23 @@ export function filterPresetsWithConfigResolved( } } +export function collectOptimizeDepsInclude(options: PluginOptions): string[] { + const result: string[] = [] + for (const preset of options.presets ?? []) { + if (typeof preset === 'object' && 'rolldown' in preset) { + result.push(...(preset.rolldown.optimizeDeps?.include ?? [])) + } + } + for (const override of options.overrides ?? []) { + for (const preset of override.presets ?? []) { + if (typeof preset === 'object' && 'rolldown' in preset) { + result.push(...(preset.rolldown.optimizeDeps?.include ?? [])) + } + } + } + return result +} + /** * Pre-compile all preset filters and return a function that * converts options to babel options for a given context. diff --git a/packages/babel/src/rolldownPreset.ts b/packages/babel/src/rolldownPreset.ts index 4b8b132..01c6c47 100644 --- a/packages/babel/src/rolldownPreset.ts +++ b/packages/babel/src/rolldownPreset.ts @@ -14,6 +14,9 @@ export type RolldownBabelPreset = { moduleType?: ModuleTypeFilter code?: GeneralHookFilter } + optimizeDeps?: { + include?: string[] + } applyToEnvironmentHook?: (environment: PartialEnvironment) => boolean configResolvedHook?: (config: ResolvedConfig) => boolean }