@ttsc/unplugin 0.26.1 → 0.27.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 +3 -3
- package/lib/bun.mjs +1 -1
- package/lib/core/index.d.ts +1 -1
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +1 -1
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.ts +76 -18
- package/lib/core/transform.js +1269 -136
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +1269 -137
- package/lib/core/transform.mjs.map +1 -1
- package/lib/turbopack.mjs +1 -1
- package/package.json +3 -3
- package/src/core/index.ts +4 -1
- package/src/core/transform.ts +1689 -172
package/lib/core/transform.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
1
2
|
import crypto from 'node:crypto';
|
|
2
3
|
import fs from 'node:fs';
|
|
3
4
|
import os from 'node:os';
|
|
@@ -6,19 +7,58 @@ import { TtscCompiler } from 'ttsc';
|
|
|
6
7
|
import { createFilesystemPathIdentityContext } from 'ttsc/path-identity';
|
|
7
8
|
import { absolutizePathsTarget, readEffectiveTsconfigPaths } from './tsconfigPaths.mjs';
|
|
8
9
|
|
|
10
|
+
const DEFAULT_FILESYSTEM_OPERATIONS = Object.freeze({
|
|
11
|
+
exists: fs.existsSync,
|
|
12
|
+
lstat: (location) => fs.lstatSync(location, { bigint: true }),
|
|
13
|
+
readFile: (location) => fs.readFileSync(location),
|
|
14
|
+
readdir: (location) => fs.readdirSync(location, { withFileTypes: true }),
|
|
15
|
+
realpath: fs.realpathSync.native,
|
|
16
|
+
stat: fs.statSync,
|
|
17
|
+
statBigInt: (location) => fs.statSync(location, { bigint: true }),
|
|
18
|
+
});
|
|
19
|
+
const TRANSFORM_CACHE_FILESYSTEM = new WeakMap();
|
|
20
|
+
const TRANSFORM_RESULT_FILESYSTEM = new WeakMap();
|
|
9
21
|
/**
|
|
10
22
|
* Caches whose owner has declared a real per-build lifecycle by calling
|
|
11
23
|
* {@link beginTtscTransformBuild} before transforms begin.
|
|
12
24
|
*/
|
|
13
25
|
const BUILD_SCOPED_TRANSFORM_CACHES = new WeakSet();
|
|
14
|
-
function createHostPathIdentityContext() {
|
|
26
|
+
function createHostPathIdentityContext(filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
15
27
|
return createFilesystemPathIdentityContext({
|
|
28
|
+
caseSensitive: filesystem.caseSensitive,
|
|
29
|
+
lstat: filesystem.lstat,
|
|
30
|
+
platform: filesystem.platform,
|
|
31
|
+
readdir: (directory) => filesystem.readdir(directory).map((entry) => entry.name),
|
|
32
|
+
realpath: filesystem.realpath,
|
|
16
33
|
throwOnRealpathError: false,
|
|
17
34
|
});
|
|
18
35
|
}
|
|
19
|
-
/**
|
|
20
|
-
function
|
|
21
|
-
return
|
|
36
|
+
/** Normalize one directory entry under the owning filesystem's case policy. */
|
|
37
|
+
function normalizeHostInputName(name, caseSensitive) {
|
|
38
|
+
return caseSensitive ? name : name.toLowerCase();
|
|
39
|
+
}
|
|
40
|
+
/** Create an empty persistent transform cache with isolated filesystem reads. */
|
|
41
|
+
function createTtscTransformCache(operations = {}) {
|
|
42
|
+
const cache = new Map();
|
|
43
|
+
TRANSFORM_CACHE_FILESYSTEM.set(cache, {
|
|
44
|
+
caseSensitive: operations.caseSensitive,
|
|
45
|
+
exists: operations.exists ?? DEFAULT_FILESYSTEM_OPERATIONS.exists,
|
|
46
|
+
lstat: operations.lstat ?? DEFAULT_FILESYSTEM_OPERATIONS.lstat,
|
|
47
|
+
readFile: operations.readFile ?? DEFAULT_FILESYSTEM_OPERATIONS.readFile,
|
|
48
|
+
readdir: operations.readdir ?? DEFAULT_FILESYSTEM_OPERATIONS.readdir,
|
|
49
|
+
realpath: operations.realpath ?? DEFAULT_FILESYSTEM_OPERATIONS.realpath,
|
|
50
|
+
stat: operations.stat ?? DEFAULT_FILESYSTEM_OPERATIONS.stat,
|
|
51
|
+
statBigInt: operations.statBigInt ?? DEFAULT_FILESYSTEM_OPERATIONS.statBigInt,
|
|
52
|
+
platform: operations.platform,
|
|
53
|
+
});
|
|
54
|
+
return cache;
|
|
55
|
+
}
|
|
56
|
+
function transformFilesystem(cache) {
|
|
57
|
+
return ((cache === undefined ? undefined : TRANSFORM_CACHE_FILESYSTEM.get(cache)) ??
|
|
58
|
+
DEFAULT_FILESYSTEM_OPERATIONS);
|
|
59
|
+
}
|
|
60
|
+
function resultFilesystem(result) {
|
|
61
|
+
return (TRANSFORM_RESULT_FILESYSTEM.get(result) ?? DEFAULT_FILESYSTEM_OPERATIONS);
|
|
22
62
|
}
|
|
23
63
|
/**
|
|
24
64
|
* Start a host build, clearing its prior generation and enabling constant-time
|
|
@@ -29,7 +69,7 @@ function createTtscTransformCache() {
|
|
|
29
69
|
* defines one process-scoped module-loading session.
|
|
30
70
|
*/
|
|
31
71
|
function beginTtscTransformBuild(cache) {
|
|
32
|
-
cache
|
|
72
|
+
clearTtscTransformCache(cache);
|
|
33
73
|
BUILD_SCOPED_TRANSFORM_CACHES.add(cache);
|
|
34
74
|
}
|
|
35
75
|
/**
|
|
@@ -40,9 +80,17 @@ function beginTtscTransformBuild(cache) {
|
|
|
40
80
|
* many edits, so that callback cannot authorize build-scoped shortcuts.
|
|
41
81
|
*/
|
|
42
82
|
function resetTtscTransformCache(cache) {
|
|
43
|
-
cache
|
|
83
|
+
clearTtscTransformCache(cache);
|
|
44
84
|
BUILD_SCOPED_TRANSFORM_CACHES.delete(cache);
|
|
45
85
|
}
|
|
86
|
+
/** Dispose generation-owned filesystem resources before clearing a cache. */
|
|
87
|
+
function clearTtscTransformCache(cache) {
|
|
88
|
+
const generations = [...cache.values()];
|
|
89
|
+
cache.clear();
|
|
90
|
+
for (const generation of generations) {
|
|
91
|
+
void generation.then(disposeCachedTransform, () => undefined);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
46
94
|
/**
|
|
47
95
|
* Apply the ttsc plugin transform to a single source file.
|
|
48
96
|
*
|
|
@@ -68,6 +116,7 @@ function resetTtscTransformCache(cache) {
|
|
|
68
116
|
* per build, not per compilation.
|
|
69
117
|
*/
|
|
70
118
|
async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
119
|
+
const filesystem = transformFilesystem(cache);
|
|
71
120
|
const clean = stripQuery(id);
|
|
72
121
|
if (clean.includes("\0")) {
|
|
73
122
|
return undefined;
|
|
@@ -79,7 +128,7 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
79
128
|
if (pluginsAreDisabled(options.plugins)) {
|
|
80
129
|
return undefined;
|
|
81
130
|
}
|
|
82
|
-
const tsconfig = resolveTsconfig(file, options.project);
|
|
131
|
+
const tsconfig = resolveTsconfig(file, options.project, filesystem);
|
|
83
132
|
const aliasPaths = createAliasPaths(aliases);
|
|
84
133
|
const key = createTransformCacheKey({
|
|
85
134
|
aliasPaths,
|
|
@@ -93,11 +142,19 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
93
142
|
// A rejected in-flight generation must not stay cached: evict it (only if
|
|
94
143
|
// it is still the current entry) so a later call re-runs the transform.
|
|
95
144
|
const cached = await awaitOrEvict(cache, key, transformed);
|
|
145
|
+
TRANSFORM_RESULT_FILESYSTEM.set(cached.result, filesystem);
|
|
96
146
|
// While this caller awaited the old Promise, another caller may have
|
|
97
147
|
// invalidated it and installed a newer authoritative generation.
|
|
98
148
|
if (cache?.get(key) !== transformed) {
|
|
99
149
|
continue;
|
|
100
150
|
}
|
|
151
|
+
const buildScoped = cache !== undefined && BUILD_SCOPED_TRANSFORM_CACHES.has(cache);
|
|
152
|
+
if (!buildScoped) {
|
|
153
|
+
await settleProjectMutationEvents(cached);
|
|
154
|
+
if (cache?.get(key) !== transformed) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
101
158
|
if (
|
|
102
159
|
// A file the plugin declared volatile must never be served from the
|
|
103
160
|
// cache: its output depends on non-file inputs, so the input-hash
|
|
@@ -107,7 +164,7 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
107
164
|
projectRoot: cached.projectRoot,
|
|
108
165
|
result: cached.result,
|
|
109
166
|
}) &&
|
|
110
|
-
matchesCachedSource(cached, file, source,
|
|
167
|
+
matchesCachedSource(cached, file, source, buildScoped)) {
|
|
111
168
|
reportSuccessDiagnostics(cached.result);
|
|
112
169
|
// A resolved `"exception"` / `"failure"` envelope makes this throw;
|
|
113
170
|
// that is a failed generation too, so evict before surfacing it.
|
|
@@ -140,7 +197,9 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
140
197
|
compilerOptions: options.compilerOptions,
|
|
141
198
|
currentFile: file,
|
|
142
199
|
currentSource: source,
|
|
200
|
+
filesystem,
|
|
143
201
|
plugins: options.plugins,
|
|
202
|
+
trackProjectMembership: cache !== undefined,
|
|
144
203
|
tsconfig,
|
|
145
204
|
});
|
|
146
205
|
cache?.set(key, transformed);
|
|
@@ -207,8 +266,20 @@ function selectOrEvict(cache, key, generation, props) {
|
|
|
207
266
|
function evictGeneration(cache, key, generation) {
|
|
208
267
|
if (cache?.get(key) === generation) {
|
|
209
268
|
cache.delete(key);
|
|
269
|
+
void generation.then(disposeCachedTransform, () => undefined);
|
|
210
270
|
}
|
|
211
271
|
}
|
|
272
|
+
/** Close one generation's directory watchers exactly once. */
|
|
273
|
+
function disposeCachedTransform(cached) {
|
|
274
|
+
const trackers = [
|
|
275
|
+
cached.projectMutationTracker,
|
|
276
|
+
cached.hostInputMutationTracker,
|
|
277
|
+
];
|
|
278
|
+
cached.projectMutationTracker = undefined;
|
|
279
|
+
cached.hostInputMutationTracker = undefined;
|
|
280
|
+
for (const tracker of trackers)
|
|
281
|
+
tracker?.close();
|
|
282
|
+
}
|
|
212
283
|
/**
|
|
213
284
|
* Derivation states keyed by the compiler result object. One result object is
|
|
214
285
|
* produced by one compile against one project root, so the root captured at
|
|
@@ -222,7 +293,7 @@ function envelopeDerivation(props) {
|
|
|
222
293
|
return existing;
|
|
223
294
|
}
|
|
224
295
|
const created = {
|
|
225
|
-
identityContext: createHostPathIdentityContext(),
|
|
296
|
+
identityContext: createHostPathIdentityContext(resultFilesystem(props.result)),
|
|
226
297
|
identities: new Map(),
|
|
227
298
|
watchInputs: new Map(),
|
|
228
299
|
};
|
|
@@ -244,6 +315,9 @@ function envelopeGraphIndexes(state, props) {
|
|
|
244
315
|
candidates: [],
|
|
245
316
|
globals: [],
|
|
246
317
|
configs: [],
|
|
318
|
+
members: new Set(),
|
|
319
|
+
inputProofs: new Map(),
|
|
320
|
+
inputProofConflicts: new Set(),
|
|
247
321
|
};
|
|
248
322
|
const graph = props.result.type === "exception" ? undefined : props.result.graph;
|
|
249
323
|
if (graph !== undefined) {
|
|
@@ -253,23 +327,73 @@ function envelopeGraphIndexes(state, props) {
|
|
|
253
327
|
}
|
|
254
328
|
const absolute = path.resolve(props.projectRoot, source);
|
|
255
329
|
const identity = derivationIdentity(state, absolute);
|
|
330
|
+
built.members.add(identity);
|
|
256
331
|
built.spellings.set(identity, absolute);
|
|
257
332
|
const entries = built.edges.get(identity) ?? [];
|
|
258
333
|
entries.push(...targets
|
|
259
334
|
.filter((target) => typeof target === "string" && target.length !== 0)
|
|
260
|
-
.map((target) =>
|
|
335
|
+
.map((target) => {
|
|
336
|
+
const absoluteTarget = path.resolve(props.projectRoot, target);
|
|
337
|
+
built.members.add(derivationIdentity(state, absoluteTarget));
|
|
338
|
+
return absoluteTarget;
|
|
339
|
+
}));
|
|
261
340
|
built.edges.set(identity, entries);
|
|
262
341
|
}
|
|
263
342
|
built.globals.push(...selectListedFiles(props.projectRoot, graph.globals));
|
|
264
343
|
built.configs.push(...selectListedFiles(props.projectRoot, graph.configs));
|
|
344
|
+
for (const input of [...built.globals, ...built.configs]) {
|
|
345
|
+
built.members.add(derivationIdentity(state, input));
|
|
346
|
+
}
|
|
265
347
|
for (const [source, candidates] of Object.entries(graph.candidates ?? {})) {
|
|
266
348
|
if (!Array.isArray(candidates)) {
|
|
267
349
|
continue;
|
|
268
350
|
}
|
|
351
|
+
const sourceIdentity = derivationIdentity(state, path.resolve(props.projectRoot, source));
|
|
352
|
+
built.members.add(sourceIdentity);
|
|
269
353
|
built.candidates.push({
|
|
270
|
-
source:
|
|
354
|
+
source: sourceIdentity,
|
|
271
355
|
files: selectListedFiles(props.projectRoot, candidates),
|
|
272
356
|
});
|
|
357
|
+
for (const candidate of candidates) {
|
|
358
|
+
if (typeof candidate !== "string" || candidate.length === 0)
|
|
359
|
+
continue;
|
|
360
|
+
built.members.add(derivationIdentity(state, path.resolve(props.projectRoot, candidate)));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
for (const [input, hash] of Object.entries(graph.inputHashes ?? {})) {
|
|
364
|
+
if (hash !== null &&
|
|
365
|
+
(typeof hash !== "string" || !/^[0-9a-f]{64}$/.test(hash))) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (graph.inputRealpaths === undefined ||
|
|
369
|
+
!Object.prototype.hasOwnProperty.call(graph.inputRealpaths, input)) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
const reportedRealpath = graph.inputRealpaths[input];
|
|
373
|
+
if (reportedRealpath !== null &&
|
|
374
|
+
(typeof reportedRealpath !== "string" ||
|
|
375
|
+
!path.isAbsolute(reportedRealpath))) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
const absolute = path.resolve(props.projectRoot, input);
|
|
379
|
+
const identity = derivationIdentity(state, absolute);
|
|
380
|
+
if (!built.members.has(identity))
|
|
381
|
+
continue;
|
|
382
|
+
const proof = {
|
|
383
|
+
hash,
|
|
384
|
+
path: absolute,
|
|
385
|
+
realpath: reportedRealpath === null ? null : path.resolve(reportedRealpath),
|
|
386
|
+
};
|
|
387
|
+
const previous = built.inputProofs.get(identity);
|
|
388
|
+
if (previous !== undefined &&
|
|
389
|
+
(previous.hash !== proof.hash ||
|
|
390
|
+
!sameHostInputRealpath(previous.realpath, proof.realpath, state.identityContext))) {
|
|
391
|
+
built.inputProofs.delete(identity);
|
|
392
|
+
built.inputProofConflicts.add(identity);
|
|
393
|
+
}
|
|
394
|
+
else if (!built.inputProofConflicts.has(identity)) {
|
|
395
|
+
built.inputProofs.set(identity, proof);
|
|
396
|
+
}
|
|
273
397
|
}
|
|
274
398
|
}
|
|
275
399
|
state.graph = built;
|
|
@@ -370,29 +494,59 @@ function selectWatchInputs(props) {
|
|
|
370
494
|
function deriveWatchInputs(state, props, fileIdentity) {
|
|
371
495
|
const graph = envelopeGraphIndexes(state, props);
|
|
372
496
|
const output = [];
|
|
373
|
-
const
|
|
497
|
+
const physicalSeen = new Set();
|
|
498
|
+
const lexicalSeen = new Set();
|
|
374
499
|
const excluded = new Set([fileIdentity]);
|
|
375
500
|
if (props.temporaryTsconfig !== undefined) {
|
|
376
501
|
excluded.add(derivationIdentity(state, props.temporaryTsconfig));
|
|
377
502
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (excluded.has(identity) || seen.has(identity)) {
|
|
389
|
-
continue;
|
|
503
|
+
const currentSpelling = path.resolve(props.file);
|
|
504
|
+
const temporarySpelling = props.temporaryTsconfig === undefined
|
|
505
|
+
? undefined
|
|
506
|
+
: path.resolve(props.temporaryTsconfig);
|
|
507
|
+
const appendLexical = (input) => {
|
|
508
|
+
const spelling = path.resolve(input);
|
|
509
|
+
if (spelling === currentSpelling ||
|
|
510
|
+
spelling === temporarySpelling ||
|
|
511
|
+
lexicalSeen.has(spelling)) {
|
|
512
|
+
return;
|
|
390
513
|
}
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
514
|
+
lexicalSeen.add(spelling);
|
|
515
|
+
physicalSeen.add(derivationIdentity(state, input));
|
|
516
|
+
output.push(input);
|
|
517
|
+
};
|
|
518
|
+
const appendPhysical = (input) => {
|
|
519
|
+
const identity = derivationIdentity(state, input);
|
|
520
|
+
if (excluded.has(identity) || physicalSeen.has(identity))
|
|
521
|
+
return;
|
|
522
|
+
physicalSeen.add(identity);
|
|
523
|
+
lexicalSeen.add(path.resolve(input));
|
|
524
|
+
output.push(input);
|
|
525
|
+
};
|
|
526
|
+
for (const input of selectFileDependencies(props))
|
|
527
|
+
appendLexical(input);
|
|
528
|
+
for (const input of selectGraphInputs(graph, state, {
|
|
529
|
+
...props,
|
|
530
|
+
complete: declaresCompleteDependencies(state, props) &&
|
|
531
|
+
!isVolatileFile(state, props),
|
|
532
|
+
}))
|
|
533
|
+
appendPhysical(input);
|
|
534
|
+
// Resolution candidates, plugin dependencies, and universal host inputs
|
|
535
|
+
// preserve lexical aliases. Physical deduplication would collapse
|
|
536
|
+
// `alias/selection.cjs` into the selected target path, so a bundler would
|
|
537
|
+
// watch only the target and miss a symlink/junction retarget.
|
|
538
|
+
for (const input of selectResolutionCandidateInputs(graph, state, props))
|
|
539
|
+
appendLexical(input);
|
|
540
|
+
for (const input of selectHostInputs(props))
|
|
541
|
+
appendLexical(input);
|
|
394
542
|
return output;
|
|
395
543
|
}
|
|
544
|
+
/** Return exact host-wide descriptor/config inputs for every output file. */
|
|
545
|
+
function selectHostInputs(props) {
|
|
546
|
+
return props.result.type === "exception"
|
|
547
|
+
? []
|
|
548
|
+
: selectListedFiles(props.projectRoot, props.result.hostInputs);
|
|
549
|
+
}
|
|
396
550
|
/**
|
|
397
551
|
* Return the module-resolution paths that can supersede a currently resolved
|
|
398
552
|
* module reachable from `file`. They remain host-owned even when a plugin
|
|
@@ -638,14 +792,13 @@ function createTransformResult(source, code) {
|
|
|
638
792
|
*
|
|
639
793
|
* Always compares the current module's in-memory source with the generation
|
|
640
794
|
* snapshot. A cache whose owner called {@link beginTtscTransformBuild} can use
|
|
641
|
-
* that comparison alone for
|
|
642
|
-
*
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* agree on the key universe.
|
|
795
|
+
* that comparison alone for a stable generation's first module delivery in the
|
|
796
|
+
* current build. An incomplete generation may not take this shortcut: otherwise
|
|
797
|
+
* a sibling output captured during a filesystem race could still be served
|
|
798
|
+
* once. Later graph-bearing requests validate the file's derived input set and
|
|
799
|
+
* project membership; graph-free envelopes conservatively re-hash the complete
|
|
800
|
+
* project and out-of-walk snapshots. Any mismatch forces a complete
|
|
801
|
+
* re-transform.
|
|
649
802
|
*/
|
|
650
803
|
function matchesCachedSource(cached, file, source, buildScoped) {
|
|
651
804
|
const identities = envelopeDerivation(cached).identityContext;
|
|
@@ -654,12 +807,346 @@ function matchesCachedSource(cached, file, source, buildScoped) {
|
|
|
654
807
|
return false;
|
|
655
808
|
}
|
|
656
809
|
if (buildScoped &&
|
|
810
|
+
cached.projectSnapshotComplete === true &&
|
|
657
811
|
!cached.servedFiles?.has(pathIdentityKey(file, identities))) {
|
|
658
812
|
return true;
|
|
659
813
|
}
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
814
|
+
if (cached.result.type !== "exception" &&
|
|
815
|
+
cached.result.graph !== undefined &&
|
|
816
|
+
cached.projectSnapshotComplete === true &&
|
|
817
|
+
cached.projectDirectories !== undefined &&
|
|
818
|
+
cached.projectMutationTracker !== undefined &&
|
|
819
|
+
cached.hostInputMutationTracker !== undefined) {
|
|
820
|
+
return matchesNarrowPersistentInputs(cached, file);
|
|
821
|
+
}
|
|
822
|
+
return matchesCompleteInputSnapshot(cached, currentKey, source);
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Validate one graph-bearing cached output against only the inputs that can
|
|
826
|
+
* affect that file. Project membership is validated once per event-loop turn,
|
|
827
|
+
* so sibling module deliveries share one directory-metadata pass instead of
|
|
828
|
+
* multiplying it by module count.
|
|
829
|
+
*/
|
|
830
|
+
function matchesNarrowPersistentInputs(cached, file) {
|
|
831
|
+
if (!matchesProjectMembership(cached)) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
const hostTracker = cached.hostInputMutationTracker;
|
|
835
|
+
if (hostTracker === undefined ||
|
|
836
|
+
hostTracker.failed ||
|
|
837
|
+
hostTracker.membershipChanged) {
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
const state = envelopeDerivation(cached);
|
|
841
|
+
const hostValidation = state.hostInputValidation;
|
|
842
|
+
if (hostValidation === undefined ||
|
|
843
|
+
!matchesUniversalHostInputs(cached, hostValidation)) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
const inputs = selectWatchInputs({
|
|
847
|
+
file,
|
|
848
|
+
projectRoot: cached.projectRoot,
|
|
849
|
+
result: cached.result,
|
|
850
|
+
temporaryTsconfig: cached.temporaryTsconfig,
|
|
851
|
+
});
|
|
852
|
+
for (const input of inputs) {
|
|
853
|
+
if (hostValidation.identities.has(derivationIdentity(state, input))) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (!matchesRecordedInput(cached, input)) {
|
|
857
|
+
return false;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Validate universal descriptor/config inputs without re-reading them for every
|
|
864
|
+
* module. Existing paths use the same nanosecond metadata manifest that guards
|
|
865
|
+
* GOROOT identity memoization; missing probes are grouped by the nearest
|
|
866
|
+
* existing directory and checked through one exact membership listing.
|
|
867
|
+
*/
|
|
868
|
+
function matchesUniversalHostInputs(cached, validation) {
|
|
869
|
+
const filesystem = resultFilesystem(cached.result);
|
|
870
|
+
for (const entry of validation.entries.values()) {
|
|
871
|
+
const signature = inputMetadataSignature(entry.path, filesystem);
|
|
872
|
+
if (signature === entry.signature)
|
|
873
|
+
continue;
|
|
874
|
+
if (entry.strict === true)
|
|
875
|
+
return false;
|
|
876
|
+
if (hostInputRealpath(entry.path, filesystem) !== entry.realpath)
|
|
877
|
+
return false;
|
|
878
|
+
if (!matchesRecordedInput(cached, entry.path)) {
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
if (signature === undefined)
|
|
882
|
+
return false;
|
|
883
|
+
entry.signature = signature;
|
|
884
|
+
}
|
|
885
|
+
for (const [directory, names] of validation.missing) {
|
|
886
|
+
let entries;
|
|
887
|
+
try {
|
|
888
|
+
entries = filesystem.readdir(directory);
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
// Only a provably absent/non-directory ancestor keeps every descendant
|
|
892
|
+
// unreachable. Permission and transient I/O failures cannot prove that
|
|
893
|
+
// a candidate is still missing, while replacing the proving directory
|
|
894
|
+
// with an exact file can itself redirect module resolution.
|
|
895
|
+
try {
|
|
896
|
+
if (!filesystem.stat(directory).isDirectory())
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
catch (statError) {
|
|
900
|
+
if (!isMissingPathError(statError))
|
|
901
|
+
return false;
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
906
|
+
const identities = envelopeDerivation(cached).identityContext;
|
|
907
|
+
const caseSensitive = identities.caseSensitive(directory);
|
|
908
|
+
if (entries.some((entry) => names.has(normalizeHostInputName(entry.name, caseSensitive)))) {
|
|
909
|
+
return false;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
/** True only for errors that prove a path cannot currently be traversed. */
|
|
915
|
+
function isMissingPathError(error) {
|
|
916
|
+
const code = error?.code;
|
|
917
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
918
|
+
}
|
|
919
|
+
/** Capture the universal-input manifest while the generation is still fresh. */
|
|
920
|
+
function captureUniversalHostInputValidation(cached, currentFile) {
|
|
921
|
+
const filesystem = resultFilesystem(cached.result);
|
|
922
|
+
const state = envelopeDerivation(cached);
|
|
923
|
+
const validation = {
|
|
924
|
+
entries: new Map(),
|
|
925
|
+
identities: new Set(),
|
|
926
|
+
missing: new Map(),
|
|
927
|
+
};
|
|
928
|
+
for (const input of selectPersistentHostInputs({
|
|
929
|
+
filesystem,
|
|
930
|
+
projectRoot: cached.projectRoot,
|
|
931
|
+
result: cached.result,
|
|
932
|
+
temporaryTsconfig: cached.temporaryTsconfig,
|
|
933
|
+
})) {
|
|
934
|
+
const generationHashes = cached.result.type === "exception"
|
|
935
|
+
? undefined
|
|
936
|
+
: cached.result.hostInputHashes;
|
|
937
|
+
const generationRealpaths = cached.result.type === "exception"
|
|
938
|
+
? undefined
|
|
939
|
+
: cached.result.hostInputRealpaths;
|
|
940
|
+
const expected = generationHashes?.[path.resolve(input)];
|
|
941
|
+
// Every persistent universal input must carry an evaluation-time
|
|
942
|
+
// fingerprint. If a plugin/native host cannot provide one, keep the fresh
|
|
943
|
+
// result but decline narrow long-lived reuse.
|
|
944
|
+
if (expected === undefined) {
|
|
945
|
+
const current = path.resolve(currentFile);
|
|
946
|
+
if (path.resolve(input) !== current)
|
|
947
|
+
return undefined;
|
|
948
|
+
// The current module may be supplied from an unsaved editor buffer. Its
|
|
949
|
+
// generation snapshot is overlaid below from `currentSource`, so a disk
|
|
950
|
+
// fingerprint would be both unavailable and the wrong authority.
|
|
951
|
+
}
|
|
952
|
+
else if (expected !== hostInputStateHash(input, filesystem)) {
|
|
953
|
+
return undefined;
|
|
954
|
+
}
|
|
955
|
+
const absoluteInput = path.resolve(input);
|
|
956
|
+
if (generationRealpaths !== undefined) {
|
|
957
|
+
if (!Object.prototype.hasOwnProperty.call(generationRealpaths, absoluteInput) ||
|
|
958
|
+
!sameHostInputRealpath(generationRealpaths[absoluteInput], hostInputRealpath(input, filesystem), state.identityContext)) {
|
|
959
|
+
return undefined;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const identity = derivationIdentity(state, input);
|
|
963
|
+
validation.identities.add(identity);
|
|
964
|
+
const before = inputMetadataSignature(input, filesystem);
|
|
965
|
+
if (!matchesRecordedInput(cached, input))
|
|
966
|
+
return undefined;
|
|
967
|
+
const after = inputMetadataSignature(input, filesystem);
|
|
968
|
+
if (before !== after)
|
|
969
|
+
return undefined;
|
|
970
|
+
if (after !== undefined) {
|
|
971
|
+
// Do not key this manifest by physical identity. A symlink/junction
|
|
972
|
+
// spelling and its selected target deliberately share that identity,
|
|
973
|
+
// but both lexical paths must survive so retargeting the alias is visible.
|
|
974
|
+
validation.entries.set(path.resolve(input), {
|
|
975
|
+
path: input,
|
|
976
|
+
realpath: hostInputRealpath(input, filesystem),
|
|
977
|
+
signature: after,
|
|
978
|
+
});
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const probe = missingPathProbe(input, filesystem);
|
|
982
|
+
if (probe.blocker !== undefined) {
|
|
983
|
+
const blockerIdentity = derivationIdentity(state, probe.blocker);
|
|
984
|
+
const signature = inputMetadataSignature(probe.blocker, filesystem);
|
|
985
|
+
if (signature === undefined)
|
|
986
|
+
return undefined;
|
|
987
|
+
validation.identities.add(blockerIdentity);
|
|
988
|
+
validation.entries.set(path.resolve(probe.blocker), {
|
|
989
|
+
path: probe.blocker,
|
|
990
|
+
realpath: hostInputRealpath(probe.blocker, filesystem),
|
|
991
|
+
signature,
|
|
992
|
+
strict: true,
|
|
993
|
+
});
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
let names = validation.missing.get(probe.directory);
|
|
997
|
+
if (names === undefined) {
|
|
998
|
+
names = new Set();
|
|
999
|
+
validation.missing.set(probe.directory, names);
|
|
1000
|
+
}
|
|
1001
|
+
names.add(normalizeHostInputName(probe.name, state.identityContext.caseSensitive(probe.directory)));
|
|
1002
|
+
}
|
|
1003
|
+
state.hostInputValidation = validation;
|
|
1004
|
+
return validation;
|
|
1005
|
+
}
|
|
1006
|
+
/** Metadata identity whose stability lets a generation reuse a content hash. */
|
|
1007
|
+
function inputMetadataSignature(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1008
|
+
try {
|
|
1009
|
+
const link = filesystem.lstat(file);
|
|
1010
|
+
let target = link;
|
|
1011
|
+
if (link.isSymbolicLink()) {
|
|
1012
|
+
try {
|
|
1013
|
+
target = filesystem.statBigInt(file);
|
|
1014
|
+
}
|
|
1015
|
+
catch {
|
|
1016
|
+
// Keep a broken link in the existing-input manifest. Its own metadata
|
|
1017
|
+
// stays stable while the target is missing, and the first successful
|
|
1018
|
+
// stat after the target appears changes this signature. Treating it as
|
|
1019
|
+
// a plain missing path would watch/list only the link's parent, which
|
|
1020
|
+
// cannot observe a target created in another directory.
|
|
1021
|
+
return [
|
|
1022
|
+
link.dev,
|
|
1023
|
+
link.ino,
|
|
1024
|
+
link.mode,
|
|
1025
|
+
link.size,
|
|
1026
|
+
link.mtimeNs,
|
|
1027
|
+
link.ctimeNs,
|
|
1028
|
+
"missing-target",
|
|
1029
|
+
].join(":");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
return [
|
|
1033
|
+
link.dev,
|
|
1034
|
+
link.ino,
|
|
1035
|
+
link.mode,
|
|
1036
|
+
link.size,
|
|
1037
|
+
link.mtimeNs,
|
|
1038
|
+
link.ctimeNs,
|
|
1039
|
+
target.dev,
|
|
1040
|
+
target.ino,
|
|
1041
|
+
target.mode,
|
|
1042
|
+
target.size,
|
|
1043
|
+
target.mtimeNs,
|
|
1044
|
+
target.ctimeNs,
|
|
1045
|
+
].join(":");
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
return undefined;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
/** Content/kind fingerprint matching the compiler host-input contract. */
|
|
1052
|
+
function hostInputStateHash(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1053
|
+
try {
|
|
1054
|
+
return hashText(filesystem.readFile(file));
|
|
1055
|
+
}
|
|
1056
|
+
catch {
|
|
1057
|
+
try {
|
|
1058
|
+
return filesystem.stat(file).isDirectory()
|
|
1059
|
+
? hashText("ttsc:host-input:directory\0")
|
|
1060
|
+
: null;
|
|
1061
|
+
}
|
|
1062
|
+
catch {
|
|
1063
|
+
return null;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
/** Fingerprint the text/kind state returned by TypeScript-Go's filesystem. */
|
|
1068
|
+
function graphInputStateHash(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1069
|
+
try {
|
|
1070
|
+
const bytes = filesystem.readFile(file);
|
|
1071
|
+
if (bytes.length >= 2 && bytes[0] === 0xff && bytes[1] === 0xfe) {
|
|
1072
|
+
const even = bytes.subarray(2, 2 + Math.floor((bytes.length - 2) / 2) * 2);
|
|
1073
|
+
return hashText(Buffer.from(even.toString("utf16le"), "utf8"));
|
|
1074
|
+
}
|
|
1075
|
+
if (bytes.length >= 2 && bytes[0] === 0xfe && bytes[1] === 0xff) {
|
|
1076
|
+
const even = Buffer.from(bytes.subarray(2, 2 + Math.floor((bytes.length - 2) / 2) * 2));
|
|
1077
|
+
even.swap16();
|
|
1078
|
+
return hashText(Buffer.from(even.toString("utf16le"), "utf8"));
|
|
1079
|
+
}
|
|
1080
|
+
const content = bytes.length >= 3 &&
|
|
1081
|
+
bytes[0] === 0xef &&
|
|
1082
|
+
bytes[1] === 0xbb &&
|
|
1083
|
+
bytes[2] === 0xbf
|
|
1084
|
+
? bytes.subarray(3)
|
|
1085
|
+
: bytes;
|
|
1086
|
+
return hashText(content);
|
|
1087
|
+
}
|
|
1088
|
+
catch {
|
|
1089
|
+
try {
|
|
1090
|
+
return filesystem.stat(file).isDirectory()
|
|
1091
|
+
? hashText("ttsc:host-input:directory\0")
|
|
1092
|
+
: null;
|
|
1093
|
+
}
|
|
1094
|
+
catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/** Physical target selected by a lexical host-input path. */
|
|
1100
|
+
function hostInputRealpath(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1101
|
+
try {
|
|
1102
|
+
return filesystem.realpath(file);
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
/** Compare two reported realpaths by filesystem identity, not Windows spelling. */
|
|
1109
|
+
function sameHostInputRealpath(left, right, identities) {
|
|
1110
|
+
if (left === undefined || (left === null) !== (right === null))
|
|
1111
|
+
return false;
|
|
1112
|
+
if (left === null || right === null)
|
|
1113
|
+
return true;
|
|
1114
|
+
return (pathIdentityKey(left, identities) === pathIdentityKey(right, identities));
|
|
1115
|
+
}
|
|
1116
|
+
/** Find one directory listing that proves an absent path is still absent. */
|
|
1117
|
+
function missingPathProbe(file, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1118
|
+
let child = path.resolve(file);
|
|
1119
|
+
for (;;) {
|
|
1120
|
+
const directory = path.dirname(child);
|
|
1121
|
+
try {
|
|
1122
|
+
const stats = filesystem.stat(directory);
|
|
1123
|
+
if (stats.isDirectory()) {
|
|
1124
|
+
return { directory, name: path.basename(child) };
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
blocker: directory,
|
|
1128
|
+
directory: path.dirname(directory),
|
|
1129
|
+
name: path.basename(directory),
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
catch { }
|
|
1133
|
+
if (directory === child) {
|
|
1134
|
+
return { directory, name: path.basename(child) };
|
|
1135
|
+
}
|
|
1136
|
+
child = directory;
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
/** Fall back to the historical whole-envelope validation without a graph. */
|
|
1140
|
+
function matchesCompleteInputSnapshot(cached, currentKey, source) {
|
|
1141
|
+
if (cached.projectSnapshotComplete !== true) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
const current = collectProjectInputSnapshot(cached.projectRoot, envelopeDerivation(cached).identityContext, resultFilesystem(cached.result));
|
|
1145
|
+
if (!current.complete) {
|
|
1146
|
+
return false;
|
|
1147
|
+
}
|
|
1148
|
+
current.hashes[currentKey] = hashText(source);
|
|
1149
|
+
if (!sameHashes(cached.inputHashes, current.hashes)) {
|
|
663
1150
|
return false;
|
|
664
1151
|
}
|
|
665
1152
|
// Re-hash the out-of-walk inputs the compiler reported for this generation
|
|
@@ -671,32 +1158,127 @@ function matchesCachedSource(cached, file, source, buildScoped) {
|
|
|
671
1158
|
// requires a tsconfig or package manifest change, both of which the project
|
|
672
1159
|
// walk above already detects.
|
|
673
1160
|
const externalHashes = cached.externalInputHashes ?? {};
|
|
674
|
-
return sameHashes(externalHashes,
|
|
1161
|
+
return (sameHashes(externalHashes, collectCachedExternalInputHashes(cached)) &&
|
|
1162
|
+
matchesExternalInputRealpaths(cached));
|
|
675
1163
|
}
|
|
676
|
-
/**
|
|
677
|
-
function
|
|
678
|
-
|
|
1164
|
+
/** Re-check graph-owned physical identities in complete-snapshot fallback. */
|
|
1165
|
+
function matchesExternalInputRealpaths(cached) {
|
|
1166
|
+
const expected = cached.externalInputRealpaths;
|
|
1167
|
+
if (expected === undefined || Object.keys(expected).length === 0)
|
|
1168
|
+
return true;
|
|
1169
|
+
const state = envelopeDerivation(cached);
|
|
1170
|
+
const filesystem = resultFilesystem(cached.result);
|
|
1171
|
+
for (const input of cached.externalInputPaths ?? []) {
|
|
1172
|
+
const identity = derivationIdentity(state, input);
|
|
1173
|
+
if (!Object.prototype.hasOwnProperty.call(expected, identity))
|
|
1174
|
+
continue;
|
|
1175
|
+
if (!sameHostInputRealpath(expected[identity], hostInputRealpath(input, filesystem), state.identityContext)) {
|
|
1176
|
+
return false;
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return true;
|
|
679
1180
|
}
|
|
680
1181
|
/**
|
|
681
|
-
*
|
|
682
|
-
*
|
|
683
|
-
*
|
|
684
|
-
*
|
|
685
|
-
*
|
|
686
|
-
* content is captured correctly.
|
|
687
|
-
*
|
|
688
|
-
* Only the project's own files are hashed. Out-of-walk program inputs the
|
|
689
|
-
* compiler also read (`node_modules` declarations, sibling-package sources) are
|
|
690
|
-
* deliberately excluded: the validator never reproduces those keys, so keying
|
|
691
|
-
* them here would make every snapshot comparison fail and the cache never hit.
|
|
1182
|
+
* Capture external-input hashes without attaching post-compile state to an
|
|
1183
|
+
* earlier graph. Graph members must carry compiler-time proof and still match
|
|
1184
|
+
* it now; plugin-declared dependency-only paths retain the historical
|
|
1185
|
+
* post-compile snapshot because their own protocol does not claim generation
|
|
1186
|
+
* fingerprints.
|
|
692
1187
|
*/
|
|
693
|
-
function
|
|
694
|
-
const
|
|
695
|
-
const
|
|
696
|
-
|
|
697
|
-
hashes
|
|
698
|
-
|
|
699
|
-
|
|
1188
|
+
function captureExternalInputSnapshot(cached, paths) {
|
|
1189
|
+
const state = envelopeDerivation(cached);
|
|
1190
|
+
const filesystem = resultFilesystem(cached.result);
|
|
1191
|
+
const graph = envelopeGraphIndexes(state, cached);
|
|
1192
|
+
const hashes = {};
|
|
1193
|
+
const realpaths = {};
|
|
1194
|
+
let complete = true;
|
|
1195
|
+
for (const input of paths) {
|
|
1196
|
+
const identity = derivationIdentity(state, input);
|
|
1197
|
+
if (graph.members.has(identity)) {
|
|
1198
|
+
const proof = graph.inputProofs.get(identity);
|
|
1199
|
+
if (proof === undefined || graph.inputProofConflicts.has(identity)) {
|
|
1200
|
+
complete = false;
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
const currentHash = graphInputStateHash(input, filesystem);
|
|
1204
|
+
if (currentHash !== proof.hash ||
|
|
1205
|
+
!sameHostInputRealpath(proof.realpath, hostInputRealpath(input, filesystem), state.identityContext)) {
|
|
1206
|
+
complete = false;
|
|
1207
|
+
}
|
|
1208
|
+
hashes[identity] = proof.hash ?? "missing";
|
|
1209
|
+
realpaths[identity] = proof.realpath;
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
hashes[identity] = hostInputStateHash(input, filesystem) ?? "missing";
|
|
1213
|
+
}
|
|
1214
|
+
return { complete, hashes, realpaths };
|
|
1215
|
+
}
|
|
1216
|
+
/** Verify every graph member still has the state read by the compiler. */
|
|
1217
|
+
function matchesCompilerGraphInputProofs(cached) {
|
|
1218
|
+
if (cached.result.type === "exception" ||
|
|
1219
|
+
cached.result.graph === undefined ||
|
|
1220
|
+
(cached.result.graph.inputHashes === undefined &&
|
|
1221
|
+
cached.result.graph.inputRealpaths === undefined)) {
|
|
1222
|
+
// Legacy sidecars remain compatible for ordinary in-project graphs. Their
|
|
1223
|
+
// out-of-walk members are still rejected by captureExternalInputSnapshot,
|
|
1224
|
+
// where a post-compile snapshot cannot prove the compiler's generation.
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
const state = envelopeDerivation(cached);
|
|
1228
|
+
const filesystem = resultFilesystem(cached.result);
|
|
1229
|
+
const graph = envelopeGraphIndexes(state, cached);
|
|
1230
|
+
if (graph.inputProofConflicts.size !== 0 ||
|
|
1231
|
+
graph.inputProofs.size !== graph.members.size) {
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
for (const identity of graph.members) {
|
|
1235
|
+
const proof = graph.inputProofs.get(identity);
|
|
1236
|
+
if (proof === undefined ||
|
|
1237
|
+
graphInputStateHash(proof.path, filesystem) !== proof.hash ||
|
|
1238
|
+
!sameHostInputRealpath(proof.realpath, hostInputRealpath(proof.path, filesystem), state.identityContext)) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
1244
|
+
/** Compare one derived input with the snapshot that owned it at generation. */
|
|
1245
|
+
function matchesRecordedInput(cached, input) {
|
|
1246
|
+
const state = envelopeDerivation(cached);
|
|
1247
|
+
const filesystem = resultFilesystem(cached.result);
|
|
1248
|
+
const projectKey = toProjectKey(cached.projectRoot, input, state.identityContext);
|
|
1249
|
+
const projectHash = Object.prototype.hasOwnProperty.call(cached.inputHashes, projectKey)
|
|
1250
|
+
? cached.inputHashes[projectKey]
|
|
1251
|
+
: undefined;
|
|
1252
|
+
const identity = derivationIdentity(state, input);
|
|
1253
|
+
const externalHash = (cached.externalInputHashes ?? {})[identity];
|
|
1254
|
+
const externalRealpaths = cached.externalInputRealpaths;
|
|
1255
|
+
const graphInput = externalRealpaths !== undefined &&
|
|
1256
|
+
Object.prototype.hasOwnProperty.call(externalRealpaths, identity);
|
|
1257
|
+
if (externalRealpaths !== undefined &&
|
|
1258
|
+
Object.prototype.hasOwnProperty.call(externalRealpaths, identity) &&
|
|
1259
|
+
!sameHostInputRealpath(externalRealpaths[identity], hostInputRealpath(input, filesystem), state.identityContext)) {
|
|
1260
|
+
return false;
|
|
1261
|
+
}
|
|
1262
|
+
// Prefer the out-of-walk spelling's own snapshot when it exists. A lexical
|
|
1263
|
+
// alias can point back into the walked project, where the physical target's
|
|
1264
|
+
// project hash is a different authority (and graph text uses BOM decoding).
|
|
1265
|
+
const recorded = externalHash ?? projectHash;
|
|
1266
|
+
if (recorded === undefined) {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
try {
|
|
1270
|
+
const current = graphInput
|
|
1271
|
+
? graphInputStateHash(input, filesystem)
|
|
1272
|
+
: hostInputStateHash(input, filesystem);
|
|
1273
|
+
return recorded === (current ?? "missing");
|
|
1274
|
+
}
|
|
1275
|
+
catch {
|
|
1276
|
+
return recorded === "missing";
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
/** Record a successfully selected module as delivered by this generation. */
|
|
1280
|
+
function markCachedSourceServed(cached, file) {
|
|
1281
|
+
(cached.servedFiles ??= new Set()).add(pathIdentityKey(file, envelopeDerivation(cached).identityContext));
|
|
700
1282
|
}
|
|
701
1283
|
/**
|
|
702
1284
|
* Hash every input file under `projectRoot` (the same walk universe
|
|
@@ -704,18 +1286,42 @@ function collectInputHashes(props) {
|
|
|
704
1286
|
* slash path. Exported so hosts without a per-build boundary (`@ttsc/metro`)
|
|
705
1287
|
* can fold the identical input universe into their own cache fingerprints.
|
|
706
1288
|
*/
|
|
707
|
-
function collectProjectInputHashes(projectRoot, identities = createHostPathIdentityContext()) {
|
|
1289
|
+
function collectProjectInputHashes(projectRoot, identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1290
|
+
return collectProjectInputSnapshot(projectRoot, identities, filesystem)
|
|
1291
|
+
.hashes;
|
|
1292
|
+
}
|
|
1293
|
+
/** Hash project files and snapshot the directory topology in one walk. */
|
|
1294
|
+
function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
708
1295
|
const hashes = {};
|
|
709
|
-
|
|
1296
|
+
const fileSignatures = {};
|
|
1297
|
+
const walked = walkProjectInputs(projectRoot, filesystem);
|
|
1298
|
+
let complete = walked.complete;
|
|
1299
|
+
for (const file of walked.files) {
|
|
710
1300
|
try {
|
|
711
|
-
|
|
1301
|
+
const before = inputMetadataSignature(file, filesystem);
|
|
1302
|
+
const contents = filesystem.readFile(file);
|
|
1303
|
+
const after = inputMetadataSignature(file, filesystem);
|
|
1304
|
+
const key = toProjectKey(projectRoot, file, identities);
|
|
1305
|
+
hashes[key] = hashText(contents);
|
|
1306
|
+
if (before === undefined || after === undefined || before !== after) {
|
|
1307
|
+
complete = false;
|
|
1308
|
+
}
|
|
1309
|
+
else {
|
|
1310
|
+
fileSignatures[key] = after;
|
|
1311
|
+
}
|
|
712
1312
|
}
|
|
713
1313
|
catch {
|
|
714
1314
|
// File watchers may observe a transform while another process is moving
|
|
715
1315
|
// or deleting files. The missing key invalidates older cache entries.
|
|
1316
|
+
complete = false;
|
|
716
1317
|
}
|
|
717
1318
|
}
|
|
718
|
-
return
|
|
1319
|
+
return {
|
|
1320
|
+
complete,
|
|
1321
|
+
fileSignatures,
|
|
1322
|
+
hashes,
|
|
1323
|
+
projectDirectories: walked.directories,
|
|
1324
|
+
};
|
|
719
1325
|
}
|
|
720
1326
|
/**
|
|
721
1327
|
* Enumerate every regular file under `root`, skipping well-known output and
|
|
@@ -725,18 +1331,39 @@ function collectProjectInputHashes(projectRoot, identities = createHostPathIdent
|
|
|
725
1331
|
* unbounded call-stack depth on deep project trees. The result is sorted so
|
|
726
1332
|
* that hash comparisons are deterministic across OS-level directory orderings.
|
|
727
1333
|
*/
|
|
728
|
-
function
|
|
729
|
-
|
|
1334
|
+
function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1335
|
+
let complete = true;
|
|
1336
|
+
const directories = [];
|
|
1337
|
+
const files = [];
|
|
730
1338
|
const stack = [root];
|
|
731
1339
|
while (stack.length !== 0) {
|
|
732
1340
|
const current = stack.pop();
|
|
1341
|
+
const before = projectDirectorySignature(current, filesystem);
|
|
1342
|
+
if (before === undefined) {
|
|
1343
|
+
complete = false;
|
|
1344
|
+
continue;
|
|
1345
|
+
}
|
|
733
1346
|
let entries;
|
|
734
1347
|
try {
|
|
735
|
-
entries =
|
|
1348
|
+
entries = filesystem.readdir(current);
|
|
736
1349
|
}
|
|
737
1350
|
catch {
|
|
1351
|
+
complete = false;
|
|
738
1352
|
continue;
|
|
739
1353
|
}
|
|
1354
|
+
const after = projectDirectorySignature(current, filesystem);
|
|
1355
|
+
if (after === undefined || before !== after) {
|
|
1356
|
+
complete = false;
|
|
1357
|
+
}
|
|
1358
|
+
directories.push({
|
|
1359
|
+
path: current,
|
|
1360
|
+
// If membership moved during enumeration, force the next delivery to
|
|
1361
|
+
// replace this generation instead of blessing a torn directory/file
|
|
1362
|
+
// snapshot as stable.
|
|
1363
|
+
signature: after !== undefined && before === after
|
|
1364
|
+
? after
|
|
1365
|
+
: `unstable:${before}:${after ?? "missing"}`,
|
|
1366
|
+
});
|
|
740
1367
|
for (const entry of entries) {
|
|
741
1368
|
if (isIgnoredProjectDirectory(entry.name)) {
|
|
742
1369
|
continue;
|
|
@@ -746,29 +1373,329 @@ function listProjectInputFiles(root) {
|
|
|
746
1373
|
stack.push(file);
|
|
747
1374
|
}
|
|
748
1375
|
else if (entry.isFile()) {
|
|
749
|
-
|
|
1376
|
+
files.push(file);
|
|
750
1377
|
}
|
|
751
1378
|
}
|
|
752
1379
|
}
|
|
753
|
-
|
|
754
|
-
|
|
1380
|
+
directories.sort((left, right) => left.path.localeCompare(right.path));
|
|
1381
|
+
files.sort();
|
|
1382
|
+
return { complete, directories, files };
|
|
1383
|
+
}
|
|
1384
|
+
/** Return a cheap identity for one directory's immediate membership. */
|
|
1385
|
+
function projectDirectorySignature(directory, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1386
|
+
try {
|
|
1387
|
+
const stats = filesystem.statBigInt(directory);
|
|
1388
|
+
if (!stats.isDirectory()) {
|
|
1389
|
+
return undefined;
|
|
1390
|
+
}
|
|
1391
|
+
return [
|
|
1392
|
+
stats.dev,
|
|
1393
|
+
stats.ino,
|
|
1394
|
+
stats.mode,
|
|
1395
|
+
stats.size,
|
|
1396
|
+
stats.mtimeNs,
|
|
1397
|
+
stats.ctimeNs,
|
|
1398
|
+
].join(":");
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
return undefined;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
/** Compare two deterministic project-directory membership snapshots. */
|
|
1405
|
+
function sameProjectDirectories(left, right) {
|
|
1406
|
+
return (left.length === right.length &&
|
|
1407
|
+
left.every((directory, index) => directory.path === right[index]?.path &&
|
|
1408
|
+
directory.signature === right[index]?.signature));
|
|
1409
|
+
}
|
|
1410
|
+
/** Watch every walked directory for membership changes after generation. */
|
|
1411
|
+
async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1412
|
+
const tracker = {
|
|
1413
|
+
close: () => undefined,
|
|
1414
|
+
failed: false,
|
|
1415
|
+
membershipChanged: false,
|
|
1416
|
+
};
|
|
1417
|
+
if (process.platform === "win32") {
|
|
1418
|
+
await registerWindowsProjectMutationTracker(tracker, directories.map((directory) => ({ directory: directory.path })), false, filesystem);
|
|
1419
|
+
return tracker;
|
|
1420
|
+
}
|
|
1421
|
+
const watchers = [];
|
|
1422
|
+
tracker.close = () => {
|
|
1423
|
+
for (const watcher of watchers)
|
|
1424
|
+
watcher.close();
|
|
1425
|
+
watchers.length = 0;
|
|
1426
|
+
};
|
|
1427
|
+
for (const directory of directories) {
|
|
1428
|
+
try {
|
|
1429
|
+
const watcher = fs.watch(directory.path, { persistent: false }, (eventType) => {
|
|
1430
|
+
if (eventType === "rename")
|
|
1431
|
+
tracker.membershipChanged = true;
|
|
1432
|
+
});
|
|
1433
|
+
watcher.on("error", () => {
|
|
1434
|
+
tracker.failed = true;
|
|
1435
|
+
});
|
|
1436
|
+
watchers.push(watcher);
|
|
1437
|
+
}
|
|
1438
|
+
catch {
|
|
1439
|
+
tracker.failed = true;
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
return tracker;
|
|
1443
|
+
}
|
|
1444
|
+
/** Watch exact universal inputs, or their nearest existing parent if missing. */
|
|
1445
|
+
async function createHostInputMutationTracker(inputs, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1446
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
1447
|
+
const namesByDirectory = new Map();
|
|
1448
|
+
for (const input of inputs) {
|
|
1449
|
+
const absolute = path.resolve(input);
|
|
1450
|
+
const probe = filesystem.exists(absolute)
|
|
1451
|
+
? { directory: path.dirname(absolute), name: path.basename(absolute) }
|
|
1452
|
+
: missingPathProbe(absolute, filesystem);
|
|
1453
|
+
const directoryIdentity = identities.resolve(probe.directory);
|
|
1454
|
+
let location = namesByDirectory.get(directoryIdentity.key);
|
|
1455
|
+
if (location === undefined) {
|
|
1456
|
+
location = {
|
|
1457
|
+
directory: directoryIdentity.path,
|
|
1458
|
+
names: new Set(),
|
|
1459
|
+
};
|
|
1460
|
+
namesByDirectory.set(directoryIdentity.key, location);
|
|
1461
|
+
}
|
|
1462
|
+
location.names.add(normalizeHostInputName(probe.name, identities.caseSensitive(directoryIdentity.path)));
|
|
1463
|
+
}
|
|
1464
|
+
const locations = [...namesByDirectory.values()].map((location) => ({
|
|
1465
|
+
directory: location.directory,
|
|
1466
|
+
names: [...location.names],
|
|
1467
|
+
}));
|
|
1468
|
+
const tracker = {
|
|
1469
|
+
close: () => undefined,
|
|
1470
|
+
failed: false,
|
|
1471
|
+
membershipChanged: false,
|
|
1472
|
+
};
|
|
1473
|
+
if (process.platform === "win32") {
|
|
1474
|
+
await registerWindowsProjectMutationTracker(tracker, locations, true, filesystem);
|
|
1475
|
+
return tracker;
|
|
1476
|
+
}
|
|
1477
|
+
const watchers = [];
|
|
1478
|
+
tracker.close = () => {
|
|
1479
|
+
for (const watcher of watchers)
|
|
1480
|
+
watcher.close();
|
|
1481
|
+
watchers.length = 0;
|
|
1482
|
+
};
|
|
1483
|
+
for (const location of locations) {
|
|
1484
|
+
try {
|
|
1485
|
+
const names = new Set(location.names);
|
|
1486
|
+
const caseSensitive = identities.caseSensitive(location.directory);
|
|
1487
|
+
const watcher = fs.watch(location.directory, { persistent: false }, (_eventType, filename) => {
|
|
1488
|
+
const reported = filename === null
|
|
1489
|
+
? null
|
|
1490
|
+
: normalizeHostInputName(String(filename), caseSensitive);
|
|
1491
|
+
if (reported === null || names.has(reported)) {
|
|
1492
|
+
tracker.membershipChanged = true;
|
|
1493
|
+
}
|
|
1494
|
+
});
|
|
1495
|
+
watcher.on("error", () => {
|
|
1496
|
+
tracker.failed = true;
|
|
1497
|
+
});
|
|
1498
|
+
watchers.push(watcher);
|
|
1499
|
+
}
|
|
1500
|
+
catch {
|
|
1501
|
+
tracker.failed = true;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return tracker;
|
|
1505
|
+
}
|
|
1506
|
+
let windowsProjectMutationBroker;
|
|
1507
|
+
/**
|
|
1508
|
+
* Register directory watches in an isolated Windows process.
|
|
1509
|
+
*
|
|
1510
|
+
* Node's Windows fs-event backend can assert in native code when a watched
|
|
1511
|
+
* temporary tree is deleted. Isolation turns that unrecoverable process abort
|
|
1512
|
+
* into an ordinary broker exit and a conservative cache miss in the host.
|
|
1513
|
+
*/
|
|
1514
|
+
async function registerWindowsProjectMutationTracker(tracker, locations, allEvents, filesystem) {
|
|
1515
|
+
const broker = getWindowsProjectMutationBroker();
|
|
1516
|
+
const normalized = locations.map((location) => {
|
|
1517
|
+
let directory;
|
|
1518
|
+
try {
|
|
1519
|
+
directory = filesystem.realpath(location.directory);
|
|
1520
|
+
}
|
|
1521
|
+
catch {
|
|
1522
|
+
directory = path.resolve(location.directory);
|
|
1523
|
+
}
|
|
1524
|
+
return {
|
|
1525
|
+
directory,
|
|
1526
|
+
...(location.names === undefined ? {} : { names: location.names }),
|
|
1527
|
+
};
|
|
1528
|
+
});
|
|
1529
|
+
broker.pendingRegistrations += 1;
|
|
1530
|
+
broker.child.ref();
|
|
1531
|
+
broker.child.channel?.ref();
|
|
1532
|
+
const id = broker.nextId++;
|
|
1533
|
+
let resolveReady;
|
|
1534
|
+
const ready = new Promise((resolve) => {
|
|
1535
|
+
resolveReady = resolve;
|
|
1536
|
+
});
|
|
1537
|
+
broker.trackers.set(id, { ready: resolveReady, tracker });
|
|
1538
|
+
tracker.close = () => {
|
|
1539
|
+
const active = broker.trackers.get(id);
|
|
1540
|
+
if (active === undefined)
|
|
1541
|
+
return;
|
|
1542
|
+
broker.trackers.delete(id);
|
|
1543
|
+
active.ready();
|
|
1544
|
+
broker.child.send?.({ id, op: "remove" });
|
|
1545
|
+
if (broker.trackers.size === 0) {
|
|
1546
|
+
broker.child.disconnect?.();
|
|
1547
|
+
broker.child.kill();
|
|
1548
|
+
if (windowsProjectMutationBroker === broker) {
|
|
1549
|
+
windowsProjectMutationBroker = undefined;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
broker.child.send?.({
|
|
1554
|
+
allEvents,
|
|
1555
|
+
locations: normalized,
|
|
1556
|
+
id,
|
|
1557
|
+
op: "add",
|
|
1558
|
+
});
|
|
1559
|
+
try {
|
|
1560
|
+
await ready;
|
|
1561
|
+
}
|
|
1562
|
+
finally {
|
|
1563
|
+
broker.pendingRegistrations -= 1;
|
|
1564
|
+
if (broker.pendingRegistrations === 0) {
|
|
1565
|
+
broker.child.unref();
|
|
1566
|
+
broker.child.channel?.unref();
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
function getWindowsProjectMutationBroker() {
|
|
1571
|
+
if (windowsProjectMutationBroker !== undefined) {
|
|
1572
|
+
return windowsProjectMutationBroker;
|
|
1573
|
+
}
|
|
1574
|
+
const child = spawn(process.execPath, ["-e", WINDOWS_WATCH_BROKER_SOURCE], {
|
|
1575
|
+
stdio: ["ignore", "ignore", "ignore", "ipc"],
|
|
1576
|
+
windowsHide: true,
|
|
1577
|
+
});
|
|
1578
|
+
const broker = {
|
|
1579
|
+
child,
|
|
1580
|
+
nextId: 1,
|
|
1581
|
+
pendingRegistrations: 0,
|
|
1582
|
+
trackers: new Map(),
|
|
1583
|
+
};
|
|
1584
|
+
const fail = () => {
|
|
1585
|
+
for (const registration of broker.trackers.values()) {
|
|
1586
|
+
registration.tracker.failed = true;
|
|
1587
|
+
registration.ready();
|
|
1588
|
+
}
|
|
1589
|
+
broker.trackers.clear();
|
|
1590
|
+
if (windowsProjectMutationBroker === broker) {
|
|
1591
|
+
windowsProjectMutationBroker = undefined;
|
|
1592
|
+
}
|
|
1593
|
+
};
|
|
1594
|
+
child.on("error", fail);
|
|
1595
|
+
child.on("exit", fail);
|
|
1596
|
+
child.on("message", (message) => {
|
|
1597
|
+
if (message === null || typeof message !== "object")
|
|
1598
|
+
return;
|
|
1599
|
+
const record = message;
|
|
1600
|
+
if (typeof record.id !== "number")
|
|
1601
|
+
return;
|
|
1602
|
+
const registration = broker.trackers.get(record.id);
|
|
1603
|
+
if (registration === undefined)
|
|
1604
|
+
return;
|
|
1605
|
+
if (record.failed === true)
|
|
1606
|
+
registration.tracker.failed = true;
|
|
1607
|
+
if (record.ready === true)
|
|
1608
|
+
registration.ready();
|
|
1609
|
+
if (record.ready !== true && record.failed !== true) {
|
|
1610
|
+
registration.tracker.membershipChanged = true;
|
|
1611
|
+
}
|
|
1612
|
+
});
|
|
1613
|
+
windowsProjectMutationBroker = broker;
|
|
1614
|
+
return broker;
|
|
1615
|
+
}
|
|
1616
|
+
const WINDOWS_WATCH_BROKER_SOURCE = [
|
|
1617
|
+
'const fs = require("node:fs");',
|
|
1618
|
+
"const groups = new Map();",
|
|
1619
|
+
'process.on("message", (message) => {',
|
|
1620
|
+
' if (message.op === "remove") {',
|
|
1621
|
+
" close(message.id);",
|
|
1622
|
+
" return;",
|
|
1623
|
+
" }",
|
|
1624
|
+
' if (message.op !== "add") return;',
|
|
1625
|
+
" const watchers = [];",
|
|
1626
|
+
" let failed = false;",
|
|
1627
|
+
" for (const location of message.locations) {",
|
|
1628
|
+
" try {",
|
|
1629
|
+
" const names = location.names === undefined ? undefined : new Set(location.names.map((name) => name.toLowerCase()));",
|
|
1630
|
+
" const watcher = fs.watch(location.directory, { persistent: false }, (event, filename) => {",
|
|
1631
|
+
" const matches = names === undefined || filename === null || names.has(String(filename).toLowerCase());",
|
|
1632
|
+
' if (matches && (message.allEvents || event === "rename")) process.send?.({ id: message.id });',
|
|
1633
|
+
" });",
|
|
1634
|
+
' watcher.on("error", () => process.send?.({ failed: true, id: message.id }));',
|
|
1635
|
+
" watchers.push(watcher);",
|
|
1636
|
+
" } catch {",
|
|
1637
|
+
" failed = true;",
|
|
1638
|
+
" }",
|
|
1639
|
+
" }",
|
|
1640
|
+
" groups.set(message.id, watchers);",
|
|
1641
|
+
" process.send?.({ failed, id: message.id, ready: true });",
|
|
1642
|
+
"});",
|
|
1643
|
+
'process.on("disconnect", () => {',
|
|
1644
|
+
" for (const id of groups.keys()) close(id);",
|
|
1645
|
+
" process.exit(0);",
|
|
1646
|
+
"});",
|
|
1647
|
+
"function close(id) {",
|
|
1648
|
+
" for (const watcher of groups.get(id) ?? []) watcher.close();",
|
|
1649
|
+
" groups.delete(id);",
|
|
1650
|
+
"}",
|
|
1651
|
+
].join("\n");
|
|
1652
|
+
/** Report whether live directory notifications preserve project membership. */
|
|
1653
|
+
function matchesProjectMembership(cached) {
|
|
1654
|
+
const tracker = cached.projectMutationTracker;
|
|
1655
|
+
return (tracker !== undefined &&
|
|
1656
|
+
tracker.failed === false &&
|
|
1657
|
+
tracker.membershipChanged === false);
|
|
1658
|
+
}
|
|
1659
|
+
/**
|
|
1660
|
+
* Yield once before persistent validation so synchronous edits can reach the
|
|
1661
|
+
* directory watchers that guard membership. Concurrent sibling deliveries share
|
|
1662
|
+
* the same barrier.
|
|
1663
|
+
*/
|
|
1664
|
+
async function settleProjectMutationEvents(cached) {
|
|
1665
|
+
const trackers = [
|
|
1666
|
+
cached.projectMutationTracker,
|
|
1667
|
+
cached.hostInputMutationTracker,
|
|
1668
|
+
].filter((tracker) => tracker !== undefined);
|
|
1669
|
+
await Promise.all(trackers.map(async (tracker) => {
|
|
1670
|
+
tracker.settle ??= new Promise((resolve) => {
|
|
1671
|
+
const settled = () => {
|
|
1672
|
+
tracker.settle = undefined;
|
|
1673
|
+
resolve();
|
|
1674
|
+
};
|
|
1675
|
+
if (process.platform === "win32")
|
|
1676
|
+
setTimeout(settled, 10);
|
|
1677
|
+
else
|
|
1678
|
+
setImmediate(settled);
|
|
1679
|
+
});
|
|
1680
|
+
await tracker.settle;
|
|
1681
|
+
}));
|
|
755
1682
|
}
|
|
756
1683
|
/**
|
|
757
1684
|
* Report whether an absolute `file` belongs to the project walk universe of
|
|
758
1685
|
* `root`: it lies under `root`, every component exists without traversing a
|
|
759
1686
|
* symbolic link, the leaf is a regular file, and no segment of the relative
|
|
760
|
-
* path is ignored. The predicate mirrors {@link
|
|
761
|
-
*
|
|
1687
|
+
* path is ignored. The predicate mirrors {@link walkProjectInputs} exactly, so
|
|
1688
|
+
* "walk-visible" here means "hashed by {@link collectProjectInputHashes}".
|
|
762
1689
|
* Missing paths and files reached through symlinks or Windows junctions are
|
|
763
1690
|
* out-of-walk inputs that only the reference graph can prove relevant.
|
|
764
1691
|
*/
|
|
765
|
-
function isProjectWalkPath(root, file,
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
const
|
|
771
|
-
const relative =
|
|
1692
|
+
function isProjectWalkPath(root, file, _identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1693
|
+
// Walk membership is lexical. Resolving `file` to physical identity first
|
|
1694
|
+
// would turn `root/alias/value.ts` into `root/target/value.ts`, hide the
|
|
1695
|
+
// symlink segment from the lstat loop below, and falsely claim the project
|
|
1696
|
+
// walk hashed a path it deliberately never followed.
|
|
1697
|
+
const resolvedRoot = path.resolve(root);
|
|
1698
|
+
const relative = path.relative(resolvedRoot, path.resolve(file));
|
|
772
1699
|
if (relative.length === 0 ||
|
|
773
1700
|
relative === ".." ||
|
|
774
1701
|
relative.startsWith(`..${path.sep}`) ||
|
|
@@ -779,12 +1706,12 @@ function isProjectWalkPath(root, file, identities = createHostPathIdentityContex
|
|
|
779
1706
|
if (segments.some(isIgnoredProjectDirectory)) {
|
|
780
1707
|
return false;
|
|
781
1708
|
}
|
|
782
|
-
let current =
|
|
1709
|
+
let current = resolvedRoot;
|
|
783
1710
|
for (let index = 0; index < segments.length; ++index) {
|
|
784
1711
|
current = path.join(current, segments[index]);
|
|
785
1712
|
let stats;
|
|
786
1713
|
try {
|
|
787
|
-
stats =
|
|
1714
|
+
stats = filesystem.lstat(current);
|
|
788
1715
|
}
|
|
789
1716
|
catch {
|
|
790
1717
|
return false;
|
|
@@ -801,27 +1728,41 @@ function isProjectWalkPath(root, file, identities = createHostPathIdentityContex
|
|
|
801
1728
|
}
|
|
802
1729
|
/**
|
|
803
1730
|
* Hash a list of absolute out-of-walk input paths: content SHA-256 for a
|
|
804
|
-
* readable file, a stable
|
|
805
|
-
*
|
|
806
|
-
*
|
|
807
|
-
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
1731
|
+
* readable file, a stable directory-kind digest for a directory candidate, and
|
|
1732
|
+
* a stable `missing` marker otherwise. Keys use filesystem identity so
|
|
1733
|
+
* case-only spellings share one snapshot entry, while reads retain the original
|
|
1734
|
+
* path supplied by the compiler. The marker is state, not an error — a recorded
|
|
1735
|
+
* input disappearing (or reappearing) must change the comparison exactly like a
|
|
1736
|
+
* content edit. Exported so `@ttsc/metro` can re-hash its recorded snapshot
|
|
1737
|
+
* with identical semantics at cache-key time.
|
|
810
1738
|
*/
|
|
811
|
-
function collectExternalInputHashes(paths) {
|
|
1739
|
+
function collectExternalInputHashes(paths, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
812
1740
|
const hashes = {};
|
|
813
|
-
const identities = createHostPathIdentityContext();
|
|
1741
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
814
1742
|
for (const file of paths) {
|
|
815
1743
|
const identity = pathIdentityKey(file, identities);
|
|
816
1744
|
if (identity in hashes) {
|
|
817
1745
|
continue;
|
|
818
1746
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1747
|
+
hashes[identity] = hostInputStateHash(file, filesystem) ?? "missing";
|
|
1748
|
+
}
|
|
1749
|
+
return hashes;
|
|
1750
|
+
}
|
|
1751
|
+
/** Re-hash a cached mixed graph/dependency input set with its owning codec. */
|
|
1752
|
+
function collectCachedExternalInputHashes(cached) {
|
|
1753
|
+
const hashes = {};
|
|
1754
|
+
const state = envelopeDerivation(cached);
|
|
1755
|
+
const graphRealpaths = cached.externalInputRealpaths ?? {};
|
|
1756
|
+
const filesystem = resultFilesystem(cached.result);
|
|
1757
|
+
for (const file of cached.externalInputPaths ??
|
|
1758
|
+
Object.keys(cached.externalInputHashes ?? {})) {
|
|
1759
|
+
const identity = derivationIdentity(state, file);
|
|
1760
|
+
if (identity in hashes)
|
|
1761
|
+
continue;
|
|
1762
|
+
hashes[identity] =
|
|
1763
|
+
(Object.prototype.hasOwnProperty.call(graphRealpaths, identity)
|
|
1764
|
+
? graphInputStateHash(file, filesystem)
|
|
1765
|
+
: hostInputStateHash(file, filesystem)) ?? "missing";
|
|
825
1766
|
}
|
|
826
1767
|
return hashes;
|
|
827
1768
|
}
|
|
@@ -834,21 +1775,19 @@ function collectExternalInputHashes(paths) {
|
|
|
834
1775
|
* that are still missing remain in this set even under the project root: the
|
|
835
1776
|
* first walk cannot hash a file that has not been created yet.
|
|
836
1777
|
*
|
|
837
|
-
* A `dependenciesComplete` declaration deliberately does not narrow
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
841
|
-
*
|
|
842
|
-
* is how a widened declaration is ever learned. The narrowing that matters
|
|
843
|
-
* lands at the bundler boundary through {@link selectWatchInputs}, which is what
|
|
844
|
-
* feeds persistent caches and watch graphs.
|
|
1778
|
+
* A `dependenciesComplete` declaration deliberately does not narrow the stored
|
|
1779
|
+
* set: other files in the same whole-project result can still own the omitted
|
|
1780
|
+
* members. Persistent validation selects the requested file's subset through
|
|
1781
|
+
* {@link selectWatchInputs}, while graph-free envelopes use this union as their
|
|
1782
|
+
* conservative fallback.
|
|
845
1783
|
*/
|
|
846
1784
|
function selectExternalInputPaths(props) {
|
|
847
1785
|
if (props.result.type === "exception") {
|
|
848
1786
|
return [];
|
|
849
1787
|
}
|
|
850
1788
|
const members = [];
|
|
851
|
-
const
|
|
1789
|
+
const filesystem = props.filesystem ?? DEFAULT_FILESYSTEM_OPERATIONS;
|
|
1790
|
+
const identities = createHostPathIdentityContext(filesystem);
|
|
852
1791
|
const resolutionCandidates = new Set();
|
|
853
1792
|
const graph = props.result.graph;
|
|
854
1793
|
if (graph !== undefined) {
|
|
@@ -882,6 +1821,17 @@ function selectExternalInputPaths(props) {
|
|
|
882
1821
|
members.push(...entries);
|
|
883
1822
|
}
|
|
884
1823
|
}
|
|
1824
|
+
if (Array.isArray(props.result.hostInputs)) {
|
|
1825
|
+
for (const input of props.result.hostInputs) {
|
|
1826
|
+
members.push(input);
|
|
1827
|
+
if (typeof input === "string" && input.length !== 0) {
|
|
1828
|
+
// Plugin discovery inputs deliberately include absent config and
|
|
1829
|
+
// resolution probes. A project walk cannot snapshot a path that does
|
|
1830
|
+
// not exist yet, even when its spelling lies below projectRoot.
|
|
1831
|
+
resolutionCandidates.add(pathIdentityKey(path.resolve(props.projectRoot, input), identities));
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
}
|
|
885
1835
|
const excluded = props.temporaryTsconfig === undefined
|
|
886
1836
|
? undefined
|
|
887
1837
|
: pathIdentityKey(props.temporaryTsconfig, identities);
|
|
@@ -892,15 +1842,18 @@ function selectExternalInputPaths(props) {
|
|
|
892
1842
|
continue;
|
|
893
1843
|
}
|
|
894
1844
|
const absolute = path.resolve(props.projectRoot, member);
|
|
1845
|
+
const spelling = path.resolve(absolute);
|
|
895
1846
|
const identity = pathIdentityKey(absolute, identities);
|
|
896
|
-
const missingCandidate = resolutionCandidates.has(identity) && !
|
|
1847
|
+
const missingCandidate = resolutionCandidates.has(identity) && !filesystem.exists(absolute);
|
|
897
1848
|
if (identity === excluded ||
|
|
898
|
-
seen.has(
|
|
1849
|
+
seen.has(spelling) ||
|
|
899
1850
|
(!missingCandidate &&
|
|
900
|
-
isProjectWalkPath(props.projectRoot, absolute, identities))) {
|
|
1851
|
+
isProjectWalkPath(props.projectRoot, absolute, identities, filesystem))) {
|
|
901
1852
|
continue;
|
|
902
1853
|
}
|
|
903
|
-
|
|
1854
|
+
// Preserve distinct lexical aliases even when they currently select the
|
|
1855
|
+
// same physical file. A later retarget must validate the alias itself.
|
|
1856
|
+
seen.add(spelling);
|
|
904
1857
|
output.push(absolute);
|
|
905
1858
|
}
|
|
906
1859
|
output.sort();
|
|
@@ -935,12 +1888,22 @@ function hashText(input) {
|
|
|
935
1888
|
return crypto.createHash("sha256").update(input).digest("hex");
|
|
936
1889
|
}
|
|
937
1890
|
async function transformProject(props) {
|
|
938
|
-
const configured = createTransformTsconfig(props);
|
|
939
1891
|
const projectRoot = path.dirname(props.tsconfig);
|
|
1892
|
+
const scratchDirectory = createTransformScratchDirectory(projectRoot, props.filesystem);
|
|
1893
|
+
let tracker;
|
|
1894
|
+
let retainTracker = false;
|
|
1895
|
+
let hostInputTracker;
|
|
940
1896
|
try {
|
|
941
|
-
const
|
|
1897
|
+
const configured = createTransformTsconfig(props, scratchDirectory);
|
|
1898
|
+
const temporaryTsconfig = configured.path === props.tsconfig ? undefined : configured.path;
|
|
1899
|
+
const identities = createHostPathIdentityContext(props.filesystem);
|
|
1900
|
+
const before = collectProjectInputSnapshot(projectRoot, identities, props.filesystem);
|
|
1901
|
+
tracker = props.trackProjectMembership
|
|
1902
|
+
? await createProjectMutationTracker(before.projectDirectories, props.filesystem)
|
|
1903
|
+
: undefined;
|
|
1904
|
+
const result = withTransformScratchEnvironment(scratchDirectory, () => new TtscCompiler({
|
|
942
1905
|
cwd: projectRoot,
|
|
943
|
-
// The generated tsconfig (if any) lives
|
|
1906
|
+
// The generated tsconfig (if any) lives outside the project directory,
|
|
944
1907
|
// so declare the real project as the plugin config anchor: utility
|
|
945
1908
|
// plugin config discovery (banner.config.*, strip.config.*,
|
|
946
1909
|
// lint.config.*) and relative configFile resolution walk the project,
|
|
@@ -950,24 +1913,47 @@ async function transformProject(props) {
|
|
|
950
1913
|
plugins: props.plugins,
|
|
951
1914
|
projectRoot,
|
|
952
1915
|
tsconfig: configured.path,
|
|
953
|
-
|
|
954
|
-
|
|
1916
|
+
env: transformScratchEnvironment(scratchDirectory),
|
|
1917
|
+
}).transform());
|
|
1918
|
+
TRANSFORM_RESULT_FILESYSTEM.set(result, props.filesystem);
|
|
1919
|
+
const persistentHostInputs = selectPersistentHostInputs({
|
|
1920
|
+
filesystem: props.filesystem,
|
|
1921
|
+
projectRoot,
|
|
1922
|
+
result,
|
|
1923
|
+
temporaryTsconfig,
|
|
1924
|
+
});
|
|
1925
|
+
hostInputTracker = props.trackProjectMembership
|
|
1926
|
+
? await createHostInputMutationTracker(persistentHostInputs, props.filesystem)
|
|
1927
|
+
: undefined;
|
|
955
1928
|
const externalInputPaths = selectExternalInputPaths({
|
|
1929
|
+
filesystem: props.filesystem,
|
|
956
1930
|
projectRoot,
|
|
957
1931
|
result,
|
|
958
1932
|
temporaryTsconfig,
|
|
959
1933
|
});
|
|
960
|
-
|
|
1934
|
+
const inputSnapshot = collectProjectInputSnapshot(projectRoot, identities, props.filesystem);
|
|
1935
|
+
let stableProjectSnapshot = before.complete &&
|
|
1936
|
+
inputSnapshot.complete &&
|
|
1937
|
+
sameHashes(before.hashes, inputSnapshot.hashes) &&
|
|
1938
|
+
sameHashes(before.fileSignatures, inputSnapshot.fileSignatures) &&
|
|
1939
|
+
sameProjectDirectories(before.projectDirectories, inputSnapshot.projectDirectories) &&
|
|
1940
|
+
tracker?.failed !== true &&
|
|
1941
|
+
tracker?.membershipChanged !== true &&
|
|
1942
|
+
hostInputTracker?.failed !== true &&
|
|
1943
|
+
hostInputTracker?.membershipChanged !== true;
|
|
1944
|
+
// Overlay the in-memory source only after proving the two on-disk snapshots
|
|
1945
|
+
// stable; an unsaved editor buffer must not look like a compile-time race.
|
|
1946
|
+
inputSnapshot.hashes[toProjectKey(projectRoot, props.currentFile, identities)] = hashText(props.currentSource);
|
|
1947
|
+
const cached = {
|
|
961
1948
|
// Capture the out-of-walk input hashes while the generation is fresh so
|
|
962
1949
|
// cache validation can re-check them; computed before dispose so the
|
|
963
1950
|
// exclusion of the temp-dir tsconfig is the only reason it never keys.
|
|
964
|
-
externalInputHashes:
|
|
1951
|
+
externalInputHashes: {},
|
|
1952
|
+
externalInputRealpaths: {},
|
|
965
1953
|
externalInputPaths,
|
|
966
|
-
inputHashes:
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
projectRoot,
|
|
970
|
-
}),
|
|
1954
|
+
inputHashes: inputSnapshot.hashes,
|
|
1955
|
+
projectDirectories: inputSnapshot.projectDirectories,
|
|
1956
|
+
projectSnapshotComplete: false,
|
|
971
1957
|
projectRoot,
|
|
972
1958
|
result,
|
|
973
1959
|
servedFiles: new Set(),
|
|
@@ -976,40 +1962,186 @@ async function transformProject(props) {
|
|
|
976
1962
|
// but deleted file would invalidate every persistent-cache snapshot.
|
|
977
1963
|
...(temporaryTsconfig === undefined ? {} : { temporaryTsconfig }),
|
|
978
1964
|
};
|
|
1965
|
+
const externalInputSnapshot = captureExternalInputSnapshot(cached, externalInputPaths);
|
|
1966
|
+
cached.externalInputHashes = externalInputSnapshot.hashes;
|
|
1967
|
+
cached.externalInputRealpaths = externalInputSnapshot.realpaths;
|
|
1968
|
+
stableProjectSnapshot =
|
|
1969
|
+
stableProjectSnapshot &&
|
|
1970
|
+
matchesCompilerGraphInputProofs(cached) &&
|
|
1971
|
+
externalInputSnapshot.complete &&
|
|
1972
|
+
captureUniversalHostInputValidation(cached, props.currentFile) !==
|
|
1973
|
+
undefined;
|
|
1974
|
+
cached.projectSnapshotComplete = stableProjectSnapshot;
|
|
1975
|
+
if (stableProjectSnapshot && tracker !== undefined) {
|
|
1976
|
+
cached.projectMutationTracker = tracker;
|
|
1977
|
+
}
|
|
1978
|
+
if (stableProjectSnapshot && hostInputTracker !== undefined) {
|
|
1979
|
+
cached.hostInputMutationTracker = hostInputTracker;
|
|
1980
|
+
}
|
|
1981
|
+
retainTracker =
|
|
1982
|
+
stableProjectSnapshot &&
|
|
1983
|
+
tracker !== undefined &&
|
|
1984
|
+
hostInputTracker !== undefined;
|
|
1985
|
+
return cached;
|
|
979
1986
|
}
|
|
980
1987
|
finally {
|
|
981
|
-
|
|
1988
|
+
try {
|
|
1989
|
+
if (!retainTracker && tracker !== undefined) {
|
|
1990
|
+
tracker.close();
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
finally {
|
|
1994
|
+
try {
|
|
1995
|
+
if (!retainTracker && hostInputTracker !== undefined) {
|
|
1996
|
+
hostInputTracker.close();
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
finally {
|
|
2000
|
+
fs.rmSync(scratchDirectory, { force: true, recursive: true });
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
982
2003
|
}
|
|
983
2004
|
}
|
|
984
|
-
|
|
2005
|
+
/** Exclude the disposed overlay tsconfig from live host-input tracking. */
|
|
2006
|
+
function selectPersistentHostInputs(props) {
|
|
2007
|
+
if (props.result.type === "exception")
|
|
2008
|
+
return [];
|
|
2009
|
+
const inputs = selectListedFiles(props.projectRoot, props.result.hostInputs);
|
|
2010
|
+
if (props.temporaryTsconfig === undefined)
|
|
2011
|
+
return inputs;
|
|
2012
|
+
const identities = createHostPathIdentityContext(props.filesystem);
|
|
2013
|
+
const temporary = pathIdentityKey(props.temporaryTsconfig, identities);
|
|
2014
|
+
return inputs.filter((input) => pathIdentityKey(input, identities) !== temporary);
|
|
2015
|
+
}
|
|
2016
|
+
function createTransformTsconfig(props, scratchDirectory) {
|
|
985
2017
|
const compilerOptions = normalizeCompilerOptionsForGeneratedTsconfig({
|
|
986
2018
|
...props.compilerOptions,
|
|
987
2019
|
...createAliasCompilerOptions(props),
|
|
988
2020
|
}, path.dirname(props.tsconfig));
|
|
989
2021
|
if (Object.keys(compilerOptions).length === 0) {
|
|
990
|
-
return {
|
|
991
|
-
path: props.tsconfig,
|
|
992
|
-
dispose: () => undefined,
|
|
993
|
-
};
|
|
2022
|
+
return { path: props.tsconfig };
|
|
994
2023
|
}
|
|
995
|
-
const
|
|
996
|
-
const file = path.join(directory, "tsconfig.json");
|
|
2024
|
+
const file = path.join(scratchDirectory, "tsconfig.json");
|
|
997
2025
|
fs.writeFileSync(file, JSON.stringify({
|
|
998
2026
|
extends: normalizePath(props.tsconfig),
|
|
999
2027
|
compilerOptions,
|
|
1000
2028
|
}, null, 2), "utf8");
|
|
2029
|
+
return { path: file };
|
|
2030
|
+
}
|
|
2031
|
+
/** Create compiler scratch storage outside the project snapshot and watchers. */
|
|
2032
|
+
function createTransformScratchDirectory(projectRoot, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
2033
|
+
const root = path.resolve(projectRoot);
|
|
2034
|
+
const canonicalRoot = filesystem.realpath(root);
|
|
2035
|
+
const platformTemp = process.platform === "win32" && process.env.LOCALAPPDATA
|
|
2036
|
+
? path.join(process.env.LOCALAPPDATA, "Temp")
|
|
2037
|
+
: "/tmp";
|
|
2038
|
+
const candidates = [
|
|
2039
|
+
os.tmpdir(),
|
|
2040
|
+
platformTemp,
|
|
2041
|
+
path.dirname(root),
|
|
2042
|
+
os.homedir(),
|
|
2043
|
+
];
|
|
2044
|
+
const canonicalCandidates = new Set();
|
|
2045
|
+
let failure;
|
|
2046
|
+
for (const candidate of new Set(candidates.map((dir) => path.resolve(dir)))) {
|
|
2047
|
+
if (pathIsWithin(candidate, root))
|
|
2048
|
+
continue;
|
|
2049
|
+
let canonicalCandidate;
|
|
2050
|
+
try {
|
|
2051
|
+
canonicalCandidate = filesystem.realpath(candidate);
|
|
2052
|
+
}
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
failure = error;
|
|
2055
|
+
continue;
|
|
2056
|
+
}
|
|
2057
|
+
if (pathIsWithin(canonicalCandidate, canonicalRoot) ||
|
|
2058
|
+
canonicalCandidates.has(canonicalCandidate)) {
|
|
2059
|
+
continue;
|
|
2060
|
+
}
|
|
2061
|
+
canonicalCandidates.add(canonicalCandidate);
|
|
2062
|
+
let directory;
|
|
2063
|
+
try {
|
|
2064
|
+
directory = fs.mkdtempSync(path.join(canonicalCandidate, "ttsc-unplugin-"));
|
|
2065
|
+
}
|
|
2066
|
+
catch (error) {
|
|
2067
|
+
failure = error;
|
|
2068
|
+
continue;
|
|
2069
|
+
}
|
|
2070
|
+
let canonicalDirectory;
|
|
2071
|
+
try {
|
|
2072
|
+
canonicalDirectory = filesystem.realpath(directory);
|
|
2073
|
+
}
|
|
2074
|
+
catch (error) {
|
|
2075
|
+
try {
|
|
2076
|
+
fs.rmdirSync(directory);
|
|
2077
|
+
}
|
|
2078
|
+
catch (cleanupError) {
|
|
2079
|
+
throw cleanupError;
|
|
2080
|
+
}
|
|
2081
|
+
failure = error;
|
|
2082
|
+
continue;
|
|
2083
|
+
}
|
|
2084
|
+
// Use the postflight canonical spelling from this point onward. Returning
|
|
2085
|
+
// the candidate-relative spelling would let another process retarget its
|
|
2086
|
+
// parent symlink/junction after validation, redirecting compiler writes or
|
|
2087
|
+
// the final recursive removal into the project.
|
|
2088
|
+
if (!pathIsWithin(canonicalDirectory, canonicalRoot)) {
|
|
2089
|
+
return canonicalDirectory;
|
|
2090
|
+
}
|
|
2091
|
+
// Refuse the result and synchronously remove only our empty random child
|
|
2092
|
+
// through the identity that the postflight check just classified.
|
|
2093
|
+
fs.rmdirSync(canonicalDirectory);
|
|
2094
|
+
}
|
|
2095
|
+
throw (failure ??
|
|
2096
|
+
new Error("ttsc: no temporary directory exists outside the project"));
|
|
2097
|
+
}
|
|
2098
|
+
function pathIsWithin(child, parent) {
|
|
2099
|
+
const relative = path.relative(parent, child);
|
|
2100
|
+
return (relative === "" ||
|
|
2101
|
+
(relative !== ".." &&
|
|
2102
|
+
!relative.startsWith(`..${path.sep}`) &&
|
|
2103
|
+
!path.isAbsolute(relative)));
|
|
2104
|
+
}
|
|
2105
|
+
/** Route all compiler/plugin scratch to one owned directory outside project. */
|
|
2106
|
+
function transformScratchEnvironment(directory) {
|
|
1001
2107
|
return {
|
|
1002
|
-
|
|
1003
|
-
|
|
2108
|
+
...process.env,
|
|
2109
|
+
TEMP: directory,
|
|
2110
|
+
TMP: directory,
|
|
2111
|
+
TMPDIR: directory,
|
|
1004
2112
|
};
|
|
1005
2113
|
}
|
|
2114
|
+
/** Scope parent-process temp consumers to the same owned scratch directory. */
|
|
2115
|
+
function withTransformScratchEnvironment(scratchDirectory, callback) {
|
|
2116
|
+
const environment = transformScratchEnvironment(scratchDirectory);
|
|
2117
|
+
const previous = {
|
|
2118
|
+
TEMP: process.env.TEMP,
|
|
2119
|
+
TMP: process.env.TMP,
|
|
2120
|
+
TMPDIR: process.env.TMPDIR,
|
|
2121
|
+
};
|
|
2122
|
+
process.env.TEMP = environment.TEMP;
|
|
2123
|
+
process.env.TMP = environment.TMP;
|
|
2124
|
+
process.env.TMPDIR = environment.TMPDIR;
|
|
2125
|
+
try {
|
|
2126
|
+
return callback();
|
|
2127
|
+
}
|
|
2128
|
+
finally {
|
|
2129
|
+
for (const [name, value] of Object.entries(previous)) {
|
|
2130
|
+
if (value === undefined)
|
|
2131
|
+
delete process.env[name];
|
|
2132
|
+
else
|
|
2133
|
+
process.env[name] = value;
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
1006
2137
|
/**
|
|
1007
2138
|
* Resolve all relative paths inside `compilerOptions` against `tsconfigDir`.
|
|
1008
2139
|
*
|
|
1009
|
-
* The generated tsconfig lives in a
|
|
1010
|
-
* (e.g. `"outDir": "../dist"`) that was meaningful relative
|
|
1011
|
-
* tsconfig must be converted to an absolute path before writing
|
|
1012
|
-
* file. Otherwise TypeScript-Go resolves it against the temp
|
|
2140
|
+
* The generated tsconfig lives in a temporary directory outside the project, so
|
|
2141
|
+
* any relative path (e.g. `"outDir": "../dist"`) that was meaningful relative
|
|
2142
|
+
* to the original tsconfig must be converted to an absolute path before writing
|
|
2143
|
+
* the generated file. Otherwise TypeScript-Go resolves it against the temp
|
|
2144
|
+
* dir.
|
|
1013
2145
|
*
|
|
1014
2146
|
* `paths` targets are absolutized for the same reason, with the extra twist
|
|
1015
2147
|
* that TypeScript-Go rejects bare non-relative targets outright (TS5090) and
|
|
@@ -1282,7 +2414,7 @@ function formatUnknownError(error) {
|
|
|
1282
2414
|
* compiler will error if that file does not exist, which is the correct
|
|
1283
2415
|
* behavior for a mis-configured project.
|
|
1284
2416
|
*/
|
|
1285
|
-
function resolveTsconfig(file, tsconfig) {
|
|
2417
|
+
function resolveTsconfig(file, tsconfig, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1286
2418
|
if (tsconfig !== undefined) {
|
|
1287
2419
|
return path.isAbsolute(tsconfig)
|
|
1288
2420
|
? tsconfig
|
|
@@ -1291,7 +2423,7 @@ function resolveTsconfig(file, tsconfig) {
|
|
|
1291
2423
|
let current = path.dirname(file);
|
|
1292
2424
|
while (true) {
|
|
1293
2425
|
const candidate = path.join(current, "tsconfig.json");
|
|
1294
|
-
if (
|
|
2426
|
+
if (filesystem.exists(candidate)) {
|
|
1295
2427
|
return candidate;
|
|
1296
2428
|
}
|
|
1297
2429
|
const parent = path.dirname(current);
|
|
@@ -1323,5 +2455,5 @@ function normalizePath(file) {
|
|
|
1323
2455
|
return file.replace(/\\/g, "/");
|
|
1324
2456
|
}
|
|
1325
2457
|
|
|
1326
|
-
export { beginTtscTransformBuild, collectExternalInputHashes, collectProjectInputHashes, createTransformResult, createTtscTransformCache, isDeclarationFile, isProjectWalkPath, pathIdentityKey, resetTtscTransformCache, stripQuery, transformTtsc };
|
|
2458
|
+
export { beginTtscTransformBuild, collectExternalInputHashes, collectProjectInputHashes, createTransformResult, createTtscTransformCache, isDeclarationFile, isProjectWalkPath, normalizeHostInputName, pathIdentityKey, resetTtscTransformCache, stripQuery, transformTtsc };
|
|
1327
2459
|
//# sourceMappingURL=transform.mjs.map
|