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
2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ catalog:

catalogs:
rolldown:
"@rollipop/rolldown": 1.0.19
"@rollipop/rolldown": 1.0.20

enableScripts: true

Expand Down
1 change: 1 addition & 0 deletions docs/content/docs/features/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"sse",
"mcp",
"custom-command",
"react-compiler",
"reanimated-worklets",
"module-federation",
"experimental"
Expand Down
73 changes: 73 additions & 0 deletions docs/content/docs/features/react-compiler.mdx
Original file line number Diff line number Diff line change
@@ -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.

<Callout type="warn">
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.
</Callout>

## 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<string \| RegExp>` | File patterns to compile. Empty means all files that enter the transform. |
| `exclude` | `Array<string \| RegExp>` | 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.
21 changes: 21 additions & 0 deletions docs/content/docs/get-started/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 3 additions & 0 deletions examples/0.84/rollipop.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export default defineConfig({
enabled: true,
autoOpen: true,
},
transformer: {
reactCompiler: {},
},
plugins: [
svg(),
myPlugin(),
Expand Down
17 changes: 7 additions & 10 deletions examples/0.84/src/screens/DetailsScreen.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -38,15 +38,12 @@ export function DetailsScreen() {
const [rows, setRows] = useState<RuntimeCheckRow[]>(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) => ({
Expand All @@ -60,7 +57,7 @@ export function DetailsScreen() {

setRows(reports.map(toReportRow));
setRunning(false);
}, []);
}

const summaryText =
summary.total === 0
Expand Down
2 changes: 2 additions & 0 deletions packages/rollipop/src/config/load-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -57,6 +58,7 @@ export async function loadConfig(options: LoadConfigOptions = {}) {
}

await invokeConfigResolved(resolvedConfig, plugins);
printConfigNotice(resolvedConfig);

return resolvedConfig;
}
Expand Down
8 changes: 8 additions & 0 deletions packages/rollipop/src/config/notice.ts
Original file line number Diff line number Diff line change
@@ -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');
}
}
70 changes: 70 additions & 0 deletions packages/rollipop/src/core/__tests__/rolldown.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,77 @@ function findReporterPlugin(options: Awaited<ReturnType<typeof resolveRolldownOp
return plugin!;
}

async function resolveTestRolldownOptions(
config: ReturnType<typeof createTestConfig>,
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();

Expand Down
18 changes: 17 additions & 1 deletion packages/rollipop/src/core/rolldown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,10 @@ export async function resolveRolldownOptions(
mode: 'Runtime',
},
} satisfies TransformOptions,
rolldownTransform,
{
...rolldownTransform,
reactCompiler: resolveReactCompilerTransformOptions(rolldownTransform.reactCompiler),
},
);

const entryPluginOptions = resolveEntryPluginOptions(config);
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading