@rn-iso/metro 1.0.0 → 1.3.0

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.
@@ -0,0 +1,26 @@
1
+ //#region index.d.ts
2
+ type FileStoreCtor = new (options: {
3
+ root: string;
4
+ }) => object;
5
+ interface MetroEvent {
6
+ type?: string;
7
+ level?: unknown;
8
+ data?: unknown;
9
+ error?: unknown;
10
+ stack?: string;
11
+ buildID?: string;
12
+ }
13
+ interface NdjsonReporter {
14
+ dir: string;
15
+ update(event: MetroEvent): void;
16
+ readonly drops: number;
17
+ }
18
+ declare function cacheRoot(name?: string | null): string;
19
+ declare function sharedCacheStores(name?: string, { FileStore }?: {
20
+ FileStore?: FileStoreCtor;
21
+ }): object[];
22
+ declare function ndjsonReporter({ dir }?: {
23
+ dir?: string;
24
+ }): NdjsonReporter;
25
+ //#endregion
26
+ export { NdjsonReporter, cacheRoot, ndjsonReporter, sharedCacheStores };
package/dist/index.js ADDED
@@ -0,0 +1,197 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
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
+ let node_fs = require("node:fs");
25
+ node_fs = __toESM(node_fs);
26
+ let node_os = require("node:os");
27
+ node_os = __toESM(node_os);
28
+ let node_path = require("node:path");
29
+ node_path = __toESM(node_path);
30
+ //#region index.ts
31
+ function configDir() {
32
+ return process.env.RN_ISO_HOME || node_path.default.join(node_os.default.homedir(), ".rn-iso");
33
+ }
34
+ function cacheNameSegment(name) {
35
+ return String(name).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^\.+/, "") || "app";
36
+ }
37
+ function cacheRoot(name) {
38
+ if (process.env.RN_ISO_METRO_CACHE) return process.env.RN_ISO_METRO_CACHE;
39
+ const root = node_path.default.join(configDir(), "metro-cache");
40
+ return name === void 0 || name === null || name === "" ? root : node_path.default.join(root, cacheNameSegment(name));
41
+ }
42
+ function registerCache({ dir, name, prune, note, entriesDepth }) {
43
+ try {
44
+ const home = configDir();
45
+ const file = node_path.default.join(home, "caches.json");
46
+ let manifest = {
47
+ version: 1,
48
+ caches: []
49
+ };
50
+ try {
51
+ const parsed = JSON.parse(node_fs.default.readFileSync(file, "utf-8"));
52
+ if (Array.isArray(parsed?.caches)) manifest = {
53
+ version: 1,
54
+ caches: parsed.caches
55
+ };
56
+ } catch {}
57
+ const others = manifest.caches.filter((c) => c.dir !== dir);
58
+ const record = {
59
+ dir,
60
+ name,
61
+ prune,
62
+ note,
63
+ registeredBy: process.cwd()
64
+ };
65
+ if (entriesDepth) record.entriesDepth = entriesDepth;
66
+ others.push(record);
67
+ node_fs.default.mkdirSync(home, { recursive: true });
68
+ node_fs.default.writeFileSync(file, JSON.stringify({
69
+ version: 1,
70
+ caches: others
71
+ }, null, 2));
72
+ } catch {}
73
+ }
74
+ function registerOnce(dir) {
75
+ registerCache({
76
+ dir,
77
+ name: "Metro transform cache",
78
+ prune: "entries",
79
+ entriesDepth: 2,
80
+ note: "shared Metro transforms; no eviction of its own"
81
+ });
82
+ }
83
+ function sharedCacheStores(name = "app", { FileStore } = {}) {
84
+ const Store = FileStore || require("metro-cache").FileStore;
85
+ const root = cacheRoot(name);
86
+ registerOnce(root);
87
+ return [new Store({ root })];
88
+ }
89
+ const NDJSON_LEVELS = /* @__PURE__ */ new Set([
90
+ "debug",
91
+ "info",
92
+ "warn",
93
+ "error",
94
+ "fatal"
95
+ ]);
96
+ function ndjsonLevel(level, fallback) {
97
+ const value = String(level === void 0 || level === null ? "" : level).toLowerCase();
98
+ if (NDJSON_LEVELS.has(value)) return value;
99
+ switch (value) {
100
+ case "log":
101
+ case "dir":
102
+ case "table":
103
+ case "group":
104
+ case "groupcollapsed":
105
+ case "groupend": return "info";
106
+ case "trace": return "debug";
107
+ case "warning": return "warn";
108
+ default: return fallback;
109
+ }
110
+ }
111
+ function formatValue(value) {
112
+ if (typeof value === "string") return value;
113
+ if (value instanceof Error) return value.message || String(value);
114
+ try {
115
+ const json = JSON.stringify(value);
116
+ if (json !== void 0) return json;
117
+ } catch {}
118
+ try {
119
+ return String(value);
120
+ } catch {
121
+ return "[unprintable]";
122
+ }
123
+ }
124
+ function formatData(data) {
125
+ if (data === void 0 || data === null) return "";
126
+ if (Array.isArray(data)) return data.map(formatValue).join(" ");
127
+ return formatValue(data);
128
+ }
129
+ function errorMessage(error) {
130
+ if (error === void 0 || error === null) return "unknown error";
131
+ if (typeof error === "string") return error;
132
+ if (typeof error.message === "string" && error.message) return error.message;
133
+ return formatValue(error);
134
+ }
135
+ function ndjsonReporter({ dir } = {}) {
136
+ const logDir = dir || node_path.default.join(process.cwd(), ".rn-iso", "logs");
137
+ let ensured = false;
138
+ let drops = 0;
139
+ function write(file, record) {
140
+ try {
141
+ if (!ensured) {
142
+ node_fs.default.mkdirSync(logDir, { recursive: true });
143
+ ensured = true;
144
+ }
145
+ node_fs.default.appendFileSync(node_path.default.join(logDir, file), JSON.stringify(record) + "\n");
146
+ } catch {
147
+ drops += 1;
148
+ ensured = false;
149
+ }
150
+ }
151
+ function update(event) {
152
+ try {
153
+ const type = event && typeof event.type === "string" ? event.type : "";
154
+ const record = {
155
+ ts: Date.now(),
156
+ src: "metro",
157
+ level: "debug",
158
+ msg: ""
159
+ };
160
+ if (type) record.event = type;
161
+ if (type === "client_log") {
162
+ record.src = "client";
163
+ record.level = ndjsonLevel(event.level, "info");
164
+ record.msg = formatData(event.data);
165
+ if (event.stack) record.stack = event.stack;
166
+ write("client.ndjson", record);
167
+ return;
168
+ }
169
+ if (type === "bundling_error" || type === "transformer_error") {
170
+ record.level = "error";
171
+ record.msg = errorMessage(event.error);
172
+ } else if (type === "bundle_build_done" || type === "bundle_build_failed") {
173
+ const what = type === "bundle_build_done" ? "bundle build done" : "bundle build failed";
174
+ record.level = "info";
175
+ record.msg = event.buildID ? `${what} (${event.buildID})` : what;
176
+ record.marker = true;
177
+ } else if (type === "unstable_server_log") {
178
+ record.level = ndjsonLevel(event.level, "info");
179
+ record.msg = formatData(event.data);
180
+ } else record.msg = formatData(event && event.data) || type || "metro event";
181
+ write("metro.ndjson", record);
182
+ } catch {
183
+ drops += 1;
184
+ }
185
+ }
186
+ return {
187
+ dir: logDir,
188
+ update,
189
+ get drops() {
190
+ return drops;
191
+ }
192
+ };
193
+ }
194
+ //#endregion
195
+ exports.cacheRoot = cacheRoot;
196
+ exports.ndjsonReporter = ndjsonReporter;
197
+ exports.sharedCacheStores = sharedCacheStores;
package/package.json CHANGED
@@ -1,17 +1,30 @@
1
1
  {
2
2
  "name": "@rn-iso/metro",
3
- "version": "1.0.0",
3
+ "version": "1.3.0",
4
4
  "description": "Metro integration for rn-iso: one transform cache shared by every worktree, plus an NDJSON reporter for structured logs.",
5
- "main": "index.js",
5
+ "license": "MIT",
6
6
  "files": [
7
- "index.js",
8
- "README.md"
7
+ "dist",
8
+ "README.md",
9
+ "LICENSE"
9
10
  ],
10
- "license": "MIT",
11
+ "main": "dist/index.js",
12
+ "types": "dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "require": "./dist/index.js",
17
+ "default": "./dist/index.js"
18
+ }
19
+ },
11
20
  "scripts": {
12
- "test": "node --test test/*.test.js"
21
+ "test": "node --test test/*.test.js",
22
+ "build": "tsdown"
13
23
  },
