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