@ttsc/metro 0.28.5 → 0.29.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/README.md CHANGED
@@ -41,11 +41,11 @@ module.exports = withTtsc(getDefaultConfig(__dirname));
41
41
 
42
42
  `withTtsc` sets `transformer.babelTransformerPath` and leaves the rest of your config untouched. It auto-detects the upstream transformer to delegate to (`@expo/metro-config/babel-transformer` for Expo, then `@react-native/metro-babel-transformer`, then the legacy `metro-react-native-babel-transformer`).
43
43
 
44
- If your config already set `transformer.babelTransformerPath`, that transformer is **chained rather than replaced**: the `ttsc` pass runs first and then delegates to it, so wrapping a working config keeps what it configured. This is what makes `react-native-svg-transformer` whose entire installation is that one assignment keep working after you adopt `@ttsc/metro`. Pass `upstreamTransformer` explicitly to override both that and auto-detection.
44
+ If your config already set `transformer.babelTransformerPath`, that transformer is **chained rather than replaced**: the `ttsc` pass runs first and then delegates to it, so wrapping a working config keeps what it configured. This is what makes `react-native-svg-transformer`, whose entire installation is that one assignment, keep working after you adopt `@ttsc/metro`. Pass `upstreamTransformer` explicitly to override both that and auto-detection.
45
45
 
46
- The value is resolved from your `projectRoot`, exactly as Metro resolves it, so a relative `"./metro-svg.cjs"` and a bare `"react-native-svg-transformer"` both mean what they mean in your project rather than inside this package. A path that names `@ttsc/metro`'s own transformer is never chained in any spelling, including a second copy installed elsewhere in your tree because delegating this transformer into itself would recurse on every file.
46
+ The value is resolved from your `projectRoot`, exactly as Metro resolves it, so a relative `"./metro-svg.cjs"` and a bare `"react-native-svg-transformer"` both mean what they mean in your project rather than inside this package. A path that names `@ttsc/metro`'s own transformer is never chained in any spelling, including a second copy installed elsewhere in your tree, because delegating this transformer into itself would recurse on every file.
47
47
 
48
- Auto-detection only skips a candidate whose entry point is genuinely **not available** the package is not installed, or it is installed but the requested subpath is not exported (Expo/React Native version skew). A candidate that _does_ resolve but fails while loading a top-level throw, an incompatible runtime ABI, or a missing peer/transitive dependency surfaces its original error (as the `cause` of a `@ttsc/metro` wrapper) instead of being treated as absent. This stops a broken Expo/React Native install from silently falling through to the wrong transformer, and stops an explicit `upstreamTransformer` failure from being reported as if the module did not exist.
48
+ Auto-detection only skips a candidate whose entry point is genuinely **not available**: the package is not installed, or it is installed but the requested subpath is not exported (Expo/React Native version skew). A candidate that _does_ resolve but fails while loading, whether from a top-level throw, an incompatible runtime ABI, or a missing peer or transitive dependency, surfaces its original error (as the `cause` of a `@ttsc/metro` wrapper) instead of being treated as absent. This stops a broken Expo/React Native install from silently falling through to the wrong transformer, and stops an explicit `upstreamTransformer` failure from being reported as if the module did not exist.
49
49
 
50
50
  ## Configuration
51
51
 
@@ -80,7 +80,7 @@ The plugin contract and `tsconfig` discovery match the Unplugin integrations. Me
80
80
 
81
81
  ### Files outside the program
82
82
 
83
- Metro resolves its own module graph, and that graph is not the set of files your `tsconfig` describes. A file Metro delivers that the compiled program does not contain is passed to the upstream transformer untransformed, and reported naming the file and the `tsconfig` it is missing from once per file per compile in each Metro worker, since a Metro worker has no build boundary to reset the report at:
83
+ Metro resolves its own module graph, and that graph is not the set of files your `tsconfig` describes. A file Metro delivers that the compiled program does not contain is passed to the upstream transformer untransformed, and reported naming the file and the `tsconfig` it is missing from, once per file per compile in each Metro worker, since a Metro worker has no build boundary to reset the report at:
84
84
 
