diff --git a/.yarnrc.yml b/.yarnrc.yml index 9a689b3..ac26c69 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -8,7 +8,7 @@ catalog: catalogs: rolldown: - "@rollipop/rolldown": 1.0.19 + "@rollipop/rolldown": 1.0.20 enableScripts: true diff --git a/docs/content/docs/features/meta.json b/docs/content/docs/features/meta.json index 86d1a4e..2beea08 100644 --- a/docs/content/docs/features/meta.json +++ b/docs/content/docs/features/meta.json @@ -11,6 +11,7 @@ "sse", "mcp", "custom-command", + "react-compiler", "reanimated-worklets", "module-federation", "experimental" diff --git a/docs/content/docs/features/react-compiler.mdx b/docs/content/docs/features/react-compiler.mdx new file mode 100644 index 0000000..02f3845 --- /dev/null +++ b/docs/content/docs/features/react-compiler.mdx @@ -0,0 +1,73 @@ +--- +title: React Compiler +--- + +Rollipop can enable rolldown's native React Compiler transform for React and React Native application code. The transform runs in the bundler pipeline, so you can remove manual memoization such as `useMemo`, `useCallback`, and `React.memo` when the compiler can safely optimize the component. + + + React Compiler changes generated application code. Enable it after validating your app locally, + and keep any manual memoization that exists for semantic behavior rather than render performance. + + +## Setup + +React Compiler is disabled by default. Pass an empty object to opt in with Rollipop's defaults: + +```ts title="rollipop.config.ts" +import { defineConfig } from 'rollipop'; + +export default defineConfig({ + transformer: { + reactCompiler: {}, + }, +}); +``` + +With the empty object form, Rollipop skips files whose module id matches `/node_modules/`. + +## Configuration + +`transformer.reactCompiler` accepts rolldown's React Compiler options. + +| Property | Type | Description | +| ----------------- | ---------------------------------------------- | ------------------------------------------------------------------------- | +| `include` | `Array` | File patterns to compile. Empty means all files that enter the transform. | +| `exclude` | `Array` | File patterns to skip. Overrides Rollipop's default exclude list. | +| `compilationMode` | `'infer' \| 'syntax' \| 'annotation' \| 'all'` | Which functions to compile. | +| `panicThreshold` | `'none' \| 'critical_errors' \| 'all_errors'` | How strictly compilation failures should be reported. | +| `target` | `'17' \| '18' \| '19'` | React runtime version target. | +| `noEmit` | `boolean` | Report diagnostics without emitting transformed code. | + +### Excluding dependencies + +If you omit `exclude`, Rollipop uses `/node_modules/` by default: + +```ts title="rollipop.config.ts" +export default defineConfig({ + transformer: { + reactCompiler: {}, + }, +}); +``` + +If you provide `exclude`, your value replaces that default. Include `/node_modules/` yourself if you still want dependency code skipped: + +```ts title="rollipop.config.ts" +export default defineConfig({ + transformer: { + reactCompiler: { + exclude: [/node_modules/, /vendor/], + }, + }, +}); +``` + +## Manual Memoization + +After enabling React Compiler, remove memoization that exists only to avoid re-renders: + +- `useMemo` for derived values that can be recomputed safely +- `useCallback` for event handlers and callbacks passed to children +- `React.memo` around components that do not need a custom equality check + +Keep memoization when it is part of observable behavior, protects expensive non-render work, or relies on a custom comparison that the compiler cannot infer. diff --git a/docs/content/docs/get-started/configuration.mdx b/docs/content/docs/get-started/configuration.mdx index 90dd55f..5abd65e 100644 --- a/docs/content/docs/get-started/configuration.mdx +++ b/docs/content/docs/get-started/configuration.mdx @@ -164,6 +164,27 @@ Flow type syntax transformation configuration. Filter for Flow transformation pipeline. Uses Rolldown's [hook filter feature](https://rolldown.rs/apis/plugin-hook-filters) to determine which files should be processed. +### transformer.reactCompiler + +React Compiler transformation configuration. + +- **Type:** `OxcReactCompilerOptions` +- **Default:** `undefined` + +React Compiler is disabled by default. Pass an empty object to enable it with Rollipop's defaults: + +```ts +export default defineConfig({ + transformer: { + reactCompiler: {}, + }, +}); +``` + +When `exclude` is omitted, Rollipop skips files whose module id matches `/node_modules/`. If you specify `exclude`, your value replaces that default. + +See [React Compiler](/docs/features/react-compiler) for setup guidance and option details. + ### transformer.babel Custom Babel transformation rules. diff --git a/examples/0.84/rollipop.config.ts b/examples/0.84/rollipop.config.ts index 058f8cd..259a7ef 100644 --- a/examples/0.84/rollipop.config.ts +++ b/examples/0.84/rollipop.config.ts @@ -14,6 +14,9 @@ export default defineConfig({ enabled: true, autoOpen: true, }, + transformer: { + reactCompiler: {}, + }, plugins: [ svg(), myPlugin(), diff --git a/examples/0.84/src/screens/DetailsScreen.tsx b/examples/0.84/src/screens/DetailsScreen.tsx index 79a83ba..cbbfe30 100644 --- a/examples/0.84/src/screens/DetailsScreen.tsx +++ b/examples/0.84/src/screens/DetailsScreen.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useState } from 'react'; +import { useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { AppButton } from '../components/AppButton'; @@ -38,15 +38,12 @@ export function DetailsScreen() { const [rows, setRows] = useState(toIdleRows); const [running, setRunning] = useState(false); - const summary = useMemo(() => { - const reports = rows.filter((row): row is RuntimeCheckReport => { - return row.status === 'passed' || row.status === 'failed'; - }); + const reports = rows.filter((row): row is RuntimeCheckReport => { + return row.status === 'passed' || row.status === 'failed'; + }); + const summary = summarizeReports(reports); - return summarizeReports(reports); - }, [rows]); - - const handleRunAll = useCallback(async () => { + async function handleRunAll() { setRunning(true); setRows((currentRows) => currentRows.map((row) => ({ @@ -60,7 +57,7 @@ export function DetailsScreen() { setRows(reports.map(toReportRow)); setRunning(false); - }, []); + } const summaryText = summary.total === 0 diff --git a/packages/rollipop/src/config/load-config.ts b/packages/rollipop/src/config/load-config.ts index e69224d..3888f55 100644 --- a/packages/rollipop/src/config/load-config.ts +++ b/packages/rollipop/src/config/load-config.ts @@ -8,6 +8,7 @@ import type { Plugin, PluginConfig, ResolvedPluginConfig } from '../core/plugins import { getDefaultConfig, type ResolvedConfig } from './defaults'; import { DefineConfigContext } from './define-config'; import { mergeConfig } from './merge-config'; +import { printConfigNotice } from './notice'; import type { Config, PluginOption } from './types'; const CONFIG_FILE_NAME = 'rollipop'; @@ -57,6 +58,7 @@ export async function loadConfig(options: LoadConfigOptions = {}) { } await invokeConfigResolved(resolvedConfig, plugins); + printConfigNotice(resolvedConfig); return resolvedConfig; } diff --git a/packages/rollipop/src/config/notice.ts b/packages/rollipop/src/config/notice.ts new file mode 100644 index 0000000..ff713d5 --- /dev/null +++ b/packages/rollipop/src/config/notice.ts @@ -0,0 +1,8 @@ +import { logger } from '../logger'; +import type { ResolvedConfig } from './defaults'; + +export function printConfigNotice(config: ResolvedConfig) { + if (config.transformer.reactCompiler != null) { + logger.info('✨ React Compiler is enabled'); + } +} diff --git a/packages/rollipop/src/core/__tests__/rolldown.spec.ts b/packages/rollipop/src/core/__tests__/rolldown.spec.ts index 6508f24..4a7eeec 100644 --- a/packages/rollipop/src/core/__tests__/rolldown.spec.ts +++ b/packages/rollipop/src/core/__tests__/rolldown.spec.ts @@ -28,7 +28,77 @@ function findReporterPlugin(options: Awaited, + contextId: string, +) { + config.devMode.hmr = false; + config.reactNative.assetRegistryPath = path.join(config.root, 'package.json'); + + return resolveRolldownOptions( + { + id: contextId, + root: config.root, + buildType: 'build', + storage: { + get: () => ({ build: {} }), + set: () => {}, + } as unknown as BundlerContext['storage'], + state: { revision: 0, latestBuildStartTime: 0 }, + }, + config, + resolveBuildOptions(config, { platform: 'ios', dev: true }), + ); +} + describe('resolveRolldownOptions', () => { + it('keeps react compiler disabled by default', async () => { + resolveRolldownOptions.cache.clear(); + + const options = await resolveTestRolldownOptions( + createTestConfig(process.cwd()), + 'test-bundler-react-compiler-disabled', + ); + + expect(options.input?.transform?.reactCompiler).toBeUndefined(); + }); + + it('enables react compiler with default exclude when configured with an empty object', async () => { + resolveRolldownOptions.cache.clear(); + + const config = createTestConfig(process.cwd()); + config.transformer.reactCompiler = {}; + + const options = await resolveTestRolldownOptions( + config, + 'test-bundler-react-compiler-empty-object', + ); + + expect(options.input?.transform?.reactCompiler).toEqual({ + exclude: [/node_modules/], + }); + }); + + it('uses user react compiler exclude patterns instead of the default node_modules rule', async () => { + resolveRolldownOptions.cache.clear(); + + const config = createTestConfig(process.cwd()); + config.transformer.reactCompiler = { + exclude: [/vendor/], + target: '18', + }; + + const options = await resolveTestRolldownOptions( + config, + 'test-bundler-react-compiler-custom-exclude', + ); + + expect(options.input?.transform?.reactCompiler).toEqual({ + exclude: [/vendor/], + target: '18', + }); + }); + it('transforms only polyfills that opt into Rollipop transform', async () => { resolveRolldownOptions.cache.clear(); diff --git a/packages/rollipop/src/core/rolldown.ts b/packages/rollipop/src/core/rolldown.ts index 2d4a94c..87a1150 100644 --- a/packages/rollipop/src/core/rolldown.ts +++ b/packages/rollipop/src/core/rolldown.ts @@ -163,7 +163,10 @@ export async function resolveRolldownOptions( mode: 'Runtime', }, } satisfies TransformOptions, - rolldownTransform, + { + ...rolldownTransform, + reactCompiler: resolveReactCompilerTransformOptions(rolldownTransform.reactCompiler), + }, ); const entryPluginOptions = resolveEntryPluginOptions(config); @@ -339,6 +342,19 @@ function resolveWorkletsConfig( ); } +function resolveReactCompilerTransformOptions( + reactCompiler: TransformOptions['reactCompiler'], +): TransformOptions['reactCompiler'] { + if (reactCompiler == null) { + return undefined; + } + + return { + ...reactCompiler, + exclude: reactCompiler.exclude ?? [/node_modules/], + }; +} + function resolveBabelPluginOptions( config: ResolvedConfig, context: BundlerContext, diff --git a/yarn.lock b/yarn.lock index 7e61f72..1ab8b55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8613,68 +8613,68 @@ __metadata: languageName: unknown linkType: soft -"@rollipop/rolldown-binding-darwin-arm64@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-darwin-arm64@npm:1.0.19" +"@rollipop/rolldown-binding-darwin-arm64@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-darwin-arm64@npm:1.0.20" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@rollipop/rolldown-binding-darwin-x64@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-darwin-x64@npm:1.0.19" +"@rollipop/rolldown-binding-darwin-x64@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-darwin-x64@npm:1.0.20" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@rollipop/rolldown-binding-linux-arm64-gnu@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-linux-arm64-gnu@npm:1.0.19" +"@rollipop/rolldown-binding-linux-arm64-gnu@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-linux-arm64-gnu@npm:1.0.20" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@rollipop/rolldown-binding-linux-arm64-musl@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-linux-arm64-musl@npm:1.0.19" +"@rollipop/rolldown-binding-linux-arm64-musl@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-linux-arm64-musl@npm:1.0.20" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@rollipop/rolldown-binding-linux-x64-gnu@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-linux-x64-gnu@npm:1.0.19" +"@rollipop/rolldown-binding-linux-x64-gnu@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-linux-x64-gnu@npm:1.0.20" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@rollipop/rolldown-binding-linux-x64-musl@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-linux-x64-musl@npm:1.0.19" +"@rollipop/rolldown-binding-linux-x64-musl@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-linux-x64-musl@npm:1.0.20" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@rollipop/rolldown-binding-win32-x64-msvc@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown-binding-win32-x64-msvc@npm:1.0.19" +"@rollipop/rolldown-binding-win32-x64-msvc@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown-binding-win32-x64-msvc@npm:1.0.20" conditions: os=win32 & cpu=x64 languageName: node linkType: hard -"@rollipop/rolldown@npm:1.0.19": - version: 1.0.19 - resolution: "@rollipop/rolldown@npm:1.0.19" +"@rollipop/rolldown@npm:1.0.20": + version: 1.0.20 + resolution: "@rollipop/rolldown@npm:1.0.20" dependencies: "@oxc-project/types": "npm:=0.137.0" "@rolldown/pluginutils": "npm:^1.0.0" - "@rollipop/rolldown-binding-darwin-arm64": "npm:1.0.19" - "@rollipop/rolldown-binding-darwin-x64": "npm:1.0.19" - "@rollipop/rolldown-binding-linux-arm64-gnu": "npm:1.0.19" - "@rollipop/rolldown-binding-linux-arm64-musl": "npm:1.0.19" - "@rollipop/rolldown-binding-linux-x64-gnu": "npm:1.0.19" - "@rollipop/rolldown-binding-linux-x64-musl": "npm:1.0.19" - "@rollipop/rolldown-binding-win32-x64-msvc": "npm:1.0.19" + "@rollipop/rolldown-binding-darwin-arm64": "npm:1.0.20" + "@rollipop/rolldown-binding-darwin-x64": "npm:1.0.20" + "@rollipop/rolldown-binding-linux-arm64-gnu": "npm:1.0.20" + "@rollipop/rolldown-binding-linux-arm64-musl": "npm:1.0.20" + "@rollipop/rolldown-binding-linux-x64-gnu": "npm:1.0.20" + "@rollipop/rolldown-binding-linux-x64-musl": "npm:1.0.20" + "@rollipop/rolldown-binding-win32-x64-msvc": "npm:1.0.20" dependenciesMeta: "@rollipop/rolldown-binding-darwin-arm64": optional: true @@ -8692,7 +8692,7 @@ __metadata: optional: true bin: rolldown: ./bin/cli.mjs - checksum: 10c0/1b2f51f4ace09024f32d4434991729898d46674f618fdef942042657b992919f3afb4d344a1243a77386497323ddb85f8dd5be9f82bfb9dbe49ca033c8a20889 + checksum: 10c0/4f05ca4474832bfc4bb2a31ad8e7fe96d983ed1de91567310598814a6923c8f577e609463bfd1895361bf9c03188e15f2008d42a49985a856c66f02eecbe4491 languageName: node linkType: hard