@ttsc/unplugin 0.21.0 → 0.23.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/lib/core/transform.d.ts +4 -3
- package/lib/core/transform.js +241 -173
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +241 -173
- package/lib/core/transform.mjs.map +1 -1
- package/package.json +4 -4
- package/src/core/transform.ts +454 -225
package/lib/core/transform.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { TtscCompiler } from 'ttsc';
|
|
6
|
+
import { createFilesystemPathIdentityContext } from 'ttsc/path-identity';
|
|
6
7
|
import { absolutizePathsTarget, readEffectiveTsconfigPaths } from './tsconfigPaths.mjs';
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,6 +11,11 @@ import { absolutizePathsTarget, readEffectiveTsconfigPaths } from './tsconfigPat
|
|
|
10
11
|
* {@link beginTtscTransformBuild} before transforms begin.
|
|
11
12
|
*/
|
|
12
13
|
const BUILD_SCOPED_TRANSFORM_CACHES = new WeakSet();
|
|
14
|
+
function createHostPathIdentityContext() {
|
|
15
|
+
return createFilesystemPathIdentityContext({
|
|
16
|
+
throwOnRealpathError: false,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
13
19
|
/** Create an empty persistent transform cache. */
|
|
14
20
|
function createTtscTransformCache() {
|
|
15
21
|
return new Map();
|
|
@@ -37,8 +43,6 @@ function resetTtscTransformCache(cache) {
|
|
|
37
43
|
cache.clear();
|
|
38
44
|
BUILD_SCOPED_TRANSFORM_CACHES.delete(cache);
|
|
39
45
|
}
|
|
40
|
-
/** Cached case-insensitivity probes for existing macOS filesystem locations. */
|
|
41
|
-
const CASE_INSENSITIVE_FILESYSTEMS = new Map();
|
|
42
46
|
/**
|
|
43
47
|
* Apply the ttsc plugin transform to a single source file.
|
|
44
48
|
*
|
|
@@ -98,7 +102,7 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
98
102
|
// A file the plugin declared volatile must never be served from the
|
|
99
103
|
// cache: its output depends on non-file inputs, so the input-hash
|
|
100
104
|
// snapshot cannot prove freshness. Fall through to a fresh transform.
|
|
101
|
-
!isVolatileFile({
|
|
105
|
+
!isVolatileFile(envelopeDerivation(cached), {
|
|
102
106
|
file,
|
|
103
107
|
projectRoot: cached.projectRoot,
|
|
104
108
|
result: cached.result,
|
|
@@ -155,7 +159,7 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
155
159
|
});
|
|
156
160
|
notifyWatchInputs(hooks, { file, projectRoot, result, temporaryTsconfig });
|
|
157
161
|
markCachedSourceServed(cached, file);
|
|
158
|
-
if (isVolatileFile({ file, projectRoot, result })) {
|
|
162
|
+
if (isVolatileFile(envelopeDerivation(cached), { file, projectRoot, result })) {
|
|
159
163
|
hooks?.markVolatile?.();
|
|
160
164
|
}
|
|
161
165
|
return createTransformResult(source, code);
|
|
@@ -205,6 +209,105 @@ function evictGeneration(cache, key, generation) {
|
|
|
205
209
|
cache.delete(key);
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* Derivation states keyed by the compiler result object. One result object is
|
|
214
|
+
* produced by one compile against one project root, so the root captured at
|
|
215
|
+
* build time is the only root the state ever sees.
|
|
216
|
+
*/
|
|
217
|
+
const ENVELOPE_DERIVATIONS = new WeakMap();
|
|
218
|
+
/** Return the derivation state of `props.result`, building it on first use. */
|
|
219
|
+
function envelopeDerivation(props) {
|
|
220
|
+
const existing = ENVELOPE_DERIVATIONS.get(props.result);
|
|
221
|
+
if (existing !== undefined) {
|
|
222
|
+
return existing;
|
|
223
|
+
}
|
|
224
|
+
const created = {
|
|
225
|
+
identityContext: createHostPathIdentityContext(),
|
|
226
|
+
identities: new Map(),
|
|
227
|
+
watchInputs: new Map(),
|
|
228
|
+
};
|
|
229
|
+
ENVELOPE_DERIVATIONS.set(props.result, created);
|
|
230
|
+
return created;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Build the reference-graph indexes of one envelope on first watch-input
|
|
234
|
+
* derivation. Malformed sections are dropped member by member, mirroring the
|
|
235
|
+
* historical per-delivery scan.
|
|
236
|
+
*/
|
|
237
|
+
function envelopeGraphIndexes(state, props) {
|
|
238
|
+
if (state.graph !== undefined) {
|
|
239
|
+
return state.graph;
|
|
240
|
+
}
|
|
241
|
+
const built = {
|
|
242
|
+
edges: new Map(),
|
|
243
|
+
spellings: new Map(),
|
|
244
|
+
candidates: [],
|
|
245
|
+
globals: [],
|
|
246
|
+
configs: [],
|
|
247
|
+
};
|
|
248
|
+
const graph = props.result.type === "exception" ? undefined : props.result.graph;
|
|
249
|
+
if (graph !== undefined) {
|
|
250
|
+
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
251
|
+
if (!Array.isArray(targets)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const absolute = path.resolve(props.projectRoot, source);
|
|
255
|
+
const identity = derivationIdentity(state, absolute);
|
|
256
|
+
built.spellings.set(identity, absolute);
|
|
257
|
+
const entries = built.edges.get(identity) ?? [];
|
|
258
|
+
entries.push(...targets
|
|
259
|
+
.filter((target) => typeof target === "string" && target.length !== 0)
|
|
260
|
+
.map((target) => path.resolve(props.projectRoot, target)));
|
|
261
|
+
built.edges.set(identity, entries);
|
|
262
|
+
}
|
|
263
|
+
built.globals.push(...selectListedFiles(props.projectRoot, graph.globals));
|
|
264
|
+
built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
|
|
265
|
+
for (const [source, candidates] of Object.entries(graph.candidates ?? {})) {
|
|
266
|
+
if (!Array.isArray(candidates)) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
built.candidates.push({
|
|
270
|
+
source: derivationIdentity(state, path.resolve(props.projectRoot, source)),
|
|
271
|
+
files: selectListedFiles(props.projectRoot, candidates),
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
state.graph = built;
|
|
276
|
+
return built;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* {@link pathIdentityKey} memoized inside one envelope's derivation state.
|
|
280
|
+
* Callers always pass already-resolved absolute paths, so the input string is a
|
|
281
|
+
* stable memo key.
|
|
282
|
+
*/
|
|
283
|
+
function derivationIdentity(state, file) {
|
|
284
|
+
const existing = state.identities.get(file);
|
|
285
|
+
if (existing !== undefined) {
|
|
286
|
+
return existing;
|
|
287
|
+
}
|
|
288
|
+
const identity = pathIdentityKey(file, state.identityContext);
|
|
289
|
+
state.identities.set(file, identity);
|
|
290
|
+
return identity;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Fold one envelope member list (`volatile`, `dependenciesComplete`) into an
|
|
294
|
+
* identity set. Members are keyed like `typescript`, so a project-relative and
|
|
295
|
+
* an absolute spelling of the same file share one identity; a malformed member
|
|
296
|
+
* is ignored rather than fatal.
|
|
297
|
+
*/
|
|
298
|
+
function collectDeclaredIdentities(state, projectRoot, listed) {
|
|
299
|
+
const output = new Set();
|
|
300
|
+
if (!Array.isArray(listed)) {
|
|
301
|
+
return output;
|
|
302
|
+
}
|
|
303
|
+
for (const entry of listed) {
|
|
304
|
+
if (typeof entry !== "string" || entry.length === 0) {
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
output.add(derivationIdentity(state, path.resolve(projectRoot, entry)));
|
|
308
|
+
}
|
|
309
|
+
return output;
|
|
310
|
+
}
|
|
208
311
|
/**
|
|
209
312
|
* Forward every derived watch input for `file` to the adapter's `addWatchFile`
|
|
210
313
|
* hook: the plugin-reported `dependencies[file]` list unioned with the
|
|
@@ -244,23 +347,44 @@ function notifyWatchInputs(hooks, props) {
|
|
|
244
347
|
* volatile keeps the baseline: the two declarations contradict, so the
|
|
245
348
|
* conservative one wins.
|
|
246
349
|
*
|
|
247
|
-
*
|
|
350
|
+
* The derived list is a pure function of the envelope and the file's filesystem
|
|
351
|
+
* identity, so it is computed at most once per generation per file: sibling and
|
|
352
|
+
* repeated deliveries replay the per-envelope memo ({@link envelopeDerivation})
|
|
353
|
+
* instead of re-walking the graph. Returns an empty list on exceptions.
|
|
248
354
|
*/
|
|
249
355
|
function selectWatchInputs(props) {
|
|
356
|
+
if (props.result.type === "exception") {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
const state = envelopeDerivation(props);
|
|
360
|
+
const fileIdentity = derivationIdentity(state, props.file);
|
|
361
|
+
const memoized = state.watchInputs.get(fileIdentity);
|
|
362
|
+
if (memoized !== undefined) {
|
|
363
|
+
return memoized;
|
|
364
|
+
}
|
|
365
|
+
const derived = deriveWatchInputs(state, props, fileIdentity);
|
|
366
|
+
state.watchInputs.set(fileIdentity, derived);
|
|
367
|
+
return derived;
|
|
368
|
+
}
|
|
369
|
+
/** Compute one file's watch-input list over the shared per-envelope state. */
|
|
370
|
+
function deriveWatchInputs(state, props, fileIdentity) {
|
|
371
|
+
const graph = envelopeGraphIndexes(state, props);
|
|
250
372
|
const output = [];
|
|
251
373
|
const seen = new Set();
|
|
252
|
-
const excluded = new Set(
|
|
253
|
-
|
|
254
|
-
|
|
374
|
+
const excluded = new Set([fileIdentity]);
|
|
375
|
+
if (props.temporaryTsconfig !== undefined) {
|
|
376
|
+
excluded.add(derivationIdentity(state, props.temporaryTsconfig));
|
|
377
|
+
}
|
|
255
378
|
for (const absolute of [
|
|
256
379
|
...selectFileDependencies(props),
|
|
257
|
-
...selectGraphInputs({
|
|
380
|
+
...selectGraphInputs(graph, state, {
|
|
258
381
|
...props,
|
|
259
|
-
complete: declaresCompleteDependencies(props) &&
|
|
382
|
+
complete: declaresCompleteDependencies(state, props) &&
|
|
383
|
+
!isVolatileFile(state, props),
|
|
260
384
|
}),
|
|
261
|
-
...selectResolutionCandidateInputs(props),
|
|
385
|
+
...selectResolutionCandidateInputs(graph, state, props),
|
|
262
386
|
]) {
|
|
263
|
-
const identity =
|
|
387
|
+
const identity = derivationIdentity(state, absolute);
|
|
264
388
|
if (excluded.has(identity) || seen.has(identity)) {
|
|
265
389
|
continue;
|
|
266
390
|
}
|
|
@@ -274,23 +398,23 @@ function selectWatchInputs(props) {
|
|
|
274
398
|
* module reachable from `file`. They remain host-owned even when a plugin
|
|
275
399
|
* declares `dependenciesComplete`: plugin code cannot vouch for a compiler
|
|
276
400
|
* resolution change that occurs without any plugin input changing.
|
|
401
|
+
*
|
|
402
|
+
* Candidate entries and their source identities come from the shared
|
|
403
|
+
* per-envelope state, so one delivery scans only the candidates themselves
|
|
404
|
+
* instead of re-resolving every candidate source.
|
|
277
405
|
*/
|
|
278
|
-
function selectResolutionCandidateInputs(props) {
|
|
279
|
-
if (props.result.type === "exception"
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
const graph = props.result.graph;
|
|
283
|
-
if (graph === undefined || graph.candidates === undefined) {
|
|
406
|
+
function selectResolutionCandidateInputs(graph, state, props) {
|
|
407
|
+
if (props.result.type === "exception" ||
|
|
408
|
+
props.result.graph?.candidates === undefined) {
|
|
284
409
|
return [];
|
|
285
410
|
}
|
|
286
|
-
const reachable = new Set(selectReachableSources(
|
|
411
|
+
const reachable = new Set(selectReachableSources(graph, state, props.file).map((source) => derivationIdentity(state, source)));
|
|
287
412
|
const output = [];
|
|
288
|
-
for (const
|
|
289
|
-
if (!reachable.has(
|
|
290
|
-
!Array.isArray(candidates)) {
|
|
413
|
+
for (const entry of graph.candidates) {
|
|
414
|
+
if (!reachable.has(entry.source)) {
|
|
291
415
|
continue;
|
|
292
416
|
}
|
|
293
|
-
output.push(...
|
|
417
|
+
output.push(...entry.files);
|
|
294
418
|
}
|
|
295
419
|
return output;
|
|
296
420
|
}
|
|
@@ -308,47 +432,33 @@ function selectResolutionCandidateInputs(props) {
|
|
|
308
432
|
* `dependencies[file]` list the complete replacement for them. Returns an empty
|
|
309
433
|
* list on exceptions or without a graph.
|
|
310
434
|
*/
|
|
311
|
-
function selectGraphInputs(props) {
|
|
312
|
-
if (props.result.type === "exception") {
|
|
313
|
-
return [];
|
|
314
|
-
}
|
|
315
|
-
const graph = props.result.graph;
|
|
316
|
-
if (graph === undefined) {
|
|
435
|
+
function selectGraphInputs(graph, state, props) {
|
|
436
|
+
if (props.result.type === "exception" || props.result.graph === undefined) {
|
|
317
437
|
return [];
|
|
318
438
|
}
|
|
319
439
|
const output = [];
|
|
320
440
|
if (!props.complete) {
|
|
321
|
-
output.push(...selectReachableEdges(
|
|
322
|
-
output.push(...
|
|
441
|
+
output.push(...selectReachableEdges(graph, state, props.file));
|
|
442
|
+
output.push(...graph.globals);
|
|
323
443
|
}
|
|
324
|
-
output.push(...
|
|
444
|
+
output.push(...graph.configs);
|
|
325
445
|
return output;
|
|
326
446
|
}
|
|
327
447
|
/**
|
|
328
448
|
* Walk the reachability closure of the graph's direct `edges` from `file`,
|
|
329
449
|
* returning the absolute path of every file reached (the starting file itself
|
|
330
|
-
* excluded, even when a cycle points back at it).
|
|
450
|
+
* excluded, even when a cycle points back at it). Reads the shared per-envelope
|
|
451
|
+
* edge index instead of rebuilding it per delivery.
|
|
331
452
|
*/
|
|
332
|
-
function selectReachableEdges(
|
|
333
|
-
const edges = new Map();
|
|
334
|
-
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
335
|
-
if (!Array.isArray(targets)) {
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
const identity = pathIdentityKey(path.resolve(projectRoot, source));
|
|
339
|
-
const entries = edges.get(identity) ?? [];
|
|
340
|
-
entries.push(...targets
|
|
341
|
-
.filter((target) => typeof target === "string" && target.length !== 0)
|
|
342
|
-
.map((target) => path.resolve(projectRoot, target)));
|
|
343
|
-
edges.set(identity, entries);
|
|
344
|
-
}
|
|
453
|
+
function selectReachableEdges(graph, state, file) {
|
|
345
454
|
const output = [];
|
|
346
|
-
const visited = new Set([
|
|
455
|
+
const visited = new Set([derivationIdentity(state, file)]);
|
|
347
456
|
const queue = [file];
|
|
348
457
|
while (queue.length !== 0) {
|
|
349
458
|
const current = queue.pop();
|
|
350
|
-
for (const target of edges.get(
|
|
351
|
-
|
|
459
|
+
for (const target of graph.edges.get(derivationIdentity(state, current)) ??
|
|
460
|
+
[]) {
|
|
461
|
+
const identity = derivationIdentity(state, target);
|
|
352
462
|
if (visited.has(identity)) {
|
|
353
463
|
continue;
|
|
354
464
|
}
|
|
@@ -364,35 +474,21 @@ function selectReachableEdges(projectRoot, file, graph) {
|
|
|
364
474
|
* including `file` itself. Resolution candidates belong to importers rather
|
|
365
475
|
* than targets, so this is intentionally distinct from selectReachableEdges.
|
|
366
476
|
*/
|
|
367
|
-
function selectReachableSources(
|
|
368
|
-
const edges = new Map();
|
|
369
|
-
const spellings = new Map();
|
|
370
|
-
for (const [source, targets] of Object.entries(graph.edges ?? {})) {
|
|
371
|
-
if (!Array.isArray(targets)) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const absolute = path.resolve(projectRoot, source);
|
|
375
|
-
const identity = pathIdentityKey(absolute);
|
|
376
|
-
spellings.set(identity, absolute);
|
|
377
|
-
const entries = edges.get(identity) ?? [];
|
|
378
|
-
entries.push(...targets
|
|
379
|
-
.filter((target) => typeof target === "string" && target.length !== 0)
|
|
380
|
-
.map((target) => path.resolve(projectRoot, target)));
|
|
381
|
-
edges.set(identity, entries);
|
|
382
|
-
}
|
|
477
|
+
function selectReachableSources(graph, state, file) {
|
|
383
478
|
const output = [file];
|
|
384
|
-
const visited = new Set([
|
|
479
|
+
const visited = new Set([derivationIdentity(state, file)]);
|
|
385
480
|
const queue = [file];
|
|
386
481
|
while (queue.length !== 0) {
|
|
387
482
|
const current = queue.pop();
|
|
388
|
-
for (const target of edges.get(
|
|
389
|
-
|
|
483
|
+
for (const target of graph.edges.get(derivationIdentity(state, current)) ??
|
|
484
|
+
[]) {
|
|
485
|
+
const identity = derivationIdentity(state, target);
|
|
390
486
|
if (visited.has(identity)) {
|
|
391
487
|
continue;
|
|
392
488
|
}
|
|
393
489
|
visited.add(identity);
|
|
394
490
|
queue.push(target);
|
|
395
|
-
output.push(spellings.get(identity) ?? target);
|
|
491
|
+
output.push(graph.spellings.get(identity) ?? target);
|
|
396
492
|
}
|
|
397
493
|
}
|
|
398
494
|
return output;
|
|
@@ -418,13 +514,15 @@ function selectListedFiles(projectRoot, listed) {
|
|
|
418
514
|
/**
|
|
419
515
|
* Report whether the plugin declared `file` volatile: its output depends on
|
|
420
516
|
* non-file inputs (environment, time, network), so neither the project
|
|
421
|
-
* transform cache nor a bundler's persistent cache may replay it.
|
|
517
|
+
* transform cache nor a bundler's persistent cache may replay it. Reads the
|
|
518
|
+
* per-envelope identity set instead of rescanning the member list.
|
|
422
519
|
*/
|
|
423
|
-
function isVolatileFile(props) {
|
|
520
|
+
function isVolatileFile(state, props) {
|
|
424
521
|
if (props.result.type === "exception") {
|
|
425
522
|
return false;
|
|
426
523
|
}
|
|
427
|
-
|
|
524
|
+
const declared = (state.volatileFiles ??= collectDeclaredIdentities(state, props.projectRoot, props.result.volatile));
|
|
525
|
+
return declared.has(derivationIdentity(state, props.file));
|
|
428
526
|
}
|
|
429
527
|
/**
|
|
430
528
|
* Report whether the envelope declared `dependencies[file]` complete, i.e. the
|
|
@@ -432,32 +530,18 @@ function isVolatileFile(props) {
|
|
|
432
530
|
* itself and the universal config chain. Callers must still keep the baseline
|
|
433
531
|
* for a file the same envelope declared volatile.
|
|
434
532
|
*/
|
|
435
|
-
function declaresCompleteDependencies(props) {
|
|
533
|
+
function declaresCompleteDependencies(state, props) {
|
|
436
534
|
if (props.result.type === "exception") {
|
|
437
535
|
return false;
|
|
438
536
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Report whether one of the envelope's transformed-file lists (`volatile`,
|
|
443
|
-
* `dependenciesComplete`) names `file`. Members are keyed like `typescript`, so
|
|
444
|
-
* a project-relative and an absolute spelling of the same file both match; a
|
|
445
|
-
* malformed member is ignored rather than fatal.
|
|
446
|
-
*/
|
|
447
|
-
function declaresFile(listed, props) {
|
|
448
|
-
if (!Array.isArray(listed)) {
|
|
449
|
-
return false;
|
|
450
|
-
}
|
|
451
|
-
return listed.some((entry) => typeof entry === "string" &&
|
|
452
|
-
entry.length !== 0 &&
|
|
453
|
-
pathIdentityKey(path.resolve(props.projectRoot, entry)) ===
|
|
454
|
-
pathIdentityKey(props.file));
|
|
537
|
+
const declared = (state.dependenciesComplete ??= collectDeclaredIdentities(state, props.projectRoot, props.result.dependenciesComplete));
|
|
538
|
+
return declared.has(derivationIdentity(state, props.file));
|
|
455
539
|
}
|
|
456
540
|
/**
|
|
457
541
|
* Extract the absolute, deduplicated dependency list for a single file from the
|
|
458
542
|
* compiler result. Mirrors {@link selectTransformedSource}'s key lookup: fast
|
|
459
|
-
* project-relative match first, then a
|
|
460
|
-
* list on exceptions or when the plugin reported nothing.
|
|
543
|
+
* project-relative match first, then a per-envelope identity index. Returns an
|
|
544
|
+
* empty list on exceptions or when the plugin reported nothing.
|
|
461
545
|
*/
|
|
462
546
|
function selectFileDependencies(props) {
|
|
463
547
|
if (props.result.type === "exception") {
|
|
@@ -467,29 +551,26 @@ function selectFileDependencies(props) {
|
|
|
467
551
|
if (dependencies === undefined) {
|
|
468
552
|
return [];
|
|
469
553
|
}
|
|
470
|
-
const
|
|
554
|
+
const state = envelopeDerivation(props);
|
|
555
|
+
const key = toProjectKey(props.projectRoot, props.file, state.identityContext);
|
|
471
556
|
let entries = dependencies[key];
|
|
472
557
|
if (entries === undefined) {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
pathIdentityKey(props.file)) {
|
|
476
|
-
entries = candidateEntries;
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
558
|
+
const index = (state.dependencyIndex ??= createEnvelopeKeyIndex(state, props.projectRoot, dependencies));
|
|
559
|
+
entries = index.get(derivationIdentity(state, props.file));
|
|
480
560
|
}
|
|
481
561
|
if (!Array.isArray(entries)) {
|
|
482
562
|
return [];
|
|
483
563
|
}
|
|
484
564
|
const output = [];
|
|
485
565
|
const seen = new Set();
|
|
566
|
+
const fileIdentity = derivationIdentity(state, props.file);
|
|
486
567
|
for (const entry of entries) {
|
|
487
568
|
if (typeof entry !== "string" || entry.length === 0) {
|
|
488
569
|
continue;
|
|
489
570
|
}
|
|
490
571
|
const absolute = path.resolve(props.projectRoot, entry);
|
|
491
|
-
const identity =
|
|
492
|
-
if (identity ===
|
|
572
|
+
const identity = derivationIdentity(state, absolute);
|
|
573
|
+
if (identity === fileIdentity || seen.has(identity)) {
|
|
493
574
|
continue;
|
|
494
575
|
}
|
|
495
576
|
seen.add(identity);
|
|
@@ -497,6 +578,21 @@ function selectFileDependencies(props) {
|
|
|
497
578
|
}
|
|
498
579
|
return output;
|
|
499
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Build a first-match identity index over one envelope key map (`typescript`,
|
|
583
|
+
* `dependencies`), mirroring the historical per-delivery scan that returned the
|
|
584
|
+
* first entry whose resolved key matched by filesystem identity.
|
|
585
|
+
*/
|
|
586
|
+
function createEnvelopeKeyIndex(state, projectRoot, keyed) {
|
|
587
|
+
const index = new Map();
|
|
588
|
+
for (const [candidate, value] of Object.entries(keyed)) {
|
|
589
|
+
const identity = derivationIdentity(state, path.resolve(projectRoot, candidate));
|
|
590
|
+
if (!index.has(identity)) {
|
|
591
|
+
index.set(identity, value);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return index;
|
|
595
|
+
}
|
|
500
596
|
/**
|
|
501
597
|
* Strip a query string or hash fragment from a bundler module id.
|
|
502
598
|
*
|
|
@@ -552,14 +648,16 @@ function createTransformResult(source, code) {
|
|
|
552
648
|
* agree on the key universe.
|
|
553
649
|
*/
|
|
554
650
|
function matchesCachedSource(cached, file, source, buildScoped) {
|
|
555
|
-
const
|
|
651
|
+
const identities = envelopeDerivation(cached).identityContext;
|
|
652
|
+
const currentKey = toProjectKey(cached.projectRoot, file, identities);
|
|
556
653
|
if (cached.inputHashes[currentKey] !== hashText(source)) {
|
|
557
654
|
return false;
|
|
558
655
|
}
|
|
559
|
-
if (buildScoped &&
|
|
656
|
+
if (buildScoped &&
|
|
657
|
+
!cached.servedFiles?.has(pathIdentityKey(file, identities))) {
|
|
560
658
|
return true;
|
|
561
659
|
}
|
|
562
|
-
const currentHashes = collectProjectInputHashes(cached.projectRoot);
|
|
660
|
+
const currentHashes = collectProjectInputHashes(cached.projectRoot, identities);
|
|
563
661
|
currentHashes[currentKey] = hashText(source);
|
|
564
662
|
if (!sameHashes(cached.inputHashes, currentHashes)) {
|
|
565
663
|
return false;
|
|
@@ -577,7 +675,7 @@ function matchesCachedSource(cached, file, source, buildScoped) {
|
|
|
577
675
|
}
|
|
578
676
|
/** Record a successfully selected module as delivered by this generation. */
|
|
579
677
|
function markCachedSourceServed(cached, file) {
|
|
580
|
-
(cached.servedFiles ??= new Set()).add(pathIdentityKey(file));
|
|
678
|
+
(cached.servedFiles ??= new Set()).add(pathIdentityKey(file, envelopeDerivation(cached).identityContext));
|
|
581
679
|
}
|
|
582
680
|
/**
|
|
583
681
|
* Build the input-hash snapshot stored alongside a fresh compiler result.
|
|
@@ -593,9 +691,11 @@ function markCachedSourceServed(cached, file) {
|
|
|
593
691
|
* them here would make every snapshot comparison fail and the cache never hit.
|
|
594
692
|
*/
|
|
595
693
|
function collectInputHashes(props) {
|
|
596
|
-
const
|
|
694
|
+
const identities = createHostPathIdentityContext();
|
|
695
|
+
const hashes = collectProjectInputHashes(props.projectRoot, identities);
|
|
597
696
|
// Overlay the in-memory source so unsaved edits invalidate the cache.
|
|
598
|
-
hashes[toProjectKey(props.projectRoot, props.currentFile)] =
|
|
697
|
+
hashes[toProjectKey(props.projectRoot, props.currentFile, identities)] =
|
|
698
|
+
hashText(props.currentSource);
|
|
599
699
|
return hashes;
|
|
600
700
|
}
|
|
601
701
|
/**
|
|
@@ -604,11 +704,11 @@ function collectInputHashes(props) {
|
|
|
604
704
|
* slash path. Exported so hosts without a per-build boundary (`@ttsc/metro`)
|
|
605
705
|
* can fold the identical input universe into their own cache fingerprints.
|
|
606
706
|
*/
|
|
607
|
-
function collectProjectInputHashes(projectRoot) {
|
|
707
|
+
function collectProjectInputHashes(projectRoot, identities = createHostPathIdentityContext()) {
|
|
608
708
|
const hashes = {};
|
|
609
709
|
for (const file of listProjectInputFiles(projectRoot)) {
|
|
610
710
|
try {
|
|
611
|
-
hashes[toProjectKey(projectRoot, file)] = hashText(fs.readFileSync(file));
|
|
711
|
+
hashes[toProjectKey(projectRoot, file, identities)] = hashText(fs.readFileSync(file));
|
|
612
712
|
}
|
|
613
713
|
catch {
|
|
614
714
|
// File watchers may observe a transform while another process is moving
|
|
@@ -662,8 +762,13 @@ function listProjectInputFiles(root) {
|
|
|
662
762
|
* Missing paths and files reached through symlinks or Windows junctions are
|
|
663
763
|
* out-of-walk inputs that only the reference graph can prove relevant.
|
|
664
764
|
*/
|
|
665
|
-
function isProjectWalkPath(root, file) {
|
|
666
|
-
|
|
765
|
+
function isProjectWalkPath(root, file, identities = createHostPathIdentityContext()) {
|
|
766
|
+
if (!identities.isWithin(root, file)) {
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
const rootKey = pathIdentityKey(root, identities);
|
|
770
|
+
const fileKey = pathIdentityKey(file, identities);
|
|
771
|
+
const relative = fileKey.slice(rootKey.length).replace(/^[/\\]+/, "");
|
|
667
772
|
if (relative.length === 0 ||
|
|
668
773
|
relative === ".." ||
|
|
669
774
|
relative.startsWith(`..${path.sep}`) ||
|
|
@@ -705,8 +810,9 @@ function isProjectWalkPath(root, file) {
|
|
|
705
810
|
*/
|
|
706
811
|
function collectExternalInputHashes(paths) {
|
|
707
812
|
const hashes = {};
|
|
813
|
+
const identities = createHostPathIdentityContext();
|
|
708
814
|
for (const file of paths) {
|
|
709
|
-
const identity = pathIdentityKey(file);
|
|
815
|
+
const identity = pathIdentityKey(file, identities);
|
|
710
816
|
if (identity in hashes) {
|
|
711
817
|
continue;
|
|
712
818
|
}
|
|
@@ -742,6 +848,7 @@ function selectExternalInputPaths(props) {
|
|
|
742
848
|
return [];
|
|
743
849
|
}
|
|
744
850
|
const members = [];
|
|
851
|
+
const identities = createHostPathIdentityContext();
|
|
745
852
|
const resolutionCandidates = new Set();
|
|
746
853
|
const graph = props.result.graph;
|
|
747
854
|
if (graph !== undefined) {
|
|
@@ -766,7 +873,7 @@ function selectExternalInputPaths(props) {
|
|
|
766
873
|
}
|
|
767
874
|
const absolute = path.resolve(props.projectRoot, candidate);
|
|
768
875
|
members.push(candidate);
|
|
769
|
-
resolutionCandidates.add(pathIdentityKey(absolute));
|
|
876
|
+
resolutionCandidates.add(pathIdentityKey(absolute, identities));
|
|
770
877
|
}
|
|
771
878
|
}
|
|
772
879
|
}
|
|
@@ -777,7 +884,7 @@ function selectExternalInputPaths(props) {
|
|
|
777
884
|
}
|
|
778
885
|
const excluded = props.temporaryTsconfig === undefined
|
|
779
886
|
? undefined
|
|
780
|
-
: pathIdentityKey(props.temporaryTsconfig);
|
|
887
|
+
: pathIdentityKey(props.temporaryTsconfig, identities);
|
|
781
888
|
const output = [];
|
|
782
889
|
const seen = new Set();
|
|
783
890
|
for (const member of members) {
|
|
@@ -785,11 +892,12 @@ function selectExternalInputPaths(props) {
|
|
|
785
892
|
continue;
|
|
786
893
|
}
|
|
787
894
|
const absolute = path.resolve(props.projectRoot, member);
|
|
788
|
-
const identity = pathIdentityKey(absolute);
|
|
895
|
+
const identity = pathIdentityKey(absolute, identities);
|
|
789
896
|
const missingCandidate = resolutionCandidates.has(identity) && !fs.existsSync(absolute);
|
|
790
897
|
if (identity === excluded ||
|
|
791
898
|
seen.has(identity) ||
|
|
792
|
-
(!missingCandidate &&
|
|
899
|
+
(!missingCandidate &&
|
|
900
|
+
isProjectWalkPath(props.projectRoot, absolute, identities))) {
|
|
793
901
|
continue;
|
|
794
902
|
}
|
|
795
903
|
seen.add(identity);
|
|
@@ -1097,17 +1205,18 @@ function selectTransformedSource(props) {
|
|
|
1097
1205
|
throw new Error(formatDiagnostics(props.result.diagnostics));
|
|
1098
1206
|
}
|
|
1099
1207
|
// Fast path: the compiler key matches the normalised project-relative path.
|
|
1100
|
-
const
|
|
1208
|
+
const state = envelopeDerivation(props);
|
|
1209
|
+
const key = toProjectKey(props.projectRoot, props.file, state.identityContext);
|
|
1101
1210
|
const direct = props.result.typescript[key];
|
|
1102
1211
|
if (direct !== undefined) {
|
|
1103
1212
|
return direct;
|
|
1104
1213
|
}
|
|
1105
|
-
// Slow path:
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1214
|
+
// Slow path: the first-match identity index of the envelope's `typescript`
|
|
1215
|
+
// keys, built once per generation instead of scanned per delivery.
|
|
1216
|
+
const index = (state.outputIndex ??= createEnvelopeKeyIndex(state, props.projectRoot, props.result.typescript));
|
|
1217
|
+
const source = index.get(derivationIdentity(state, props.file));
|
|
1218
|
+
if (source !== undefined) {
|
|
1219
|
+
return source;
|
|
1111
1220
|
}
|
|
1112
1221
|
throw new Error(`ttsc transform did not return output for ${props.file}`);
|
|
1113
1222
|
}
|
|
@@ -1194,62 +1303,21 @@ function resolveTsconfig(file, tsconfig) {
|
|
|
1194
1303
|
}
|
|
1195
1304
|
return path.resolve(process.cwd(), "tsconfig.json");
|
|
1196
1305
|
}
|
|
1197
|
-
function toProjectKey(root, file) {
|
|
1198
|
-
|
|
1306
|
+
function toProjectKey(root, file, identities = createHostPathIdentityContext()) {
|
|
1307
|
+
const rootKey = pathIdentityKey(root, identities);
|
|
1308
|
+
const fileKey = pathIdentityKey(file, identities);
|
|
1309
|
+
if (!identities.isWithin(root, file)) {
|
|
1310
|
+
return normalizePath(fileKey);
|
|
1311
|
+
}
|
|
1312
|
+
return normalizePath(fileKey.slice(rootKey.length).replace(/^[/\\]+/, ""));
|
|
1199
1313
|
}
|
|
1200
1314
|
/**
|
|
1201
1315
|
* Build a comparison key for a path without changing the spelling handed to a
|
|
1202
1316
|
* filesystem or bundler. Windows is case-insensitive; macOS is probed per
|
|
1203
1317
|
* existing filesystem location so case-sensitive volumes keep distinct paths.
|
|
1204
1318
|
*/
|
|
1205
|
-
function pathIdentityKey(file) {
|
|
1206
|
-
|
|
1207
|
-
return filesystemIsCaseInsensitive(absolute)
|
|
1208
|
-
? absolute.toLowerCase()
|
|
1209
|
-
: absolute;
|
|
1210
|
-
}
|
|
1211
|
-
function filesystemIsCaseInsensitive(file) {
|
|
1212
|
-
if (process.platform === "win32") {
|
|
1213
|
-
return true;
|
|
1214
|
-
}
|
|
1215
|
-
if (process.platform !== "darwin") {
|
|
1216
|
-
return false;
|
|
1217
|
-
}
|
|
1218
|
-
let existing = file;
|
|
1219
|
-
while (!fs.existsSync(existing)) {
|
|
1220
|
-
const parent = path.dirname(existing);
|
|
1221
|
-
if (parent === existing) {
|
|
1222
|
-
return false;
|
|
1223
|
-
}
|
|
1224
|
-
existing = parent;
|
|
1225
|
-
}
|
|
1226
|
-
let resolved;
|
|
1227
|
-
try {
|
|
1228
|
-
resolved = fs.realpathSync.native(existing);
|
|
1229
|
-
}
|
|
1230
|
-
catch {
|
|
1231
|
-
return false;
|
|
1232
|
-
}
|
|
1233
|
-
const cached = CASE_INSENSITIVE_FILESYSTEMS.get(resolved);
|
|
1234
|
-
if (cached !== undefined) {
|
|
1235
|
-
return cached;
|
|
1236
|
-
}
|
|
1237
|
-
const alternate = togglePathCase(resolved);
|
|
1238
|
-
const insensitive = alternate !== undefined && fs.existsSync(alternate);
|
|
1239
|
-
CASE_INSENSITIVE_FILESYSTEMS.set(resolved, insensitive);
|
|
1240
|
-
return insensitive;
|
|
1241
|
-
}
|
|
1242
|
-
function togglePathCase(file) {
|
|
1243
|
-
for (let index = file.length - 1; index >= 0; --index) {
|
|
1244
|
-
const character = file[index];
|
|
1245
|
-
if (character >= "a" && character <= "z") {
|
|
1246
|
-
return `${file.slice(0, index)}${character.toUpperCase()}${file.slice(index + 1)}`;
|
|
1247
|
-
}
|
|
1248
|
-
if (character >= "A" && character <= "Z") {
|
|
1249
|
-
return `${file.slice(0, index)}${character.toLowerCase()}${file.slice(index + 1)}`;
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
return undefined;
|
|
1319
|
+
function pathIdentityKey(file, identities = createHostPathIdentityContext()) {
|
|
1320
|
+
return identities.resolve(file).key;
|
|
1253
1321
|
}
|
|
1254
1322
|
function normalizePath(file) {
|
|
1255
1323
|
return file.replace(/\\/g, "/");
|