Conversation
|
|
@whydidoo is attempting to deploy a commit to the Callstack Team on Vercel. A member of the Team first needs to authorize it. |
|
@whydidoo can you check the merge conflicts? |
| "expo-router": "~56.2.7", | ||
| "expo-status-bar": "~56.0.4", | ||
| "react": "19.2.3", | ||
| "react-native": "0.85.3", |
There was a problem hiding this comment.
I recently updated our react native version to "0.86.0"
also note that we have some of our package versions in catalogs in pnpm workspace
would be good to keep react native versions in sync across projects I think
There was a problem hiding this comment.
Expo SDK 57 currently expects React Native 0.86.2 and React 19.2.3.
I’ve kept both Expo examples on the same Expo-compatible versions rather than forcing the default catalog versions. We could add a dedicated Expo SDK 57 catalog later to keep those versions centralized as well.
|
lets try and align the Expo package and examples with the workspace catalogs At the moment the new Expo projects use direct pins for React, React Native, Community CLI packages, the React Native Babel preset, React types, TypeScript, and Module Federation, while existing testers use the shared catalog entries. In particular, the Expo apps use React Native 0.86.2 while the catalog is 0.86.0; if 0.86.2 is the Expo 57-compatible version, please update the shared React Native catalog to 0.86.2 and have the relevant examples consume it through the catalog. The Expo SDK packages themselves can live in a dedicated Expo 57 catalog, shared by both Expo fixtures, so their versions do not drift. The same applies to any Expo-specific React, CLI, or Babel compatibility pins that genuinely cannot use the default catalog. regarding typescript version I have a separate pr that updates typescript to v7 which we could merge before this and then update those expo projects to be on v7 as well |
- align Expo 57 fixtures with workspace catalogs and React Native 0.86.2 - tolerate whitespace changes in generated Android and iOS config anchors - stop package manager lookup at package.json workspaces - use Rspack RuleSetRules for Expo Router configuration - add regression coverage for config anchors and workspace detection
MikitasK
left a comment
There was a problem hiding this comment.
excellent work overall 👏 👍
just 2 comments about forwarding custom Expo entry & supporting Rspack 2 cache invalidation, plus 2 non-blocking edge-cases related suggestions:
| entry: ResolvedExpoEntry, | ||
| loaderPath: string | ||
| ): boolean { | ||
| if (entry.request !== EXPO_ROUTER_ENTRY) return false; |
There was a problem hiding this comment.
Should this resolve the entry path instead of comparing the request string?
Below here is ai reasoning for why it might be necessary. I'm not fully certain but wondering what you think.
In native Release builds the Expo template resolves ENTRY_FILE / entryFile to an absolute path (expo/scripts/resolveAppEntry <root> <platform> absolute), and the config plugin leaves that line in place. Re.Pack forwards it unchanged into env.entry, and the init-generated config passes it on via new ExpoPlugin({ entry, platform }). request is then /…/node_modules/expo-router/entry.js, this check returns false, and the raw entry-classic.js gets bundled together with @expo/metro-runtime.
I checked with a production compile of a small Expo Router fixture: with new ExpoPlugin({ platform }) (the tester-expo shape) the bootstrap loader is installed and @expo/metro-runtime is absent; with new ExpoPlugin({ platform, entry: <absolute path> }) the loader is skipped and it is bundled. The build still succeeds, so nothing visibly breaks today, but dev and Release then boot through different code and the Metro-free path the README and tests describe isn't taken on the documented setup.
Something like this would cover it:
function isExpoRouterEntry(entry: ResolvedExpoEntry): boolean {
if (entry.request === EXPO_ROUTER_ENTRY) return true;
try {
const routerEntryPath = fs.realpathSync(
require.resolve(EXPO_ROUTER_ENTRY, { paths: [entry.projectRoot] })
);
return entry.entryPath === routerEntryPath;
} catch {
return false;
}
}realpathSync on both sides matters for pnpm's symlinked layout (resolveExpoEntry already realpaths entryPath). Alternatively resolveExpoEntry could normalise request back to expo-router/entry when the resolved file matches, and this check stays as is. Either way a test that passes the absolute entry would be good to have.
There was a problem hiding this comment.
Fixed: absolute Router entries now use the resolved real path. Added iOS/Android regression tests.
| } from './types.js'; | ||
|
|
||
| export const CONFIG_PLUGIN = '@callstack/repack-expo'; | ||
| export const RSPACK_COMMANDS = '@callstack/repack/commands/rspack'; |
There was a problem hiding this comment.
Since #1424 this entry point is a deprecated shim that prints a warning on every CLI invocation (react-native webpack-start in tester-expo shows it as the first line). The other testers and repack-init were switched to @callstack/repack/commands, which detects Rspack from rspack.config.* and registers the same command names, so init, doctor and both Expo apps' react-native.config.js can point there instead. Nothing else needs to change; no test pins the old string.
There was a problem hiding this comment.
Done, switched init/doctor and both Expo apps to @callstack/repack/commands.
| ); | ||
| }; | ||
|
|
||
| export = createRunOncePlugin(withRepackExpo, '@callstack/repack-expo', '0.0.0'); |
There was a problem hiding this comment.
Could this read the version from package.json instead of the '0.0.0' literal? Expo only records it in the plugin history, so it is harmless today, but test/package-boundary.test.cjs:13 and :40 also assert the literal, so a version change will fail those tests. Comparing against packageJson.version in the tests would avoid that.
There was a problem hiding this comment.
Done, the plugin and tests now read the version from package.json.
|
|
| "extra": { | ||
| "eas": { | ||
| "projectId": "49a167cf-9412-446e-ba71-0c07c816ed93" | ||
| } | ||
| }, | ||
| "owner": "wuskas" |
There was a problem hiding this comment.
owner and extra.eas.projectId point at a personal Expo account, so anyone else running EAS against this app gets an ownership error. We need to figure out what these should be for a Callstack-owned project; I'll take care of that, so no change needed from your side here.
There was a problem hiding this comment.
Yeah, this was only added for local testing. I can remove it.
| "peerDependencies": { | ||
| "@callstack/repack": "workspace:^", | ||
| "@rspack/core": ">=1", | ||
| "expo": ">=56" |
There was a problem hiding this comment.
The tests need babel-preset-expo, react-native, expo-router, @module-federation/enhanced and source-map, but none of them resolve from this package; the fixtures get them by symlinking apps/tester-expo/node_modules, packages/repack/node_modules/@module-federation/enhanced and packages/dev-server's source-map (public-environment.test.cjs:72, repack-plugin-options.test.cjs:55, module-federation-source-map.test.cjs:14,41, expo-plugin.test.cjs:174). That ties this package's suite to the tester app's install and to other packages' private node_modules. Could these be declared as devDependencies here (catalog / catalog:expo57) and the fixtures populated by resolving from this package's own root instead?
There was a problem hiding this comment.
Done, added the devDependencies here. Fixtures now use this package’s own node_modules. Tests pass.
| function normalizePublishRange( | ||
| name: string, | ||
| range: string | ||
| ): string | undefined { | ||
| if (!range.startsWith('workspace:')) return range; | ||
| try { | ||
| const sibling = readJson( | ||
| path.resolve(__dirname, `../../../${name.split('/').at(-1)}/package.json`) | ||
| ); | ||
| const version = sibling.version; | ||
| if (typeof version !== 'string') return undefined; | ||
| return range === 'workspace:^' ? `^${version}` : version; | ||
| } catch { | ||
| return undefined; | ||
| } | ||
| } |
There was a problem hiding this comment.
This walks up to packages/repack/package.json from dist/cli/, which only exists inside this monorepo. In the published package the branch is never taken: pnpm pack already rewrites workspace:^ to ^5.3.0 in peerDependencies, and releases go through pnpm changeset publish. So it only affects init runs from within the repo, and no test asserts the value it produces (cli.test.cjs:37 only checks for *). Could we drop it and copy the peer range as-is, keeping monorepo layout assumptions out of shipped code?
There was a problem hiding this comment.
Done, removed the lookup and copy peer ranges as-is. Verified pnpm pack rewrites workspace:^.
Remove repack:release:android and repack:release:ios aliases from tester-expo-widget while keeping the verified host release commands. Document why standalone widget Release currently fails: its config publishes nested lazy chunks as remote files, but the standalone Release resolver expects them inside the installed application. Keep standalone Release support deferred. Preserve the widget's production remote build commands and the remote-in-host workflow validated on Android and iOS.
|
Done, added all three to the Changesets ignore list |
The catalog bump to react-native 0.86.2 left the committed Podfile.lock files of tester-app, tester-federation and tester-federation-v2 pinned to 0.86.0, so every pod install rewrote them. Also picks up the ReactTestApp-Resources pod that tester-federation-v2's app.json resources require.
… with react-native 0.86.2
The expo57 catalog duplicated libraries that the other tester apps already use, at different versions (React 19.2.3 vs 19.2.8, worklets 0.10.1 vs 0.11.3, Module Federation 2.1.0 vs 2.8.0, CLI 20.1.2 vs 20.2.0, and others), so the workspace installed two copies of each. - Keep only Expo packages in the expo57 catalog and bump them to the Expo 57.0.26 release (expo ~57.0.26 plus matching expo-* floors). - Move shared libraries (reanimated, worklets, safe-area-context, screens, async-storage, Module Federation, community CLI) into the testers catalog and point every tester app at it. - Bump react-native and @react-native/* to 0.86.3 across the workspace, matching Expo 57.0.26, and update the Podfile.lock files. - Override react-native-screens to the catalog version: expo-router declares it as a regular dependency, which otherwise resolves a second, newer copy than the native one the apps link. - Derive the Expo host and widget Module Federation requiredVersion values from the installed packages instead of hardcoding them. - Drop the react-native-worklets@0.10.1 packageExtensions patch; 0.11.3 declares those Babel dependencies itself. The shared libraries are slightly ahead of Expo SDK 57's recommended pins; the Expo apps build and run with them on iOS and Android.
The main test matrix runs `pnpm test` on Node.js 18, but the package requires Node.js >=20.19 and Expo's tooling depends on newer APIs (for example `util.parseEnv` in @expo/env), so 10 tests failed there. The test script now reads `engines.node` from package.json and skips with a message on older Node.js versions; otherwise it builds and runs the node:test suite as before.
The owner and EAS projectId tied the tester app to a personal Expo account. Nothing reads them locally (Expo Updates is disabled), and prebuild output is unchanged without them; EAS commands will now ask to link a project instead.
…reset Re.Pack's Babel loader locates hermes-parser through the application's @react-native/babel-preset. Expo applications use babel-preset-expo and don't install that preset, so every module failed to compile with "Failed to import 'hermes-parser'". The repository fixtures hid this because they install the preset. The Expo Babel loader now passes Re.Pack a hermes-parser resolved, in order, through the application's babel-preset-expo (so it follows the Expo SDK), its @react-native/babel-preset, or the hermes-parser bundled with repack-expo (^0.36.0, the version used by React Native 0.86 and babel-preset-expo 57). An application-configured hermesParserPath still takes precedence.
The Expo Babel caller keeps ES modules (supportsStaticESM), so Rspack applied Node's fully-specified ESM resolution to .mjs files and .js files in "type": "module" packages. React Native libraries such as React Navigation and AsyncStorage publish such builds with Metro-style extensionless imports that rely on platform extensions (for example ./useBackButton -> useBackButton.native.js), so they failed with "Module not found". Non-Expo Re.Pack builds transform imports to CommonJS and never hit this; tester-expo didn't either because Expo Router vendors React Navigation. Babel rules configured by ExpoPlugin, including its default rule, now set resolve.fullySpecified to false unless the rule sets it explicitly.
| try { | ||
| const presetPath = require.resolve(preset, { paths: [projectRoot] }); | ||
| const syntaxPluginPath = require.resolve( | ||
| 'babel-plugin-syntax-hermes-parser', |
There was a problem hiding this comment.
The React Native 0.88 Babel preset uses flow-parser instead of babel-plugin-syntax-hermes-parser. Once Expo adopts that change, this resolver may fall back to the bundled Hermes parser instead of the one used by the preset. We can add support for that in a follow-up.
Summary
This PR introduces first-class Expo support for Re.Pack through a new
@callstack/repack-expopackage.It allows Expo SDK 56 applications using prebuild/CNG to use Rspack and the existing Re.Pack runtime, including ScriptManager and Module Federation v2, without changing the core Re.Pack packages.
The package remains private while we validate the initial integration contract.
What’s included
ExpoPluginfor Rspack that owns the required Expo/Re.Pack defaults:EXPO_PUBLIC_*environment variablesnpx @callstack/repack-expo initnpx @callstack/repack-expo doctorModule Federation v2
Module Federation remains explicit and application-owned, matching regular Re.Pack behavior.
The implementation supports:
React.lazy()chunks alongside remote widgetsRemote widgets cannot provide native dependencies. Native modules must already be installed and linked in the host application.
Native integration
@callstack/repack-expo initonly updates application-owned configuration.Native changes are applied through the Expo Config Plugin during
expo prebuild. The integration does not require users to maintain custom Swift, Kotlin, Gradle or Xcode changes manually.Development applications are launched with:
npm run repack:start npm run repack:ios # or npm run repack:android