Skip to content

Commit 2847e11

Browse files
committed
feat: add expo plugin
1 parent aaff309 commit 2847e11

File tree

12 files changed

+1770
-13
lines changed

12 files changed

+1770
-13
lines changed

.changeset/fluffy-mice-roll.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@callstack/repack-plugin-expo": minor
3+
---
4+
5+
Add Expo Router + CNG Plugin

packages/plugin-expo/README.md

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
<div align="center">
2+
<img src="https://raw.githubusercontent.com/callstack/repack/HEAD/logo.png" width="650" alt="Re.Pack logo" />
3+
<h3>A toolkit to build your React Native application with Rspack or Webpack.</h3>
4+
</div>
5+
<div align="center">
6+
7+
[![mit licence][license-badge]][license]
8+
[![npm downloads][npm-downloads-badge]][npm-downloads]
9+
[![Chat][chat-badge]][chat]
10+
[![PRs Welcome][prs-welcome-badge]][prs-welcome]
11+
12+
</div>
13+
14+
`@callstack/repack-plugin-expo` is a plugin for [`@callstack/repack`](https://github.com/callstack/repack) that compliments the integration of Expo Router and Expo CNG into your React Native projects.
15+
16+
## About
17+
18+
This plugin helps and compliments the process of enabling Expo Router and Expo CNG in Re.Pack projects by defining necessary globals that are expected by Expo Router at runtime. However, it is not sufficient on its own for a complete setup. For comprehensive guidance on using Expo Router and Expo CNG with Re.Pack, please refer to our [official documentation](https://re-pack.dev/).
19+
20+
## Installation
21+
22+
```sh
23+
npm install -D @callstack/repack-plugin-expo
24+
```
25+
26+
## Usage
27+
28+
### Plugin
29+
30+
To add the plugin to your Re.Pack configuration, update your `rspack.config.js` or `webpack.config.js` as follows:
31+
32+
```js
33+
import { ExpoPlugin } from "@callstack/repack-plugin-expo";
34+
35+
export default (env) => {
36+
// ...
37+
return {
38+
// ...
39+
plugins: [
40+
// ...
41+
new ExpoPlugin(),
42+
],
43+
};
44+
};
45+
```
46+
47+
### CNG Configuration Plugin
48+
49+
To add the configuration plugin to your Expo CNG configuration update your `app.json` or `app.config.js` as follows:
50+
51+
```jsonc
52+
{
53+
// ...
54+
"plugins": [
55+
// ...
56+
"expo-router",
57+
"@callstack/repack-plugin-expo/plugin"
58+
]
59+
}
60+
```
61+
62+
---
63+
64+
Check out our website at https://re-pack.dev for more info and documentation or our GitHub: https://github.com/callstack/repack.
65+
66+
<!-- badges -->
67+
68+
[license-badge]: https://img.shields.io/npm/l/@callstack/repack?style=for-the-badge
69+
[license]: https://github.com/callstack/repack/blob/main/LICENSE
70+
[npm-downloads-badge]: https://img.shields.io/npm/dm/@callstack/repack?style=for-the-badge
71+
[npm-downloads]: https://www.npmjs.com/package/@callstack/repack
72+
[prs-welcome-badge]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=for-the-badge
73+
[prs-welcome]: ./CONTRIBUTING.md
74+
[chat-badge]: https://img.shields.io/discord/426714625279524876.svg?style=for-the-badge
75+
[chat]: https://discord.gg/Q4yr2rTWYF

packages/plugin-expo/package.json

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"name": "@callstack/repack-plugin-expo",
3+
"version": "5.1.3",
4+
"description": "A plugin for @callstack/repack that integrates Expo CNG and Router",
5+
"author": "Carlos Eduardo <[email protected]>",
6+
"contributors": ["Jakub Romańczyk <[email protected]>"],
7+
"license": "MIT",
8+
"homepage": "https://github.com/callstack/repack",
9+
"repository": "github:callstack/repack",
10+
"type": "commonjs",
11+
"main": "dist/index.js",
12+
"types": "dist/index.d.ts",
13+
"files": ["dist", "plugin"],
14+
"exports": {
15+
".": {
16+
"types": "./dist/index.d.ts",
17+
"default": "./dist/index.js"
18+
},
19+
"./plugin": {
20+
"types": "./plugin/index.d.ts",
21+
"default": "./plugin/index.js"
22+
}
23+
},
24+
"keywords": [
25+
"repack",
26+
"re.pack",
27+
"plugin",
28+
"repack-plugin",
29+
"expo-router",
30+
"expo-cng",
31+
"expo"
32+
],
33+
"publishConfig": {
34+
"registry": "https://registry.npmjs.org/",
35+
"access": "public"
36+
},
37+
"engineStrict": true,
38+
"engines": {
39+
"node": ">=18"
40+
},
41+
"peerDependencies": {
42+
"@callstack/repack": "workspace:*",
43+
"@callstack/repack-plugin-expo-modules": "workspace:*",
44+
"expo": "catalog:"
45+
},
46+
"devDependencies": {
47+
"@callstack/repack": "workspace:*",
48+
"@callstack/repack-plugin-expo-modules": "workspace:*",
49+
"expo": "catalog:",
50+
"@rspack/core": "catalog:",
51+
"@types/node": "catalog:",
52+
"webpack": "catalog:"
53+
},
54+
"scripts": {
55+
"build": "tsc -p tsconfig.build.json",
56+
"typecheck": "tsc --noEmit"
57+
}
58+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from '../dist/config-plugin.js';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
module.exports = require('../dist/config-plugin.js');
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// plugins/expo-repack-plugin.js
2+
import type { ConfigPlugin } from 'expo/config-plugins';
3+
import { withAppBuildGradle, withXcodeProject } from 'expo/config-plugins';
4+
5+
/**
6+
* Configuration plugin to remove some expo defaults to ensure that re-pack works correctly.
7+
*
8+
* @param current Current expo configuration
9+
* @returns Modified expo configuration
10+
*/
11+
const plugin: ConfigPlugin = (current) => {
12+
let res = current;
13+
14+
// iOS
15+
// Replace $CLI_PATH and $BUNDLE_COMMAND in the Xcode project (this will ensure that the correct CLI is used in production builds)
16+
res = withXcodeProject(res, (configuration) => {
17+
const xcodeProject = configuration.modResults;
18+
const bundleReactNativeCodeAndImagesBuildPhase =
19+
xcodeProject.buildPhaseObject(
20+
'PBXShellScriptBuildPhase',
21+
'Bundle React Native code and images'
22+
);
23+
24+
if (!bundleReactNativeCodeAndImagesBuildPhase) return configuration;
25+
26+
const script = JSON.parse(
27+
bundleReactNativeCodeAndImagesBuildPhase.shellScript
28+
);
29+
const patched = script
30+
.replace(
31+
/if \[\[ -z "\$CLI_PATH" \]\]; then[\s\S]*?fi\n?/g,
32+
`export CLI_PATH="$("$NODE_BINARY" --print "require('path').dirname(require.resolve('@react-native-community/cli/package.json')) + '/build/bin.js'")"`
33+
)
34+
.replace(/if \[\[ -z "\$BUNDLE_COMMAND" \]\]; then[\s\S]*?fi\n?/g, '');
35+
36+
bundleReactNativeCodeAndImagesBuildPhase.shellScript =
37+
JSON.stringify(patched);
38+
return configuration;
39+
});
40+
41+
// Android
42+
// Replace cliFile and bundleCommand in the app/build.gradle file (this will ensure that the correct CLI is used in production builds)
43+
res = withAppBuildGradle(res, (configuration) => {
44+
const buildGradle = configuration.modResults.contents;
45+
const patched = buildGradle
46+
.replace(/cliFile.*/, '')
47+
.replace(/bundleCommand.*/, 'bundleCommand = "bundle"');
48+
49+
configuration.modResults.contents = patched;
50+
return configuration;
51+
});
52+
53+
return res;
54+
};
55+
56+
export default plugin;

packages/plugin-expo/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { ExpoPlugin } from './plugin.js';

packages/plugin-expo/src/plugin.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { resolve } from 'node:path';
2+
import { ExpoModulesPlugin } from '@callstack/repack-plugin-expo-modules';
3+
import type { Compiler as RspackCompiler } from '@rspack/core';
4+
import type { Compiler as WebpackCompiler } from 'webpack';
5+
6+
interface ExpoRouterOptions {
7+
/**
8+
* Base URL for the Expo Router.
9+
*
10+
* By default, it's set to an empty string.
11+
*/
12+
baseUrl?: string;
13+
14+
/**
15+
* Routes root directory.
16+
*
17+
* By default, it's set to <projectRoot>/app (which is the default for Expo Router).
18+
* It should match the `root` option in your `expo-router` `app.json` plugin configuration.
19+
*
20+
* @see https://docs.expo.dev/router/reference/src-directory/#custom-directory
21+
*/
22+
root?: string;
23+
}
24+
25+
interface ExpoPluginOptions {
26+
/**
27+
* Project root directory.
28+
*
29+
* By default, it's set to the current working directory.
30+
*/
31+
root?: string;
32+
33+
/**
34+
* Wheter to enable Expo Router support.
35+
*
36+
* By default, it's disabled.
37+
*
38+
* If set to `true`, the default options will be used.
39+
* If you want to customize the options, pass an object with the desired options.
40+
*
41+
* @see ExpoRouterOptions
42+
*/
43+
router?: boolean | ExpoRouterOptions;
44+
45+
/**
46+
* Target application platform (e.g. `ios`, `android`).
47+
*
48+
* By default, the platform is inferred from `compiler.options.name` which is set by Re.Pack.
49+
*/
50+
platform?: string;
51+
}
52+
53+
export class ExpoPlugin {
54+
constructor(private options: ExpoPluginOptions = {}) {}
55+
56+
resolveRouterOptions(projectRoot: string) {
57+
if (!this.options.router) {
58+
return null;
59+
}
60+
61+
return typeof this.options.router === 'object'
62+
? this.options.router
63+
: { baseUrl: '', root: resolve(projectRoot, 'app') };
64+
}
65+
66+
apply(compiler: RspackCompiler): void;
67+
apply(compiler: WebpackCompiler): void;
68+
69+
apply(__compiler: unknown) {
70+
const compiler = __compiler as RspackCompiler;
71+
72+
const root = this.options.root ?? process.cwd();
73+
const router = this.resolveRouterOptions(root);
74+
75+
const platform = this.options.platform ?? (compiler.options.name as string);
76+
77+
// Apply Expo Modules support
78+
new ExpoModulesPlugin({ platform }).apply(compiler);
79+
80+
new compiler.webpack.DefinePlugin({
81+
'process.env.EXPO_PROJECT_ROOT': JSON.stringify(root),
82+
// If Expo Router is enabled, pass additional environment variables
83+
...(router
84+
? {
85+
'process.env.EXPO_BASE_URL': JSON.stringify(router.baseUrl),
86+
'process.env.EXPO_ROUTER_ABS_APP_ROOT': JSON.stringify(router.root),
87+
'process.env.EXPO_ROUTER_APP_ROOT': JSON.stringify(router.root),
88+
'process.env.EXPO_ROUTER_IMPORT_MODE': JSON.stringify('sync'),
89+
}
90+
: {}),
91+
}).apply(compiler);
92+
93+
// Add proxy configuration in development
94+
if (
95+
compiler.options.mode === 'development' &&
96+
!!compiler.options.devServer
97+
) {
98+
compiler.options.devServer.proxy ??= [];
99+
100+
// Redirect `/.expo/.virtual-metro-entry` to `/index` to match metro behavior in Expo managed projects
101+
compiler.options.devServer.proxy.push({
102+
context: ['/.expo/.virtual-metro-entry'],
103+
pathRewrite: { '^/.expo/.virtual-metro-entry': '/index' },
104+
});
105+
}
106+
}
107+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDir": "src",
6+
"paths": {}
7+
}
8+
}

packages/plugin-expo/tsconfig.json

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"extends": "../../tsconfig.base.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"rootDirs": ["src", "../repack/src", "../dev-server/src"],
6+
"paths": {
7+
"@callstack/repack": ["../repack/src/index.ts"],
8+
"@callstack/repack/*": ["../repack/src/*"],
9+
"@callstack/repack-dev-server": ["../dev-server/src/index.ts"],
10+
"@callstack/repack-dev-server/*": ["../dev-server/src/*"],
11+
"@callstack/repack-plugin-expo-modules": [
12+
"../plugin-expo-modules/src/index.ts"
13+
],
14+
"@callstack/repack-plugin-expo-modules/*": [
15+
"../plugin-expo-modules/src/*"
16+
]
17+
}
18+
},
19+
"include": ["src/**/*"]
20+
}

0 commit comments

Comments
 (0)