85
85
  ```
86
86
  ttsc: /app/scripts/tool.ts is not part of the program described by /app/tsconfig.json,
@@ -88,7 +88,7 @@ so it was left untransformed. Add it to that project's "include" if ttsc plugins
88
88
  should apply to it.
89
89
  ```
90
90
 
91
- This is not a build error. The file is simply not this project's to transform, and the usual cause is a Metro graph reaching past the `tsconfig`'s `include` a source beside `src` rather than inside it, or one in a sibling workspace folder. Add it to that project's `include` if `ttsc` plugins should apply to it.
91
+ This is not a build error. The file is simply not this project's to transform, and the usual cause is a Metro graph reaching past the `tsconfig`'s `include`, such as a source beside `src` rather than inside it or one in a sibling workspace folder. Add it to that project's `include` if `ttsc` plugins should apply to it.
92
92
 
93
93
  The report matters because passing through is not the same as leaving alone: a file that skips the `ttsc` pass keeps whatever plugin-driven syntax it carries, which fails at runtime rather than at build time. Declaration and JavaScript files never reach the pass at all, so they are not reported; they are filtered before it, as `include` / `exclude` above describes.
94
94
 
@@ -96,13 +96,15 @@ This is the shared core's answer rather than Metro's own, so every `@ttsc/unplug
96
96
 
97
97
  ## Cache invalidation
98
98
 
99
- Metro keys its transform cache on each file's own content plus one static transformer key, and its babel-transformer contract has no per-file dependency registration. A `ttsc` transform can depend on a _type_ in another file, so `@ttsc/metro` folds a project fingerprint into that static key: every regular file reached by the non-following project walk, plus reference-graph inputs outside that walk (`node_modules` declarations, monorepo sibling sources, files reached through symlinks or Windows junctions, and out-of-root tsconfig `extends` ancestry) recorded under `node_modules/.cache/ttsc-metro`. Editing any of them re-keys the next run, so `metro bundle` and dev-server starts pick up cross-file type changes without `--reset-cache`.
99
+ Metro keys its transform cache on each file's own content plus one static transformer key, and its babel-transformer contract has no per-file dependency registration. A `ttsc` transform can depend on a _type_ in another file, so `@ttsc/metro` folds a project fingerprint into that static key: every regular file reached by the project walk, every effective config source, and transform inputs recorded under `node_modules/.cache/ttsc-metro`. In a workspace with nested or directory-linked `tsconfig.json` projects, each lexical subtree is walked under the nearest config that transformed files use; the project-map traversal follows symlinks and Windows junctions while a physical ancestor set cuts cycles without collapsing independent aliases. The project map itself is hashed so adding or removing a nested or linked project config also changes the key. Worker discovery retains the exact candidate predicates that selected its project instead of reconstructing them after selection. A module passed through because its Program has no output still records those candidates and the universal config inputs that can later admit it. `withTtsc` gives each run a private identity, `getCacheKey` records the exact file state and static coverage that identity hashed, and workers compare every derived input's compiler-generation hash, realpath, or resolution predicate with that baseline. Inputs outside proven static coverage stay in one batched worker snapshot. A missing input, an out-of-root `watchFolders` project, a symlink or Windows junction transition, an `extends` change, or any A-to-B-to-A change between static keying and transformation taints the observation and rotates the next snapshot epoch, so it cannot collide with the earlier A key. Editing any fingerprinted input re-keys the next run, so `metro bundle` and dev-server starts pick up cross-file type changes without `--reset-cache`.
100
+
101
+ Snapshot compaction is serialized across Metro config processes. If two builds prepare the same project cache at once, one compacts the shared worker documents and the contender uses a private non-reusable key for that run instead of racing the main snapshot rewrite.
100
102
 
101
103
  The granularity is project-level by necessity: Metro evaluates the transformer key once per run, so any fingerprinted change re-transforms every file on the next run. What remains outside the mechanism's reach:
102
104
 
103
105
  - **Within a running dev server**, Metro re-transforms only files its watcher reports changed. Editing a type in file B updates a dependent file A on A's next transform: save A, or restart the dev server (no `--reset-cache` needed).
104
106
  - **Files a plugin declares `volatile`** depend on non-file inputs that no fingerprint can represent; while a volatile declaration is recorded, cross-run cache reuse is disabled entirely.
105
- - **If snapshot persistence fails**, a recovery document beside `node_modules/.cache/ttsc-metro` makes every later key non-reusable until the pending observations are compacted under a fresh epoch; if an older readable snapshot exists and neither the snapshot nor recovery location is writable, the transform fails instead of authorizing stale reuse.
107
+ - **If snapshot persistence fails**, a recovery document beside `node_modules/.cache/ttsc-metro` makes every later key non-reusable until the pending observations are compacted under a fresh epoch. If preparation cannot persist even that document, its run token forces a nonce across bundle and process boundaries. A worker whose run token authorized a reusable key fails the transform if neither location accepts its observation, even when the old main snapshot is temporarily unreadable.
106
108
 
107
109
  ## Caveats (v1)
108
110
 
@@ -14,20 +14,20 @@
14
14
  * `projectRoot` plus the resolved tsconfig's directory when it lies outside),
15
15
  * hashed with the exact walk universe the `@ttsc/unplugin` transform core
16
16
  * validates its own cache against.
17
- * - **Recorded out-of-walk inputs.** The transform core cannot walk files outside
18
- * the roots or under ignored directories, but the host-owned reference graph
19
- * (samchon/ttsc#718) reports them per transform. Workers record them into a
20
- * snapshot under `node_modules/.cache/ttsc-metro`; the next run's
21
- * `getCacheKey` re-hashes the recorded set.
17
+ * - **Recorded transform inputs.** The host-owned reference graph
18
+ * (samchon/ttsc#718) reports each transform's derived inputs. Workers retain
19
+ * them under `node_modules/.cache/ttsc-metro`, compare their generation state
20
+ * with the exact main-process key baseline, and batch one durable write per
21
+ * delivered module.
22
22
  *
23
23
  * Snapshot layout: one main file carrying a random epoch id plus per-worker
24
24
  * files with unique names, so concurrent workers never race a shared write.
25
- * `withTtsc` (the single config process, before workers exist) compacts worker
26
- * files into the main file. Readers take the union of every file, reading the
27
- * worker files strictly before the main file: the compactor renames the merged
28
- * main into place strictly before deleting a worker file, so a worker file that
29
- * disappears mid-read is always already merged into the main the reader loads
30
- * afterwards.
25
+ * `withTtsc` compacts worker files into the main file before its workers exist,
26
+ * under a process-shared lock for builds using the same project cache. Readers
27
+ * take the union of every file, reading the worker files strictly before the
28
+ * main file: the compactor renames the merged main into place strictly before
29
+ * deleting a worker file, so a worker file that disappears mid-read is always
30
+ * already merged into the main the reader loads afterwards.
31
31
  *
32
32
  * Sound degradations, by design:
33
33
  *
@@ -43,16 +43,22 @@
43
43
  * until a later run records the volatile declaration gone.
44
44
  * - A recorded input that disappears hashes as a stable `missing` marker, so
45
45
  * deletion and reappearance both move the key.
46
+ * - A worker state that differs from the static key's run baseline taints the
47
+ * observation; compaction rotates the epoch so even A -> B -> A cannot reuse
48
+ * output stored under the earlier A key.
46
49
  */
47
50
  import { readProjectMembershipPolicy } from "@ttsc/unplugin/api";
51
+ import type { TtscProjectDiscoveryFilesystem, TtscProjectTreeDiscoveryFilesystem, TtscWatchInput } from "@ttsc/unplugin/api";
48
52
  /** Union of the snapshot state readable on disk. */
49
53
  interface SnapshotState {
50
54
  /** Random epoch id minted when the main snapshot was created. */
51
55
  id: string;
52
- /** Absolute paths of every recorded out-of-walk input. */
56
+ /** Absolute paths of every recorded derived transform input. */
53
57
  files: string[];
54
58
  /** Whether any recorded transform declared volatile output. */
55
59
  volatile: boolean;
60
+ /** Whether a transform observed state different from its run's static key. */
61
+ tainted: boolean;
56
62
  }
57
63
  /**
58
64
  * Resolve the base directory both fingerprint sides agree on: Metro's
@@ -68,8 +74,8 @@ export declare function resolveFingerprintBase(projectRoot: string | undefined):
68
74
  * already inside the base walk (an explicit out-of-root `project`, or a
69
75
  * monorepo-root tsconfig discovered above the app). Matching the transform
70
76
  * core's own validation universe keeps the invariant simple: everything the
71
- * core treats as an input is fingerprinted, either by a walk here or by the
72
- * recorded out-of-walk snapshot.
77
+ * core treats as an input is fingerprinted by the walk, the recorded snapshot,
78
+ * or both.
73
79
  */
74
80
  export declare function fingerprintRoots(base: string, explicitProject: string | undefined): string[];
75
81
  /**
@@ -87,10 +93,18 @@ export declare function fingerprintRoots(base: string, explicitProject: string |
87
93
  export interface TtscMetroProjectView {
88
94
  /** The base directory both fingerprint sides agree on. */
89
95
  readonly base: string;
96
+ /** Config candidates observed while selecting this transform's project. */
97
+ readonly discoveryInputs: readonly TtscWatchInput[];
90
98
  /** The caller's explicit `project`, if any. */
91
99
  readonly explicitProject: string | undefined;
92
100
  /** The membership policy resolved for that project. */
93
101
  readonly policy: ReturnType<typeof readProjectMembershipPolicy>;
102
+ /** The policy used by the routed static walk. */
103
+ readonly walkPolicy: ReturnType<typeof readProjectMembershipPolicy>;
104
+ /** Lexical roots whose fingerprint uses this project's policy. */
105
+ readonly roots: readonly string[];
106
+ /** The exact config selected for this project. */
107
+ readonly tsconfig: string;
94
108
  }
95
109
  /**
96
110
  * Resolve one transform's project view, once, for every watch input it reports.
@@ -102,6 +116,8 @@ export interface TtscMetroProjectView {
102
116
  export declare function resolveProjectView(props: {
103
117
  compilerOptions?: Record<string, unknown>;
104
118
  explicitProject?: string;
119
+ filename?: string;
120
+ projectDiscoveryFilesystem?: TtscProjectDiscoveryFilesystem;
105
121
  projectRoot?: string;
106
122
  }): TtscMetroProjectView;
107
123
  /**
@@ -112,22 +128,28 @@ export declare function resolveProjectView(props: {
112
128
  export declare function computeProjectFingerprint(props: {
113
129
  compilerOptions?: Record<string, unknown>;
114
130
  explicitProject?: string;
131
+ /** Test seam for proving that incomplete implicit enumeration fails closed. */
132
+ projectDiscoveryFilesystem?: TtscProjectTreeDiscoveryFilesystem;
115
133
  projectRoot?: string;
134
+ /** Private identity transported from `withTtsc` to this Metro run. */
135
+ runId?: string;
116
136
  }): string;
117
137
  /**
118
- * Prepare the snapshot for a new run. Called from `withTtsc` in the single
119
- * Metro config process, before any worker exists: creates the main snapshot
120
- * (fresh epoch id) when missing or corrupt, compacts leftover worker files into
121
- * it, and sweeps unparseable worker files plus crash-leftover temp files. An
122
- * unparseable worker file's recordings are unrecoverable, so its removal mints
123
- * a fresh epoch id every key that might have depended on the lost recordings
124
- * is soundly orphaned, and later runs stabilize instead of degrading to a nonce
125
- * forever. A failed rewrite leaves a recovery document outside the snapshot
126
- * directory so `getCacheKey` degrades to a nonce until a later compaction
127
- * succeeds. If an older readable main exists and neither location is writable,
128
- * preparation throws instead of authorizing stale reuse.
138
+ * Prepare the snapshot for a new run. Called from `withTtsc` before any worker
139
+ * exists: creates the main snapshot (fresh epoch id) when missing or corrupt,
140
+ * compacts leftover worker files into it, and sweeps unparseable worker files
141
+ * plus crash-leftover temp files. Concurrent config processes are serialized; a
142
+ * contender takes a non-reusable run token instead of racing the mutable main
143
+ * rewrite. An unparseable worker file's recordings are unrecoverable, so its
144
+ * removal mints a fresh epoch id every key that might have depended on the
145
+ * lost recordings is soundly orphaned, and later runs stabilize instead of
146
+ * degrading to a nonce forever. A failed rewrite leaves a recovery document
147
+ * outside the snapshot directory and returns a non-reusable run token. The
148
+ * token crosses bundle and process boundaries, so `getCacheKey` degrades to a
149
+ * nonce even when neither snapshot location can persist the failure and an old
150
+ * main later reappears.
129
151
  */
130
- export declare function prepareSnapshot(projectRoot: string | undefined): void;
152
+ export declare function prepareSnapshot(projectRoot: string | undefined): string;
131
153
  /**
132
154
  * Read the unioned snapshot state, or `undefined` when the main snapshot is
133
155
  * missing or any snapshot file is corrupt (a torn or foreign write means the
@@ -135,16 +157,15 @@ export declare function prepareSnapshot(projectRoot: string | undefined): void;
135
157
  */
136
158
  export declare function readSnapshotState(base: string): SnapshotState | undefined;
137
159
  /**
138
- * Recorder held by each Metro worker. It persists out-of-walk watch inputs and
139
- * missing in-walk paths delivered through the transform core's `addWatchFile`
140
- * hook, plus any volatile declaration. Existing in-walk files stay covered by
141
- * the project walk; a missing path must be retained because its creation is a
142
- * state change that the initial walk could not hash. A clean in-walk transform
143
- * also writes a document so it can clear a volatile declaration from an earlier
144
- * run. The unique name makes worker writes race-free; `withTtsc` compacts the
145
- * files on the next run.
160
+ * Recorder held by each Metro worker. It persists every derived watch input and
161
+ * any volatile declaration, compares compiler-generation evidence with the
162
+ * matching main-process run baseline, and marks any temporal mismatch tainted.
163
+ * A clean transform also writes a document so it can clear a volatile
164
+ * declaration from an earlier run. One cumulative document is flushed per
165
+ * delivered module; the unique name makes worker writes race-free, and
166
+ * `withTtsc` compacts the files on the next run.
146
167
  */
147
- export declare function createSnapshotRecorder(): {
168
+ export declare function createSnapshotRecorder(runId?: string): {
148
169
  record: (props: {
149
170
  input: string;
150
171
  /**
@@ -159,15 +180,15 @@ export declare function createSnapshotRecorder(): {
159
180
  * compiler-options overlay reached one half and not the other, and the
160
181
  * input was then covered by neither (samchon/ttsc#1316).
161
182
  *
162
- * Resolving it once per transform is also what makes it affordable.
163
- * `record` runs once per watch input rather than once per file, and
164
- * validating the memo means stat-ing the whole `extends` chain, measured at
165
- * 12 microseconds per stat — a few thousand modules times fifteen inputs
166
- * each cost over half a second per run for an answer that cannot change
167
- * between two inputs of one file.
183
+ * Resolving it once per transform also keeps every entry in the module's
184
+ * batch attached to the exact same config graph and policy.
168
185
  */
169
186
  project: TtscMetroProjectView;
170
187
  }) => void;
188
+ recordMany: (props: {
189
+ inputs: readonly TtscWatchInput[];
190
+ project: TtscMetroProjectView;
191
+ }) => void;
171
192
  recordVolatile: (props: {
172
193
  project: TtscMetroProjectView;
173
194
  }) => void;