Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
fa35b70
chore: add repack
OlimpiaZurek Jul 21, 2026
07b2f00
fix: sign in not working under repack
OlimpiaZurek Jul 21, 2026
c90d413
fix: CI checks
OlimpiaZurek Jul 21, 2026
2aaa1f6
chore: add OXC parser for mobile
OlimpiaZurek Jul 22, 2026
c1b3c1a
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 22, 2026
4bc297b
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 22, 2026
6073a74
fix config
OlimpiaZurek Jul 22, 2026
7f2076e
fix profiling aliases
OlimpiaZurek Jul 22, 2026
ec46b09
fix: Hermes loop var capture
OlimpiaZurek Jul 22, 2026
553baae
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 28, 2026
10edc2e
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 28, 2026
e4de033
fix lint
OlimpiaZurek Jul 28, 2026
9010cb7
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 28, 2026
f73a534
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 28, 2026
7cafdb3
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 29, 2026
de7d01d
move rspack config
OlimpiaZurek Jul 29, 2026
b494849
fix(repack): enable production JS bundle minification
OlimpiaZurek Jul 29, 2026
b5ce702
Merge branch 'main' into ft/repack-poc
OlimpiaZurek Jul 31, 2026
0534679
fix persistent cache and worklets crashes
OlimpiaZurek Jul 31, 2026
6b73156
spell check fix
OlimpiaZurek Jul 31, 2026
abbbb11
fix ts
OlimpiaZurek Jul 31, 2026
caea815
fix: defer imports via inline-requires instead of SWC lazy mode
OlimpiaZurek Aug 3, 2026
7fa2e2a
fix spellcheck
OlimpiaZurek Aug 3, 2026
81bc961
Exclude dotlottie-react prebuilt bundle from babel-swc loader
elirangoshen Aug 13, 2026
50ec572
Route non-babel node_modules through the OXC pipeline
elirangoshen Aug 21, 2026
842920a
Shorten config comments
elirangoshen Aug 21, 2026
8b41085
Merge remote-tracking branch 'upstream/main' into eliran/2733-dotlott…
elirangoshen Aug 21, 2026
9f1f584
Remove comments that restate the code
elirangoshen Aug 25, 2026
d7aafb9
Merge remote-tracking branch 'upstream/main' into eliran/2733-dotlott…
elirangoshen Aug 25, 2026
dbfc762
Fix tsconfig formatting
elirangoshen Aug 25, 2026
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: 3 additions & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ react {
//
// A list of extra flags to pass to the 'bundle' commands.
// See https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
// extraPackagerArgs = []
// Force `--minify true` for Repack: RN gradle task hardcodes minifyEnabled=false when Hermes is on
// (Metro-era assumption). Under Repack SWC minifier does real DCE + tree-shake; commander last-wins.
extraPackagerArgs = ["--minify", "true"]

/* Hermes Commands */
// The hermes compiler command to run. By default it is 'hermesc'
Expand Down
12 changes: 12 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,13 @@ if (process.env.CAPTURE_METRICS === 'true') {
]);
}

const repack = {
...metro,
plugins: metro.plugins
.filter((plugin) => !(Array.isArray(plugin) && plugin[0] === 'module-resolver' && plugin[2] !== 'extra-alias'))
.map((plugin) => (Array.isArray(plugin) && plugin[0] === 'react-native-worklets/plugin' ? 'react-native-worklets/plugin' : plugin)),
};

