@stim-cli/metro 1.0.0-rc.2 → 1.0.0-rc.20

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,91 +1,59 @@
1
1
  # @stim-cli/metro
2
2
 
3
- > Commands use `stim`. If it is not installed globally, replace `stim` with
4
- > `npx stim-cli`.
3
+ Optional Metro integration for Stim. It exports a transform cache shared across
4
+ worktrees and an NDJSON reporter for structured Metro and client logs.
5
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.
6
+ If Stim is not installed globally, replace `stim` with `npx stim-cli`.
9
7
 
10
8
  ```bash
11
- npm i -D @stim-cli/metro
9
+ npm install --save-dev @stim-cli/metro
12
10
  ```
13
11
 
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.
12
+ ## Shared transforms
19
13
 
20
14
  ```js
21
- // metro.config.js
22
15
  const { sharedCacheStores } = require('@stim-cli/metro');
23
16
 
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
17
+ config.cacheStores = sharedCacheStores('my-app');
37
18
  ```
38
19
 
39
- Entries are trimmed individually — one file per cache key — so trimming costs
40
- only the entries nothing has touched, not the whole cache.
20
+ Set `STIM_METRO_CACHE` to override the shared cache location.
41
21
 
42
- stim-cli is an optional peer. Without it the cache works exactly the same; it is
43
- just invisible to housekeeping.
22
+ ## Optional second tier
44
23
 
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.
24
+ The filesystem cache above is always the first tier. When the project selects a
25
+ cache provider, the exported store reads it after a local miss, writes provider
26
+ hits back to the filesystem, and queues new transforms for the provider without
27
+ blocking Metro. Provider failures are misses.
50
28
 
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.
29
+ ```json
30
+ {
31
+ "cache": {
32
+ "provider": "./tools/cache-provider.cjs",
33
+ "options": { "bucket": "mobile-cache" }
34
+ }
35
+ }
36
+ ```
59
37
 
