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