14
24
  "peerDependencies": {
15
25
  "metro-cache": "*"
26
+ },
27
+ "engines": {
28
+ "node": ">=22"
16
29
  }
17
- }
30
+ }
package/index.js DELETED
@@ -1,273 +0,0 @@
1
- // The two things rn-iso wires into Metro: a transform cache shared by every
2
- // worktree on the machine, and a reporter that writes the dev server's events
3
- // as NDJSON.
4
- //
5
- // Metro's default cache lives under the project, so a second worktree starts
6
- // cold and re-transforms the whole module graph -- thousands of modules, every
7
- // time. Pointing every checkout at one store means only the first one pays.
8
- //
9
- // const { sharedCacheStores } = require('@rn-iso/metro');
10
- // config.cacheStores = sharedCacheStores('myapp');
11
- //
12
- // The thin part is the FileStore. The part worth packaging is telling rn-iso the
13
- // cache exists, so `gc` can report and trim it -- Metro's FileStore has
14
- // no eviction logic whatsoever, so without that it grows until the disk does.
15
- //
16
- // The reporter is the other half, and it only works when Metro is hosted
17
- // programmatically: both the Expo CLI and the React Native CLI overwrite
18
- // config.reporter after loading metro.config.js, so a reporter set there is
19
- // discarded. rn-iso's supervisor hosts Metro itself and passes this one in.
20
- //
21
- // const { ndjsonReporter } = require('@rn-iso/metro');
22
- // config.reporter = ndjsonReporter({ dir: '<root>/.rn-iso/logs' });
23
-
24
- const fs = require('node:fs');
25
- const os = require('node:os');
26
- const path = require('node:path');
27
-
28
- // THIS RESOLUTION EXISTS THREE TIMES: here, in
29
- // packages/expo-build-cache/index.js, and in rn-iso's own src/paths.js
30
- // (sharedMetroCache / sharedBuildCache). This package cannot import that module
31
- // -- it has to work on a machine with no rn-iso installed at all -- so the
32
- // duplication is deliberate, the same way buildCacheKey is duplicated between
33
- // the build-cache implementations. Change one and you must change all three:
34
- // when they drift, one entry point writes a cache the other will never read,
35
- // and neither of them says so. rn-iso's test/cache-packages.test.js asserts all
36
- // three agree.
37
- //
38
- // RN_ISO_METRO_CACHE comes first because it did before the layout existed, and
39
- // quietly ignoring an override someone already set reads as an empty cache
40
- // rather than as an error. It names one directory, so it wins for a named cache
41
- // too -- otherwise half the stores on a machine would move and half would not.
42
- function configDir() {
43
- return process.env.RN_ISO_HOME || path.join(os.homedir(), '.rn-iso');
44
- }
45
-
46
- // Anything that is not a plain path segment is replaced, and leading dots go, so
47
- // a scoped package name cannot climb out of the cache root.
48
- function cacheNameSegment(name) {
49
- return String(name).replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^\.+/, '') || 'app';
50
- }
51
-
52
- function cacheRoot(name) {
53
- if (process.env.RN_ISO_METRO_CACHE) return process.env.RN_ISO_METRO_CACHE;
54
- const root = path.join(configDir(), 'metro-cache');
55
- return name === undefined || name === null || name === '' ? root : path.join(root, cacheNameSegment(name));
56
- }
57
-
58
- // Registering makes this cache visible to `rn-iso gc`'s report, which is the
59
- // only thing that will ever trim it -- Metro's FileStore has no eviction of its
60
- // own.
61
- //
62
- // The manifest is written directly rather than through rn-iso's own module, for
63
- // two reasons that both made the import silently do nothing:
64
- // - the documented way to use the CLI is `npx rn-iso`, so it is usually not a
65
- // dependency of the project and the specifier does not resolve at all
66
- // - rn-iso is an ES module, so `require` of it throws ERR_REQUIRE_ESM on Node
67
- // before 20.19
68
- // A dynamic import fixes the second and not the first.
69
- function registerCache({ dir, name, prune, note, entriesDepth }) {
70
- try {
71
- const home = configDir();
72
- const file = path.join(home, 'caches.json');
73
- let manifest = { version: 1, caches: [] };
74
- try {
75
- const parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
76
- if (Array.isArray(parsed?.caches)) manifest = { version: 1, caches: parsed.caches };
77
- } catch {
78
- // No manifest yet, or an unreadable one: start clean rather than fail.
79
- }
80
- // Keyed on the directory so repeated calls update rather than accumulate --
81
- // these run on every build.
82
- const others = manifest.caches.filter(c => c.dir !== dir);
83
- const record = { dir, name, prune, note, registeredBy: process.cwd() };
84
- // Only written when the caller sets it: an absent depth means the entries
85
- // are the directory's immediate children, which is the common case.
86
- if (entriesDepth) record.entriesDepth = entriesDepth;
87
- others.push(record);
88
- fs.mkdirSync(home, { recursive: true });
89
- fs.writeFileSync(file, JSON.stringify({ version: 1, caches: others }, null, 2));
90
- } catch {
91
- // A cache that cannot announce itself still works; it is just invisible.
92
- }
93
- }
94
-
95
- function registerOnce(dir) {
96
- registerCache({
97
- dir,
98
- name: 'Metro transform cache',
99
- // One file per cache key, so entries nothing has touched can go
100
- // individually rather than emptying the whole store. FileStore shards one
101
- // level above them, so the entries are two deep.
102
- prune: 'entries',
103
- entriesDepth: 2,
104
- note: 'shared Metro transforms; no eviction of its own',
105
- });
106
- }
107
-
108
- // `name` only distinguishes one app's cache from another's on the same machine:
109
- // it is a subdirectory of the shared root, not a directory of its own. Metro
110
- // keys entries by content, so sharing one store between unrelated projects would
111
- // be correct but pointlessly large.
112
- function sharedCacheStores(name = 'app', { FileStore } = {}) {
113
- const Store = FileStore || require('metro-cache').FileStore;
114
- const root = cacheRoot(name);
115
- registerOnce(root);
116
- return [new Store({ root })];
117
- }
118
-
119
- // --- the NDJSON reporter ------------------------------------------------
120
- //
121
- // One JSON object per line, per rn-iso's log record contract:
122
- //
123
- // { ts, src: 'metro'|'client', level: 'debug'|'info'|'warn'|'error'|'fatal',
124
- // msg, event?, stack?, marker? }
125
- //
126
- // Two files, because the two sources answer different questions: metro.ndjson
127
- // is the bundler (did it build, what failed to transform) and client.ndjson is
128
- // the app (what the running code logged and threw). Metro forwards the latter
129
- // through the same reporter, so splitting here is what keeps `logs --source
130
- // client` from being a grep over bundler chatter.
131
- //
132
- // The rules this file lives by: a logging failure must never become a dev
133
- // server failure. Metro calls update() from inside its own build pipeline, so a
134
- // throw here -- an event shape from a Metro version this package never saw, an
135
- // unwritable log directory -- would take the server down with it. Every path
136
- // swallows and counts instead.
137
-
138
- const NDJSON_LEVELS = new Set(['debug', 'info', 'warn', 'error', 'fatal']);
139
-
140
- // Metro speaks the console's vocabulary on client logs (`log`, `trace`,
141
- // `group`) and its own on server logs. Anything unrecognized falls back to the
142
- // caller's default rather than inventing a level.
143
- function ndjsonLevel(level, fallback) {
144
- const value = String(level === undefined || level === null ? '' : level).toLowerCase();
145
- if (NDJSON_LEVELS.has(value)) return value;
146
- switch (value) {
147
- case 'log':
148
- case 'dir':
149
- case 'table':
150
- case 'group':
151
- case 'groupcollapsed':
152
- case 'groupend':
153
- return 'info';
154
- case 'trace':
155
- return 'debug';
156
- case 'warning':
157
- return 'warn';
158
- default:
159
- return fallback;
160
- }
161
- }
162
-
163
- // Client logs arrive as the console's argument list, so they are joined the way
164
- // a console would print them. A value that cannot be stringified (a circular
165
- // object, a proxy that throws) still has to produce something.
166
- function formatValue(value) {
167
- if (typeof value === 'string') return value;
168
- if (value instanceof Error) return value.message || String(value);
169
- try {
170
- const json = JSON.stringify(value);
171
- if (json !== undefined) return json;
172
- } catch {
173
- // Fall through to String(), which handles circular structures.
174
- }
175
- try {
176
- return String(value);
177
- } catch {
178
- return '[unprintable]';
179
- }
180
- }
181
-
182
- function formatData(data) {
183
- if (data === undefined || data === null) return '';
184
- if (Array.isArray(data)) return data.map(formatValue).join(' ');
185
- return formatValue(data);
186
- }
187
-
188
- // Metro wraps its failures differently depending on where they came from: a
189
- // resolution failure is an Error, a transformer failure can be a plain object
190
- // carrying only a message.
191
- function errorMessage(error) {
192
- if (error === undefined || error === null) return 'unknown error';
193
- if (typeof error === 'string') return error;
194
- if (typeof error.message === 'string' && error.message) return error.message;
195
- return formatValue(error);
196
- }
197
-
198
- function ndjsonReporter({ dir } = {}) {
199
- const logDir = dir || path.join(process.cwd(), '.rn-iso', 'logs');
200
- let ensured = false;
201
- let drops = 0;
202
-
203
- // Lazily: constructing a reporter must not create directories for a server
204
- // that may never start, and the log directory is workspace-local, so it may
205
- // not exist yet at all.
206
- function write(file, record) {
207
- try {
208
- if (!ensured) {
209
- fs.mkdirSync(logDir, { recursive: true });
210
- ensured = true;
211
- }
212
- fs.appendFileSync(path.join(logDir, file), JSON.stringify(record) + '\n');
213
- } catch {
214
- // An unwritable log directory is a housekeeping problem, not a build one.
215
- // The count is what makes it visible instead of silent.
216
- drops += 1;
217
- ensured = false;
218
- }
219
- }
220
-
221
- function update(event) {
222
- try {
223
- const type = event && typeof event.type === 'string' ? event.type : '';
224
- const record = { ts: Date.now(), src: 'metro', level: 'debug', msg: '' };
225
- if (type) record.event = type;
226
-
227
- if (type === 'client_log') {
228
- record.src = 'client';
229
- record.level = ndjsonLevel(event.level, 'info');
230
- record.msg = formatData(event.data);
231
- // Passed through as-is: symbolication happens on the reading side, and
232
- // a stack this reporter could not parse is still better than no stack.
233
- if (event.stack) record.stack = event.stack;
234
- write('client.ndjson', record);
235
- return;
236
- }
237
-
238
- if (type === 'bundling_error' || type === 'transformer_error') {
239
- record.level = 'error';
240
- record.msg = errorMessage(event.error);
241
- } else if (type === 'bundle_build_done') {
242
- record.level = 'info';
243
- record.msg = event.buildID ? `bundle build done (${event.buildID})` : 'bundle build done';
244
- // The marker resets the window `rn-iso logs --errors` reports over: a
245
- // successful build is the point past which older errors are history.
246
- record.marker = true;
247
- } else if (type === 'unstable_server_log') {
248
- record.level = ndjsonLevel(event.level, 'info');
249
- record.msg = formatData(event.data);
250
- } else {
251
- // Everything else is kept at debug rather than dropped: the event name
252
- // is often the only evidence of what the server was doing before it
253
- // failed, and debug costs nothing to a default query.
254
- record.msg = formatData(event && event.data) || type || 'metro event';
255
- }
256
-
257
- write('metro.ndjson', record);
258
- } catch {
259
- // The event shape came from a package this one does not version.
260
- drops += 1;
261
- }
262
- }
263
-
264
- return {
265
- dir: logDir,
266
- update,
267
- get drops() {
268
- return drops;
269
- },
270
- };
271
- }
272
-
273
- module.exports = { sharedCacheStores, cacheRoot, ndjsonReporter };