@rollipop/expo 0.0.0 → 1.0.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Geunhyeok LEE
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1 +1,91 @@
1
- # rollipop-empty
1
+ # @rollipop/expo
2
+
3
+ Use Rollipop for Expo SDK 55–58 native development, exports, and native bundle builds.
4
+
5
+ Run `npx @rollipop/init@beta` from your Expo project root for automatic JSON config setup and matching `rollipop` and `@rollipop/expo` development dependencies, then install the updated dependencies. Dynamic JS/TS config files receive manual instructions. See the [Expo guide](https://rollipop.dev/docs/experimental/expo) for details.
6
+
7
+ For manual setup:
8
+
9
+ ```sh
10
+ yarn add -D rollipop@beta @rollipop/expo@beta
11
+ ```
12
+
13
+ Install both packages at the same version. `rollipop` is a required peer dependency of `@rollipop/expo` and must be declared in your project.
14
+
15
+ Add one plugin entry to your existing Expo configuration:
16
+
17
+ ```json
18
+ {
19
+ "expo": {
20
+ "plugins": ["@rollipop/expo"]
21
+ }
22
+ }
23
+ ```
24
+
25
+ Keep existing plugins such as `expo-router`. Continue using `expo start`, `expo run:ios`, `expo run:android`, and `expo export --platform ios --platform android`.
26
+
27
+ The plugin also configures Expo Updates resource generation during `expo prebuild`. Regenerate native projects after adding it if you keep `ios/` or `android/` in source control. Continuous Native Generation and EAS prebuild apply the same mods automatically.
28
+
29
+ ## Release status
30
+
31
+ This integration is a prerelease. Publish this workspace together with the Rollipop inspector integration. Native runtime, builds, and package installation have been verified locally; cloud EAS signing, upload, and store submission have not been verified.
32
+
33
+ The release workflow uses `id-token: write` and publishes public packages with provenance. Before the first automated release, ensure this package exists on npm and configure its [Trusted Publisher](https://docs.npmjs.com/trusted-publishers/) for GitHub owner `rollipop-dev`, repository `rollipop`, and workflow `release.yml`. This package's repository metadata points to that repository and the `packages/expo` directory.
34
+
35
+ Known engine limitation: the pinned `@rollipop/rolldown@1.1.1` can shift source locations after Worklets-generated unmapped code. The same `oxc_sourcemap` bug exists upstream; [a proposed fix](https://github.com/oxc-project/oxc-sourcemap/pull/471) was closed without merging. The regression in `test/sourcemap.test.cjs` retains the correct original-location assertions and is marked `it.skip` with the upstream PR context.
36
+
37
+ ## Compatibility
38
+
39
+ At startup, the adapter checks that `rollipop` is installed in the project and reports matching-version installation commands if it is missing.
40
+
41
+ The adapter supports Expo SDK 55–58 with the matching `@expo/cli` major. Patch and minor CLI releases are accepted when the required private API contract is available; incompatible SDK/CLI pairs and missing methods fail with a diagnostic. Hooks are scoped to projects that enabled the plugin.
42
+
43
+ SDK policies and private CLI bindings live in `src/compatibility/`. SDK 55–57 use Expo's source initialization entry, while SDK 58 uses its compiled entry. The development server, Router, exports, and Updates integration share the same implementation.
44
+
45
+ Hermes syntax targeting follows the SDK default (classic Hermes on SDK 55; V1 on SDK 56–58), `expo-build-properties`, generated native properties, and native build environment settings. When the two platforms use different engines, the shared configuration selects classic Hermes syntax, which also runs on V1. Expo's Hermes compiler helper still selects the project's RN-matched compiler. Configure native engine selection through Expo; the adapter derives Rollipop's `runtimeTarget` from those settings. On classic Hermes, a scoped Constants compatibility transform preserves native HostObject values that SWC's object-rest helper would otherwise drop.
46
+
47
+ | SDK | Expo / CLI | React Native | Worklets | Hermes bytecode |
48
+ | --- | ----------------- | ------------ | -------- | --------------- |
49
+ | 55 | 55.0.32 / 55.0.37 | 0.83.10 | 0.7.4 | 96 |
50
+ | 56 | 56.0.24 / 56.1.27 | 0.85.3 | 0.8.3 | 98 |
51
+ | 57 | 57.0.31 / 57.0.32 | 0.86.3 | 0.10.1 | 98 |
52
+ | 58 | 58.0.19 / 58.1.15 | 0.88.0 | 0.13.0 | 99 |
53
+
54
+ Each matrix entry was checked with an installed package tarball for iOS and Android: development manifests/bundles/maps, native JS/Hermes exports, assets, Router type updates, Inspector connections, React Compiler memoization, Worklets output, and Updates resource generation. These checks reject Metro graph creation. Simulator runtime and native builds were previously verified on SDK 58 with Expo Go 58.0.5; the expanded SDK 55–57 matrix covers the JS build pipeline, without additional native app builds.
55
+
56
+ Installed Worklets automatically enables Rollipop's native SWC transform. `experiments.reactCompiler` enables the compiler through the existing `transform.jsx.compiler` configuration. Compiler opt-outs include the standard `use no memo` and `use no forget` directives plus `worklet` and `widget`. No additional Babel Worklets plugin or second OXC compiler pass is installed.
57
+
58
+ Expo Router retains its runtime, dynamic routes, route groups, synchronous native context, and typed routes. Both `app/` and `src/app/` follow Expo's root selection; the Router plugin's custom root is also respected. Typed declarations update when routes are added or removed.
59
+
60
+ Exports preserve Expo's artifact writers, per-scale asset hashes, native resource names, Hermes compilation, and composed source maps. Expo Updates' separate asset-discovery process is connected through native preloads generated by the config plugin.
61
+
62
+ ## Configuration and limits
63
+
64
+ An optional `rollipop.config.ts` or supported JS/JSON variant supplies Rollipop plugins, transforms, and resolver settings. Custom Metro and Babel files produce a diagnostic because their plugins are not executed. Migrate those customizations to Rollipop configuration. Default native bundling, Worklets, and React Compiler require no Babel configuration.
65
+
66
+ This package targets iOS and Android. Web, DOM/CSS components, API route servers, middleware, React Server Components, and server functions are unsupported. The plugin disables SDK 58's implicit API-server export for native-only apps even when the template has `web.output: 'server'`; explicit server configuration and actual server route files fail with a diagnostic. Native async route splitting and tunnel URLs are unsupported; use synchronous routes and `--lan` or `--localhost`.
67
+
68
+ `EXPO_PUBLIC_*` variables are inlined at startup. Restart the CLI after changing environment files. Expo's LogBox modules and native asset resolver are preserved. The adapter folds LogBox's web-only branches before dependency scanning, including during HMR. As in Expo CLI, the default native warning UI is React Native LogBox; the opt-in `EXPO_UNSTABLE_LOG_BOX` overlay requires Expo's DOM/CSS pipeline and is unsupported.
69
+
70
+ ## Development
71
+
72
+ The [SDK 58 example](../../examples/expo-58/README.md) exercises Router, a local image, UI-thread Worklets, Reanimated, and React Compiler.
73
+
74
+ From the repository root:
75
+
76
+ ```sh
77
+ yarn workspace @rollipop/expo test
78
+ yarn workspace @rollipop/expo typecheck
79
+ yarn workspace @rollipop/expo build
80
+ ```
81
+
82
+ The implementation is written in TypeScript under `src/`. The build emits CommonJS modules and type declarations under `dist/`; the root `app.plugin.js` delegates to the compiled config plugin so Expo can discover it. `prepack` builds the package before publishing. Tests exercise the compiled modules, SDK policy selection, CLI contract guards, engine settings, and delegation to projects that did not opt in.
83
+
84
+ ## Programmatic registration
85
+
86
+ ```js
87
+ const register = require('@rollipop/expo');
88
+ register(__dirname);
89
+ ```
90
+
91
+ Use the config plugin for ordinary apps so native Updates preloads and native-only Router configuration are applied as well. Direct registration only installs CLI hooks.
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./dist/app.plugin.cjs');
@@ -0,0 +1,25 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).exports, mod, mod.exports), cb = null), mod.exports);
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ exports.__commonJSMin = __commonJSMin;
25
+ exports.__toESM = __toESM;
@@ -0,0 +1,171 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_index = require("./compatibility/index.cjs");
3
+ const require_export = require("./export.cjs");
4
+ const require_router = require("./router.cjs");
5
+ const require_config = require("./config.cjs");
6
+ const require_inspector = require("./inspector.cjs");
7
+ const require_updates = require("./updates.cjs");
8
+ let node_path = require("node:path");
9
+ node_path = require_runtime.__toESM(node_path);
10
+ //#region src/adapter.ts
11
+ const PATCH = Symbol.for("@rollipop/expo");
12
+ /**
13
+ * Register the Expo CLI adapter synchronously for one project in the current Node process.
14
+ * Use the Expo config plugin to also configure native Expo Updates build scripts.
15
+ */
16
+ function register(projectRoot) {
17
+ const compatibility = require_index.resolveExpoCompatibility(projectRoot);
18
+ const { projectRoot: root, expoRequire } = compatibility;
19
+ const prototype = compatibility.cli.getServerPrototype();
20
+ const installUpdates = () => require_updates.installUpdatesExports({
21
+ compatibility,
22
+ async buildAssetsAsync(options) {
23
+ const { exp } = expoRequire("@expo/config").getConfig(root);
24
+ const { convertEntryPointToRelative, resolveEntryPoint } = expoRequire("@expo/config/paths");
25
+ return (await prototype.nativeExportBundleAsync.call({ projectRoot: root }, exp, {
26
+ platform: options.platform,
27
+ mainModuleName: convertEntryPointToRelative(root, options.entryFile ?? resolveEntryPoint(root)),
28
+ mode: options.dev ? "development" : "production",
29
+ minify: false,
30
+ bytecode: false,
31
+ serializerIncludeMaps: false
32
+ }, /* @__PURE__ */ new Map())).assets;
33
+ }
34
+ });
35
+ const registeredRoots = prototype[PATCH];
36
+ if (registeredRoots) {
37
+ registeredRoots.set(root, compatibility);
38
+ installUpdates();
39
+ return;
40
+ }
41
+ const roots = /* @__PURE__ */ new Map([[root, compatibility]]);
42
+ Object.defineProperty(prototype, PATCH, { value: roots });
43
+ const enabled = (server) => roots.has(node_path.default.resolve(server.projectRoot));
44
+ const start = prototype.startImplementationAsync;
45
+ prototype.startImplementationAsync = async function(options) {
46
+ const compatibility = roots.get(node_path.default.resolve(this.projectRoot));
47
+ if (!compatibility) return start.call(this, options);
48
+ const { expoRequire } = compatibility;
49
+ const { getConfig } = expoRequire("@expo/config");
50
+ const { exp } = getConfig(this.projectRoot);
51
+ require_router.configureNativeRouter(exp);
52
+ if (options.location.hostType === "tunnel") throw new Error("@rollipop/expo does not support tunnel URLs; use --lan or --localhost");
53
+ const api = await import("rollipop");
54
+ if (options.resetDevServer) await api.resetCache();
55
+ await this.initUrlCreator(options);
56
+ const origin = this.getUrlCreator().constructUrl({ scheme: "http" });
57
+ const host = new URL(origin).hostname;
58
+ if (options.isExporting) return {
59
+ server: {
60
+ close: (done) => done?.(),
61
+ addListener() {}
62
+ },
63
+ location: {
64
+ port: options.port,
65
+ host,
66
+ url: origin,
67
+ protocol: "http"
68
+ },
69
+ middleware: {},
70
+ messageSocket: { broadcast() {} }
71
+ };
72
+ const manifest = await this.getManifestMiddlewareAsync(options);
73
+ const config = await require_config.default(api, compatibility, options.mode ?? "development", exp, origin);
74
+ config.plugins.push({
75
+ name: "@rollipop/expo:manifest",
76
+ configureServer(server) {
77
+ server.middlewares.use(manifest.getHandler());
78
+ }
79
+ });
80
+ const server = await api.createDevServer(config, {
81
+ host,
82
+ port: options.port,
83
+ inspector: require_inspector.default(compatibility, origin)
84
+ });
85
+ server.instance.server.prependListener("request", (request) => {
86
+ const url = new URL(request.url, origin);
87
+ const extension = node_path.default.extname(url.pathname);
88
+ if (!url.pathname.startsWith("/hot/") && (extension === ".bundle" || extension === ".map")) request.url = `/index${extension}${url.search}`;
89
+ });
90
+ server.instance.addHook("preValidation", (request, _reply, done) => {
91
+ if (request.url.startsWith("/symbolicate") && Array.isArray(request.body?.stack)) for (const frame of request.body.stack) {
92
+ const url = URL.parse(frame.file);
93
+ if (url && !url.pathname.startsWith("/hot/") && url.pathname.endsWith(".bundle")) {
94
+ url.pathname = "/index.bundle";
95
+ frame.file = url.href;
96
+ }
97
+ }
98
+ done();
99
+ });
100
+ try {
101
+ await server.instance.listen({
102
+ host,
103
+ port: options.port
104
+ });
105
+ } catch (error) {
106
+ await server.instance.close();
107
+ throw error;
108
+ }
109
+ process.env.EXPO_DEV_SERVER_ORIGIN = origin;
110
+ console.log(`[@rollipop/expo] Rollipop listening at ${origin}`);
111
+ return {
112
+ server: { close: (done) => server.instance.close().then(() => done?.(), done) },
113
+ location: {
114
+ port: options.port,
115
+ host,
116
+ url: origin,
117
+ protocol: "http"
118
+ },
119
+ middleware: server.middlewares,
120
+ messageSocket: { broadcast: server.message.broadcast }
121
+ };
122
+ };
123
+ for (const method of ["watchEnvironmentVariables", "waitForTypeScriptAsync"]) {
124
+ const original = prototype[method];
125
+ prototype[method] = function(...args) {
126
+ return enabled(this) ? Promise.resolve(false) : original.apply(this, args);
127
+ };
128
+ }
129
+ const nativeExport = prototype.nativeExportBundleAsync;
130
+ prototype.nativeExportBundleAsync = async function(exp, options, files, extra = {}) {
131
+ const compatibility = roots.get(node_path.default.resolve(this.projectRoot));
132
+ if (!compatibility) return nativeExport.call(this, exp, options, files, extra);
133
+ const { expoRequire } = compatibility;
134
+ require_router.configureNativeRouter(exp);
135
+ if (!["ios", "android"].includes(options.platform)) throw new Error("@rollipop/expo supports iOS and Android; use expo export --platform ios --platform android");
136
+ const api = await import("rollipop");
137
+ const config = await require_config.default(api, compatibility, options.mode, exp);
138
+ const { getMetroServerRoot } = expoRequire("@expo/config/paths");
139
+ config.entry = node_path.default.resolve(getMetroServerRoot(this.projectRoot), options.mainModuleName);
140
+ const assets = [];
141
+ config.plugins.push({
142
+ name: "@rollipop/expo:export-assets",
143
+ async generateBundle() {
144
+ for (const id of this.getModuleIds()) if (config.resolve.assetExtensions.includes(node_path.default.extname(id).slice(1))) assets.push(await api.AssetUtils.resolveScaledAssets({
145
+ projectRoot: config.root,
146
+ assetPath: id,
147
+ platform: options.platform,
148
+ preferNativePlatform: true
149
+ }));
150
+ }
151
+ });
152
+ const output = await api.runBuild(config, {
153
+ platform: options.platform,
154
+ dev: options.mode !== "production",
155
+ minify: options.minify,
156
+ sourcemap: !!options.serializerIncludeMaps
157
+ });
158
+ console.log(`[@rollipop/expo] Rollipop built ${options.platform} (${options.mode})`);
159
+ return require_export.createExportArtifactsAsync({
160
+ compatibility,
161
+ options,
162
+ extra,
163
+ code: output.code,
164
+ map: output.map,
165
+ assets
166
+ });
167
+ };
168
+ installUpdates();
169
+ }
170
+ //#endregion
171
+ module.exports = register;
@@ -0,0 +1,7 @@
1
+ //#region src/adapter.d.ts
2
+ /**
3
+ * Register the Expo CLI adapter synchronously for one project in the current Node process.
4
+ * Use the Expo config plugin to also configure native Expo Updates build scripts.
5
+ */
6
+ declare function register(projectRoot: string): void;
7
+ export = register;
@@ -0,0 +1,13 @@
1
+ const require_register = require("./register.cjs");
2
+ const require_updates = require("./updates.cjs");
3
+ let node_module = require("node:module");
4
+ //#region src/app.plugin.ts
5
+ /** Enable Rollipop for Expo CLI and configure native Expo Updates build scripts. */
6
+ const withRollipop = (config) => {
7
+ require_register(config._internal.projectRoot);
8
+ const { configureNativeRouter } = (0, node_module.createRequire)(__filename)("./router.cjs");
9
+ configureNativeRouter(config);
10
+ return require_updates.withUpdatesPreload(config);
11
+ };
12
+ //#endregion
13
+ module.exports = withRollipop;
@@ -0,0 +1,5 @@
1
+ import { ConfigPlugin } from "expo/config-plugins";
2
+ //#region src/app.plugin.d.ts
3
+ /** Enable Rollipop for Expo CLI and configure native Expo Updates build scripts. */
4
+ declare const withRollipop: ConfigPlugin;
5
+ export = withRollipop;
@@ -0,0 +1,96 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_constants = require("./compatibility/constants.cjs");
3
+ let node_module = require("node:module");
4
+ let node_path = require("node:path");
5
+ node_path = require_runtime.__toESM(node_path);
6
+ let node_fs = require("node:fs");
7
+ node_fs = require_runtime.__toESM(node_fs);
8
+ let rollipop_filter = require("rollipop/filter");
9
+ //#region src/compat.ts
10
+ const ASSET_RESOLVER = /[/\\]expo-asset[/\\](?:build[/\\]resolveAssetSource\.native\.js|src[/\\]resolveAssetSource\.native\.ts)$/;
11
+ function createExpoCompatPlugins(compatibility, runtimeTarget) {
12
+ const projectRequire = (0, node_module.createRequire)(node_path.default.join(compatibility.projectRoot, "package.json"));
13
+ const expoRoot = node_path.default.dirname(projectRequire.resolve("expo/package.json"));
14
+ const expoHmrPath = node_path.default.join(expoRoot, "src/async-require/hmr.ts");
15
+ const nativeRoot = node_path.default.dirname(projectRequire.resolve("react-native/package.json"));
16
+ const nativeHmrPath = node_path.default.join(nativeRoot, "Libraries/Utilities/HMRClient.js");
17
+ return [
18
+ {
19
+ name: "@rollipop/expo:initialization",
20
+ config: {
21
+ polyfills: [{
22
+ type: "plain",
23
+ path: node_path.default.join(expoRoot, "virtual/streams.js")
24
+ }],
25
+ prelude: [compatibility.winterEntry]
26
+ }
27
+ },
28
+ {
29
+ name: "@rollipop/expo:hmr",
30
+ load: {
31
+ filter: [(0, rollipop_filter.include)((0, rollipop_filter.id)((0, rollipop_filter.exactRegex)(expoHmrPath)))],
32
+ handler() {
33
+ return `export { default } from ${JSON.stringify(nativeHmrPath)};`;
34
+ }
35
+ }
36
+ },
37
+ {
38
+ name: "@rollipop/expo:asset-resolver",
39
+ load: {
40
+ filter: [(0, rollipop_filter.include)((0, rollipop_filter.id)(ASSET_RESOLVER))],
41
+ handler(filename) {
42
+ return node_fs.default.readFileSync(filename, "utf8") + `
43
+ import { Image as RollipopImage } from 'react-native';
44
+ export const setCustomSourceTransformer = RollipopImage.resolveAssetSource.setCustomSourceTransformer;
45
+ `;
46
+ }
47
+ }
48
+ },
49
+ ...runtimeTarget === "hermes" ? [{
50
+ name: "@rollipop/expo:classic-hermes-constants",
51
+ load: {
52
+ filter: [(0, rollipop_filter.include)((0, rollipop_filter.id)(require_constants.CONSTANTS_MODULE))],
53
+ handler: require_constants.loadClassicHermesConstants
54
+ }
55
+ }] : []
56
+ ];
57
+ }
58
+ function createExpoLogBoxPlugin(api, platform) {
59
+ return {
60
+ name: "@rollipop/expo:log-box-platform",
61
+ transform: {
62
+ filter: [(0, rollipop_filter.include)((0, rollipop_filter.id)(/[/\\]@expo[/\\]log-box[/\\](?:src|build)[/\\](?:index\.(?:js|tsx)|Data[/\\]LogBoxData\.(?:js|tsx))$/))],
63
+ handler(code, filename) {
64
+ const transformed = api.rolldownExperimental.transformSync(filename, code, {
65
+ define: { "process.env.EXPO_OS": JSON.stringify(platform) },
66
+ sourcemap: true
67
+ });
68
+ if (transformed.errors.length) this.error(transformed.errors[0]);
69
+ const result = api.rolldownExperimental.minifySync(filename, transformed.code, {
70
+ mangle: false,
71
+ compress: {
72
+ target: "es2015",
73
+ unused: false,
74
+ keepNames: {
75
+ function: true,
76
+ class: true
77
+ },
78
+ joinVars: false,
79
+ sequences: false
80
+ },
81
+ codegen: { removeWhitespace: false },
82
+ sourcemap: true,
83
+ inputMap: transformed.map
84
+ });
85
+ if (result.errors.length) this.error(result.errors[0]);
86
+ return {
87
+ code: result.code,
88
+ map: result.map
89
+ };
90
+ }
91
+ }
92
+ };
93
+ }
94
+ //#endregion
95
+ exports.createExpoLogBoxPlugin = createExpoLogBoxPlugin;
96
+ exports.default = createExpoCompatPlugins;
@@ -0,0 +1,51 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ let node_module = require("node:module");
3
+ let node_path = require("node:path");
4
+ node_path = require_runtime.__toESM(node_path);
5
+ //#region src/compatibility/cli.ts
6
+ function createExpoCli(expoRequire, version) {
7
+ const cliRoot = node_path.default.dirname(expoRequire.resolve("@expo/cli/package.json"));
8
+ const cliRequire = (0, node_module.createRequire)(node_path.default.join(cliRoot, "package.json"));
9
+ function loadFunction(filename, name) {
10
+ const exports = cliRequire(filename);
11
+ if (typeof exports[name] !== "function") throw new Error(`@rollipop/expo: unsupported @expo/cli ${version}; missing ${name} in ${filename}`);
12
+ return exports[name];
13
+ }
14
+ return {
15
+ getServerPrototype() {
16
+ const prototype = loadFunction(node_path.default.join(cliRoot, "build/src/start/server/metro/MetroBundlerDevServer.js"), "MetroBundlerDevServer").prototype;
17
+ for (const method of [
18
+ "startImplementationAsync",
19
+ "nativeExportBundleAsync",
20
+ "initUrlCreator",
21
+ "getManifestMiddlewareAsync",
22
+ "getUrlCreator",
23
+ "watchEnvironmentVariables",
24
+ "waitForTypeScriptAsync"
25
+ ]) if (typeof prototype[method] !== "function") throw new Error(`@rollipop/expo: unsupported @expo/cli ${version}; missing ${method}`);
26
+ return prototype;
27
+ },
28
+ getRouterDirectoryModuleIdWithManifest(projectRoot, exp) {
29
+ return loadFunction(node_path.default.join(cliRoot, "build/src/start/server/metro/router.js"), "getRouterDirectoryModuleIdWithManifest")(projectRoot, exp);
30
+ },
31
+ createDebugMiddleware(options) {
32
+ return loadFunction(node_path.default.join(cliRoot, "build/src/start/server/metro/debugging/createDebugMiddleware.js"), "createDebugMiddleware")(options);
33
+ },
34
+ buildHermesBundleAsync(options) {
35
+ const metroRoot = node_path.default.dirname(expoRequire.resolve("@expo/metro-config/package.json"));
36
+ return loadFunction(node_path.default.join(metroRoot, "build/serializer/exportHermes.js"), "buildHermesBundleAsync")(options);
37
+ },
38
+ getTypedRoutesDeclarationFile(context, options) {
39
+ return loadFunction("@expo/router-server/build/typed-routes/generate", "getTypedRoutesDeclarationFile")(context, options);
40
+ },
41
+ getUpdatesExportPaths() {
42
+ return [
43
+ node_path.default.join(cliRoot, "build/src/expoUpdatesExports.js"),
44
+ expoRequire.resolve("@expo/cli/internal/unstable-expo-updates-exports"),
45
+ expoRequire.resolve("expo/internal/unstable-expo-updates-cli-exports")
46
+ ];
47
+ }
48
+ };
49
+ }
50
+ //#endregion
51
+ exports.createExpoCli = createExpoCli;
@@ -0,0 +1,11 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ let node_fs = require("node:fs");
3
+ node_fs = require_runtime.__toESM(node_fs);
4
+ //#region src/compatibility/constants.ts
5
+ const CONSTANTS_MODULE = /[/\\]expo-constants[/\\]build[/\\]Constants\.js$/;
6
+ function loadClassicHermesConstants(filename) {
7
+ return node_fs.default.readFileSync(filename, "utf8").replace("const { name, appOwnership, ...nativeConstants } = (ExponentConstants || {});", "const { name, appOwnership, ...nativeConstants } = Object.fromEntries(Object.keys(ExponentConstants || {}).map(key => [key, ExponentConstants[key]]));");
8
+ }
9
+ //#endregion
10
+ exports.CONSTANTS_MODULE = CONSTANTS_MODULE;
11
+ exports.loadClassicHermesConstants = loadClassicHermesConstants;
@@ -0,0 +1,45 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ const require_cli = require("./cli.cjs");
3
+ let node_module = require("node:module");
4
+ let node_path = require("node:path");
5
+ node_path = require_runtime.__toESM(node_path);
6
+ let semver = require("semver");
7
+ //#region src/compatibility/index.ts
8
+ const strategies = [
9
+ {
10
+ range: ">=55 <56",
11
+ winterEntry: "expo/src/winter/index.ts",
12
+ hermesV1: false
13
+ },
14
+ {
15
+ range: ">=56 <58",
16
+ winterEntry: "expo/src/winter/index.ts",
17
+ hermesV1: true
18
+ },
19
+ {
20
+ range: ">=58 <59",
21
+ winterEntry: "expo/build/winter",
22
+ hermesV1: true
23
+ }
24
+ ];
25
+ function resolveExpoCompatibility(projectRoot) {
26
+ const root = node_path.default.resolve(projectRoot);
27
+ const appRequire = (0, node_module.createRequire)(node_path.default.join(root, "package.json"));
28
+ const expoRequire = (0, node_module.createRequire)(appRequire.resolve("expo/package.json"));
29
+ const sdkVersion = appRequire("expo/package.json").version;
30
+ const cliVersion = expoRequire("@expo/cli/package.json").version;
31
+ const sdk = (0, semver.parse)(sdkVersion);
32
+ const strategy = strategies.find(({ range }) => (0, semver.satisfies)(sdkVersion, range));
33
+ if (!sdk || !strategy || !(0, semver.satisfies)(cliVersion, `>=${sdk.major}.0.0 <${sdk.major + 1}.0.0`)) throw new Error(`@rollipop/expo requires Expo SDK 55–58 with a matching @expo/cli major; found expo ${sdkVersion}, CLI ${cliVersion}`);
34
+ return {
35
+ projectRoot: root,
36
+ sdkVersion,
37
+ cliVersion,
38
+ expoRequire,
39
+ winterEntry: appRequire.resolve(strategy.winterEntry),
40
+ defaultRuntimeTarget: strategy.hermesV1 ? "hermes-v1" : "hermes",
41
+ cli: require_cli.createExpoCli(expoRequire, cliVersion)
42
+ };
43
+ }
44
+ //#endregion
45
+ exports.resolveExpoCompatibility = resolveExpoCompatibility;
@@ -0,0 +1,31 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ let node_path = require("node:path");
3
+ node_path = require_runtime.__toESM(node_path);
4
+ let node_fs = require("node:fs");
5
+ node_fs = require_runtime.__toESM(node_fs);
6
+ //#region src/compatibility/runtime.ts
7
+ function boolean(value) {
8
+ if (value === true || value === "true" || value === "1") return true;
9
+ if (value === false || value === "false" || value === "0") return false;
10
+ }
11
+ function resolveRuntimeTarget(compatibility, exp) {
12
+ const { projectRoot, defaultRuntimeTarget } = compatibility;
13
+ const defaults = defaultRuntimeTarget === "hermes-v1";
14
+ let ios = defaults;
15
+ let android = defaults;
16
+ for (const plugin of exp.plugins ?? []) {
17
+ if (!Array.isArray(plugin) || plugin[0] !== "expo-build-properties") continue;
18
+ const options = plugin[1];
19
+ ios = boolean(options?.ios?.useHermesV1) ?? boolean(options?.useHermesV1) ?? ios;
20
+ android = boolean(options?.android?.useHermesV1) ?? boolean(options?.useHermesV1) ?? android;
21
+ }
22
+ const podfile = node_path.default.join(projectRoot, "ios/Podfile.properties.json");
23
+ if (node_fs.default.existsSync(podfile)) ios = boolean(JSON.parse(node_fs.default.readFileSync(podfile, "utf8"))["expo.useHermesV1"]) ?? ios;
24
+ const gradle = node_path.default.join(projectRoot, "android/gradle.properties");
25
+ if (node_fs.default.existsSync(gradle)) android = boolean([...node_fs.default.readFileSync(gradle, "utf8").matchAll(/^\s*hermesV1Enabled\s*=\s*(\S+)\s*$/gm)].at(-1)?.[1]) ?? android;
26
+ ios = boolean(process.env.RCT_HERMES_V1_ENABLED) ?? ios;
27
+ android = boolean(process.env.ORG_GRADLE_PROJECT_hermesV1Enabled) ?? android;
28
+ return ios && android ? "hermes-v1" : "hermes";
29
+ }
30
+ //#endregion
31
+ exports.resolveRuntimeTarget = resolveRuntimeTarget;
@@ -0,0 +1,93 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_compat = require("./compat.cjs");
3
+ const require_runtime$1 = require("./compatibility/runtime.cjs");
4
+ const require_export = require("./export.cjs");
5
+ const require_hmr = require("./hmr.cjs");
6
+ const require_router = require("./router.cjs");
7
+ const require_services = require("./services.cjs");
8
+ const require_worklets = require("./worklets.cjs");
9
+ let node_path = require("node:path");
10
+ node_path = require_runtime.__toESM(node_path);
11
+ let node_fs = require("node:fs");
12
+ node_fs = require_runtime.__toESM(node_fs);
13
+ //#region src/config.ts
14
+ const warned = /* @__PURE__ */ new Set();
15
+ async function makeConfig(api, compatibility, mode, exp, devServerOrigin) {
16
+ const { projectRoot, expoRequire } = compatibility;
17
+ const { resolveEntryPoint } = expoRequire("@expo/config/paths");
18
+ const configFile = [
19
+ "ts",
20
+ "mts",
21
+ "cts",
22
+ "js",
23
+ "mjs",
24
+ "cjs",
25
+ "json"
26
+ ].map((extension) => node_path.default.join(projectRoot, `rollipop.config.${extension}`)).find((file) => node_fs.default.existsSync(file));
27
+ const defaults = configFile ? await api.loadConfig({
28
+ cwd: projectRoot,
29
+ configFile,
30
+ mode
31
+ }) : await api.getDefaultConfig(projectRoot, mode);
32
+ if (!warned.has(projectRoot)) {
33
+ const customConfig = [
34
+ "babel.config.js",
35
+ "babel.config.cjs",
36
+ "babel.config.mjs",
37
+ "babel.config.json",
38
+ ".babelrc",
39
+ ".babelrc.js",
40
+ ".babelrc.cjs",
41
+ "metro.config.js",
42
+ "metro.config.cjs",
43
+ "metro.config.mjs"
44
+ ].filter((file) => node_fs.default.existsSync(node_path.default.join(projectRoot, file)));
45
+ if (customConfig.length) console.warn(`[@rollipop/expo] ${customConfig.join(", ")} is not executed. Expo defaults and Worklets are automatic; migrate custom transforms/resolvers to rollipop.config.ts.`);
46
+ warned.add(projectRoot);
47
+ }
48
+ const runtimeTarget = require_runtime$1.resolveRuntimeTarget(compatibility, exp);
49
+ const compat = require_compat.default(compatibility, runtimeTarget);
50
+ const router = require_router.createRouterPlugin(projectRoot, require_router.getRouterRoot(compatibility, exp));
51
+ const entry = resolveEntryPoint(projectRoot);
52
+ const assetRegistryPath = typeof defaults.reactNative.assetRegistryPath === "function" ? await defaults.reactNative.assetRegistryPath(projectRoot) : defaults.reactNative.assetRegistryPath;
53
+ const overrides = {
54
+ configFile: configFile ?? node_path.default.join(projectRoot, "app.json"),
55
+ entry,
56
+ runtimeTarget,
57
+ transform: { define: {
58
+ ...Object.fromEntries(Object.entries(process.env).filter(([key]) => key.startsWith("EXPO_PUBLIC_")).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)])),
59
+ ...devServerOrigin ? { "process.env.EXPO_DEV_SERVER_ORIGIN": JSON.stringify(devServerOrigin) } : {}
60
+ } },
61
+ dev: { hmr: { runtimeImplement: require_hmr.default } },
62
+ plugins: [
63
+ ...compat,
64
+ ...router ? [router] : [],
65
+ require_services.createExpoServicesPlugin(compatibility)
66
+ ],
67
+ rolldownOptions(options, { platform, buildType }) {
68
+ if (!["ios", "android"].includes(platform)) throw new Error("@rollipop/expo supports iOS and Android only");
69
+ Object.assign(options.input.transform.define, {
70
+ "process.env.EXPO_OS": JSON.stringify(platform),
71
+ "process.env.EXPO_SERVER": "false",
72
+ "process.env.EXPO_BASE_URL": "\"\""
73
+ });
74
+ options.input.plugins.unshift(require_compat.createExpoLogBoxPlugin(api, platform), require_export.createExpoAssetPlugin({
75
+ api,
76
+ projectRoot,
77
+ platform,
78
+ assetExtensions: defaults.resolve.assetExtensions,
79
+ assetRegistryPath,
80
+ isNativeExport: buildType === "build"
81
+ }));
82
+ return options;
83
+ }
84
+ };
85
+ return api.mergeConfig(defaults, ...compat.map((plugin) => plugin.config ?? {}), router?.config ?? {}, require_worklets.default(projectRoot), exp.experiments?.reactCompiler ? { transform: { jsx: { compiler: { customOptOutDirectives: [
86
+ "use no memo",
87
+ "use no forget",
88
+ "worklet",
89
+ "widget"
90
+ ] } } } } : {}, overrides);
91
+ }
92
+ //#endregion
93
+ exports.default = makeConfig;
@@ -0,0 +1,92 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_hermes = require("./hermes.cjs");
3
+ let node_path = require("node:path");
4
+ node_path = require_runtime.__toESM(node_path);
5
+ let rollipop_filter = require("rollipop/filter");
6
+ let node_crypto = require("node:crypto");
7
+ let node_fs_promises = require("node:fs/promises");
8
+ node_fs_promises = require_runtime.__toESM(node_fs_promises);
9
+ //#region src/export.ts
10
+ const md5 = (value) => (0, node_crypto.createHash)("md5").update(value).digest("hex");
11
+ async function enrichExpoAssetAsync(asset, { platform, isNativeExport = false } = {}) {
12
+ const indices = asset.scales.map((scale, index) => asset.scales.indexOf(scale) === index ? index : -1).filter((index) => index !== -1);
13
+ const files = indices.map((index) => asset.files[index]);
14
+ return {
15
+ ...asset,
16
+ httpServerLocation: platform === "android" && isNativeExport && asset.httpServerLocation.startsWith("/assets/..") ? `/assets/rollipop/${asset.httpServerLocation.slice(8)}` : asset.httpServerLocation,
17
+ files,
18
+ scales: indices.map((index) => asset.scales[index]),
19
+ fileHashes: await Promise.all(files.map(async (file) => md5(await node_fs_promises.default.readFile(file))))
20
+ };
21
+ }
22
+ function createExpoAssetPlugin({ api, projectRoot, platform, assetExtensions, assetRegistryPath = "react-native/asset-registry", isNativeExport = false }) {
23
+ return {
24
+ name: "@rollipop/expo:asset-hashes",
25
+ load: {
26
+ order: "pre",
27
+ filter: [(0, rollipop_filter.include)((0, rollipop_filter.id)(new RegExp(`\\.(?:${assetExtensions.join("|")})$`)))],
28
+ async handler(id) {
29
+ if (!assetExtensions.includes(node_path.default.extname(id).slice(1))) return null;
30
+ const asset = await api.AssetUtils.resolveScaledAssets({
31
+ projectRoot,
32
+ assetPath: id,
33
+ platform,
34
+ preferNativePlatform: true
35
+ });
36
+ return {
37
+ code: api.AssetUtils.generateAssetRegistryCode(assetRegistryPath, await enrichExpoAssetAsync(asset, {
38
+ platform,
39
+ isNativeExport
40
+ })),
41
+ moduleType: "js"
42
+ };
43
+ }
44
+ }
45
+ };
46
+ }
47
+ async function createExportArtifactsAsync({ compatibility, options, extra = {}, code, map, assets }) {
48
+ const hash = md5(code);
49
+ const extension = options.bytecode ? "hbc" : "js";
50
+ const filename = `_expo/static/js/${options.platform}/index-${hash}.${extension}`;
51
+ const mapFilename = `${filename}.map`;
52
+ let source = code.replace(/^\/\/# sourceMappingURL=.*$/gm, "");
53
+ let sourceMap = map ? JSON.parse(map.toString()) : null;
54
+ if (sourceMap) sourceMap.file = filename;
55
+ if (sourceMap && !options.inlineSourceMap) source += `\n//# sourceMappingURL=${extra.sourceMapUrl ?? node_path.default.posix.basename(mapFilename)}`;
56
+ if (options.bytecode) {
57
+ const result = await require_hermes.default({
58
+ compatibility,
59
+ projectRoot: compatibility.projectRoot,
60
+ code: source,
61
+ map: sourceMap ? JSON.stringify(sourceMap) : null,
62
+ filename: options.mainModuleName,
63
+ minify: options.minify ?? options.mode === "production"
64
+ });
65
+ source = result.hbc;
66
+ sourceMap = result.sourcemap ? JSON.parse(result.sourcemap) : null;
67
+ if (sourceMap) sourceMap.file = filename;
68
+ } else if (sourceMap && options.inlineSourceMap) source += `\n//# sourceMappingURL=data:application/json;base64,${Buffer.from(JSON.stringify(sourceMap)).toString("base64")}`;
69
+ const artifacts = [{
70
+ type: "js",
71
+ filename,
72
+ source,
73
+ metadata: {}
74
+ }];
75
+ if (sourceMap && !options.inlineSourceMap) artifacts.push({
76
+ type: "map",
77
+ filename: mapFilename,
78
+ source: JSON.stringify(sourceMap),
79
+ metadata: {}
80
+ });
81
+ return {
82
+ artifacts,
83
+ assets: await Promise.all(assets.map((asset) => enrichExpoAssetAsync(asset, {
84
+ platform: options.platform,
85
+ isNativeExport: true
86
+ })))
87
+ };
88
+ }
89
+ //#endregion
90
+ exports.createExpoAssetPlugin = createExpoAssetPlugin;
91
+ exports.createExportArtifactsAsync = createExportArtifactsAsync;
92
+ exports.enrichExpoAssetAsync = enrichExpoAssetAsync;
@@ -0,0 +1,6 @@
1
+ //#region src/hermes.ts
2
+ async function compileHermesAsync({ compatibility, ...options }) {
3
+ return compatibility.cli.buildHermesBundleAsync(options);
4
+ }
5
+ //#endregion
6
+ exports.default = compileHermesAsync;
package/dist/hmr.cjs ADDED
@@ -0,0 +1,19 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ let node_fs = require("node:fs");
3
+ node_fs = require_runtime.__toESM(node_fs);
4
+ //#region src/hmr.ts
5
+ var hmr_default = node_fs.default.readFileSync(require.resolve("rollipop/hmr-runtime"), "utf8") + `
6
+ globalThis.__rollipop_runtime__.subscribeGraph(({ runtime }) => {
7
+ const reload = runtime.reload.bind(runtime);
8
+ runtime.reload = (reason) => {
9
+ if (typeof globalThis.expo?.reloadAppAsync === 'function') {
10
+ if (runtime.reloadRequested) return;
11
+ runtime.reloadRequested = true;
12
+ return globalThis.expo.reloadAppAsync(reason || 'Rollipop HMR');
13
+ }
14
+ return reload(reason);
15
+ };
16
+ });
17
+ `;
18
+ //#endregion
19
+ exports.default = hmr_default;
@@ -0,0 +1,15 @@
1
+ //#region src/inspector.ts
2
+ function createExpoInspector(compatibility, serverBaseUrl) {
3
+ const { debugMiddleware, debugWebsocketEndpoints } = compatibility.cli.createDebugMiddleware({
4
+ serverBaseUrl,
5
+ reporter: { update({ level, data }) {
6
+ console[level](...data);
7
+ } }
8
+ });
9
+ return {
10
+ middleware: debugMiddleware,
11
+ websocketEndpoints: debugWebsocketEndpoints
12
+ };
13
+ }
14
+ //#endregion
15
+ exports.default = createExpoInspector;
@@ -0,0 +1,62 @@
1
+ //#region package.json
2
+ var require_package = /* @__PURE__ */ require("./_virtual/_rolldown/runtime.cjs").__commonJSMin(((exports, module) => {
3
+ module.exports = {
4
+ "name": "@rollipop/expo",
5
+ "version": "1.0.0-beta.2",
6
+ "description": "Use Rollipop with Expo native apps through an Expo config plugin.",
7
+ "homepage": "https://github.com/rollipop-dev/rollipop/tree/main/packages/expo#readme",
8
+ "bugs": { "url": "https://github.com/rollipop-dev/rollipop/issues" },
9
+ "license": "MIT",
10
+ "author": "leegeunhyeok <dev.ghlee@gmail.com> (https://github.com/leegeunhyeok)",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/rollipop-dev/rollipop.git",
14
+ "directory": "packages/expo"
15
+ },
16
+ "files": ["dist", "app.plugin.js"],
17
+ "type": "commonjs",
18
+ "main": "./dist/register.cjs",
19
+ "types": "./dist/register.d.cts",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/register.d.cts",
23
+ "default": "./dist/register.cjs"
24
+ },
25
+ "./app.plugin.js": {
26
+ "types": "./dist/app.plugin.d.cts",
27
+ "default": "./app.plugin.js"
28
+ },
29
+ "./updates-preload.cjs": "./dist/updates-preload.cjs",
30
+ "./package.json": "./package.json"
31
+ },
32
+ "scripts": {
33
+ "prepack": "yarn build",
34
+ "typecheck": "tsc --noEmit",
35
+ "build": "vp pack",
36
+ "test": "yarn build && node --test test/*.test.cjs"
37
+ },
38
+ "dependencies": { "semver": "7.8.5" },
39
+ "devDependencies": {
40
+ "@types/semver": "7.8.0",
41
+ "expo": "58.0.17",
42
+ "rollipop": "workspace:*",
43
+ "source-map": "0.8.0",
44
+ "typescript": "catalog:",
45
+ "vite": "catalog:vp",
46
+ "vite-plus": "catalog:vp"
47
+ },
48
+ "peerDependencies": {
49
+ "expo": ">=55.0.0 <59.0.0",
50
+ "rollipop": "workspace:*"
51
+ },
52
+ "engines": { "node": "^22.18.0 || ^24.11.0 || >=26.0.0" },
53
+ "installConfig": { "hoistingLimits": "none" }
54
+ };
55
+ }));
56
+ //#endregion
57
+ Object.defineProperty(exports, "default", {
58
+ enumerable: true,
59
+ get: function() {
60
+ return require_package();
61
+ }
62
+ });
@@ -0,0 +1,21 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_package$1 = require("./package.cjs");
3
+ let node_module = require("node:module");
4
+ let node_path = require("node:path");
5
+ node_path = require_runtime.__toESM(node_path);
6
+ //#region src/register.ts
7
+ /** Enable the adapter only after checking the project's required Rollipop peer. */
8
+ function register(projectRoot) {
9
+ const root = node_path.default.resolve(projectRoot);
10
+ const projectRequire = (0, node_module.createRequire)(node_path.default.join(root, "package.json"));
11
+ try {
12
+ projectRequire.resolve("rollipop/package.json");
13
+ } catch (error) {
14
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
15
+ const { version } = require_package$1.default;
16
+ throw new Error(`[@rollipop/expo] Missing peer dependency "rollipop" in ${root}.\nInstall it alongside @rollipop/expo, then restart Expo CLI:\n yarn add -D rollipop@${version}\n npm install --save-dev rollipop@${version}\n pnpm add -D rollipop@${version}`);
17
+ }
18
+ (0, node_module.createRequire)(__filename)("./adapter.cjs")(root);
19
+ }
20
+ //#endregion
21
+ module.exports = register;
@@ -0,0 +1,4 @@
1
+ //#region src/register.d.ts
2
+ /** Enable the adapter only after checking the project's required Rollipop peer. */
3
+ declare function register(projectRoot: string): void;
4
+ export = register;
@@ -0,0 +1,101 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ let node_module = require("node:module");
3
+ let node_path = require("node:path");
4
+ node_path = require_runtime.__toESM(node_path);
5
+ let node_fs = require("node:fs");
6
+ node_fs = require_runtime.__toESM(node_fs);
7
+ let rollipop_filter = require("rollipop/filter");
8
+ //#region src/router.ts
9
+ function createRouterPlugin(projectRoot, appRoot) {
10
+ const projectRequire = (0, node_module.createRequire)(node_path.default.join(projectRoot, "package.json"));
11
+ let routerRoot;
12
+ try {
13
+ routerRoot = node_path.default.dirname(projectRequire.resolve("expo-router/package.json"));
14
+ } catch (error) {
15
+ if (error.code !== "MODULE_NOT_FOUND") throw error;
16
+ return null;
17
+ }
18
+ const contexts = new Set([
19
+ "_ctx.js",
20
+ "_ctx.ios.js",
21
+ "_ctx.android.js"
22
+ ].map((file) => node_path.default.join(routerRoot, file)));
23
+ return {
24
+ name: "@rollipop/expo:router",
25
+ config: { rolldownOptions(options) {
26
+ Object.assign(options.input.transform.define, {
27
+ "process.env.EXPO_PROJECT_ROOT": JSON.stringify(projectRoot),
28
+ "process.env.EXPO_ROUTER_ABS_APP_ROOT": JSON.stringify(appRoot),
29
+ "process.env.EXPO_ROUTER_APP_ROOT": JSON.stringify(node_path.default.relative(routerRoot, appRoot)),
30
+ "process.env.EXPO_ROUTER_IMPORT_MODE": "\"sync\""
31
+ });
32
+ return options;
33
+ } },
34
+ load: {
35
+ filter: [...contexts].map((file) => (0, rollipop_filter.include)((0, rollipop_filter.id)((0, rollipop_filter.exactRegex)(file)))),
36
+ handler(id) {
37
+ if (!contexts.has(id)) return;
38
+ if (!node_fs.default.existsSync(appRoot)) throw new Error(`Expo Router route directory is missing: ${appRoot}`);
39
+ const files = [];
40
+ const visit = (directory) => {
41
+ this.addWatchFile(directory);
42
+ for (const entry of node_fs.default.readdirSync(directory, { withFileTypes: true })) {
43
+ const absolute = node_path.default.join(directory, entry.name);
44
+ if (entry.isDirectory()) visit(absolute);
45
+ else if (entry.isFile()) {
46
+ const key = `./${node_path.default.relative(appRoot, absolute).split(node_path.default.sep).join("/")}`;
47
+ if (/(?:\+api|\+middleware)\.[tj]sx?$/.test(key)) throw new Error(`@rollipop/expo does not support API route servers or middleware: ${key}`);
48
+ if (/^\.\/(?!(?:(?:.*\+api)|(?:\+html)|(?:\+middleware))\.[tj]sx?$).*\.[tj]sx?$/.test(key)) files.push([key, absolute]);
49
+ }
50
+ }
51
+ };
52
+ visit(appRoot);
53
+ files.sort(([left], [right]) => left.localeCompare(right));
54
+ return `const routes = {\n${files.map(([key, absolute]) => ` ${JSON.stringify(key)}: () => require(${JSON.stringify(absolute)})`).join(",\n")}\n};
55
+ export function ctx(key) {
56
+ if (!Object.prototype.hasOwnProperty.call(routes, key)) {
57
+ const error = new Error("Cannot find route module '" + key + "'");
58
+ error.code = 'MODULE_NOT_FOUND';
59
+ throw error;
60
+ }
61
+ return routes[key]();
62
+ }
63
+ ctx.keys = () => Object.keys(routes);
64
+ ctx.resolve = (key) => {
65
+ if (!Object.prototype.hasOwnProperty.call(routes, key)) return ctx(key);
66
+ return key;
67
+ };
68
+ ctx.id = ${JSON.stringify(appRoot)};
69
+ `;
70
+ }
71
+ },
72
+ hotUpdate({ type, file, modules }) {
73
+ const relative = node_path.default.relative(appRoot, file);
74
+ if (relative.startsWith(`..${node_path.default.sep}`) || node_path.default.isAbsolute(relative)) return;
75
+ if (type !== "delete" && node_fs.default.existsSync(file) && !node_fs.default.statSync(file).isDirectory() && this.getModuleInfo(file) != null) return modules.filter((id) => !contexts.has(id));
76
+ if (type !== "delete" && node_fs.default.existsSync(file)) return;
77
+ return [.../* @__PURE__ */ new Set([...modules.filter((id) => id !== file), ...[...contexts].filter((id) => this.getModuleInfo(id) != null)])];
78
+ }
79
+ };
80
+ }
81
+ function getRouterRoot(compatibility, exp) {
82
+ const { projectRoot, cli } = compatibility;
83
+ return node_path.default.resolve(projectRoot, cli.getRouterDirectoryModuleIdWithManifest(projectRoot, exp));
84
+ }
85
+ function configureNativeRouter(config) {
86
+ if (config.experiments?.reactServerComponentRoutes || config.experiments?.reactServerFunctions) throw new Error("@rollipop/expo does not support React Server Components or server functions");
87
+ const router = config.extra?.router;
88
+ if (router?.apiRoutes === true) throw new Error("@rollipop/expo does not support API route servers");
89
+ config.extra = {
90
+ ...config.extra,
91
+ router: {
92
+ ...router,
93
+ apiRoutes: false
94
+ }
95
+ };
96
+ return config;
97
+ }
98
+ //#endregion
99
+ exports.configureNativeRouter = configureNativeRouter;
100
+ exports.createRouterPlugin = createRouterPlugin;
101
+ exports.getRouterRoot = getRouterRoot;
@@ -0,0 +1,41 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_router = require("./router.cjs");
3
+ let node_module = require("node:module");
4
+ let node_path = require("node:path");
5
+ node_path = require_runtime.__toESM(node_path);
6
+ let node_fs = require("node:fs");
7
+ node_fs = require_runtime.__toESM(node_fs);
8
+ //#region src/services.ts
9
+ function createExpoServicesPlugin(compatibility) {
10
+ const { projectRoot, expoRequire, cli } = compatibility;
11
+ return {
12
+ name: "@rollipop/expo:services",
13
+ configureServer(server) {
14
+ const { exp } = expoRequire("@expo/config").getConfig(projectRoot);
15
+ if (!exp.experiments?.typedRoutes) return;
16
+ const projectRequire = (0, node_module.createRequire)(node_path.default.join(projectRoot, "package.json"));
17
+ const routerDirectory = require_router.getRouterRoot(compatibility, exp);
18
+ const { requireContext } = projectRequire("expo-router/internal/testing");
19
+ const { EXPO_ROUTER_CTX_IGNORE } = projectRequire("expo-router/_ctx-shared");
20
+ const typesDirectory = node_path.default.join(projectRoot, ".expo/types");
21
+ const updateRoutes = () => {
22
+ const context = requireContext(routerDirectory, true, EXPO_ROUTER_CTX_IGNORE);
23
+ const declaration = cli.getTypedRoutesDeclarationFile(context, exp.extra?.router);
24
+ node_fs.default.mkdirSync(typesDirectory, { recursive: true });
25
+ node_fs.default.writeFileSync(node_path.default.join(typesDirectory, "router.d.ts"), declaration);
26
+ };
27
+ let timer;
28
+ const watcher = node_fs.default.watch(routerDirectory, { recursive: true }, () => {
29
+ clearTimeout(timer);
30
+ timer = setTimeout(updateRoutes, 100);
31
+ });
32
+ server.instance.addHook("onClose", async () => {
33
+ clearTimeout(timer);
34
+ watcher.close();
35
+ });
36
+ updateRoutes();
37
+ }
38
+ };
39
+ }
40
+ //#endregion
41
+ exports.createExpoServicesPlugin = createExpoServicesPlugin;
@@ -0,0 +1,10 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ const require_register = require("./register.cjs");
3
+ let node_path = require("node:path");
4
+ node_path = require_runtime.__toESM(node_path);
5
+ //#region src/updates-preload.ts
6
+ if (node_path.default.basename(process.argv[1] ?? "") === "createUpdatesResources.js") {
7
+ const projectRoot = process.argv[3];
8
+ if (projectRoot) require_register(projectRoot);
9
+ }
10
+ //#endregion
@@ -0,0 +1 @@
1
+ export {}
@@ -0,0 +1,80 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ let node_module = require("node:module");
3
+ let node_path = require("node:path");
4
+ node_path = require_runtime.__toESM(node_path);
5
+ let node_fs_promises = require("node:fs/promises");
6
+ node_fs_promises = require_runtime.__toESM(node_fs_promises);
7
+ //#region src/updates.ts
8
+ const roots = /* @__PURE__ */ new Map();
9
+ const BUILD_ASSETS = Symbol("@rollipop/expo:updates-assets");
10
+ const replacements = /* @__PURE__ */ new Map();
11
+ function installUpdatesExports({ compatibility, buildAssetsAsync }) {
12
+ const { projectRoot, cli } = compatibility;
13
+ roots.set(node_path.default.resolve(projectRoot), buildAssetsAsync);
14
+ const filenames = cli.getUpdatesExportPaths();
15
+ const cliRoot = filenames[0];
16
+ const original = require(filenames[0]);
17
+ for (const name of ["createMetroServerAndBundleRequestAsync", "exportEmbedAssetsAsync"]) if (typeof original[name] !== "function") throw new Error(`@rollipop/expo: unsupported @expo/cli ${compatibility.cliVersion}; missing ${name}`);
18
+ let replacement = replacements.get(cliRoot);
19
+ if (!replacement) replacement = {
20
+ ...original,
21
+ async createMetroServerAndBundleRequestAsync(root, options) {
22
+ const build = roots.get(node_path.default.resolve(root));
23
+ if (!build) return original.createMetroServerAndBundleRequestAsync(root, options);
24
+ return {
25
+ server: {
26
+ [BUILD_ASSETS]: () => build(options),
27
+ end() {}
28
+ },
29
+ bundleRequest: options
30
+ };
31
+ },
32
+ async exportEmbedAssetsAsync(server, request, root, options) {
33
+ const build = server[BUILD_ASSETS];
34
+ if (build) return build();
35
+ return original.exportEmbedAssetsAsync(server, request, root, options);
36
+ }
37
+ };
38
+ replacements.set(cliRoot, replacement);
39
+ for (const filename of filenames) {
40
+ require(filename);
41
+ require.cache[filename].exports = replacement;
42
+ }
43
+ }
44
+ const BEGIN = "# @generated begin @rollipop/expo-updates";
45
+ const END = "# @generated end @rollipop/expo-updates";
46
+ function withUpdatesPreload(config) {
47
+ const projectRoot = config._internal.projectRoot;
48
+ const { withDangerousMod, withAppBuildGradle } = (0, node_module.createRequire)(node_path.default.join(projectRoot, "package.json"))("expo/config-plugins");
49
+ config = withDangerousMod(config, ["ios", async (mod) => {
50
+ const filename = node_path.default.join(mod.modRequest.platformProjectRoot, ".xcode.env");
51
+ const clean = (await node_fs_promises.default.readFile(filename, "utf8").catch((error) => {
52
+ if (error.code === "ENOENT") return "";
53
+ throw error;
54
+ })).replace(new RegExp(`${BEGIN}[\\s\\S]*?${END}\\n?`, "g"), "").trimEnd();
55
+ const generated = [
56
+ BEGIN,
57
+ `ROLLIPOP_EXPO_UPDATES_PRELOAD="$("$NODE_BINARY" --print "require.resolve('@rollipop/expo/updates-preload.cjs')")"`,
58
+ "export NODE_OPTIONS=\"${NODE_OPTIONS:+$NODE_OPTIONS }--require \\\"$ROLLIPOP_EXPO_UPDATES_PRELOAD\\\"\"",
59
+ END
60
+ ].join("\n");
61
+ await node_fs_promises.default.writeFile(filename, `${clean}\n${generated}\n`);
62
+ return mod;
63
+ }]);
64
+ return withAppBuildGradle(config, (mod) => {
65
+ const begin = "// @generated begin @rollipop/expo-updates";
66
+ const end = "// @generated end @rollipop/expo-updates";
67
+ const clean = mod.modResults.contents.replace(new RegExp(`${begin}[\\s\\S]*?${end}\\n?`, "g"), "");
68
+ const generated = [
69
+ begin,
70
+ `def rollipopExpoUpdatesPreload = (nodeExecutableAndArgs.get() + ["--print", "require.resolve('@rollipop/expo/updates-preload.cjs')"]).execute(null, rootDir).text.trim()`,
71
+ "nodeExecutableAndArgs.set(nodeExecutableAndArgs.get() + [\"--require\", rollipopExpoUpdatesPreload])",
72
+ end
73
+ ].map((line) => ` ${line}`).join("\n");
74
+ mod.modResults.contents = clean.replace(/react\s*\{/, (match) => `${match}\n${generated}`);
75
+ return mod;
76
+ });
77
+ }
78
+ //#endregion
79
+ exports.installUpdatesExports = installUpdatesExports;
80
+ exports.withUpdatesPreload = withUpdatesPreload;
@@ -0,0 +1,24 @@
1
+ const require_runtime = require("./_virtual/_rolldown/runtime.cjs");
2
+ let node_module = require("node:module");
3
+ let node_path = require("node:path");
4
+ node_path = require_runtime.__toESM(node_path);
5
+ let semver = require("semver");
6
+ //#region src/worklets.ts
7
+ function createWorkletsConfig(projectRoot) {
8
+ const appRequire = (0, node_module.createRequire)(node_path.default.join(projectRoot, "package.json"));
9
+ const installed = (name) => {
10
+ try {
11
+ return appRequire(`${name}/package.json`);
12
+ } catch (error) {
13
+ if (error.code === "MODULE_NOT_FOUND") return null;
14
+ throw error;
15
+ }
16
+ };
17
+ const reanimated = installed("react-native-reanimated");
18
+ const worklets = installed("react-native-worklets");
19
+ if (reanimated && !(0, semver.satisfies)(reanimated.version, ">=4.0.0")) throw new Error("@rollipop/expo requires Reanimated 4+ on Expo SDK 55–58");
20
+ if (reanimated && !worklets) throw new Error("Reanimated requires react-native-worklets. Run: npx expo install react-native-reanimated react-native-worklets");
21
+ return worklets ? { experimental: { worklets: {} } } : {};
22
+ }
23
+ //#endregion
24
+ exports.default = createWorkletsConfig;
package/package.json CHANGED
@@ -1,4 +1,63 @@
1
1
  {
2
2
  "name": "@rollipop/expo",
3
- "version": "0.0.0"
4
- }
3
+ "version": "1.0.0-beta.2",
4
+ "description": "Use Rollipop with Expo native apps through an Expo config plugin.",
5
+ "homepage": "https://github.com/rollipop-dev/rollipop/tree/main/packages/expo#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/rollipop-dev/rollipop/issues"
8
+ },
9
+ "license": "MIT",
10
+ "author": "leegeunhyeok <dev.ghlee@gmail.com> (https://github.com/leegeunhyeok)",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/rollipop-dev/rollipop.git",
14
+ "directory": "packages/expo"
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "app.plugin.js"
19
+ ],
20
+ "type": "commonjs",
21
+ "main": "./dist/register.cjs",
22
+ "types": "./dist/register.d.cts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/register.d.cts",
26
+ "default": "./dist/register.cjs"
27
+ },
28
+ "./app.plugin.js": {
29
+ "types": "./dist/app.plugin.d.cts",
30
+ "default": "./app.plugin.js"
31
+ },
32
+ "./updates-preload.cjs": "./dist/updates-preload.cjs",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "scripts": {
36
+ "prepack": "yarn build",
37
+ "typecheck": "tsc --noEmit",
38
+ "build": "vp pack",
39
+ "test": "yarn build && node --test test/*.test.cjs"
40
+ },
41
+ "dependencies": {
42
+ "semver": "7.8.5"
43
+ },
44
+ "devDependencies": {
45
+ "@types/semver": "7.8.0",
46
+ "expo": "58.0.17",
47
+ "rollipop": "1.0.0-beta.2",
48
+ "source-map": "0.8.0",
49
+ "typescript": "7.0.2",
50
+ "vite": "@voidzero-dev/vite-plus-core@1.3.0",
51
+ "vite-plus": "1.3.0"
52
+ },
53
+ "peerDependencies": {
54
+ "expo": ">=55.0.0 <59.0.0",
55
+ "rollipop": "1.0.0-beta.2"
56
+ },
57
+ "engines": {
58
+ "node": "^22.18.0 || ^24.11.0 || >=26.0.0"
59
+ },
60
+ "installConfig": {
61
+ "hoistingLimits": "none"
62
+ }
63
+ }
package/.editorconfig DELETED
@@ -1,10 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- end_of_line = lf
5
- insert_final_newline = true
6
-
7
- [*.{js,json,yml}]
8
- charset = utf-8
9
- indent_style = space
10
- indent_size = 2
package/.gitattributes DELETED
@@ -1,4 +0,0 @@
1
- /.yarn/** linguist-vendored
2
- /.yarn/releases/* binary
3
- /.yarn/plugins/**/* binary
4
- /.pnp.* binary linguist-generated