module.exports = (api) => {
if (!process.env.KNIP) {
console.debug('babel.config.js');
Expand All @@ -170,12 +177,17 @@ module.exports = (api) => {
}

// For `react-native` (iOS/Android) caller will be "metro"
// For `@callstack/repack` (Re.Pack native bundler) caller will be "@callstack/repack"
// For jest, it will be babel-jest
// The web build and Storybook (Rsbuild) don't call into this file at all
const runningIn = api.caller((args = {}) => args.name);
if (!process.env.KNIP) {
console.debug(' - running in: ', runningIn);
}

if (runningIn === '@callstack/repack') {
return repack;
}

return ['metro', 'babel-jest'].includes(runningIn) ? metro : {};
};
2 changes: 1 addition & 1 deletion config/eslint/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ const config = defineConfig([

// Rspack loaders receive their `this` from the bundler, and it's standard practice to use it
{
files: ['config/rsbuild/loaders/*-loader.mjs'],
files: ['config/rsbuild/loaders/*-loader.mjs', 'config/repack/*-loader.mjs'],
rules: {
'no-invalid-this': 'off',
},
Expand Down
132 changes: 132 additions & 0 deletions config/repack/cjs-inline-requires-loader.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
/**
* Lowers ES modules to CommonJS and defers each `require()` to its first use site — the same
* import-cycle tolerance Metro gets from `inlineRequires` (see metro.config.js), which this app
* relies on to boot.
*
* Uses Metro's own `inlineRequiresPlugin` rather than SWC's `module.lazy`. Both defer ~100% of
* imports, but SWC's lazy mode emits a memoizing wrapper function per imported module — ~103k
* extra functions here, worth ~7MB of Hermes bytecode. Inlining the require at the use site
* defers identically and emits no wrapper.
*
* Needed as a separate stage because OXC transpiles only and has no CJS lowering.
*/
import babel from '@babel/core';
import remapping from '@jridgewell/remapping';
import {rspack} from '@rspack/core';
// CommonJS with lazy getters, so named ESM imports are not statically detectable.
import metroTransformPlugins from 'metro-transform-plugins';

const {inlineRequiresPlugin} = metroTransformPlugins;

/** SWC's interop wrappers for `import x from` and `import * as x from`. */
const INTEROP_HELPERS = new Set(['_interop_require_default', '_interop_require_wildcard']);

/**
* Extends inline-requires to SWC's interop-wrapped imports, which Metro's plugin alone cannot
* defer: it requires `arguments[0]` to be a string literal (SWC passes a `require()` call) and
* skips any call whose function is bound locally (SWC declares the helper in-file). Without this,
* default and namespace imports stay hoisted and eager — about half of all imports here, and
* exactly the cycle tolerance the loader exists to provide.
*
* Metro sidesteps the problem with a global `_$$_IMPORT_DEFAULT('x')` helper that resolves the
* module id at runtime. That is not portable to webpack, which must see a literal `require('x')`
* to add the module to the graph at all. So instead of hoisting the helper, we inline the whole
* initializer — interop call and literal require together — down to each use site.
*
* Repeating the interop call per use site is cheap: `_interop_require_wildcard` early-returns for
* `__esModule` objects and memoizes the rest in a WeakMap, and `_interop_require_default` is a
* single object literal on the non-ESM path.
*/
function inlineInteropRequiresPlugin({types: t}) {
const isInteropRequire = (node) =>
t.isCallExpression(node) &&
t.isIdentifier(node.callee) &&
INTEROP_HELPERS.has(node.callee.name) &&
node.arguments.length >= 1 &&
t.isCallExpression(node.arguments[0]) &&
t.isIdentifier(node.arguments[0].callee, {name: 'require'}) &&
node.arguments[0].arguments.length === 1 &&
t.isStringLiteral(node.arguments[0].arguments[0]);

return {
visitor: {
Program(programPath) {
for (const binding of Object.values(programPath.scope.bindings)) {
if (!binding.path.isVariableDeclarator() || !isInteropRequire(binding.path.node.init)) {
continue;
}
// Reassignment would make the inlined copies diverge from the original binding.
if (!binding.constant) {
continue;
}
// With no references there is nothing to inline into, and dropping the
// declaration would stop the module loading at all - side-effect imports rely
// on it. Metro's plugin does remove these; staying conservative here.
if (binding.referencePaths.length === 0) {
continue;
}
for (const reference of binding.referencePaths) {
reference.replaceWith(t.cloneNode(binding.path.node.init, true));
}
binding.path.remove();
}
},
},
};
}

export default async function cjsInlineRequiresLoader(source, inputSourceMap) {
const callback = this.async();
try {
const options = this.getOptions() || {};
// On prebuilt bundles SWC can emit a multi-source map, which remapping() below rejects —
// the node_modules rule passes `sourcemap: false` to skip maps entirely.
const sourceMaps = options.sourcemap !== undefined ? options.sourcemap : !!this.sourceMap;

// App source only needs block-scoping (Hermes shares one binding across loop iterations).
// node_modules also ship async generators, which Hermes cannot parse, so the node_modules
// rule opts into lowering those too; everything else modern Hermes handles natively.
const envInclude = options.hermesLowering ? ['transform-block-scoping', 'transform-async-to-generator', 'transform-async-generator-functions'] : ['transform-block-scoping'];

const swcResult = await rspack.experiments.swc.transform(source, {
filename: this.resourcePath,
isModule: true,
env: {
targets: {node: 24},
include: envInclude,
},
jsc: options.hermesLowering
? {
// Loose-mode class lowering, same as the babel config's {loose: true} plugins.
assumptions: {setPublicClassFields: true, privateFieldsAsProperties: true},
}
: undefined,
module: {type: 'commonjs', lazy: false},
sourceMaps,
inputSourceMap: inputSourceMap ? JSON.stringify(inputSourceMap) : undefined,
});

const babelResult = await babel.transformAsync(swcResult.code, {
babelrc: false,
configFile: false,
filename: this.resourcePath,
// Interop-wrapped imports first, then Metro's plugin for the bare `require` form.
plugins: [inlineInteropRequiresPlugin, inlineRequiresPlugin],
sourceMaps,
// The minifier handles formatting; keeping newlines makes build output readable.
compact: false,
});

if (!sourceMaps || !babelResult.map) {
callback(null, babelResult.code);
return;
}

// swcResult.map already folds in inputSourceMap (passed above), so composing the Babel map
// onto it is enough to keep stack traces pointing at original sources.
const map = swcResult.map ? remapping([babelResult.map, swcResult.map], () => null) : babelResult.map;
callback(null, babelResult.code, map);
} catch (error) {
callback(error);
}
}
17 changes: 17 additions & 0 deletions config/repack/expoVirtualEnv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* Replacement for `expo/virtual/env` when bundling natively with Re.Pack (see rspack.config.mjs).
*
* Expo's own module is `export const env = process.env` and relies on Expo's Metro integration to
* populate EXPO_PUBLIC_* values at runtime — which doesn't exist under Re.Pack, leaving them
* undefined. Most importantly, `expo/src/winter/runtime.native` checks EXPO_PUBLIC_USE_RN_FETCH
* and, when falsy, replaces global fetch with expo/fetch — which needs ReadableStream (absent in
* Hermes) and breaks every API request. Keep React Native's built-in fetch, same as Metro builds
* (see metro.config.js / babel.config.js).
*/
const env: Record<string, string> = {
EXPO_PUBLIC_USE_RN_FETCH: '1',
};

// Must be a named export: expo's compiled code imports it as `import {env} from 'expo/virtual/env'`.
// eslint-disable-next-line import/prefer-default-export
export {env};
Loading
Loading