@ttsc/unplugin 0.28.3 → 0.28.5
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 +70 -8
- package/lib/api.js +3 -0
- package/lib/api.js.map +1 -1
- package/lib/api.mjs +1 -0
- package/lib/api.mjs.map +1 -1
- package/lib/core/index.d.cts +17 -6
- package/lib/core/index.d.mts +17 -6
- package/lib/core/index.d.ts +17 -6
- package/lib/core/index.js +117 -21
- package/lib/core/index.js.map +1 -1
- package/lib/core/index.mjs +115 -21
- package/lib/core/index.mjs.map +1 -1
- package/lib/core/transform.d.cts +93 -25
- package/lib/core/transform.d.mts +93 -25
- package/lib/core/transform.d.ts +93 -25
- package/lib/core/transform.js +803 -121
- package/lib/core/transform.js.map +1 -1
- package/lib/core/transform.mjs +804 -122
- package/lib/core/transform.mjs.map +1 -1
- package/lib/core/tsconfigPaths.d.cts +61 -0
- package/lib/core/tsconfigPaths.d.mts +61 -0
- package/lib/core/tsconfigPaths.d.ts +61 -0
- package/lib/core/tsconfigPaths.js +190 -7
- package/lib/core/tsconfigPaths.js.map +1 -1
- package/lib/core/tsconfigPaths.mjs +188 -8
- package/lib/core/tsconfigPaths.mjs.map +1 -1
- package/lib/next.d.cts +27 -10
- package/lib/next.d.mts +27 -10
- package/lib/next.d.ts +27 -10
- package/lib/next.js +229 -8
- package/lib/next.js.map +1 -1
- package/lib/next.mjs +229 -8
- package/lib/next.mjs.map +1 -1
- package/lib/turbopack.d.cts +5 -4
- package/lib/turbopack.d.mts +5 -4
- package/lib/turbopack.d.ts +5 -4
- package/lib/turbopack.js +13 -7
- package/lib/turbopack.js.map +1 -1
- package/lib/turbopack.mjs +14 -8
- package/lib/turbopack.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/index.ts +122 -21
- package/src/core/transform.ts +1073 -132
- package/src/core/tsconfigPaths.ts +254 -8
- package/src/next.ts +262 -10
- package/src/turbopack.ts +13 -9
package/lib/core/transform.mjs
CHANGED
|
@@ -5,10 +5,51 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { TtscCompiler } from 'ttsc';
|
|
7
7
|
import { createFilesystemPathIdentityContext } from 'ttsc/path-identity';
|
|
8
|
-
import { absolutizePathsTarget, readEffectiveTsconfigPaths } from './tsconfigPaths.mjs';
|
|
8
|
+
import { mergeMembershipPolicyOverlay, readProjectMembershipPolicy, PERMISSIVE_PROJECT_MEMBERSHIP_POLICY, absolutizePathsTarget, readEffectiveTsconfigPaths } from './tsconfigPaths.mjs';
|
|
9
9
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
10
|
+
/**
|
|
11
|
+
* A verdict about one generation that later deliveries replay instead of
|
|
12
|
+
* repeating the whole compile behind it.
|
|
13
|
+
*
|
|
14
|
+
* The two kinds are replayed on different evidence, and each carries its own: a
|
|
15
|
+
* pass verdict knows the pass it belongs to, and an unstable generation knows
|
|
16
|
+
* the recorded environment it was proven against.
|
|
17
|
+
*/
|
|
18
|
+
class TtscTerminalGenerationError extends Error {
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The compile succeeded and produced no output for one requested module,
|
|
22
|
+
* because the program does not contain it.
|
|
23
|
+
*
|
|
24
|
+
* Not a terminal generation error, and deliberately not a build failure. It is
|
|
25
|
+
* a fact about one file, and the answer to it is to leave that file to the host
|
|
26
|
+
* (samchon/ttsc#1308). It is a distinct type rather than a message match so the
|
|
27
|
+
* decision travels as a type: `@ttsc/metro` used to recognise this case by
|
|
28
|
+
* searching the message text for "did not return output", which is how one
|
|
29
|
+
* product came to hold two different answers to one condition.
|
|
30
|
+
*/
|
|
31
|
+
class TtscMissingProgramOutputError extends Error {
|
|
32
|
+
/** The module the bundler asked for. */
|
|
33
|
+
file;
|
|
34
|
+
/** The project config whose program does not contain it. */
|
|
35
|
+
tsconfig;
|
|
36
|
+
constructor(file, tsconfig) {
|
|
37
|
+
super(`ttsc: ${file} is not part of the program described by ${tsconfig}, so it was left untransformed. Add it to that project's "include" if ttsc plugins should apply to it.`);
|
|
38
|
+
this.name = "TtscMissingProgramOutputError";
|
|
39
|
+
this.file = file;
|
|
40
|
+
this.tsconfig = tsconfig;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* A bounded proof failure that stays authoritative until its inputs change.
|
|
45
|
+
*
|
|
46
|
+
* This is the adapter failing to _obtain_ a coherent snapshot — a race it lost
|
|
47
|
+
* — so a later attempt may well succeed with the same inputs. It is retried
|
|
48
|
+
* when its recorded environment moves, and a new delivery epoch grants it the
|
|
49
|
+
* one fresh attempt the per-pass cache clear used to give it
|
|
50
|
+
* (samchon/ttsc#1300).
|
|
51
|
+
*/
|
|
52
|
+
class TtscUnstableGenerationError extends TtscTerminalGenerationError {
|
|
12
53
|
validation;
|
|
13
54
|
constructor(message, validation) {
|
|
14
55
|
super(message);
|
|
@@ -16,11 +57,58 @@ class TtscUnstableGenerationError extends Error {
|
|
|
16
57
|
this.validation = validation;
|
|
17
58
|
}
|
|
18
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A compile this pass already attempted, whose envelope failed outright.
|
|
62
|
+
*
|
|
63
|
+
* The envelope cannot say whether the host reported diagnostics about the
|
|
64
|
+
* project or failed to run at all: an ordinary type error arrives as an
|
|
65
|
+
* `"exception"` carrying the compiler's own diagnostic text, exactly as a
|
|
66
|
+
* crashed host would. Sniffing that message to tell the two apart would be a
|
|
67
|
+
* guess, so the adapter uses the one boundary it genuinely owns. Inside a pass
|
|
68
|
+
* the answer is already settled, so every later module replays it instead of
|
|
69
|
+
* repeating a whole-project transform to reach the same verdict, which is what
|
|
70
|
+
* made a single broken save cost one compile per delivered module
|
|
71
|
+
* (samchon/ttsc#1303).
|
|
72
|
+
*
|
|
73
|
+
* The scope is exactly the pass. A host whose `buildStart` repeats drops the
|
|
74
|
+
* verdict at its next rebuild, so a transient host failure costs that one
|
|
75
|
+
* rebuild. A host with no pass boundary never retains one at all and keeps
|
|
76
|
+
* retrying on its very next delivery. Between them sits a host that opens
|
|
77
|
+
* exactly one pass for its whole process — Bun's runtime plugin, and a Vite dev
|
|
78
|
+
* server configured with `server.watch: null` — where the verdict lasts the
|
|
79
|
+
* session. That follows from what those hosts already publish about themselves,
|
|
80
|
+
* that their session is one immutable load session and the remedy for changed
|
|
81
|
+
* inputs is to restart, and it is the deliberate trade: without it, one type
|
|
82
|
+
* error costs such a session a whole-project compile per delivered module,
|
|
83
|
+
* which is the workload samchon/ttsc#970 is about.
|
|
84
|
+
*
|
|
85
|
+
* It carries the original error's message, stack and `cause` rather than
|
|
86
|
+
* replacing them, so what a bundler reports is what it reported before the
|
|
87
|
+
* verdict existed.
|
|
88
|
+
*/
|
|
89
|
+
class TtscPassVerdictError extends TtscTerminalGenerationError {
|
|
90
|
+
/** The delivery pass this verdict belongs to, and its whole scope. */
|
|
91
|
+
epoch;
|
|
92
|
+
constructor(original, epoch) {
|
|
93
|
+
super(original instanceof Error
|
|
94
|
+
? original.message
|
|
95
|
+
: formatUnknownError(original), { cause: original });
|
|
96
|
+
if (original instanceof Error) {
|
|
97
|
+
this.name = original.name;
|
|
98
|
+
if (original.stack !== undefined)
|
|
99
|
+
this.stack = original.stack;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
this.name = "TtscPassVerdictError";
|
|
103
|
+
}
|
|
104
|
+
this.epoch = epoch;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
19
107
|
/** Proof witnesses retained beside a compiler result without extending its API. */
|
|
20
108
|
const TRANSFORM_GENERATION_FAILURES = new WeakMap();
|
|
21
109
|
/** Retry baselines retained only for attempts that could not be published. */
|
|
22
110
|
const TRANSFORM_FAILED_GENERATION_VALIDATIONS = new WeakMap();
|
|
23
|
-
/**
|
|
111
|
+
/** Cache promises whose unchanged terminal verdict may be replayed. */
|
|
24
112
|
const TERMINAL_TRANSFORM_GENERATIONS = new WeakMap();
|
|
25
113
|
/** Maximum witnesses printed and retained for each failed transform attempt. */
|
|
26
114
|
const MAX_GENERATION_PROOF_FAILURES = 8;
|
|
@@ -40,10 +128,27 @@ const DEFAULT_FILESYSTEM_OPERATIONS = Object.freeze({
|
|
|
40
128
|
const TRANSFORM_CACHE_FILESYSTEM = new WeakMap();
|
|
41
129
|
const TRANSFORM_RESULT_FILESYSTEM = new WeakMap();
|
|
42
130
|
/**
|
|
43
|
-
*
|
|
44
|
-
* {@link beginTtscTransformBuild}
|
|
131
|
+
* The current delivery epoch of each cache whose owner has declared a real
|
|
132
|
+
* per-pass lifecycle by calling {@link beginTtscTransformBuild}.
|
|
133
|
+
*
|
|
134
|
+
* A _delivery epoch_ is one bundler pass: the window inside which each module
|
|
135
|
+
* is requested at most once, so its first delivery may be settled against the
|
|
136
|
+
* state the pass started from. It is deliberately not the same fact as whether
|
|
137
|
+
* the generation is still valid, which the recorded snapshot answers.
|
|
138
|
+
* Conflating the two is what made every host with a repeating `buildStart` —
|
|
139
|
+
* webpack and Rspack watch, Rollup and Rolldown watch, `vite build --watch`,
|
|
140
|
+
* esbuild rebuild — discard a perfectly good whole-project compile on every
|
|
141
|
+
* edit (samchon/ttsc#1300).
|
|
142
|
+
*
|
|
143
|
+
* Absent from the map means persistent validation: a host with no pass boundary
|
|
144
|
+
* at all (a watching Vite dev server, Metro, the Turbopack loader), where every
|
|
145
|
+
* delivery proves the generation for itself.
|
|
45
146
|
*/
|
|
46
|
-
const
|
|
147
|
+
const TRANSFORM_CACHE_EPOCHS = new WeakMap();
|
|
148
|
+
/** The pass a delivery belongs to, or `undefined` under persistent validation. */
|
|
149
|
+
function transformCacheEpoch(cache) {
|
|
150
|
+
return cache === undefined ? undefined : TRANSFORM_CACHE_EPOCHS.get(cache);
|
|
151
|
+
}
|
|
47
152
|
function createHostPathIdentityContext(filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
48
153
|
return createFilesystemPathIdentityContext({
|
|
49
154
|
caseSensitive: filesystem.caseSensitive,
|
|
@@ -83,27 +188,37 @@ function resultFilesystem(result) {
|
|
|
83
188
|
return (TRANSFORM_RESULT_FILESYSTEM.get(result) ?? DEFAULT_FILESYSTEM_OPERATIONS);
|
|
84
189
|
}
|
|
85
190
|
/**
|
|
86
|
-
*
|
|
87
|
-
*
|
|
191
|
+
* Open a new delivery pass, enabling constant-time first delivery for every
|
|
192
|
+
* module this pass asks for.
|
|
193
|
+
*
|
|
194
|
+
* This deliberately retains the cached generation. The pass boundary is a
|
|
195
|
+
* statement about _deliveries_ — each module is requested at most once inside
|
|
196
|
+
* it — not about whether the compiled program is still correct, which the
|
|
197
|
+
* generation's own recorded snapshot answers and which
|
|
198
|
+
* {@link matchesCachedSource} proves once at the pass's first delivery. Clearing
|
|
199
|
+
* here instead made a host whose `buildStart` repeats recompile the whole
|
|
200
|
+
* project on every rebuild even when no compiler input had changed
|
|
201
|
+
* (samchon/ttsc#1300). Use {@link resetTtscTransformCache} to actually discard a
|
|
202
|
+
* generation and its watchers.
|
|
88
203
|
*
|
|
89
|
-
* Hosts without a guaranteed
|
|
90
|
-
*
|
|
204
|
+
* Hosts without a guaranteed pass boundary use persistent validation unless
|
|
205
|
+
* they have another immutable lifecycle. Bun runtime setup, for example,
|
|
91
206
|
* defines one process-scoped module-loading session.
|
|
92
207
|
*/
|
|
93
208
|
function beginTtscTransformBuild(cache) {
|
|
94
|
-
|
|
95
|
-
BUILD_SCOPED_TRANSFORM_CACHES.add(cache);
|
|
209
|
+
TRANSFORM_CACHE_EPOCHS.set(cache, (TRANSFORM_CACHE_EPOCHS.get(cache) ?? 0) + 1);
|
|
96
210
|
}
|
|
97
211
|
/**
|
|
98
|
-
*
|
|
212
|
+
* Discard every generation, dispose its watchers, and return the cache to
|
|
213
|
+
* persistent validation mode.
|
|
99
214
|
*
|
|
100
|
-
* This is
|
|
101
|
-
*
|
|
102
|
-
*
|
|
215
|
+
* This is the unconditional lifecycle boundary, and it is distinct from
|
|
216
|
+
* {@link beginTtscTransformBuild}: a pass ending is not a reason to throw a
|
|
217
|
+
* proven compile away, while a session ending is.
|
|
103
218
|
*/
|
|
104
219
|
function resetTtscTransformCache(cache) {
|
|
105
220
|
clearTtscTransformCache(cache);
|
|
106
|
-
|
|
221
|
+
TRANSFORM_CACHE_EPOCHS.delete(cache);
|
|
107
222
|
}
|
|
108
223
|
/** Dispose generation-owned filesystem resources before clearing a cache. */
|
|
109
224
|
function clearTtscTransformCache(cache) {
|
|
@@ -159,15 +274,19 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
159
274
|
tsconfig,
|
|
160
275
|
});
|
|
161
276
|
for (;;) {
|
|
277
|
+
// Read once per iteration, before the cache is consulted, so a delivery
|
|
278
|
+
// belongs to the pass that was current when it started examining the
|
|
279
|
+
// generation. A pass opened while this one awaits an in-flight compile is
|
|
280
|
+
// picked up by the next iteration, which is the one that runs when the
|
|
281
|
+
// entry it awaited turns out to have been superseded.
|
|
282
|
+
const epoch = transformCacheEpoch(cache);
|
|
162
283
|
let transformed = cache?.get(key);
|
|
163
284
|
if (transformed !== undefined) {
|
|
164
285
|
const terminal = TERMINAL_TRANSFORM_GENERATIONS.get(transformed);
|
|
165
286
|
if (terminal !== undefined) {
|
|
166
|
-
// A
|
|
167
|
-
// invitation for every later module to repeat the whole compile.
|
|
168
|
-
|
|
169
|
-
// establishes that a new generation could differ.
|
|
170
|
-
if (!failedGenerationEnvironmentChanged(terminal.validation, {
|
|
287
|
+
// A terminal verdict is an answer about one observed environment, not an
|
|
288
|
+
// invitation for every later module to repeat the whole compile.
|
|
289
|
+
if (replaysTerminalGeneration(terminal, epoch, {
|
|
171
290
|
currentFile: file,
|
|
172
291
|
currentSource: source,
|
|
173
292
|
filesystem,
|
|
@@ -189,8 +308,7 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
189
308
|
if (cache?.get(key) !== transformed) {
|
|
190
309
|
continue;
|
|
191
310
|
}
|
|
192
|
-
|
|
193
|
-
if (!buildScoped) {
|
|
311
|
+
if (epoch === undefined) {
|
|
194
312
|
await settleProjectMutationEvents(cached);
|
|
195
313
|
if (cache?.get(key) !== transformed) {
|
|
196
314
|
continue;
|
|
@@ -205,15 +323,33 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
205
323
|
projectRoot: cached.projectRoot,
|
|
206
324
|
result: cached.result,
|
|
207
325
|
}) &&
|
|
208
|
-
matchesCachedSource(cached, file, source,
|
|
209
|
-
reportSuccessDiagnostics(cached
|
|
326
|
+
matchesCachedSource(cached, file, source, epoch)) {
|
|
327
|
+
reportSuccessDiagnostics(cached, epoch);
|
|
210
328
|
// A resolved `"exception"` / `"failure"` envelope makes this throw;
|
|
211
|
-
// that is a failed generation too, so
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
329
|
+
// that is a failed generation too, so it is retained for this pass or
|
|
330
|
+
// evicted outside one before being surfaced.
|
|
331
|
+
let code;
|
|
332
|
+
try {
|
|
333
|
+
code = selectOrEvict(cache, key, transformed, epoch, {
|
|
334
|
+
file,
|
|
335
|
+
projectRoot: cached.projectRoot,
|
|
336
|
+
result: cached.result,
|
|
337
|
+
tsconfig: cached.tsconfig,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
if (!(error instanceof TtscMissingProgramOutputError)) {
|
|
342
|
+
notifyFailedGenerationInputs(hooks, cached);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
// The compile is fine and simply has nothing for this module, so the
|
|
346
|
+
// module goes back to the host untransformed rather than failing the
|
|
347
|
+
// build (samchon/ttsc#1308). It still counts as delivered in this
|
|
348
|
+
// pass, and there is nothing to watch for a file with no output.
|
|
349
|
+
reportMissingProgramOutput(cached, error, epoch);
|
|
350
|
+
markCachedSourceServed(cached, file);
|
|
351
|
+
return undefined;
|
|
352
|
+
}
|
|
217
353
|
notifyWatchInputs(hooks, cached, file);
|
|
218
354
|
markCachedSourceServed(cached, file);
|
|
219
355
|
return createTransformResult(source, code);
|
|
@@ -233,6 +369,10 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
233
369
|
compilerOptions: options.compilerOptions,
|
|
234
370
|
currentFile: file,
|
|
235
371
|
currentSource: source,
|
|
372
|
+
// Stamp the pass this compile was started for, not the one it happens
|
|
373
|
+
// to finish in: a boundary crossed mid-compile leaves the generation
|
|
374
|
+
// belonging to the earlier pass, so the next pass re-proves it.
|
|
375
|
+
deliveryEpoch: epoch,
|
|
236
376
|
filesystem,
|
|
237
377
|
plugins: options.plugins,
|
|
238
378
|
trackProjectMembership: cache !== undefined,
|
|
@@ -246,12 +386,25 @@ async function transformTtsc(id, source, options, aliases, cache, hooks) {
|
|
|
246
386
|
continue;
|
|
247
387
|
}
|
|
248
388
|
const { projectRoot, result } = cached;
|
|
249
|
-
reportSuccessDiagnostics(
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
389
|
+
reportSuccessDiagnostics(cached, epoch);
|
|
390
|
+
let code;
|
|
391
|
+
try {
|
|
392
|
+
code = selectOrEvict(cache, key, generation, epoch, {
|
|
393
|
+
file,
|
|
394
|
+
projectRoot,
|
|
395
|
+
result,
|
|
396
|
+
tsconfig: cached.tsconfig,
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
if (!(error instanceof TtscMissingProgramOutputError)) {
|
|
401
|
+
notifyFailedGenerationInputs(hooks, cached);
|
|
402
|
+
throw error;
|
|
403
|
+
}
|
|
404
|
+
reportMissingProgramOutput(cached, error, epoch);
|
|
405
|
+
markCachedSourceServed(cached, file);
|
|
406
|
+
return undefined;
|
|
407
|
+
}
|
|
255
408
|
notifyWatchInputs(hooks, cached, file);
|
|
256
409
|
markCachedSourceServed(cached, file);
|
|
257
410
|
if (isVolatileFile(envelopeDerivation(cached), { file, projectRoot, result })) {
|
|
@@ -287,20 +440,107 @@ async function awaitOrEvict(cache, key, generation) {
|
|
|
287
440
|
}
|
|
288
441
|
}
|
|
289
442
|
/**
|
|
290
|
-
* Extract the transformed source,
|
|
291
|
-
*
|
|
292
|
-
* {@link selectTransformedSource}
|
|
293
|
-
*
|
|
443
|
+
* Extract the transformed source, and decide what a throwing generation is.
|
|
444
|
+
*
|
|
445
|
+
* {@link selectTransformedSource} throws for two different reasons, and only one
|
|
446
|
+
* of them is about the generation. A host `"exception"` or a compiler
|
|
447
|
+
* `"failure"` means the compile produced nothing for anyone: inside a pass that
|
|
448
|
+
* verdict is retained and replayed, because evicting it made every remaining
|
|
449
|
+
* module repeat the whole-project transform only to reach the identical answer
|
|
450
|
+
* (samchon/ttsc#1303), and outside a pass it keeps being evicted so a
|
|
451
|
+
* long-lived worker retries on its very next delivery exactly as before.
|
|
452
|
+
*
|
|
453
|
+
* A `"success"` envelope that has no output for the module asking is the other
|
|
454
|
+
* reason, and it is a fact about that one file: an ordinary condition for a
|
|
455
|
+
* module the bundle reaches and the tsconfig program does not contain. It is
|
|
456
|
+
* neither retained nor evicted. The error reaches the caller, and the
|
|
457
|
+
* generation, which compiled perfectly well for every other module, stays.
|
|
294
458
|
*/
|
|
295
|
-
function selectOrEvict(cache, key, generation, props) {
|
|
459
|
+
function selectOrEvict(cache, key, generation, epoch, props) {
|
|
296
460
|
try {
|
|
297
461
|
return selectTransformedSource(props);
|
|
298
462
|
}
|
|
299
463
|
catch (error) {
|
|
300
|
-
|
|
464
|
+
const verdict = retainPassVerdict(cache, key, generation, epoch, props.result, error);
|
|
465
|
+
if (verdict !== undefined) {
|
|
466
|
+
throw verdict;
|
|
467
|
+
}
|
|
468
|
+
// A generation that compiled fine and simply has no output for the module
|
|
469
|
+
// asking is not a failed generation. Discarding it made every later module
|
|
470
|
+
// recompile the whole project to reach the same answer, which is the cost
|
|
471
|
+
// samchon/ttsc#1303 is about, for a bundle that merely reaches a file the
|
|
472
|
+
// tsconfig program does not contain.
|
|
473
|
+
if (props.result.type !== "success") {
|
|
474
|
+
evictGeneration(cache, key, generation);
|
|
475
|
+
}
|
|
301
476
|
throw error;
|
|
302
477
|
}
|
|
303
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* Retain the verdict of a compile this pass already attempted, or return
|
|
481
|
+
* `undefined` when nothing may be retained.
|
|
482
|
+
*
|
|
483
|
+
* Only inside a delivery pass. A pass is the window in which every delivery is
|
|
484
|
+
* settled against the state the pass started from, so an attempt it already
|
|
485
|
+
* made is part of that state and the remaining modules replay it rather than
|
|
486
|
+
* each repeating a whole-project transform to reach the same answer. Outside a
|
|
487
|
+
* pass there is no such window, and a long-lived worker must keep retrying on
|
|
488
|
+
* its very next delivery so a transient host failure never becomes permanent.
|
|
489
|
+
*/
|
|
490
|
+
function retainPassVerdict(cache, key, generation, epoch, result, error) {
|
|
491
|
+
// Only an envelope that failed outright is a statement about the generation.
|
|
492
|
+
// `selectTransformedSource` also throws for a file the compile simply has no
|
|
493
|
+
// output for, which is an ordinary condition for a module the bundle reaches
|
|
494
|
+
// but the tsconfig program does not contain, and which says nothing about the
|
|
495
|
+
// other modules. Retaining that would fail the whole pass, naming a file none
|
|
496
|
+
// of them asked about.
|
|
497
|
+
if (result.type === "success" ||
|
|
498
|
+
epoch === undefined ||
|
|
499
|
+
cache?.get(key) !== generation) {
|
|
500
|
+
return undefined;
|
|
501
|
+
}
|
|
502
|
+
const existing = TERMINAL_TRANSFORM_GENERATIONS.get(generation);
|
|
503
|
+
if (existing !== undefined) {
|
|
504
|
+
return existing;
|
|
505
|
+
}
|
|
506
|
+
const verdict = new TtscPassVerdictError(error, epoch);
|
|
507
|
+
TERMINAL_TRANSFORM_GENERATIONS.set(generation, verdict);
|
|
508
|
+
return verdict;
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Whether a terminal verdict still answers for this delivery.
|
|
512
|
+
*
|
|
513
|
+
* Inside the pass that produced or confirmed it, it is replayed without
|
|
514
|
+
* re-probing anything: the pass settles every delivery against the state it
|
|
515
|
+
* started from, so re-walking the project once per module would spend exactly
|
|
516
|
+
* the cost this gate exists to remove.
|
|
517
|
+
*
|
|
518
|
+
* Across passes the two kinds part company. A pass verdict is dropped, because
|
|
519
|
+
* a new pass is the first boundary at which the host itself claims something
|
|
520
|
+
* may have changed, and the compile it stood for was never proven against a
|
|
521
|
+
* recorded environment. An unstable generation was, so it keeps its own rule:
|
|
522
|
+
* one fresh attempt per pass, and otherwise replayed until that recorded
|
|
523
|
+
* environment provably moves.
|
|
524
|
+
*/
|
|
525
|
+
function replaysTerminalGeneration(terminal, epoch, props) {
|
|
526
|
+
if (terminal instanceof TtscPassVerdictError) {
|
|
527
|
+
// A pass verdict has no recorded environment to re-confirm against, so the
|
|
528
|
+
// pass that produced it is its whole scope.
|
|
529
|
+
return epoch !== undefined && terminal.epoch === epoch;
|
|
530
|
+
}
|
|
531
|
+
if (!(terminal instanceof TtscUnstableGenerationError)) {
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
// An unstable generation does have one, and confirming it per delivery is the
|
|
535
|
+
// behaviour its own contract describes, so the pass does not cache that
|
|
536
|
+
// answer. A new pass still grants the fresh attempt the per-pass cache clear
|
|
537
|
+
// used to give it.
|
|
538
|
+
if (epoch !== undefined &&
|
|
539
|
+
terminal.validation.cached.deliveryEpoch !== epoch) {
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
return !failedGenerationEnvironmentChanged(terminal.validation, props);
|
|
543
|
+
}
|
|
304
544
|
/**
|
|
305
545
|
* Delete a failed generation from the cache only when it is still the entry
|
|
306
546
|
* stored under `key`. The identity check prevents an older failed generation's
|
|
@@ -544,6 +784,47 @@ function collectDeclaredIdentities(state, projectRoot, listed) {
|
|
|
544
784
|
* transform scratch tree (see
|
|
545
785
|
* {@link TtscCachedProjectTransform.scratchDirectory}).
|
|
546
786
|
*/
|
|
787
|
+
/**
|
|
788
|
+
* Register the failed generation's own project inputs so the host can observe
|
|
789
|
+
* the fix.
|
|
790
|
+
*
|
|
791
|
+
* A successful delivery registers the derived watch inputs, which is how a
|
|
792
|
+
* type-only file that no bundler graph contains still invalidates the modules
|
|
793
|
+
* depending on it. A failed one used to register nothing: `selectWatchInputs`
|
|
794
|
+
* returns an empty list for an `"exception"` envelope, and the throw happens
|
|
795
|
+
* before `notifyWatchInputs` is reached at all. When the failing compile is the
|
|
796
|
+
* first of a watching session, that leaves no channel through which the fix can
|
|
797
|
+
* arrive: the user repairs a file the bundler does not track, nothing is
|
|
798
|
+
* invalidated, and the error stays on screen (samchon/ttsc#1312).
|
|
799
|
+
*
|
|
800
|
+
* The generation records the project walk even when the compile failed, so the
|
|
801
|
+
* files a fix would touch are exactly what it already holds. The cost is paid
|
|
802
|
+
* only on a failure, and only until the next compile succeeds and narrows the
|
|
803
|
+
* set back to the derived inputs.
|
|
804
|
+
*/
|
|
805
|
+
function notifyFailedGenerationInputs(hooks, cached) {
|
|
806
|
+
const addWatchFile = hooks?.addWatchFile;
|
|
807
|
+
if (addWatchFile === undefined) {
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
for (const key of Object.keys(cached.inputHashes)) {
|
|
811
|
+
const input = path.resolve(cached.projectRoot, key);
|
|
812
|
+
if (isTransformScratchInput(input, cached.scratchDirectory)) {
|
|
813
|
+
continue;
|
|
814
|
+
}
|
|
815
|
+
// No evidence argument, deliberately. `missing: false` would be a claim
|
|
816
|
+
// this path cannot back: a failed generation is replayed for the rest of
|
|
817
|
+
// its pass without re-proving its inputs, so the walk that recorded them
|
|
818
|
+
// may be older than the delivery, and one of them having been deleted is a
|
|
819
|
+
// live reason for that compile to have failed. Letting the adapter probe
|
|
820
|
+
// also routes an absent input to the missing-input poll, which is the only
|
|
821
|
+
// channel through which restoring it can invalidate anything: a bundler
|
|
822
|
+
// watch on a path that does not exist registers nothing, and no module
|
|
823
|
+
// graph carries a type-only input. It costs one `existsSync` per input,
|
|
824
|
+
// and only where the adapter reads evidence at all, which is Vite serve.
|
|
825
|
+
addWatchFile(input);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
547
828
|
function notifyWatchInputs(hooks, cached, file) {
|
|
548
829
|
const addWatchFile = hooks?.addWatchFile;
|
|
549
830
|
if (addWatchFile === undefined) {
|
|
@@ -922,16 +1203,17 @@ function createTransformResult(source, code) {
|
|
|
922
1203
|
* state.
|
|
923
1204
|
*
|
|
924
1205
|
* Always compares the current module's in-memory source with the generation
|
|
925
|
-
* snapshot. A cache
|
|
926
|
-
*
|
|
927
|
-
*
|
|
928
|
-
*
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
*
|
|
1206
|
+
* snapshot. A cache with a delivery epoch can use that comparison alone for a
|
|
1207
|
+
* stable generation's first delivery of each module in the current pass, once
|
|
1208
|
+
* the pass's own first delivery has proven the whole generation still matches
|
|
1209
|
+
* the filesystem. An incomplete generation may not take this shortcut:
|
|
1210
|
+
* otherwise a sibling output captured during a filesystem race could still be
|
|
1211
|
+
* served once. Later graph-bearing requests validate the file's derived input
|
|
1212
|
+
* set and project membership; graph-free envelopes conservatively re-hash the
|
|
1213
|
+
* complete project and out-of-walk snapshots. Any mismatch forces a complete
|
|
932
1214
|
* re-transform.
|
|
933
1215
|
*/
|
|
934
|
-
function matchesCachedSource(cached, file, source,
|
|
1216
|
+
function matchesCachedSource(cached, file, source, epoch) {
|
|
935
1217
|
const identities = envelopeDerivation(cached).identityContext;
|
|
936
1218
|
const currentKey = toProjectKey(cached.projectRoot, file, identities);
|
|
937
1219
|
const identity = pathIdentityKey(file, identities);
|
|
@@ -941,10 +1223,24 @@ function matchesCachedSource(cached, file, source, buildScoped) {
|
|
|
941
1223
|
if (expected !== hashText(source)) {
|
|
942
1224
|
return false;
|
|
943
1225
|
}
|
|
944
|
-
if (
|
|
945
|
-
cached.
|
|
946
|
-
|
|
947
|
-
|
|
1226
|
+
if (epoch !== undefined && cached.projectSnapshotComplete === true) {
|
|
1227
|
+
if (cached.deliveryEpoch !== epoch) {
|
|
1228
|
+
// The pass's first delivery. The generation was settled against an
|
|
1229
|
+
// earlier pass, so prove the whole of it once — every input the envelope
|
|
1230
|
+
// declares, the directory membership, the universal host inputs, and the
|
|
1231
|
+
// out-of-walk snapshot — before any of this pass's deliveries may be
|
|
1232
|
+
// settled against it. That proof is what a per-pass recompile used to buy
|
|
1233
|
+
// (samchon/ttsc#1300), at a walk instead of a compile.
|
|
1234
|
+
if (!matchesCompleteInputSnapshot(cached, currentKey, source)) {
|
|
1235
|
+
return false;
|
|
1236
|
+
}
|
|
1237
|
+
cached.deliveryEpoch = epoch;
|
|
1238
|
+
cached.servedFiles?.clear();
|
|
1239
|
+
return true;
|
|
1240
|
+
}
|
|
1241
|
+
if (!cached.servedFiles?.has(identity)) {
|
|
1242
|
+
return true;
|
|
1243
|
+
}
|
|
948
1244
|
}
|
|
949
1245
|
if (cached.result.type !== "exception" &&
|
|
950
1246
|
cached.result.graph !== undefined &&
|
|
@@ -1652,7 +1948,12 @@ function matchesCompleteInputSnapshot(cached, currentKey, source) {
|
|
|
1652
1948
|
const declaredInputs = declaredProjectInputKeys(state, cached);
|
|
1653
1949
|
const current = collectProjectInputSnapshot(cached.projectRoot, state.identityContext, resultFilesystem(cached.result), cached.inputSignatures === undefined
|
|
1654
1950
|
? undefined
|
|
1655
|
-
: { hashes: cached.inputHashes, signatures: cached.inputSignatures }
|
|
1951
|
+
: { hashes: cached.inputHashes, signatures: cached.inputSignatures }, {
|
|
1952
|
+
// Judge membership by the rule the compile ran under, and read only the
|
|
1953
|
+
// inputs this comparison actually consults.
|
|
1954
|
+
declaredKeys: declaredInputs,
|
|
1955
|
+
policy: cached.membershipPolicy,
|
|
1956
|
+
});
|
|
1656
1957
|
if (!walkSnapshotComplete(current, declaredInputs)) {
|
|
1657
1958
|
return false;
|
|
1658
1959
|
}
|
|
@@ -1943,24 +2244,36 @@ function markCachedSourceServed(cached, file) {
|
|
|
1943
2244
|
* slash path. Exported so hosts without a per-build boundary (`@ttsc/metro`)
|
|
1944
2245
|
* can fold the identical input universe into their own cache fingerprints.
|
|
1945
2246
|
*/
|
|
1946
|
-
function collectProjectInputHashes(projectRoot, identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
1947
|
-
return collectProjectInputSnapshot(projectRoot, identities, filesystem
|
|
1948
|
-
|
|
2247
|
+
function collectProjectInputHashes(projectRoot, identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy) {
|
|
2248
|
+
return collectProjectInputSnapshot(projectRoot, identities, filesystem, undefined, {
|
|
2249
|
+
policy,
|
|
2250
|
+
}).hashes;
|
|
1949
2251
|
}
|
|
1950
2252
|
/** Hash project files and snapshot the directory topology in one walk. */
|
|
1951
|
-
function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, proven) {
|
|
2253
|
+
function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, proven, options) {
|
|
1952
2254
|
const hashes = {};
|
|
1953
2255
|
const fileSignatures = {};
|
|
1954
2256
|
const provenSignatures = {};
|
|
1955
2257
|
const unstableFiles = new Set();
|
|
1956
2258
|
let attributed = true;
|
|
1957
|
-
const walked = walkProjectInputs(projectRoot, filesystem);
|
|
2259
|
+
const walked = walkProjectInputs(projectRoot, filesystem, options?.policy);
|
|
1958
2260
|
const walkFailures = [...walked.failures];
|
|
1959
2261
|
let complete = walked.complete;
|
|
1960
2262
|
for (const file of walked.files) {
|
|
1961
2263
|
try {
|
|
1962
|
-
const before = inputMetadataEvidence(file, filesystem);
|
|
1963
2264
|
const key = toProjectKey(projectRoot, file, identities);
|
|
2265
|
+
// A caller validating a generation compares hashes over that
|
|
2266
|
+
// generation's declared inputs alone (`sameHashes` takes the declared key
|
|
2267
|
+
// set), so reading anything else is work whose result is never consulted.
|
|
2268
|
+
// Skipping it is what keeps a directory full of emitted files from
|
|
2269
|
+
// costing a read per file on the pass that first sees them
|
|
2270
|
+
// (samchon/ttsc#1307). Capture passes supply no restriction and still
|
|
2271
|
+
// record the whole walk.
|
|
2272
|
+
if (options?.declaredKeys !== undefined &&
|
|
2273
|
+
!options.declaredKeys.has(key)) {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
const before = inputMetadataEvidence(file, filesystem);
|
|
1964
2277
|
// A file whose signature still equals the one captured around the read
|
|
1965
2278
|
// that produced the recorded hash carries that content, so the whole
|
|
1966
2279
|
// project does not have to be re-read to prove one delivery. A signature
|
|
@@ -2023,18 +2336,22 @@ function collectProjectInputSnapshot(projectRoot, identities, filesystem = DEFAU
|
|
|
2023
2336
|
};
|
|
2024
2337
|
}
|
|
2025
2338
|
/**
|
|
2026
|
-
* Enumerate every regular file under `root`, skipping
|
|
2027
|
-
*
|
|
2339
|
+
* Enumerate every regular file under `root`, skipping the directories no
|
|
2340
|
+
* configuration can name ({@link isIgnoredProjectDirectory}) and the ones the
|
|
2341
|
+
* resolved configuration excludes ({@link isExcludedProjectDirectory}).
|
|
2028
2342
|
*
|
|
2029
2343
|
* Uses an iterative DFS instead of `fs.readdirSync` recursion to avoid
|
|
2030
2344
|
* unbounded call-stack depth on deep project trees. The result is sorted so
|
|
2031
2345
|
* that hash comparisons are deterministic across OS-level directory orderings.
|
|
2032
2346
|
*/
|
|
2033
|
-
function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
2347
|
+
function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
|
|
2034
2348
|
let complete = true;
|
|
2035
|
-
const directories = [];
|
|
2036
2349
|
const failures = [];
|
|
2037
2350
|
const files = [];
|
|
2351
|
+
// Collected in one pass, then digested in a second. A directory's digest has
|
|
2352
|
+
// to know whether each child directory can hold program inputs, and the walk
|
|
2353
|
+
// learns that only after descending, so the two cannot be one pass.
|
|
2354
|
+
const visited = [];
|
|
2038
2355
|
const stack = [root];
|
|
2039
2356
|
while (stack.length !== 0) {
|
|
2040
2357
|
const current = stack.pop();
|
|
@@ -2066,33 +2383,99 @@ function walkProjectInputs(root, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
|
2066
2383
|
path: current,
|
|
2067
2384
|
});
|
|
2068
2385
|
}
|
|
2069
|
-
|
|
2386
|
+
const visit = {
|
|
2387
|
+
childDirectories: [],
|
|
2388
|
+
entries: [],
|
|
2389
|
+
ownInput: false,
|
|
2070
2390
|
path: current,
|
|
2071
2391
|
// If membership moved during enumeration, force the next delivery to
|
|
2072
2392
|
// replace this generation instead of blessing a torn directory/file
|
|
2073
2393
|
// snapshot as stable.
|
|
2074
|
-
|
|
2075
|
-
?
|
|
2394
|
+
stable: after !== undefined && before === after
|
|
2395
|
+
? undefined
|
|
2076
2396
|
: `unstable:${before}:${after ?? "missing"}`,
|
|
2077
|
-
}
|
|
2397
|
+
};
|
|
2078
2398
|
for (const entry of entries) {
|
|
2079
2399
|
if (isIgnoredProjectDirectory(entry.name)) {
|
|
2080
2400
|
continue;
|
|
2081
2401
|
}
|
|
2082
2402
|
const file = path.join(current, entry.name);
|
|
2403
|
+
if (entry.isDirectory() && isExcludedProjectDirectory(file, policy)) {
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
const possible = isPossibleProgramEntry(entry, policy);
|
|
2407
|
+
visit.entries.push({
|
|
2408
|
+
kind: [
|
|
2409
|
+
entry.isDirectory(),
|
|
2410
|
+
entry.isFile(),
|
|
2411
|
+
entry.isSymbolicLink(),
|
|
2412
|
+
].join(":"),
|
|
2413
|
+
name: entry.name,
|
|
2414
|
+
possible,
|
|
2415
|
+
});
|
|
2083
2416
|
if (entry.isDirectory()) {
|
|
2417
|
+
visit.childDirectories.push(file);
|
|
2084
2418
|
stack.push(file);
|
|
2085
2419
|
}
|
|
2086
|
-
else if (entry.isFile()) {
|
|
2420
|
+
else if (entry.isFile() && possible) {
|
|
2421
|
+
// Only a file that could enter the program is hashed. A file that
|
|
2422
|
+
// could not is either irrelevant to every generation, or it is one the
|
|
2423
|
+
// compiler actually read, in which case the graph reports it and
|
|
2424
|
+
// `isProjectWalkPath` now agrees it is out of the walk, so it is
|
|
2425
|
+
// recorded and proven by the out-of-walk snapshot instead. Hashing an
|
|
2426
|
+
// emitted tree here bought nothing and cost a read per file, including
|
|
2427
|
+
// in `@ttsc/metro`, whose fingerprint re-keys every transformed file
|
|
2428
|
+
// (samchon/ttsc#1307).
|
|
2087
2429
|
files.push(file);
|
|
2430
|
+
visit.ownInput = true;
|
|
2088
2431
|
}
|
|
2089
2432
|
}
|
|
2433
|
+
visited.push(visit);
|
|
2434
|
+
}
|
|
2435
|
+
// A directory matters to program membership only if its subtree can hold a
|
|
2436
|
+
// program input. Propagate that up from the directories that hold one, so a
|
|
2437
|
+
// bundler creating `out/` and filling it with JavaScript a project admitting
|
|
2438
|
+
// none can never compile is not a membership change at any level: not in the
|
|
2439
|
+
// directory itself, and not in the parent that now lists it
|
|
2440
|
+
// (samchon/ttsc#1307).
|
|
2441
|
+
const byPath = new Map(visited.map((visit) => [visit.path, visit]));
|
|
2442
|
+
const relevant = new Set();
|
|
2443
|
+
for (const visit of visited) {
|
|
2444
|
+
if (!visit.ownInput) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
let current = visit.path;
|
|
2448
|
+
while (current !== undefined && !relevant.has(current)) {
|
|
2449
|
+
relevant.add(current);
|
|
2450
|
+
const parent = path.dirname(current);
|
|
2451
|
+
current = parent === current || !byPath.has(parent) ? undefined : parent;
|
|
2452
|
+
}
|
|
2090
2453
|
}
|
|
2454
|
+
const directories = visited.map((visit) => {
|
|
2455
|
+
const membership = visit.entries
|
|
2456
|
+
.filter((entry) => entry.possible &&
|
|
2457
|
+
(!visit.childDirectories.includes(path.join(visit.path, entry.name)) ||
|
|
2458
|
+
relevant.has(path.join(visit.path, entry.name))))
|
|
2459
|
+
.map((entry) => `${entry.name}:${entry.kind}`);
|
|
2460
|
+
return {
|
|
2461
|
+
path: visit.path,
|
|
2462
|
+
relevant: relevant.has(visit.path),
|
|
2463
|
+
signature: visit.stable ??
|
|
2464
|
+
hashText(membership.sort().join(String.fromCharCode(0))),
|
|
2465
|
+
};
|
|
2466
|
+
});
|
|
2091
2467
|
directories.sort((left, right) => left.path.localeCompare(right.path));
|
|
2092
2468
|
files.sort();
|
|
2093
2469
|
return { complete, directories, failures, files };
|
|
2094
2470
|
}
|
|
2095
|
-
/**
|
|
2471
|
+
/**
|
|
2472
|
+
* Return a directory's metadata stamp, used to detect that its membership moved
|
|
2473
|
+
* _while_ the walk was enumerating it, and to feed the observed-clock floor.
|
|
2474
|
+
*
|
|
2475
|
+
* This is the right instrument for that job and the wrong one for comparing two
|
|
2476
|
+
* generations: it moves for ignored entries too. {@link walkProjectInputs}
|
|
2477
|
+
* records the filtered membership digest for the comparison instead.
|
|
2478
|
+
*/
|
|
2096
2479
|
function projectDirectorySignature(directory, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
2097
2480
|
try {
|
|
2098
2481
|
const stats = filesystem.statBigInt(directory);
|
|
@@ -2117,9 +2500,24 @@ function projectDirectorySignature(directory, filesystem = DEFAULT_FILESYSTEM_OP
|
|
|
2117
2500
|
}
|
|
2118
2501
|
/** Compare two deterministic project-directory membership snapshots. */
|
|
2119
2502
|
function sameProjectDirectories(left, right) {
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2503
|
+
// Compare only the directories that can hold program inputs, on either side.
|
|
2504
|
+
// A directory irrelevant on both is not part of the program's membership at
|
|
2505
|
+
// all, so its appearance, disappearance or churn says nothing: that is a
|
|
2506
|
+
// bundler's output tree. One that gained or lost relevance is present in the
|
|
2507
|
+
// comparison from the side where it counts, and so is caught.
|
|
2508
|
+
const select = (snapshots) => new Map(snapshots
|
|
2509
|
+
.filter((directory) => directory.relevant)
|
|
2510
|
+
.map((directory) => [directory.path, directory]));
|
|
2511
|
+
const leftRelevant = select(left);
|
|
2512
|
+
const rightRelevant = select(right);
|
|
2513
|
+
const paths = new Set([...leftRelevant.keys(), ...rightRelevant.keys()]);
|
|
2514
|
+
for (const location of paths) {
|
|
2515
|
+
if (leftRelevant.get(location)?.signature !==
|
|
2516
|
+
rightRelevant.get(location)?.signature) {
|
|
2517
|
+
return false;
|
|
2518
|
+
}
|
|
2519
|
+
}
|
|
2520
|
+
return true;
|
|
2123
2521
|
}
|
|
2124
2522
|
/**
|
|
2125
2523
|
* Open one directory's change notification through the cache-owned watch seam,
|
|
@@ -2136,7 +2534,7 @@ function openDirectoryWatch(filesystem, directory, listener, onError) {
|
|
|
2136
2534
|
return { close: () => watcher.close() };
|
|
2137
2535
|
}
|
|
2138
2536
|
/** Watch every walked directory for membership changes after generation. */
|
|
2139
|
-
async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
2537
|
+
async function createProjectMutationTracker(directories, filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
|
|
2140
2538
|
const tracker = {
|
|
2141
2539
|
changes: new Set(),
|
|
2142
2540
|
changesOmitted: false,
|
|
@@ -2145,7 +2543,7 @@ async function createProjectMutationTracker(directories, filesystem = DEFAULT_FI
|
|
|
2145
2543
|
membershipChanged: false,
|
|
2146
2544
|
};
|
|
2147
2545
|
if (process.platform === "win32" && filesystem.watch === undefined) {
|
|
2148
|
-
await registerWindowsProjectMutationTracker(tracker, directories.map((directory) => ({ directory: directory.path })), false, filesystem);
|
|
2546
|
+
await registerWindowsProjectMutationTracker(tracker, directories.map((directory) => ({ directory: directory.path })), false, filesystem, (location, filename) => reportsProgramMembership(path.join(location, filename), filename, policy, filesystem));
|
|
2149
2547
|
return tracker;
|
|
2150
2548
|
}
|
|
2151
2549
|
const watchers = [];
|
|
@@ -2157,11 +2555,16 @@ async function createProjectMutationTracker(directories, filesystem = DEFAULT_FI
|
|
|
2157
2555
|
for (const directory of directories) {
|
|
2158
2556
|
try {
|
|
2159
2557
|
watchers.push(openDirectoryWatch(filesystem, directory.path, (eventType, filename) => {
|
|
2160
|
-
if (eventType
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2558
|
+
if (eventType !== "rename") {
|
|
2559
|
+
return;
|
|
2560
|
+
}
|
|
2561
|
+
if (filename !== null &&
|
|
2562
|
+
!reportsProgramMembership(path.join(directory.path, filename), filename, policy, filesystem)) {
|
|
2563
|
+
return;
|
|
2164
2564
|
}
|
|
2565
|
+
recordProjectMutation(tracker, filename === null
|
|
2566
|
+
? directory.path
|
|
2567
|
+
: path.join(directory.path, filename));
|
|
2165
2568
|
}, () => {
|
|
2166
2569
|
tracker.failed = true;
|
|
2167
2570
|
}));
|
|
@@ -2246,6 +2649,80 @@ async function createHostInputMutationTracker(inputs, filesystem, covered, event
|
|
|
2246
2649
|
}
|
|
2247
2650
|
return tracker;
|
|
2248
2651
|
}
|
|
2652
|
+
/**
|
|
2653
|
+
* Whether a path lies inside a directory the configuration excludes.
|
|
2654
|
+
*
|
|
2655
|
+
* Lexical, exactly like the walk and like {@link isProjectWalkPath}, and for the
|
|
2656
|
+
* reason that predicate states: walk membership is lexical, so resolving a path
|
|
2657
|
+
* to physical identity first would collapse two spellings the walk keeps apart
|
|
2658
|
+
* and claim it covered a subtree it never followed. A junction whose target the
|
|
2659
|
+
* walk hashes under its own name is exactly that, and canonicalizing here would
|
|
2660
|
+
* suppress every event in it.
|
|
2661
|
+
*
|
|
2662
|
+
* `strictly` excludes an exact match, for the case where the excluded entry
|
|
2663
|
+
* names a file rather than a directory: `exclude` accepts one, the walk applies
|
|
2664
|
+
* exclusion to directories alone, so that file is still hashed and its events
|
|
2665
|
+
* must keep counting.
|
|
2666
|
+
*/
|
|
2667
|
+
function insideExcludedProjectDirectory(location, policy, strictly) {
|
|
2668
|
+
if (policy.excludedDirectories.length === 0) {
|
|
2669
|
+
return false;
|
|
2670
|
+
}
|
|
2671
|
+
const resolved = path.resolve(location);
|
|
2672
|
+
return policy.excludedDirectories.some((excluded) => {
|
|
2673
|
+
const target = path.resolve(excluded);
|
|
2674
|
+
if (strictly && target === resolved) {
|
|
2675
|
+
return false;
|
|
2676
|
+
}
|
|
2677
|
+
return pathIsWithin(resolved, target);
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Whether one directory event can be a change to the program's membership.
|
|
2682
|
+
*
|
|
2683
|
+
* The live tracker has to answer the same question the membership digest does,
|
|
2684
|
+
* or the two disagree about the same project: a bundler writing content-hashed
|
|
2685
|
+
* output fires a rename per rebuild, and treating that as membership kept the
|
|
2686
|
+
* cost samchon/ttsc#1307 removes on every host that has no build boundary,
|
|
2687
|
+
* which is every host the narrow path exists for.
|
|
2688
|
+
*
|
|
2689
|
+
* A name that could be a program input counts, unless it sits under a directory
|
|
2690
|
+
* the walk never descends into. A name that could not still counts when the
|
|
2691
|
+
* path is now a directory, because the walk's watches were opened for the
|
|
2692
|
+
* directories that existed when the generation was captured, so a directory
|
|
2693
|
+
* created since is not watched and the sources that may appear in it would
|
|
2694
|
+
* otherwise be invisible. A directory the configuration excludes is the
|
|
2695
|
+
* exception: the walk cannot see inside it, so the tracker must not either, or
|
|
2696
|
+
* emptying and recreating an `outDir` costs a compile per build. An event whose
|
|
2697
|
+
* name the host did not report is unattributable and always counts.
|
|
2698
|
+
*/
|
|
2699
|
+
function reportsProgramMembership(location, filename, policy, filesystem) {
|
|
2700
|
+
if (isPossibleProgramFileName(filename, policy)) {
|
|
2701
|
+
// A name the program could admit. It still says nothing if it lies inside a
|
|
2702
|
+
// directory the walk never descends into, because the digest cannot see
|
|
2703
|
+
// there either and the tracker must not be the one side that reacts.
|
|
2704
|
+
return !insideExcludedProjectDirectory(location, policy, true);
|
|
2705
|
+
}
|
|
2706
|
+
let directory;
|
|
2707
|
+
try {
|
|
2708
|
+
directory = filesystem.lstat(location).isDirectory();
|
|
2709
|
+
}
|
|
2710
|
+
catch {
|
|
2711
|
+
// Gone again, or unreadable. Its name could not have been a program input,
|
|
2712
|
+
// and a directory removed under this one reports its own contents leaving
|
|
2713
|
+
// through the watch that was opened on it.
|
|
2714
|
+
return false;
|
|
2715
|
+
}
|
|
2716
|
+
if (!directory) {
|
|
2717
|
+
return false;
|
|
2718
|
+
}
|
|
2719
|
+
// A directory counts, because it can hold sources and the tracker is not
|
|
2720
|
+
// watching it yet, unless the configuration says the program does not contain
|
|
2721
|
+
// it. Emptying and recreating an `outDir`, which is what `emptyOutDir` and
|
|
2722
|
+
// `output.clean` do on every build, would otherwise void the generation once
|
|
2723
|
+
// per build on every host that has no build boundary.
|
|
2724
|
+
return !insideExcludedProjectDirectory(location, policy, false);
|
|
2725
|
+
}
|
|
2249
2726
|
/** Record enough exact mutation evidence without retaining an event stream. */
|
|
2250
2727
|
function recordProjectMutation(tracker, changed) {
|
|
2251
2728
|
tracker.membershipChanged = true;
|
|
@@ -2266,8 +2743,21 @@ let windowsProjectMutationBroker;
|
|
|
2266
2743
|
* temporary tree is deleted. Isolation turns that unrecoverable process abort
|
|
2267
2744
|
* into an ordinary broker exit and a conservative cache miss in the host.
|
|
2268
2745
|
*/
|
|
2269
|
-
async function registerWindowsProjectMutationTracker(tracker, locations, allEvents, filesystem
|
|
2746
|
+
async function registerWindowsProjectMutationTracker(tracker, locations, allEvents, filesystem,
|
|
2747
|
+
/**
|
|
2748
|
+
* Optional filter for the project-directory tracker, whose events have to be
|
|
2749
|
+
* narrowed to program membership exactly as the in-process watcher's are. The
|
|
2750
|
+
* name-watching trackers pass none, since they already watch exact names.
|
|
2751
|
+
*/
|
|
2752
|
+
membership) {
|
|
2270
2753
|
const broker = getWindowsProjectMutationBroker();
|
|
2754
|
+
// The child watches canonical directories, and reports its events under that
|
|
2755
|
+
// spelling. Everything else in the adapter speaks the walk's own spelling,
|
|
2756
|
+
// which on Windows can be an 8.3 short form of the same directory, so keep
|
|
2757
|
+
// the way back: a filter that compared the child's spelling against the
|
|
2758
|
+
// configuration's would be comparing two names for one directory that share
|
|
2759
|
+
// no common prefix (samchon/ttsc#1307).
|
|
2760
|
+
const spellings = new Map();
|
|
2271
2761
|
const normalized = locations.map((location) => {
|
|
2272
2762
|
let directory;
|
|
2273
2763
|
try {
|
|
@@ -2276,6 +2766,7 @@ async function registerWindowsProjectMutationTracker(tracker, locations, allEven
|
|
|
2276
2766
|
catch {
|
|
2277
2767
|
directory = path.resolve(location.directory);
|
|
2278
2768
|
}
|
|
2769
|
+
spellings.set(directory, location.directory);
|
|
2279
2770
|
return {
|
|
2280
2771
|
directory,
|
|
2281
2772
|
...(location.names === undefined ? {} : { names: location.names }),
|
|
@@ -2289,7 +2780,12 @@ async function registerWindowsProjectMutationTracker(tracker, locations, allEven
|
|
|
2289
2780
|
const ready = new Promise((resolve) => {
|
|
2290
2781
|
resolveReady = resolve;
|
|
2291
2782
|
});
|
|
2292
|
-
broker.trackers.set(id, {
|
|
2783
|
+
broker.trackers.set(id, {
|
|
2784
|
+
membership,
|
|
2785
|
+
ready: resolveReady,
|
|
2786
|
+
spellings,
|
|
2787
|
+
tracker,
|
|
2788
|
+
});
|
|
2293
2789
|
tracker.drain = () => drainWindowsProjectMutationBroker(broker);
|
|
2294
2790
|
tracker.close = () => {
|
|
2295
2791
|
const active = broker.trackers.get(id);
|
|
@@ -2384,9 +2880,17 @@ function getWindowsProjectMutationBroker() {
|
|
|
2384
2880
|
registration.ready();
|
|
2385
2881
|
if (record.ready !== true && record.failed !== true) {
|
|
2386
2882
|
if (typeof record.directory === "string") {
|
|
2883
|
+
// The walk's spelling for this directory, which is what every
|
|
2884
|
+
// comparison and every recorded witness downstream expects.
|
|
2885
|
+
const reported = registration.spellings.get(record.directory) ?? record.directory;
|
|
2886
|
+
if (typeof record.filename === "string" &&
|
|
2887
|
+
registration.membership !== undefined &&
|
|
2888
|
+
!registration.membership(reported, record.filename)) {
|
|
2889
|
+
return;
|
|
2890
|
+
}
|
|
2387
2891
|
recordProjectMutation(registration.tracker, typeof record.filename === "string"
|
|
2388
|
-
? path.join(
|
|
2389
|
-
:
|
|
2892
|
+
? path.join(reported, record.filename)
|
|
2893
|
+
: reported);
|
|
2390
2894
|
}
|
|
2391
2895
|
else {
|
|
2392
2896
|
registration.tracker.membershipChanged = true;
|
|
@@ -2565,7 +3069,7 @@ async function settleProjectMutationEvents(cached) {
|
|
|
2565
3069
|
* Missing paths and files reached through symlinks or Windows junctions are
|
|
2566
3070
|
* out-of-walk inputs that only the reference graph can prove relevant.
|
|
2567
3071
|
*/
|
|
2568
|
-
function isProjectWalkPath(root, file, _identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS) {
|
|
3072
|
+
function isProjectWalkPath(root, file, _identities = createHostPathIdentityContext(), filesystem = DEFAULT_FILESYSTEM_OPERATIONS, policy = PERMISSIVE_PROJECT_MEMBERSHIP_POLICY) {
|
|
2569
3073
|
// Walk membership is lexical. Resolving `file` to physical identity first
|
|
2570
3074
|
// would turn `root/alias/value.ts` into `root/target/value.ts`, hide the
|
|
2571
3075
|
// symlink segment from the lstat loop below, and falsely claim the project
|
|
@@ -2579,7 +3083,20 @@ function isProjectWalkPath(root, file, _identities = createHostPathIdentityConte
|
|
|
2579
3083
|
return false;
|
|
2580
3084
|
}
|
|
2581
3085
|
const segments = relative.split(path.sep);
|
|
2582
|
-
|
|
3086
|
+
// The last segment is the file itself, which the walk names rather than
|
|
3087
|
+
// descends into, so only the directory components decide walk membership.
|
|
3088
|
+
if (segments.slice(0, -1).some(isIgnoredProjectDirectory)) {
|
|
3089
|
+
return false;
|
|
3090
|
+
}
|
|
3091
|
+
if (isExcludedProjectDirectory(path.dirname(path.resolve(file)), policy)) {
|
|
3092
|
+
return false;
|
|
3093
|
+
}
|
|
3094
|
+
// The walk hashes only files that could enter the program, so a path it does
|
|
3095
|
+
// not hash is out of the walk by definition. Answering otherwise would leave
|
|
3096
|
+
// a graph input the compiler really read in neither snapshot: absent from
|
|
3097
|
+
// `inputHashes` because the walk skipped it, and absent from the out-of-walk
|
|
3098
|
+
// snapshot because this predicate claimed the walk covered it.
|
|
3099
|
+
if (!isPossibleProgramFileName(path.basename(file), policy)) {
|
|
2583
3100
|
return false;
|
|
2584
3101
|
}
|
|
2585
3102
|
let current = resolvedRoot;
|
|
@@ -2767,7 +3284,7 @@ function selectExternalInputPaths(props) {
|
|
|
2767
3284
|
isTransformScratchInput(absolute, props.scratchDirectory) ||
|
|
2768
3285
|
seen.has(spelling) ||
|
|
2769
3286
|
(!missingCandidate &&
|
|
2770
|
-
isProjectWalkPath(props.projectRoot, absolute, identities, filesystem))) {
|
|
3287
|
+
isProjectWalkPath(props.projectRoot, absolute, identities, filesystem, props.membershipPolicy))) {
|
|
2771
3288
|
continue;
|
|
2772
3289
|
}
|
|
2773
3290
|
// Preserve distinct lexical aliases even when they currently select the
|
|
@@ -2930,21 +3447,44 @@ function insideProject(directory, projectRoot) {
|
|
|
2930
3447
|
*/
|
|
2931
3448
|
const NOTIFIABLE_ABSENCE_DIRECTORY_LIMIT = 512;
|
|
2932
3449
|
function isIgnoredProjectDirectory(name) {
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
3450
|
+
// The residue of what used to be a fifteen-name list, kept to the three
|
|
3451
|
+
// directories no tsconfig can name and no program can contain: the VCS
|
|
3452
|
+
// store, the package manager's tree (TypeScript's own default `exclude`
|
|
3453
|
+
// carries it too), and ttsc's own plugin cache. Everything else the old list
|
|
3454
|
+
// guessed at, and guessing was wrong in both directions: a bundler writing
|
|
3455
|
+
// to an unnamed directory changed project membership with its own output,
|
|
3456
|
+
// while a real source directory named `build` or `temp` was dropped from the
|
|
3457
|
+
// walk and its new files were never seen (samchon/ttsc#1307). Those are now
|
|
3458
|
+
// decided by `ITtscProjectMembershipPolicy`, which reads the configuration
|
|
3459
|
+
// that actually knows.
|
|
3460
|
+
return name === ".git" || name === ".ttsc" || name === "node_modules";
|
|
3461
|
+
}
|
|
3462
|
+
/**
|
|
3463
|
+
* Whether the resolved configuration keeps this directory out of the program.
|
|
3464
|
+
*
|
|
3465
|
+
* Compared by physical containment rather than by name, so `outDir: "./dist"`
|
|
3466
|
+
* excludes that one directory instead of every directory called `dist` at every
|
|
3467
|
+
* depth, which is the distinction the name list could not draw.
|
|
3468
|
+
*/
|
|
3469
|
+
function isExcludedProjectDirectory(directory, policy) {
|
|
3470
|
+
return insideExcludedProjectDirectory(directory, policy, false);
|
|
3471
|
+
}
|
|
3472
|
+
/**
|
|
3473
|
+
* Whether this entry could enter the program, and so whether its appearance or
|
|
3474
|
+
* removal is a membership change.
|
|
3475
|
+
*
|
|
3476
|
+
* A directory always could, since it can hold sources. A file could only if it
|
|
3477
|
+
* carries an extension the resolved configuration admits, which is what makes a
|
|
3478
|
+
* bundle emitted beside the sources invisible to a project that compiles no
|
|
3479
|
+
* JavaScript.
|
|
3480
|
+
*/
|
|
3481
|
+
function isPossibleProgramEntry(entry, policy) {
|
|
3482
|
+
return entry.isFile() ? isPossibleProgramFileName(entry.name, policy) : true;
|
|
3483
|
+
}
|
|
3484
|
+
/** The same question for a bare file name, for callers holding no `Dirent`. */
|
|
3485
|
+
function isPossibleProgramFileName(name, policy) {
|
|
3486
|
+
const lowered = name.toLowerCase();
|
|
3487
|
+
return policy.inputExtensions.some((extension) => lowered.endsWith(extension));
|
|
2948
3488
|
}
|
|
2949
3489
|
/**
|
|
2950
3490
|
* Compare two project-walk snapshots.
|
|
@@ -3298,7 +3838,7 @@ function failedGenerationEnvironmentChanged(validation, props) {
|
|
|
3298
3838
|
expectedSourceHash !== currentSourceHash) {
|
|
3299
3839
|
return true;
|
|
3300
3840
|
}
|
|
3301
|
-
const current = collectProjectInputSnapshot(validation.cached.projectRoot, identities, props.filesystem);
|
|
3841
|
+
const current = collectProjectInputSnapshot(validation.cached.projectRoot, identities, props.filesystem, undefined, { policy: validation.cached.membershipPolicy });
|
|
3302
3842
|
if (validation.projectWalkComplete !==
|
|
3303
3843
|
walkSnapshotComplete(current, validation.declaredInputs) ||
|
|
3304
3844
|
validation.projectWalkFailures !==
|
|
@@ -3398,9 +3938,14 @@ async function captureTransformGeneration(props) {
|
|
|
3398
3938
|
const configured = createTransformTsconfig(props, scratchDirectory);
|
|
3399
3939
|
const temporaryTsconfig = configured.path === props.tsconfig ? undefined : configured.path;
|
|
3400
3940
|
const identities = createHostPathIdentityContext(props.filesystem);
|
|
3401
|
-
|
|
3941
|
+
// Read from the project's own tsconfig rather than the generated one: a
|
|
3942
|
+
// relative `outDir` is anchored at the config that declares it, and the
|
|
3943
|
+
// generated config lives in a system temp directory. The caller's
|
|
3944
|
+
// compiler-options overlay still wins, since it wins for the compile too.
|
|
3945
|
+
const membershipPolicy = mergeMembershipPolicyOverlay(readProjectMembershipPolicy(props.tsconfig), props.compilerOptions, projectRoot);
|
|
3946
|
+
const before = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
|
|
3402
3947
|
tracker = props.trackProjectMembership
|
|
3403
|
-
? await createProjectMutationTracker(before.projectDirectories, props.filesystem)
|
|
3948
|
+
? await createProjectMutationTracker(before.projectDirectories, props.filesystem, membershipPolicy)
|
|
3404
3949
|
: undefined;
|
|
3405
3950
|
const result = withTransformScratchEnvironment(scratchDirectory, () => new TtscCompiler({
|
|
3406
3951
|
cwd: projectRoot,
|
|
@@ -3465,12 +4010,13 @@ async function captureTransformGeneration(props) {
|
|
|
3465
4010
|
: undefined;
|
|
3466
4011
|
const externalInputPaths = selectExternalInputPaths({
|
|
3467
4012
|
filesystem: props.filesystem,
|
|
4013
|
+
membershipPolicy,
|
|
3468
4014
|
projectRoot,
|
|
3469
4015
|
result,
|
|
3470
4016
|
scratchDirectory,
|
|
3471
4017
|
temporaryTsconfig,
|
|
3472
4018
|
});
|
|
3473
|
-
const inputSnapshot = collectProjectInputSnapshot(projectRoot, identities, props.filesystem);
|
|
4019
|
+
const inputSnapshot = collectProjectInputSnapshot(projectRoot, identities, props.filesystem, undefined, { policy: membershipPolicy });
|
|
3474
4020
|
// Whether the recorded snapshot describes one coherent state of the
|
|
3475
4021
|
// project. A membership event during the compile taints it exactly like an
|
|
3476
4022
|
// unstable walk pair; whether notifications can be *opened* is a separate
|
|
@@ -3505,6 +4051,12 @@ async function captureTransformGeneration(props) {
|
|
|
3505
4051
|
delete inputSnapshot.provenSignatures[currentFileKey];
|
|
3506
4052
|
}
|
|
3507
4053
|
const cached = {
|
|
4054
|
+
// The pass this compile was started for. Its snapshot describes the
|
|
4055
|
+
// project as of this compile, so it is settled for this pass and any
|
|
4056
|
+
// later pass must re-prove it.
|
|
4057
|
+
...(props.deliveryEpoch === undefined
|
|
4058
|
+
? {}
|
|
4059
|
+
: { deliveryEpoch: props.deliveryEpoch }),
|
|
3508
4060
|
// Capture the out-of-walk input hashes while the generation is fresh so
|
|
3509
4061
|
// cache validation can re-check them; computed before dispose so the
|
|
3510
4062
|
// scratch-tree exclusion is the only reason its disposed artifacts never
|
|
@@ -3514,7 +4066,9 @@ async function captureTransformGeneration(props) {
|
|
|
3514
4066
|
externalInputPaths,
|
|
3515
4067
|
inputHashes: inputSnapshot.hashes,
|
|
3516
4068
|
inputSignatures: inputSnapshot.provenSignatures,
|
|
4069
|
+
membershipPolicy,
|
|
3517
4070
|
projectDirectories: inputSnapshot.projectDirectories,
|
|
4071
|
+
tsconfig: props.tsconfig,
|
|
3518
4072
|
projectSnapshotComplete: false,
|
|
3519
4073
|
projectRoot,
|
|
3520
4074
|
result,
|
|
@@ -3874,10 +4428,37 @@ function readPaths(value) {
|
|
|
3874
4428
|
function createAliasPaths(aliases) {
|
|
3875
4429
|
const paths = {};
|
|
3876
4430
|
for (const alias of normalizeAliases(aliases)) {
|
|
3877
|
-
if (typeof alias.find !== "string"
|
|
4431
|
+
if (typeof alias.find !== "string") {
|
|
4432
|
+
// Vite's array form accepts a `RegExp` find, and `{ find: /^~/ }` is a
|
|
4433
|
+
// common way to spell a prefix alias. A tsconfig `paths` map has no
|
|
4434
|
+
// regular-expression form, so there is nothing to translate it into
|
|
4435
|
+
// (samchon/ttsc#1315). Reducing the simple prefix cases to a string is
|
|
4436
|
+
// possible in principle and deliberately not done: telling `/^~/` from
|
|
4437
|
+
// `/^~(?=\/)/` or `/^@app/` — which matches `@apple` too — means
|
|
4438
|
+
// implementing enough of a regular-expression engine that a wrong
|
|
4439
|
+
// reduction becomes likely, and a mistranslated alias resolves imports to
|
|
4440
|
+
// the wrong file silently, which is worse than not forwarding it.
|
|
4441
|
+
//
|
|
4442
|
+
// Not reported, unlike the wildcard below, and that asymmetry is the
|
|
4443
|
+
// whole point: Vite merges two `RegExp` aliases of its own into every
|
|
4444
|
+
// resolved config, `/^\/?@vite\/env/` and `/^\/?@vite\/client/`. Measured
|
|
4445
|
+
// on a bare project with no user aliases at all, `resolve.alias` has
|
|
4446
|
+
// exactly those two entries under both `serve` and `build`, so a report
|
|
4447
|
+
// on this form would fire twice for every Vite user in every build, name
|
|
4448
|
+
// aliases they never wrote, and say nothing about their configuration.
|
|
4449
|
+
// A diagnostic that cannot distinguish the user's input from the host's
|
|
4450
|
+
// is noise, and noise is what teaches people to stop reading the channel
|
|
4451
|
+
// the out-of-program report depends on. The documentation carries this
|
|
4452
|
+
// form instead, in both README and guide.
|
|
4453
|
+
continue;
|
|
4454
|
+
}
|
|
4455
|
+
if (alias.find.length === 0) {
|
|
3878
4456
|
continue;
|
|
3879
4457
|
}
|
|
3880
4458
|
if (alias.find.includes("*")) {
|
|
4459
|
+
// A `paths` key reads `*` as its own wildcard, so forwarding a `find`
|
|
4460
|
+
// that already contains one cannot preserve the caller's meaning.
|
|
4461
|
+
reportUntranslatableAlias(JSON.stringify(alias.find), 'a "paths" key already reads "*" as its own wildcard');
|
|
3881
4462
|
continue;
|
|
3882
4463
|
}
|
|
3883
4464
|
const key = alias.find.replace(/\/+$/, "");
|
|
@@ -3892,9 +4473,50 @@ function createAliasPaths(aliases) {
|
|
|
3892
4473
|
}
|
|
3893
4474
|
return paths;
|
|
3894
4475
|
}
|
|
4476
|
+
/**
|
|
4477
|
+
* Alias descriptions already reported in this process.
|
|
4478
|
+
*
|
|
4479
|
+
* The message is about configuration rather than about a module:
|
|
4480
|
+
* `resolve.alias` is resolved once and then consulted on every delivery, so
|
|
4481
|
+
* reporting per delivery would repeat one statement about the config for every
|
|
4482
|
+
* file in the bundle. Keyed by the description, so a Vite dev server that
|
|
4483
|
+
* reloads its config reports again only when the alias itself changed.
|
|
4484
|
+
*/
|
|
4485
|
+
const REPORTED_UNTRANSLATABLE_ALIASES = new Set();
|
|
4486
|
+
/**
|
|
4487
|
+
* Tell the user once that an alias they declared is not reaching the compile.
|
|
4488
|
+
*
|
|
4489
|
+
* A dropped alias is not silent in its consequence — the compile resolves
|
|
4490
|
+
* through the tsconfig's own `paths`, and a module that resolves for the
|
|
4491
|
+
* bundler but not for the compiler surfaces as the out-of-program report
|
|
4492
|
+
* (samchon/ttsc#1308) — but that report names the module, not the alias, so the
|
|
4493
|
+
* user cannot learn from it that a configuration they wrote was ignored.
|
|
4494
|
+
*
|
|
4495
|
+
* Only the wildcard form reaches here. Every entry it names was written by the
|
|
4496
|
+
* user, because nothing injects one; the `RegExp` form is left to the
|
|
4497
|
+
* documentation precisely because Vite does inject those, and
|
|
4498
|
+
* {@link createAliasPaths} carries that measurement.
|
|
4499
|
+
*/
|
|
4500
|
+
function reportUntranslatableAlias(description, reason) {
|
|
4501
|
+
if (REPORTED_UNTRANSLATABLE_ALIASES.has(description)) {
|
|
4502
|
+
return;
|
|
4503
|
+
}
|
|
4504
|
+
REPORTED_UNTRANSLATABLE_ALIASES.add(description);
|
|
4505
|
+
process.stderr.write(`ttsc: the Vite alias ${description} was not forwarded to the compile, because ${reason}. Declare it in your tsconfig's "paths" if ttsc must resolve through it.\n`);
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* Collect the host's declared aliases without deciding which of them can be
|
|
4509
|
+
* expressed as `paths`.
|
|
4510
|
+
*
|
|
4511
|
+
* That decision belongs to {@link createAliasPaths} alone. It used to be split:
|
|
4512
|
+
* this function's type guard required a string `find` and dropped Vite's
|
|
4513
|
+
* `RegExp` form before `createAliasPaths` ever saw it, which left
|
|
4514
|
+
* `createAliasPaths`'s own non-string branch unreachable and put the drop
|
|
4515
|
+
* somewhere nothing could report it (samchon/ttsc#1315).
|
|
4516
|
+
*/
|
|
3895
4517
|
function normalizeAliases(aliases) {
|
|
3896
4518
|
if (Array.isArray(aliases)) {
|
|
3897
|
-
return aliases.filter(
|
|
4519
|
+
return aliases.filter(isDeclaredAlias);
|
|
3898
4520
|
}
|
|
3899
4521
|
if (typeof aliases === "object" && aliases !== null) {
|
|
3900
4522
|
return Object.entries(aliases)
|
|
@@ -3939,12 +4561,11 @@ function isRelativeSpecifier(value) {
|
|
|
3939
4561
|
value.startsWith(".\\") ||
|
|
3940
4562
|
value.startsWith("..\\"));
|
|
3941
4563
|
}
|
|
3942
|
-
function
|
|
4564
|
+
function isDeclaredAlias(value) {
|
|
3943
4565
|
return (typeof value === "object" &&
|
|
3944
4566
|
value !== null &&
|
|
3945
4567
|
"find" in value &&
|
|
3946
4568
|
"replacement" in value &&
|
|
3947
|
-
typeof value.find === "string" &&
|
|
3948
4569
|
typeof value.replacement === "string");
|
|
3949
4570
|
}
|
|
3950
4571
|
/**
|
|
@@ -3977,19 +4598,57 @@ function selectTransformedSource(props) {
|
|
|
3977
4598
|
if (source !== undefined) {
|
|
3978
4599
|
return source;
|
|
3979
4600
|
}
|
|
3980
|
-
throw new
|
|
4601
|
+
throw new TtscMissingProgramOutputError(props.file, props.tsconfig);
|
|
3981
4602
|
}
|
|
3982
4603
|
/**
|
|
3983
|
-
*
|
|
4604
|
+
* Tell the user once that a module was left untransformed, and why.
|
|
4605
|
+
*
|
|
4606
|
+
* The condition is ordinary and the build continues, but it must never be
|
|
4607
|
+
* silent: a file the program does not contain keeps whatever plugin syntax it
|
|
4608
|
+
* carries, so a typia `assert<T>()` in it becomes a runtime failure rather than
|
|
4609
|
+
* a build failure. One line per file per generation per pass, on the channel
|
|
4610
|
+
* the generation's other non-fatal diagnostics already use, so a bundle that
|
|
4611
|
+
* reaches many such files does not repeat itself per delivery.
|
|
4612
|
+
*/
|
|
4613
|
+
function reportMissingProgramOutput(cached, error, epoch) {
|
|
4614
|
+
const reported = (cached.missingOutputReported ??= new Set());
|
|
4615
|
+
if (cached.missingOutputEpoch !== epoch) {
|
|
4616
|
+
cached.missingOutputEpoch = epoch;
|
|
4617
|
+
reported.clear();
|
|
4618
|
+
}
|
|
4619
|
+
if (reported.has(error.file)) {
|
|
4620
|
+
return;
|
|
4621
|
+
}
|
|
4622
|
+
reported.add(error.file);
|
|
4623
|
+
process.stderr.write(`${error.message}
|
|
4624
|
+
`);
|
|
4625
|
+
}
|
|
4626
|
+
/**
|
|
4627
|
+
* Forward non-fatal plugin diagnostics to stderr, once per generation per pass.
|
|
3984
4628
|
*
|
|
3985
4629
|
* A `success` result may still carry warnings or informational messages from
|
|
3986
|
-
* plugins
|
|
3987
|
-
*
|
|
4630
|
+
* plugins — `@ttsc/lint` reports every rule below error severity this way.
|
|
4631
|
+
* These are surfaced via stderr rather than throwing so the build continues.
|
|
4632
|
+
* Failures and exceptions are handled by the caller.
|
|
4633
|
+
*
|
|
4634
|
+
* They describe one compile of one program, so writing them per delivery
|
|
4635
|
+
* printed the same warning once per module and scaled the noise with exactly
|
|
4636
|
+
* the reuse the cache exists to provide (samchon/ttsc#1304). A pass that reuses
|
|
4637
|
+
* a retained generation still surfaces them once, because a build's warnings
|
|
4638
|
+
* are part of what that build reports; a host with no pass boundary surfaces
|
|
4639
|
+
* them once per generation, which is the same rule with one pass.
|
|
3988
4640
|
*/
|
|
3989
|
-
function reportSuccessDiagnostics(
|
|
4641
|
+
function reportSuccessDiagnostics(cached, epoch) {
|
|
4642
|
+
const result = cached.result;
|
|
3990
4643
|
if (result.type !== "success" || result.diagnostics === undefined) {
|
|
3991
4644
|
return;
|
|
3992
4645
|
}
|
|
4646
|
+
if (cached.diagnosticsReported === true &&
|
|
4647
|
+
cached.diagnosticsEpoch === epoch) {
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
cached.diagnosticsReported = true;
|
|
4651
|
+
cached.diagnosticsEpoch = epoch;
|
|
3993
4652
|
const text = formatDiagnostics(result.diagnostics);
|
|
3994
4653
|
if (text.length !== 0) {
|
|
3995
4654
|
process.stderr.write(`${text}\n`);
|
|
@@ -4013,7 +4672,7 @@ function formatDiagnostics(diagnostics) {
|
|
|
4013
4672
|
diag.line === undefined
|
|
4014
4673
|
? undefined
|
|
4015
4674
|
: `${diag.line}:${diag.character ?? 1}`,
|
|
4016
|
-
diag.messageText,
|
|
4675
|
+
stripTerminalEscapes(diag.messageText),
|
|
4017
4676
|
]
|
|
4018
4677
|
.filter((part) => part !== undefined && part !== "")
|
|
4019
4678
|
.join(": "))
|
|
@@ -4021,15 +4680,38 @@ function formatDiagnostics(diagnostics) {
|
|
|
4021
4680
|
}
|
|
4022
4681
|
function formatUnknownError(error) {
|
|
4023
4682
|
if (error instanceof Error) {
|
|
4024
|
-
return error.message;
|
|
4683
|
+
return stripTerminalEscapes(error.message);
|
|
4025
4684
|
}
|
|
4026
4685
|
if (typeof error === "object" &&
|
|
4027
4686
|
error !== null &&
|
|
4028
4687
|
"message" in error &&
|
|
4029
4688
|
typeof error.message === "string") {
|
|
4030
|
-
return error.message;
|
|
4689
|
+
return stripTerminalEscapes(error.message);
|
|
4031
4690
|
}
|
|
4032
|
-
return String(error);
|
|
4691
|
+
return stripTerminalEscapes(String(error));
|
|
4692
|
+
}
|
|
4693
|
+
/**
|
|
4694
|
+
* Remove terminal colour and cursor sequences from text the adapter surfaces.
|
|
4695
|
+
*
|
|
4696
|
+
* An ordinary type error reaches the adapter as an `"exception"` envelope whose
|
|
4697
|
+
* `error` is the host's own rendered output, colour and all, and the envelope
|
|
4698
|
+
* carries no structured diagnostics to format instead. What the adapter hands
|
|
4699
|
+
* back is not going to a terminal: it becomes the `Error` a bundler reports, so
|
|
4700
|
+
* it lands in a Vite overlay, a webpack error report or a CI annotation, where
|
|
4701
|
+
* the escapes render as literal noise around the file and line the reader needs
|
|
4702
|
+
* (samchon/ttsc#1312).
|
|
4703
|
+
*
|
|
4704
|
+
* The colour originates in the host's rendering rather than in anything this
|
|
4705
|
+
* adapter configures, so this is the adapter-side repair, applied to every
|
|
4706
|
+
* message it surfaces rather than to one call site.
|
|
4707
|
+
*/
|
|
4708
|
+
function stripTerminalEscapes(text) {
|
|
4709
|
+
// Built from a char code so no control byte lives in this source file, and
|
|
4710
|
+
// written with `[[]` (a class holding one literal bracket) so the pattern
|
|
4711
|
+
// needs no backslash escapes to survive the string it is assembled from.
|
|
4712
|
+
const escape = String.fromCharCode(27);
|
|
4713
|
+
const controlSequence = new RegExp(escape + "[[][0-9;?]*[ -/]*[@-~]", "g");
|
|
4714
|
+
return text.replace(controlSequence, "");
|
|
4033
4715
|
}
|
|
4034
4716
|
/**
|
|
4035
4717
|
* Locate the tsconfig that should govern the transform for `file`.
|