@zntc/react-native 0.1.2 → 0.1.3
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/README.md +79 -19
- package/dist/dev-server/platform-state.d.ts +24 -0
- package/dist/index.js +58 -23
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -1,48 +1,108 @@
|
|
|
1
1
|
# @zntc/react-native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
English · **[한국어](./README_KO.md)**
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> ZNTC React Native platform layer — RN preset + Metro-compatible dev server + Reanimated worklets / Flow / Hermes.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
- **RN 상수 / helpers** — `RN_GLOBAL_IDENTIFIERS` / `tryResolve` / `resolveRnPolyfills`.
|
|
7
|
+
[](https://www.npmjs.com/package/@zntc/react-native)
|
|
8
|
+
[](https://github.com/ohah/zntc/blob/main/LICENSE)
|
|
9
|
+
|
|
10
|
+
`@zntc/react-native` adapts the [ZNTC](https://github.com/ohah/zntc) toolchain to React Native. It turns a small user input into Metro-compatible NAPI build options, ships a Metro-compatible HMR dev server, and wires in the RN-specific transforms (Flow, Reanimated worklets, Hermes target) — all built into the ZNTC core, **without Babel**.
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
What it provides:
|
|
13
|
+
|
|
14
|
+
- **RN preset** — `buildRnBundleOptions(input)` / `bundleRn(input)` / `watchRn(input)`. RN-specific build options (Hermes/ES5 target, Flow, automatic-dev JSX, worklets, dev mode, Fast Refresh, polyfills, RN prelude banner) are applied automatically.
|
|
15
|
+
- **Metro-compatible dev server** — `serveRn(options)` / `buildRnDevServerOptions(input)`. Per-platform watch, HMR bridge over the `/hot` endpoint, and terminal actions, wired together for you.
|
|
16
|
+
- **Metro HMR adapter** — `createMetroHmrAdapter()` emits messages compatible with the RN runtime's HMRClient interface (`hmr:update-start` / `hmr:update` / `hmr:update-done` / `hmr:reload` / `hmr:error` / `log`).
|
|
17
|
+
- **RN runtime** — `runtime/zntc-hmr-client.cjs`, an HMRClient-compatible client for the RN runtime.
|
|
18
|
+
- **Plugin factories** — `createAssetPlugin` / `createBabelPlugin` / `createCodegenPlugin` / `createRequireContextPlugin` / `createMetroResolveRequestPlugin`.
|
|
19
|
+
- **RN constants / helpers** — `RN_GLOBAL_IDENTIFIERS` / `tryResolve` / `resolveRnPolyfills`.
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
- iOS / Android native build orchestration (run-android / run-ios / autolinking) — `@react-native-community/cli` 영역
|
|
21
|
+
Out of scope (handled elsewhere): iOS / Android native build orchestration (`run-android` / `run-ios` / autolinking) belongs to `@react-native-community/cli`.
|
|
17
22
|
|
|
18
|
-
##
|
|
23
|
+
## Installation
|
|
19
24
|
|
|
20
25
|
```bash
|
|
21
26
|
bun add -D @zntc/react-native @zntc/core
|
|
22
|
-
#
|
|
23
|
-
#
|
|
27
|
+
# npm i -D @zntc/react-native @zntc/core
|
|
28
|
+
# pnpm add -D @zntc/react-native @zntc/core
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Some features rely on optional peer packages — install the ones your setup needs:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
bun add -D @babel/core @react-native/babel-preset metro-resolver react-native
|
|
24
35
|
```
|
|
25
36
|
|
|
26
|
-
|
|
37
|
+
`@react-native-community/cli-server-api` is required for the dev server's reload / dev-menu broadcasts.
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
### Attaching to an existing React Native CLI project
|
|
42
|
+
|
|
43
|
+
The simplest path is the scaffolder, which rewrites the `start` / `bundle:*` scripts of an existing RN CLI app to use ZNTC (Metro fallback is preserved):
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx @zntc/init
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
See the [React Native guide](https://ohah.github.io/zntc/guides/react-native/) for details.
|
|
50
|
+
|
|
51
|
+
### RN preset — `buildRnBundleOptions`
|
|
52
|
+
|
|
53
|
+
Convert a small RN input into ZNTC NAPI build options, then run a build:
|
|
27
54
|
|
|
28
55
|
```ts
|
|
29
56
|
import { init, build } from '@zntc/core';
|
|
30
57
|
import { buildRnBundleOptions } from '@zntc/react-native';
|
|
31
58
|
|
|
32
59
|
await init();
|
|
60
|
+
|
|
33
61
|
const result = await build(
|
|
34
62
|
buildRnBundleOptions({
|
|
35
63
|
entry: '/abs/path/index.ts',
|
|
36
64
|
projectRoot: '/abs/path',
|
|
37
|
-
rnPlatform: 'ios',
|
|
65
|
+
rnPlatform: 'ios', // 'ios' | 'android'
|
|
38
66
|
dev: false,
|
|
39
67
|
sourcemap: true,
|
|
40
68
|
}),
|
|
41
69
|
);
|
|
42
70
|
```
|
|
43
71
|
|
|
44
|
-
|
|
72
|
+
`bundleRn(input)` is a one-call shorthand for `build(buildRnBundleOptions(input))`, and `watchRn(input)` starts a watching build.
|
|
73
|
+
|
|
74
|
+
The preset auto-enables the RN-compatible defaults (Hermes/ES5 target, Flow, worklets, polyfills, RN prelude banner, asset loaders, and so on). In dev mode it additionally enables automatic-dev JSX, Fast Refresh, and the dev-mode runtime. You can layer user overrides on top via `input.override` (dictionaries deep-merge, arrays/primitives replace).
|
|
75
|
+
|
|
76
|
+
### Metro-compatible dev server — `serveRn`
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { buildRnDevServerOptions, serveRn } from '@zntc/react-native';
|
|
80
|
+
|
|
81
|
+
const handle = await serveRn(
|
|
82
|
+
buildRnDevServerOptions({
|
|
83
|
+
bundle: {
|
|
84
|
+
entry: '/abs/path/index.ts',
|
|
85
|
+
projectRoot: '/abs/path',
|
|
86
|
+
rnPlatform: 'ios',
|
|
87
|
+
dev: true,
|
|
88
|
+
},
|
|
89
|
+
port: 8081,
|
|
90
|
+
host: 'localhost',
|
|
91
|
+
}),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// handle.url / handle.port — connect the RN app to this server
|
|
95
|
+
// await handle.stop(); — graceful shutdown
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`serveRn` lazily loads `@react-native-community/cli-server-api` and the RN dev middleware, runs a per-platform watching build, serves HMR over `/hot`, and sets up terminal actions (reload / dev menu). The HMR messages are Metro HMRClient-compatible, so the standard RN runtime connects without changes.
|
|
99
|
+
|
|
100
|
+
## Documentation
|
|
101
|
+
|
|
102
|
+
- Monorepo: <https://github.com/ohah/zntc>
|
|
103
|
+
- Docs: <https://ohah.github.io/zntc>
|
|
104
|
+
- React Native guide: <https://ohah.github.io/zntc/guides/react-native/>
|
|
105
|
+
|
|
106
|
+
## License
|
|
45
107
|
|
|
46
|
-
|
|
47
|
-
- #2540 — 본 패키지 신설
|
|
48
|
-
- #2538 — Zig 단일 dev server (후속)
|
|
108
|
+
MIT
|
|
@@ -20,6 +20,30 @@ export interface PlatformStateCallbacks {
|
|
|
20
20
|
onReady?: (state: PlatformState, event: WatchReadyEvent) => void;
|
|
21
21
|
onRebuild?: (state: PlatformState, event: WatchRebuildEvent) => void;
|
|
22
22
|
}
|
|
23
|
+
export interface BundleRefresher {
|
|
24
|
+
/** stale 신호 — generation 을 bump 해 in-flight build 가 이 변경을 마스킹하지 못하게 한다. */
|
|
25
|
+
markStale(): void;
|
|
26
|
+
/** stale 이면 build, in-flight 면 coalesce. */
|
|
27
|
+
refresh(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* in-flight build coalescing + generation guard. `build` 는 시작 시점 소스를 빌드하므로,
|
|
31
|
+
* 진행 중 `markStale()` 이 또 불리면(파일 재변경) 그 build 완료가 stale 을 clear 하면 안 된다
|
|
32
|
+
* (더 새로운 변경을 마스킹 → stale bundle 제공). generation 으로 가드: build 시작 시 generation
|
|
33
|
+
* 을 캡처, 완료 시 generation 이 그대로일 때만 `clearStale`. 새 staleness 가 들어왔으면 stale 을
|
|
34
|
+
* 유지해 다음 `refresh()` 가 최신 소스로 rebuild 한다. `bundleRn`/state 의존을 콜백으로 주입해
|
|
35
|
+
* race-safety 로직만 결정적으로 유닛 테스트할 수 있게 분리.
|
|
36
|
+
*/
|
|
37
|
+
export declare function createBundleRefresher(deps: {
|
|
38
|
+
/** 이미 신선(!bundleStale && bundle≠null)하면 build 스킵. */
|
|
39
|
+
isFresh: () => boolean;
|
|
40
|
+
/** bundleRn + state.bundle/buildError 갱신. 내부 try/catch 라 reject 하지 않음. */
|
|
41
|
+
build: () => Promise<void>;
|
|
42
|
+
/** stale=true. */
|
|
43
|
+
setStale: () => void;
|
|
44
|
+
/** stale=false. */
|
|
45
|
+
clearStale: () => void;
|
|
46
|
+
}): BundleRefresher;
|
|
23
47
|
/**
|
|
24
48
|
* platform 별 watch + state 생성. 첫 build 는 비동기 — caller 가
|
|
25
49
|
* `waitForBuild(state)` 로 대기. RN runtime 이 ios+android 동시 요청 시
|
package/dist/index.js
CHANGED
|
@@ -2226,7 +2226,10 @@ function createMetroResolveRequestPlugin(opts) {
|
|
|
2226
2226
|
try {
|
|
2227
2227
|
const result = resolveRequest({ originModulePath: args.importer ?? "", platform: metroPlatform, resolveRequest: fallbackResolver }, args.path, metroPlatform);
|
|
2228
2228
|
if (result.type === "sourceFile")return { path: result.filePath };
|
|
2229
|
-
if (result.type === "assetFiles")
|
|
2229
|
+
if (result.type === "assetFiles") {
|
|
2230
|
+
const assetPath = result.filePaths[0];
|
|
2231
|
+
return assetPath ? { path: assetPath } : null;
|
|
2232
|
+
}
|
|
2230
2233
|
if (result.type === "empty")return { disabled: true };
|
|
2231
2234
|
} catch (err) {
|
|
2232
2235
|
if (err.message === DELEGATE_TO_DEFAULT_SENTINEL)return null;
|
|
@@ -2531,28 +2534,42 @@ function getBundleText(result) {
|
|
|
2531
2534
|
function getBundleSourceMapText(result) {
|
|
2532
2535
|
return result.outputFiles.find((file) => file.path.endsWith(".map"))?.text ?? null;
|
|
2533
2536
|
}
|
|
2534
|
-
function
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
state.bundleStale = false;
|
|
2547
|
-
}).catch((err) => {
|
|
2548
|
-
state.bundle = null;
|
|
2549
|
-
state.buildError = err instanceof Error ? err.message : String(err);
|
|
2550
|
-
state.bundleStale = false;
|
|
2537
|
+
function createBundleRefresher(deps) {
|
|
2538
|
+
let inFlight = null,generation = 0;
|
|
2539
|
+
function markStale() {
|
|
2540
|
+
deps.setStale();
|
|
2541
|
+
generation += 1;
|
|
2542
|
+
}
|
|
2543
|
+
function refresh() {
|
|
2544
|
+
if (deps.isFresh())return Promise.resolve();
|
|
2545
|
+
if (inFlight)return inFlight;
|
|
2546
|
+
const startGen = generation;
|
|
2547
|
+
inFlight = deps.build().then(() => {
|
|
2548
|
+
if (generation === startGen)deps.clearStale();
|
|
2551
2549
|
}).finally(() => {
|
|
2552
|
-
|
|
2550
|
+
inFlight = null;
|
|
2553
2551
|
});
|
|
2554
|
-
return
|
|
2552
|
+
return inFlight;
|
|
2555
2553
|
}
|
|
2554
|
+
return { markStale, refresh };
|
|
2555
|
+
}
|
|
2556
|
+
function createPlatformState(options,platform,callbacks) {
|
|
2557
|
+
const outputDir = mkdtempSync(join$2(tmpdir(), `zntc-rn-${platform}-`)),outputPath = join$2(outputDir, "bundle.js"),platformBundle = { ...options.bundle, rnPlatform: platform };
|
|
2558
|
+
let refresher;
|
|
2559
|
+
const state = { platform, outputDir, outputPath, handle: undefined, bundle: null, bundleStale: false, refreshBundle: () => refresher.refresh(), sourceMapCache: null, buildError: null, fileCount: 1, lastRebuildTime: Date.now() };
|
|
2560
|
+
refresher = createBundleRefresher({ isFresh: () => !state.bundleStale && state.bundle !== null, build: () => bundleRn(platformBundle).then((result) => {
|
|
2561
|
+
state.bundle = getBundleText(result);
|
|
2562
|
+
const sourceMap = getBundleSourceMapText(result);
|
|
2563
|
+
state.sourceMapCache = sourceMap ? postProcessSourceMap(sourceMap) : null;
|
|
2564
|
+
state.buildError = null;
|
|
2565
|
+
}).catch((err) => {
|
|
2566
|
+
state.bundle = null;
|
|
2567
|
+
state.buildError = err instanceof Error ? err.message : String(err);
|
|
2568
|
+
}), setStale: () => {
|
|
2569
|
+
state.bundleStale = true;
|
|
2570
|
+
}, clearStale: () => {
|
|
2571
|
+
state.bundleStale = false;
|
|
2572
|
+
} });
|
|
2556
2573
|
if (process.env.ZNTC_DEBUG_TERMINAL === "1") {
|
|
2557
2574
|
process.stderr.write(`[zntc:rn-dev:debug] watchRn[${platform}] sourcemap=${platformBundle.sourcemap} dev=${platformBundle.dev} outfile=${outputPath}\n`);
|
|
2558
2575
|
}
|
|
@@ -2576,7 +2593,7 @@ function createPlatformState(options,platform,callbacks) {
|
|
|
2576
2593
|
state.sourceMapCache = null;
|
|
2577
2594
|
if (platformBundle.dev) {
|
|
2578
2595
|
if (event.graphChanged) {
|
|
2579
|
-
|
|
2596
|
+
refresher.markStale();
|
|
2580
2597
|
return state.refreshBundle().then(() => {
|
|
2581
2598
|
if (state.buildError) {
|
|
2582
2599
|
callbacks?.onRebuild?.(state, { ...event, success: false, error: state.buildError });
|
|
@@ -2586,7 +2603,7 @@ function createPlatformState(options,platform,callbacks) {
|
|
|
2586
2603
|
});
|
|
2587
2604
|
}
|
|
2588
2605
|
if (event.updates && event.updates.length > 0) {
|
|
2589
|
-
|
|
2606
|
+
refresher.markStale();
|
|
2590
2607
|
}
|
|
2591
2608
|
callbacks?.onRebuild?.(state, event);
|
|
2592
2609
|
return;
|
|
@@ -2757,9 +2774,23 @@ function isOpenUrlRoute(pathname,method) {
|
|
|
2757
2774
|
}
|
|
2758
2775
|
function resolveOpener(platform=process.platform) {
|
|
2759
2776
|
if (platform === "darwin")return { command: "open", args: (t) => [t] };
|
|
2760
|
-
if (platform === "win32")return { command: "
|
|
2777
|
+
if (platform === "win32")return { command: "rundll32", args: (t) => ["url.dll,FileProtocolHandler", t] };
|
|
2761
2778
|
return { command: "xdg-open", args: (t) => [t] };
|
|
2762
2779
|
}
|
|
2780
|
+
const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
|
|
2781
|
+
function isSafeBrowserUrl(target) {
|
|
2782
|
+
for (let i = 0; i < target.length; i += 1) {
|
|
2783
|
+
const c = target.charCodeAt(i);
|
|
2784
|
+
if (c <= 0x20 || c === 0x22 || c === 0x27 || c === 0x5c)return false;
|
|
2785
|
+
}
|
|
2786
|
+
let parsed;
|
|
2787
|
+
try {
|
|
2788
|
+
parsed = new URL(target);
|
|
2789
|
+
} catch {
|
|
2790
|
+
return false;
|
|
2791
|
+
}
|
|
2792
|
+
return ALLOWED_PROTOCOLS.has(parsed.protocol);
|
|
2793
|
+
}
|
|
2763
2794
|
async function handleOpenUrl(req,res,spawner=spawn,platform=process.platform) {
|
|
2764
2795
|
let body = {};
|
|
2765
2796
|
try {
|
|
@@ -2773,6 +2804,10 @@ async function handleOpenUrl(req,res,spawner=spawn,platform=process.platform) {
|
|
|
2773
2804
|
sendJson(res, 400, { error: "Invalid URL" });
|
|
2774
2805
|
return;
|
|
2775
2806
|
}
|
|
2807
|
+
if (!isSafeBrowserUrl(target)) {
|
|
2808
|
+
sendJson(res, 400, { error: "URL must be an http(s) URL without control characters" });
|
|
2809
|
+
return;
|
|
2810
|
+
}
|
|
2776
2811
|
try {
|
|
2777
2812
|
const { command:command, args:args } = resolveOpener(platform),child = spawner(command, args(target), { detached: true, stdio: "ignore" });
|
|
2778
2813
|
child.unref();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zntc/react-native",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "ZNTC React Native platform — Metro HMR adapter + RN preset (buildRnBundleOptions) + plugin factories + RN runtime",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bundler",
|
|
@@ -48,13 +48,13 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@react-native-community/cli-server-api": "^15.0.0",
|
|
51
|
-
"@zntc/core": "0.1.
|
|
51
|
+
"@zntc/core": "0.1.3",
|
|
52
52
|
"jsc-safe-url": "^0.2.4",
|
|
53
53
|
"source-map": "^0.7.4"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@types/node": "^25.5.2",
|
|
57
|
-
"@zntc/server": "0.1.
|
|
57
|
+
"@zntc/server": "0.1.3"
|
|
58
58
|
},
|
|
59
59
|
"optionalDependencies": {
|
|
60
60
|
"@babel/core": "^7.26.0",
|