@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 CHANGED
@@ -1,48 +1,108 @@
1
1
  # @zntc/react-native
2
2
 
3
- ZNTC React Native platform layer (#2540).
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
- - **RN preset** — `buildRnBundleOptions(input)` / `bundleRn(input)` / `watchRn(input)`. RN-specific NAPI build 옵션 (target=es5, flow, jsx=automatic-dev, devMode, reactRefresh, polyfills, runBeforeMain, banner) 자동 적용.
8
- - **Metro HMR adapter** — `createMetroHmrAdapter()` (`@zntc/server.HmrChannel` 위 thin wrapper). RN runtime 의 HMRClient interface 호환 메시지 (`hmr:update-start` / `hmr:update` / `hmr:update-done` / `hmr:reload` / `hmr:error` / `log`) 송출.
9
- - **RN runtime** — `runtime/zntc-hmr-client.cjs` (Metro HMRClient 인터페이스 호환 RN runtime).
10
- - **Plugin factories**`createAssetPlugin` / `createBabelPlugin` / `createCodegenPlugin` / `createRequireContextPlugin` / `createMetroResolveRequestPlugin`.
11
- - **RN 상수 / helpers** — `RN_GLOBAL_IDENTIFIERS` / `tryResolve` / `resolveRnPolyfills`.
7
+ [![npm](https://img.shields.io/npm/v/@zntc/react-native.svg)](https://www.npmjs.com/package/@zntc/react-native)
8
+ [![License](https://img.shields.io/badge/license-MIT-blue.svg)](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
- - HTTP server / dev-middleware / asset registry / Rozenite DevTools / open-stack-frame / symbolicate RN-specific runtime 영역, 사용자 측 (예: bungae) 가 운영
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
- # 사용 환경에 따라 optional:
23
- # bun add -D @babel/core @react-native/babel-preset metro-resolver react-native
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
- ## 관련 epic
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
- - #2539 — `@zntc/web` + `@zntc/server` 분리 (선행, 완료)
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")return { path: result.filePaths[0] ?? args.path };
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 createPlatformState(options,platform,callbacks) {
2535
- const outputDir = mkdtempSync(join$2(tmpdir(), `zntc-rn-${platform}-`)),outputPath = join$2(outputDir, "bundle.js"),platformBundle = { ...options.bundle, rnPlatform: platform };
2536
- let refreshPromise = null;
2537
- const state = { platform, outputDir, outputPath, handle: undefined, bundle: null, bundleStale: false, refreshBundle: () => refreshPlatformBundle(), sourceMapCache: null, buildError: null, fileCount: 1, lastRebuildTime: Date.now() };
2538
- function refreshPlatformBundle() {
2539
- if (!state.bundleStale && state.bundle !== null)return Promise.resolve();
2540
- if (refreshPromise)return refreshPromise;
2541
- refreshPromise = bundleRn(platformBundle).then((result) => {
2542
- state.bundle = getBundleText(result);
2543
- const sourceMap = getBundleSourceMapText(result);
2544
- state.sourceMapCache = sourceMap ? postProcessSourceMap(sourceMap) : null;
2545
- state.buildError = null;
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
- refreshPromise = null;
2550
+ inFlight = null;
2553
2551
  });
2554
- return refreshPromise;
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
- state.bundleStale = true;
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
- state.bundleStale = true;
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: "cmd", args: (t) => ["/c", "start", "", t] };
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.2",
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.2",
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.2"
57
+ "@zntc/server": "0.1.3"
58
58
  },
59
59
  "optionalDependencies": {
60
60
  "@babel/core": "^7.26.0",