60
- ## The NDJSON log reporter
38
+ Under `stim start` the supervisor passes the resolved selection to Metro. A
39
+ Metro process outside Stim reads the nearest committed `.stim.json`. See
40
+ [`@stim-cli/cache`](https://www.npmjs.com/package/@stim-cli/cache) for the
41
+ provider contract. `clear()` only clears the local tier.
61
42
 
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`.
43
+ ## Log reporter
66
44
 
67
45
  ```js
68
46
  const { ndjsonReporter } = require('@stim-cli/metro');
69
47
 
70
- config.reporter = ndjsonReporter({
71
- dir: '~/.stim-cli/workspaces/my-app--<16hex-path-digest>/logs',
72
- });
73
- await Metro.runServer(config, { host, port });
48
+ config.reporter = ndjsonReporter({ dir: '/absolute/log/directory' });
74
49
  ```
75
50
 
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.
51
+ The reporter must be attached to the config passed directly to
52
+ `Metro.runServer`. React Native and Expo CLIs can replace a reporter declared in
53
+ `metro.config.js`.
54
+
55
+ Stim supplies this integration automatically for Stim-managed servers. Add the
56
+ package directly only for Metro processes that run outside Stim.
57
+
58
+ See the [cache package documentation](https://appandflow.github.io/stim/docs/cache-packages)
59
+ for current usage and cleanup guidance.
package/dist/index.d.mts CHANGED
@@ -1,13 +1,25 @@
1
+ import { CacheProviderConfig, LoadCacheProviderResult, WarnOnce } from "@stim-cli/cache";
1
2
  //#region index.d.ts
2
3
  type FileStoreCtor = new (options: {
3
4
  root: string;
4
5
  }) => object;
6
+ interface SharedCacheStoresOptions {
7
+ FileStore?: FileStoreCtor;
8
+ env?: NodeJS.ProcessEnv;
9
+ cwd?: string;
10
+ loadProvider?: (input: {
11
+ projectRoot: string;
12
+ config: CacheProviderConfig;
13
+ }) => Promise<LoadCacheProviderResult>;
14
+ warn?: WarnOnce;
15
+ }
5
16
  interface MetroEvent {
6
17
  type?: string;
7
18
  level?: unknown;
8
19
  data?: unknown;
9
20
  error?: unknown;
10
- stack?: string;
21
+ stack?: unknown;
22
+ codeFrame?: unknown;
11
23
  buildID?: string;
12
24
  bundleDetails?: {
13
25
  platform?: string | null;
@@ -19,11 +31,9 @@ interface NdjsonReporter {
19
31
  readonly drops: number;
20
32
  }
21
33
  declare function cacheRoot(name?: string | null): string;
22
- declare function sharedCacheStores(name?: string, { FileStore }?: {
23
- FileStore?: FileStoreCtor;
24
- }): object[];
34
+ declare function sharedCacheStores(name?: string, { FileStore, env, cwd, loadProvider, warn }?: SharedCacheStoresOptions): object[];
25
35
  declare function ndjsonReporter({ dir }?: {
26
36
  dir?: string;
27
37
  }): NdjsonReporter;
28
38
  //#endregion
29
- export { NdjsonReporter, cacheRoot, ndjsonReporter, sharedCacheStores };
39
+ export { NdjsonReporter, SharedCacheStoresOptions, cacheRoot, ndjsonReporter, sharedCacheStores };
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
+ import { cacheProviderConfigFromEnv, cacheProviderEnvIsSet, createTieredMetroStore, loadCacheProvider } from "@stim-cli/cache";
4
5
  import { METRO_NAMED_CACHE_LAYOUT, metroCacheRoot, registerCache, tagSharedStore, workspaceLogDir } from "@stim-cli/core";
5
6
  //#region index.ts
6
7
  const requireFromHere = createRequire(import.meta.url);
@@ -18,7 +19,45 @@ function registerOnce(dir, replaces = []) {
18
19
  replaces
19
20
  });
20
21
  }
21
- function sharedCacheStores(name = "app", { FileStore } = {}) {
22
+ function isPlainObject(value) {
23
+ return value !== null && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+ function repositoryRoot(startDir) {
26
+ let dir = startDir;
27
+ for (;;) {
28
+ if (fs.existsSync(path.join(dir, ".git"))) return dir;
29
+ const parent = path.dirname(dir);
30
+ if (parent === dir) return null;
31
+ dir = parent;
32
+ }
33
+ }
34
+ function committedProviderConfig(startDir) {
35
+ const start = path.resolve(startDir);
36
+ const stop = repositoryRoot(start) ?? start;
37
+ let dir = start;
38
+ for (;;) {
39
+ let parsed;
40
+ try {
41
+ parsed = JSON.parse(fs.readFileSync(path.join(dir, ".stim.json"), "utf-8"));
42
+ } catch {
43
+ parsed = null;
44
+ }
45
+ const cache = isPlainObject(parsed) && isPlainObject(parsed.cache) ? parsed.cache : null;
46
+ const reference = cache?.provider;
47
+ if (typeof reference === "string" && reference.trim() !== "") return {
48
+ provider: reference.trim(),
49
+ options: isPlainObject(cache?.options) ? cache.options : {},
50
+ baseDir: dir
51
+ };
52
+ const parent = path.dirname(dir);
53
+ if (dir === stop || parent === dir) return null;
54
+ dir = parent;
55
+ }
56
+ }
57
+ function warnToStderr(_code, message) {
58
+ process.stderr.write(`warning: ${message}\n`);
59
+ }
60
+ function sharedCacheStores(name = "app", { FileStore, env = process.env, cwd = process.cwd(), loadProvider = loadCacheProvider, warn = warnToStderr } = {}) {
22
61
  const Store = FileStore || requireFromHere("metro-cache").FileStore;
23
62
  const parent = cacheRoot();
24
63
  const root = cacheRoot(name);
@@ -29,7 +68,20 @@ function sharedCacheStores(name = "app", { FileStore } = {}) {
29
68
  entriesDepth: 2,
30
69
  layout: null
31
70
  }]);
32
- return [tagSharedStore(new Store({ root }), root)];
71
+ const local = tagSharedStore(new Store({ root }), root);
72
+ const config = cacheProviderConfigFromEnv(env) ?? (cacheProviderEnvIsSet(env) ? null : committedProviderConfig(cwd));
73
+ if (!config) return [local];
74
+ const tiered = createTieredMetroStore({
75
+ local,
76
+ projectRoot: path.resolve(cwd),
77
+ cacheName: name,
78
+ loadProvider: () => loadProvider({
79
+ projectRoot: path.resolve(cwd),
80
+ config
81
+ }),
82
+ warn
83
+ });
84
+ return [tagSharedStore(tiered, root)];
33
85
  }
34
86
  const NDJSON_LEVELS = /* @__PURE__ */ new Set([
35
87
  "debug",
@@ -77,6 +129,23 @@ function errorMessage(error) {
77
129
  if (typeof error.message === "string" && error.message) return error.message;
78
130
  return formatValue(error);
79
131
  }
132
+ function symbolicationMessage(codeFrame) {
133
+ if (!isPlainObject(codeFrame) || typeof codeFrame.content !== "string") return "Call Stack";
134
+ const fileName = typeof codeFrame.fileName === "string" ? codeFrame.fileName : "unknown source";
135
+ const location = isPlainObject(codeFrame.location) ? codeFrame.location : null;
136
+ return `Code: ${fileName}${typeof location?.row === "number" ? `:${location.row}` : ""}${typeof location?.column === "number" ? `:${location.column}` : ""}\n${codeFrame.content}\nCall Stack`;
137
+ }
138
+ function symbolicationStack(stack) {
139
+ if (!Array.isArray(stack)) return [];
140
+ return stack.filter((frame) => isPlainObject(frame) && frame.collapse !== true).map((frame) => {
141
+ const normalized = {};
142
+ if (typeof frame.file === "string") normalized.file = frame.file;
143
+ if (typeof frame.lineNumber === "number") normalized.line = frame.lineNumber;
144
+ if (typeof frame.column === "number") normalized.column = frame.column;
145
+ if (typeof frame.methodName === "string") normalized.fn = frame.methodName;
146
+ return normalized;
147
+ }).filter((frame) => Object.keys(frame).length > 0);
148
+ }
80
149
  function ndjsonReporter({ dir } = {}) {
81
150
  const logDir = dir || workspaceLogDir(process.cwd());
82
151
  let ensured = false;
@@ -133,6 +202,16 @@ function ndjsonReporter({ dir } = {}) {
133
202
  write("client.ndjson", record);
134
203
  return;
135
204
  }
205
+ if (type === "client_symbolication") {
206
+ const stack = symbolicationStack(event.stack);
207
+ if (!(isPlainObject(event.codeFrame) && typeof event.codeFrame.content === "string") && stack.length === 0) return;
208
+ record.src = "client";
209
+ record.level = "info";
210
+ record.msg = symbolicationMessage(event.codeFrame);
211
+ if (stack.length > 0) record.stack = stack;
212
+ write("client.ndjson", record);
213
+ return;
214
+ }
136
215
  if (type === "bundling_error" || type === "transformer_error") {
137
216
  record.level = "error";
138
217
  record.msg = errorMessage(event.error);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stim-cli/metro",
3
- "version": "1.0.0-rc.2",
4
- "description": "Metro integration for stim-cli: one transform cache shared by every worktree, plus an NDJSON reporter for structured logs.",
3
+ "version": "1.0.0-rc.20",
4
+ "description": "Metro integration for Stim: shared transforms across worktrees and structured logs.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,17 +23,18 @@
23
23
  "default": "./dist/index.mjs"
24
24
  }
25
25
  },
26
- "scripts": {
27
- "test": "node --test test/*.test.js",
28
- "build": "tsdown"
29
- },
30
26
  "dependencies": {
31
- "@stim-cli/core": "^1.0.0-rc.2"
27
+ "@stim-cli/cache": "^1.0.0-rc.20",
28
+ "@stim-cli/core": "^1.0.0-rc.20"
32
29
  },
33
30
  "peerDependencies": {
34
31
  "metro-cache": "*"
35
32
  },
36
33
  "engines": {
37
34
  "node": "^20.19.4 || >=22.12.0"
35
+ },
36
+ "scripts": {
37
+ "test": "node --test test/*.test.js",
38
+ "build": "tsdown"
38
39
  }
39
- }
40
+ }