@stim-cli/metro 1.0.0-rc.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Janic Duplessis
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 ADDED
@@ -0,0 +1,91 @@
1
+ # @stim-cli/metro
2
+
3
+ > Commands use `stim`. If it is not installed globally, replace `stim` with
4
+ > `npx stim-cli`.
5
+
6
+ The two things [`stim-cli`](https://www.npmjs.com/package/stim-cli) wires into
7
+ Metro: one transform cache shared by every worktree on the machine, and a
8
+ reporter that writes the dev server's events as NDJSON.
9
+
10
+ ```bash
11
+ npm i -D @stim-cli/metro
12
+ ```
13
+
14
+ ## The shared transform cache
15
+
16
+ Metro's default cache lives under the project, so a second worktree starts cold
17
+ and re-transforms the whole module graph — thousands of modules, every time.
18
+ Pointing every checkout at one store means only the first one pays.
19
+
20
+ ```js
21
+ // metro.config.js
22
+ const { sharedCacheStores } = require('@stim-cli/metro');
23
+
24
+ const config = getDefaultConfig(__dirname);
25
+ config.cacheStores = sharedCacheStores('myapp');
26
+ module.exports = config;
27
+ ```
28
+
29
+ The `FileStore` itself is six lines; what this packages is the housekeeping.
30
+ **Metro's cache has no eviction logic whatsoever**, so left alone it grows until
31
+ the disk does. Registering it with
32
+ [`stim-cli`](https://www.npmjs.com/package/stim-cli) makes it visible:
33
+
34
+ ```bash
35
+ stim gc # what it has grown to (reported on every run)
36
+ stim gc --delete --older-than 30 # drop entries unused for 30 days
37
+ ```
38
+
39
+ Entries are trimmed individually — one file per cache key — so trimming costs
40
+ only the entries nothing has touched, not the whole cache.
41
+
42
+ stim-cli is an optional peer. Without it the cache works exactly the same; it is
43
+ just invisible to housekeeping.
44
+
45
+ The parent location can be overridden by `STIM_CLI_METRO_CACHE`, or
46
+ machine-wide by `caches.metroCache` in `~/.stim-cli/config.json` (an absolute
47
+ path; the env var wins). The sanitized name passed to `sharedCacheStores` is
48
+ always appended below that parent, so `sharedCacheStores('@scope/app')` uses
49
+ `<parent>/-scope-app`. The CLI and this package resolve both identically.
50
+
51
+ Earlier versions wrote a named store directly into an overridden parent. A new
52
+ registration marks the named layout and replaces an exact unmarked legacy
53
+ parent entry. If an older package registers the parent again later, current
54
+ `stim gc` ignores that provably legacy entry while a marked child is
55
+ registered, so it cannot prune the child at the wrong depth. A marked named
56
+ store that later becomes another override parent remains visible but report-only
57
+ while its marked child exists. The old root-level cache files are left untouched
58
+ for manual cleanup.
59
+
60
+ ## The NDJSON log reporter
61
+
62
+ `ndjsonReporter({ dir })` is a Metro `Reporter` that writes every event the dev
63
+ server emits as one JSON object per line: bundler events and transform failures
64
+ into `<dir>/metro.ndjson`, forwarded in-app console logs and redboxes into
65
+ `<dir>/client.ndjson`.
66
+
67
+ ```js
68
+ const { ndjsonReporter } = require('@stim-cli/metro');
69
+
70
+ config.reporter = ndjsonReporter({
71
+ dir: '~/.stim-cli/workspaces/my-app--<16hex-path-digest>/logs',
72
+ });
73
+ await Metro.runServer(config, { host, port });
74
+ ```
75
+
76
+ **It only survives when you host Metro yourself.** Both the Expo CLI and the
77
+ React Native CLI overwrite `config.reporter` after loading `metro.config.js`, so
78
+ a reporter set there is discarded without a word. Setting it on a config you
79
+ pass to `Metro.runServer` is the path that works, and it is how `stim start`
80
+ captures a bare React Native project's logs.
81
+
82
+ Each record is `{ ts, src, level, msg }` plus, when they apply, `event` (the
83
+ Metro event name), `stack` (passed through as Metro gave it) and `marker: true`
84
+ (written on a finished bundle build, which is what `stim logs --errors` counts
85
+ errors from). When used by stim-cli, `dir` defaults to
86
+ `$STIM_CLI_HOME/workspaces/<readable-project-slug>--<16hex-path-digest>/logs`
87
+ (by default `~/.stim-cli/workspaces/...`), outside the working directory.
88
+
89
+ A logging failure is never a server failure: an unwritable directory or an event
90
+ shape from a Metro version this package has never seen is counted on
91
+ `reporter.drops` and swallowed, not thrown into Metro's build pipeline.
@@ -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.mjs ADDED
@@ -0,0 +1,140 @@
1
+ import { createRequire } from "node:module";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { METRO_NAMED_CACHE_LAYOUT, metroCacheRoot, registerCache, tagSharedStore, workspaceLogDir } from "@stim-cli/core";
5
+ //#region index.ts
6
+ const requireFromHere = createRequire(import.meta.url);
7
+ function cacheRoot(name) {
8
+ return metroCacheRoot(name);
9
+ }
10
+ function registerOnce(dir, replaces = []) {
11
+ registerCache({
12
+ dir,
13
+ name: "Metro transform cache",
14
+ prune: "entries",
15
+ entriesDepth: 2,
16
+ layout: METRO_NAMED_CACHE_LAYOUT,
17
+ note: "shared Metro transforms; no eviction of its own",
18
+ replaces
19
+ });
20
+ }
21
+ function sharedCacheStores(name = "app", { FileStore } = {}) {
22
+ const Store = FileStore || requireFromHere("metro-cache").FileStore;
23
+ const parent = cacheRoot();
24
+ const root = cacheRoot(name);
25
+ registerOnce(root, root === parent ? [] : [{
26
+ dir: parent,
27
+ name: "Metro transform cache",
28
+ prune: "entries",
29
+ entriesDepth: 2,
30
+ layout: null
31
+ }]);
32
+ return [tagSharedStore(new Store({ root }), root)];
33
+ }
34
+ const NDJSON_LEVELS = /* @__PURE__ */ new Set([
35
+ "debug",
36
+ "info",
37
+ "warn",
38
+ "error",
39
+ "fatal"
40
+ ]);
41
+ function ndjsonLevel(level, fallback) {
42
+ const value = String(level === void 0 || level === null ? "" : level).toLowerCase();
43
+ if (NDJSON_LEVELS.has(value)) return value;
44
+ switch (value) {
45
+ case "log":
46
+ case "dir":
47
+ case "table":
48
+ case "group":
49
+ case "groupcollapsed":
50
+ case "groupend": return "info";
51
+ case "trace": return "debug";
52
+ case "warning": return "warn";
53
+ default: return fallback;
54
+ }
55
+ }
56
+ function formatValue(value) {
57
+ if (typeof value === "string") return value;
58
+ if (value instanceof Error) return value.message || String(value);
59
+ try {
60
+ const json = JSON.stringify(value);
61
+ if (json !== void 0) return json;
62
+ } catch {}
63
+ try {
64
+ return String(value);
65
+ } catch {
66
+ return "[unprintable]";
67
+ }
68
+ }
69
+ function formatData(data) {
70
+ if (data === void 0 || data === null) return "";
71
+ if (Array.isArray(data)) return data.map(formatValue).join(" ");
72
+ return formatValue(data);
73
+ }
74
+ function errorMessage(error) {
75
+ if (error === void 0 || error === null) return "unknown error";
76
+ if (typeof error === "string") return error;
77
+ if (typeof error.message === "string" && error.message) return error.message;
78
+ return formatValue(error);
79
+ }
80
+ function ndjsonReporter({ dir } = {}) {
81
+ const logDir = dir || workspaceLogDir(process.cwd());
82
+ let ensured = false;
83
+ let drops = 0;
84
+ function write(file, record) {
85
+ try {
86
+ if (!ensured) {
87
+ fs.mkdirSync(logDir, { recursive: true });
88
+ ensured = true;
89
+ }
90
+ fs.appendFileSync(path.join(logDir, file), JSON.stringify(record) + "\n");
91
+ } catch {
92
+ drops += 1;
93
+ ensured = false;
94
+ }
95
+ }
96
+ function update(event) {
97
+ try {
98
+ const type = event && typeof event.type === "string" ? event.type : "";
99
+ const record = {
100
+ ts: Date.now(),
101
+ src: "metro",
102
+ level: "debug",
103
+ msg: ""
104
+ };
105
+ if (type) record.event = type;
106
+ if (type === "client_log") {
107
+ record.src = "client";
108
+ record.level = ndjsonLevel(event.level, "info");
109
+ record.msg = formatData(event.data);
110
+ if (event.stack) record.stack = event.stack;
111
+ write("client.ndjson", record);
112
+ return;
113
+ }
114
+ if (type === "bundling_error" || type === "transformer_error") {
115
+ record.level = "error";
116
+ record.msg = errorMessage(event.error);
117
+ } else if (type === "bundle_build_done" || type === "bundle_build_failed") {
118
+ const what = type === "bundle_build_done" ? "bundle build done" : "bundle build failed";
119
+ record.level = "info";
120
+ record.msg = event.buildID ? `${what} (${event.buildID})` : what;
121
+ record.marker = true;
122
+ } else if (type === "unstable_server_log") {
123
+ record.level = ndjsonLevel(event.level, "info");
124
+ record.msg = formatData(event.data);
125
+ } else record.msg = formatData(event && event.data) || type || "metro event";
126
+ write("metro.ndjson", record);
127
+ } catch {
128
+ drops += 1;
129
+ }
130
+ }
131
+ return {
132
+ dir: logDir,
133
+ update,
134
+ get drops() {
135
+ return drops;
136
+ }
137
+ };
138
+ }
139
+ //#endregion
140
+ export { cacheRoot, ndjsonReporter, sharedCacheStores };
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@stim-cli/metro",
3
+ "version": "1.0.0-rc.0",
4
+ "description": "Metro integration for stim-cli: one transform cache shared by every worktree, plus an NDJSON reporter for structured logs.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/appandflow/stim-cli.git",
9
+ "directory": "packages/metro"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "type": "module",
17
+ "main": "dist/index.mjs",
18
+ "types": "dist/index.d.mts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.mts",
22
+ "module-sync": "./dist/index.mjs",
23
+ "default": "./dist/index.mjs"
24
+ }
25
+ },
26
+ "dependencies": {
27
+ "@stim-cli/core": "^1.0.0-rc.0"
28
+ },
29
+ "peerDependencies": {
30
+ "metro-cache": "*"
31
+ },
32
+ "engines": {
33
+ "node": "^20.19.4 || >=22.12.0"
34
+ },
35
+ "scripts": {
36
+ "test": "node --test test/*.test.js",
37
+ "build": "tsdown"
38
+ }
39
+ }