@ttsc/metro 0.28.4 → 0.28.6

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.
@@ -53,6 +53,8 @@ export interface ResolvedTtscMetroOptions {
53
53
  include: string[];
54
54
  /** Resolved exclude patterns (never `undefined`). */
55
55
  exclude: string[];
56
+ /** Private run identity shared by `getCacheKey` and Metro workers. */
57
+ snapshotRunId?: string;
56
58
  }
57
59
 
58
60
  /**
@@ -71,8 +73,14 @@ export const ENV_KEY = "TTSC_METRO_OPTIONS";
71
73
  * Serialise user options for transport to the worker processes via
72
74
  * {@link ENV_KEY}.
73
75
  */
74
- export function serializeOptions(options: TtscMetroOptions): string {
75
- return JSON.stringify(options ?? {});
76
+ export function serializeOptions(
77
+ options: TtscMetroOptions,
78
+ snapshotRunId?: string,
79
+ ): string {
80
+ return JSON.stringify({
81
+ ...(options ?? {}),
82
+ ...(snapshotRunId === undefined ? {} : { __snapshotRunId: snapshotRunId }),
83
+ });
76
84
  }
77
85
 
78
86
  /**
@@ -98,10 +106,16 @@ export function resolveOptionsFromEnv(): ResolvedTtscMetroOptions {
98
106
  : undefined,
99
107
  include: toStringArray(parsed.include),
100
108
  exclude: toStringArray(parsed.exclude),
109
+ ...(typeof parsed.__snapshotRunId === "string" &&
110
+ parsed.__snapshotRunId.length !== 0
111
+ ? { snapshotRunId: parsed.__snapshotRunId }
112
+ : {}),
101
113
  };
102
114
  }
103
115
 
104
- function parse(raw: string | undefined): TtscMetroOptions {
116
+ function parse(
117
+ raw: string | undefined,
118
+ ): TtscMetroOptions & { __snapshotRunId?: unknown } {
105
119
  if (raw === undefined || raw.length === 0) {
106
120
  return {};
107
121
  }
@@ -111,7 +125,7 @@ function parse(raw: string | undefined): TtscMetroOptions {
111
125
  // and booleans (all valid JSON) degrade to defaults rather than leaking a
112
126
  // wrong-shaped value downstream.
113
127
  return typeof value === "object" && value !== null && !Array.isArray(value)
114
- ? (value as TtscMetroOptions)
128
+ ? (value as TtscMetroOptions & { __snapshotRunId?: unknown })
115
129
  : {};
116
130
  } catch {
117
131
  return {};
package/src/index.ts CHANGED
@@ -79,16 +79,17 @@ export function withTtsc<T extends MetroConfigLike>(
79
79
  config: T,
80
80
  options: TtscMetroOptions = {},
81
81
  ): T {
82
- process.env[ENV_KEY] = serializeOptions(
83
- inheritConfiguredTransformer(config, options),
84
- );
85
82
  // Prepare the reference-graph snapshot backing the transformer's cache-key
86
83
  // fingerprint (see `core/fingerprint.ts`). This runs in the single Metro
87
84
  // config process before any worker exists, so it is the race-free moment to
88
85
  // mint the snapshot epoch and compact the previous run's worker files.
89
- prepareSnapshot(
86
+ const snapshotRunId = prepareSnapshot(
90
87
  typeof config.projectRoot === "string" ? config.projectRoot : undefined,
91
88
  );
89
+ process.env[ENV_KEY] = serializeOptions(
90
+ inheritConfiguredTransformer(config, options),
91
+ snapshotRunId,
92
+ );
92
93
  return {
93
94
  ...config,
94
95
  transformer: {
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import {
19
19
  createTtscTransformCache,
20
+ isTransformTarget,
20
21
  resolveOptions,
21
22
  transformTtsc,
22
23
  } from "@ttsc/unplugin/api";
@@ -36,14 +37,6 @@ import { resolveUpstreamTransformer } from "./core/upstream";
36
37
 
37
38
  const nodeRequire = createRequire(import.meta.url);
38
39
 
39
- /**
40
- * Matches the TypeScript source extensions the ttsc pass handles (`.ts`,
41
- * `.tsx`, `.cts`, `.mts`). JavaScript and declaration files are passed straight
42
- * through to the upstream transformer.
43
- */
44
- const TS_EXTENSION = /\.[cm]?tsx?$/;
45
- const DECLARATION = /\.d\.[cm]?ts$/;
46
-
47
40
  /**
48
41
  * Per-worker singletons. Metro loads this module once per worker process and
49
42
  * reuses it across every file that worker handles, so the resolved options, the
@@ -53,13 +46,19 @@ const DECLARATION = /\.d\.[cm]?ts$/;
53
46
  let resolved: ResolvedTtscMetroOptions | undefined;
54
47
  let unpluginOptions: ReturnType<typeof resolveOptions> | undefined;
55
48
  const cache = createTtscTransformCache();
56
- const snapshotRecorder = createSnapshotRecorder();
49
+ let snapshotRecorder: ReturnType<typeof createSnapshotRecorder> | undefined;
57
50
 
58
51
  /** Lazily resolve the worker-side options (from {@link resolveOptionsFromEnv}). */
59
52
  function options(): ResolvedTtscMetroOptions {
60
53
  return (resolved ??= resolveOptionsFromEnv());
61
54
  }
62
55
 
56
+ /** The recorder bound to the private run identity inherited by this worker. */
57
+ function recorder(): ReturnType<typeof createSnapshotRecorder> {
58
+ const opts = options();
59
+ return (snapshotRecorder ??= createSnapshotRecorder(opts.snapshotRunId));
60
+ }
61
+
63
62
  /**
64
63
  * Resolve Metro's per-file `filename` to an absolute path.
65
64
  *
@@ -121,15 +120,25 @@ export async function transform(params: {
121
120
  : undefined;
122
121
  const explicitProject =
123
122
  typeof opts.ttsc.project === "string" ? opts.ttsc.project : undefined;
123
+ const filename = resolveAbsoluteFilename(params.filename, params.options);
124
124
  const project = resolveProjectView({
125
125
  compilerOptions: opts.ttsc.compilerOptions,
126
126
  explicitProject,
127
+ filename,
127
128
  projectRoot,
128
129
  });
130
+ // Freeze the implicit selection made above into this call. Re-running
131
+ // discovery inside the transform after a config candidate changes would
132
+ // attach one project's recorder evidence to another project's compiler
133
+ // output.
134
+ const transformOptions = {
135
+ ...unpluginOptions,
136
+ project: project.tsconfig,
137
+ };
129
138
  const result = await transformTtsc(
130
- resolveAbsoluteFilename(params.filename, params.options),
139
+ filename,
131
140
  params.src,
132
- unpluginOptions,
141
+ transformOptions,
133
142
  undefined,
134
143
  cache,
135
144
  {
@@ -144,11 +153,15 @@ export async function transform(params: {
144
153
  // memo means stat-ing the whole `extends` chain, which is an answer
145
154
  // that cannot change between two inputs of one file
146
155
  // (samchon/ttsc#1316).
147
- addWatchFile: (input) => snapshotRecorder.record({ input, project }),
156
+ addWatchFiles: (inputs) =>
157
+ recorder().recordMany({
158
+ inputs: [...project.discoveryInputs, ...inputs],
159
+ project,
160
+ }),
148
161
  // A volatile declaration means the output depends on non-file inputs
149
162
  // that no file fingerprint can represent; the snapshot marks it and
150
163
  // getCacheKey degrades to a per-run nonce (no cross-run reuse).
151
- markVolatile: () => snapshotRecorder.recordVolatile({ project }),
164
+ markVolatile: () => recorder().recordVolatile({ project }),
152
165
  },
153
166
  );
154
167
  // A file the program does not contain comes back as `undefined` from the
@@ -179,16 +192,15 @@ export async function transform(params: {
179
192
  * transformer's own key (forwarded Metro's args, e.g. `projectRoot`, so a
180
193
  * `babel.config.js` change still busts the cache);
181
194
  * - The project fingerprint (see `core/fingerprint.ts`): every input file under
182
- * the project walk (tsconfig, plugin configs, type-only siblings) plus the
183
- * recorded out-of-walk reference-graph members from previous transforms
184
- * (`node_modules` declarations, monorepo sibling sources, out-of-root config
185
- * ancestry).
195
+ * the routed project walks, every effective config source, the previous
196
+ * transforms' derived inputs, and the epoch that isolates a worker state
197
+ * differing from this run's exact main-process baseline.
186
198
  *
187
- * A change to any fingerprinted input re-keys every transformed file
188
- * project-level granularity, forced by Metro's single static key replacing
189
- * the former manual `--reset-cache` step. Resolving the upstream is
190
- * deliberately non-fatal here: a missing peer must not crash cache-key
191
- * computation. See the README "Caveats" and samchon/ttsc#721.
199
+ * A change to any fingerprinted input re-keys every transformed file at
200
+ * project-level granularity, forced by Metro's single static key, replacing the
201
+ * former manual `--reset-cache` step. Resolving the upstream is deliberately
202
+ * non-fatal here: a missing peer must not crash cache-key computation. See the
203
+ * README "Caveats" and samchon/ttsc#721.
192
204
  */
193
205
  export function getCacheKey(...args: unknown[]): string {
194
206
  const opts = options();
@@ -215,6 +227,7 @@ export function getCacheKey(...args: unknown[]): string {
215
227
  explicitProject:
216
228
  typeof opts.ttsc.project === "string" ? opts.ttsc.project : undefined,
217
229
  projectRoot: cacheKeyProjectRoot(args),
230
+ runId: opts.snapshotRunId,
218
231
  }),
219
232
  );
220
233
  return hash.digest("hex");
@@ -267,15 +280,15 @@ function upstreamCacheKey(
267
280
 
268
281
  /**
269
282
  * Decide whether a file should run through the ttsc pass. Only TypeScript
270
- * sources (`.ts`/`.tsx`/`.cts`/`.mts`, excluding `.d.ts`) qualify; `exclude`
271
- * substrings win over `include`, and an empty `include` means "all TypeScript".
272
- * Exported for unit testing.
283
+ * sources (`.ts`/`.tsx`/`.mts`/`.cts`, excluding every declaration form)
284
+ * qualify; `exclude` substrings win over `include`, and an empty `include`
285
+ * means "all TypeScript". Exported for unit testing.
273
286
  */
274
287
  export function shouldTransform(
275
288
  filename: string,
276
289
  opts: ResolvedTtscMetroOptions,
277
290
  ): boolean {
278
- if (!TS_EXTENSION.test(filename) || DECLARATION.test(filename)) {
291
+ if (!isTransformTarget(filename)) {
279
292
  return false;
280
293
  }
281
294
  if (opts.exclude.some((pattern) => filename.includes(pattern))) {