@redue/cli 0.1.0-alpha.11 → 0.1.0-alpha.13
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/CHANGELOG.md +73 -0
- package/README.md +37 -4
- package/docs/user/agents.md +31 -14
- package/docs/user/configuration.md +17 -0
- package/docs/user/direct-typescript.md +44 -6
- package/docs/user/json.md +2 -0
- package/docs/user/pilot.md +4 -4
- package/docs/user/supported-projects.md +5 -5
- package/package.json +2 -2
- package/src/agent-integration.mjs +63 -19
- package/src/agent-policy.md +24 -66
- package/src/cached-state.mjs +24 -1
- package/src/claude-hook.mjs +59 -26
- package/src/cli.mjs +43 -19
- package/src/daemon.mjs +88 -40
- package/src/declared-plan.mjs +14 -2
- package/src/direct-run-certificate.mjs +84 -17
- package/src/direct-typescript-probe.mjs +2 -2
- package/src/direct-typescript.mjs +31 -2
- package/src/index.mjs +41 -11
- package/src/linux-inotify.py +10 -0
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +3 -15
- package/src/pnpm-inputs.mjs +3 -2
- package/src/probe-deadline.mjs +37 -0
- package/src/run-recorder.cjs +15 -2
- package/src/typescript-input-facts.mjs +32 -8
- package/src/typescript-list.mjs +28 -7
- package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/src/daemon.mjs
CHANGED
|
@@ -8,18 +8,20 @@ import {Worker} from 'node:worker_threads';
|
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import {subscribe,hasHistory,backend} from './platform-observation.mjs';
|
|
10
10
|
import {controlEndpoint} from './control-endpoint.mjs';
|
|
11
|
-
import {observedRelative,withinPath,realObservedPath} from './path-identity.mjs';
|
|
11
|
+
import {observedRelative,withinPath,realObservedPath,racyWindowMs} from './path-identity.mjs';
|
|
12
12
|
import micromatch from './glob.mjs';
|
|
13
13
|
import {sha} from './plan.mjs';
|
|
14
|
-
import {InputIndex} from './index.mjs';
|
|
14
|
+
import {InputIndex,dehydrateIndex} from './index.mjs';
|
|
15
15
|
import {permittedRoots} from './installed-inputs.mjs';
|
|
16
16
|
import {assertOwnedStatePlacement} from './owned-state.mjs';
|
|
17
17
|
import {atomicJson,readReceipts,readReceiptSnapshot,receiptRevision} from './state-store.mjs';
|
|
18
18
|
import {history,closeHistory} from './observation.mjs';
|
|
19
19
|
import {inputKey,planGuard,toolchainKey} from './decision-validation.mjs';
|
|
20
|
-
import {queriesHold} from './typescript-list.mjs';
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
20
|
+
import {queriesHold,runFactReason as runFactChanged} from './typescript-list.mjs';
|
|
21
|
+
import {nextProbeHint,probeDeadline,probeFloorMs} from './probe-deadline.mjs';
|
|
22
|
+
import {statusSummary} from './cached-state.mjs';
|
|
23
|
+
import {directProject,directProbeValue,reviewedTypeScriptVersions} from './direct-typescript.mjs';
|
|
24
|
+
import {buildInfoPath,recordedCertificate} from './direct-run-certificate.mjs';
|
|
23
25
|
import {timing,mark,traceBase,traceContext,traceScope,traceEnvironment,traceId} from './decision-profile.mjs';
|
|
24
26
|
|
|
25
27
|
const configFile=path.resolve(process.argv[2]);
|
|
@@ -152,6 +154,14 @@ function reuseCertificate(row,key,allowReuse,forceFull,checked) {
|
|
|
152
154
|
metrics.probeReuses=(metrics.probeReuses||0)+1;
|
|
153
155
|
return null;
|
|
154
156
|
}
|
|
157
|
+
// The current full-probe deadline for a check; see probe-deadline.mjs. Hints
|
|
158
|
+
// are kept per check name, so a plan rebuild (each Windows decision) keeps them.
|
|
159
|
+
const probeHints=new Map();
|
|
160
|
+
function fullProbeDeadline(row) {
|
|
161
|
+
const run=receipts[row.plan.name]?.invocation;
|
|
162
|
+
return probeDeadline({floor:probeFloorMs(row.plan.qualification),hint:probeHints.get(row.plan.name),
|
|
163
|
+
runMs:run?.status==='exited'?run.durationMs:0});
|
|
164
|
+
}
|
|
155
165
|
function probe(row,allowReuse=false,{launch=true}={}) {
|
|
156
166
|
const started=performance.now();
|
|
157
167
|
const supported=new Map([
|
|
@@ -204,18 +214,21 @@ function probe(row,allowReuse=false,{launch=true}={}) {
|
|
|
204
214
|
row.probeCache=null;
|
|
205
215
|
}
|
|
206
216
|
const values=[];let checkpoint=null;
|
|
217
|
+
// The slowest of this check's probes sets the hint for all of them.
|
|
218
|
+
const deadline=fullProbeDeadline(row);let hint=0;
|
|
219
|
+
const learn=()=>{if(hint)probeHints.set(row.plan.name,hint);};
|
|
207
220
|
for(const argv of row.plan.probes) {
|
|
208
|
-
|
|
209
|
-
// Native NTFS runs have exceeded the generic 5 s cap under load, despite
|
|
210
|
-
// completing normally and with the duplicated work already removed.
|
|
211
|
-
const timeout=row.plan.qualification==='pnpm-tsc-v1'||
|
|
212
|
-
(process.platform==='win32'&&['typescript-noemit-v1','npm-typescript-direct-v1','yarn-workspace-tsc-v1']
|
|
213
|
-
.includes(row.plan.qualification))?10000:5000;
|
|
221
|
+
const timeout=deadline,launched=performance.now();
|
|
214
222
|
mark('daemon.probe_launch',{mode:'full',reason:fullReason,deadlineMs:timeout});
|
|
215
|
-
|
|
223
|
+
// A built-in probe gets its deadline, so its inner compiler calls share it.
|
|
224
|
+
const out=spawnSync(argv[0],[...argv.slice(1),...(cacheable?['--redue-checkpoint']:[]),
|
|
225
|
+
...(builtIn?[`--redue-deadline=${timeout}`]:[])],{cwd:row.plan.cwd,
|
|
216
226
|
timeout,maxBuffer:1024*1024,stdio:['ignore','pipe','pipe'],env:traceEnvironment()});
|
|
217
227
|
mark('daemon.probe_complete',{mode:'full',childPid:out.pid,exit:out.status,signal:out.signal,code:out.error?.code});
|
|
228
|
+
const timedOut=out.error?.code==='ETIMEDOUT';
|
|
229
|
+
if(!out.error||timedOut)hint=Math.max(hint,nextProbeHint({durationMs:performance.now()-launched,deadlineMs:timeout,timedOut}));
|
|
218
230
|
if(out.error||out.signal||out.status!==0) {
|
|
231
|
+
learn();
|
|
219
232
|
if(!row.probeError){row.revision++;row.lastReason='declared state probe became unavailable';}
|
|
220
233
|
row.probeHash=null;row.probeAt=0;
|
|
221
234
|
const code=['typescript-noemit-v1','npm-typescript-direct-v1','yarn-workspace-tsc-v1','pnpm-tsc-v1']
|
|
@@ -223,7 +236,7 @@ function probe(row,allowReuse=false,{launch=true}={}) {
|
|
|
223
236
|
/VSTATE_REASON:([a-z-]+)/.exec(out.stderr?.toString()||'')?.[1]:null;
|
|
224
237
|
const explanations={
|
|
225
238
|
'direct-typescript-implementation-unreviewed':'installed TypeScript bytes or membership differ from the reviewed compiler',
|
|
226
|
-
'direct-typescript-version-unsupported'
|
|
239
|
+
'direct-typescript-version-unsupported':`direct compiler recipe supports reviewed TypeScript ${reviewedTypeScriptVersions.join(', ')} only`,
|
|
227
240
|
'direct-execution-environment-unsupported':'direct compiler rejects development helpers, Node cache/preloads, and unreviewed runtime overrides',
|
|
228
241
|
'direct-node-version-unsupported':'direct compiler recipe supports Node 22 and 24',
|
|
229
242
|
'npm-launcher-unobserved-inputs':'npm launcher uses unobserved notifier inputs; execution is recording-only',
|
|
@@ -232,7 +245,11 @@ function probe(row,allowReuse=false,{launch=true}={}) {
|
|
|
232
245
|
'typescript-executable-changed':'local TypeScript executable changed or is missing',
|
|
233
246
|
'typescript-config-unavailable':'TypeScript configuration cannot be resolved',
|
|
234
247
|
'typescript-config-graph-unsupported':'TypeScript configuration extends, references, or uses compiler features outside this contract',
|
|
235
|
-
'typescript-incremental-unsupported':'TypeScript
|
|
248
|
+
'typescript-incremental-unsupported':'TypeScript composite compilation needs explicit review',
|
|
249
|
+
'typescript-buildinfo-outside-repository':'TypeScript incremental build-info file is outside the observed repository',
|
|
250
|
+
'typescript-incremental-unsound':'TypeScript assumeChangesOnlyAffectDirectDependencies makes an incremental result depend on build history',
|
|
251
|
+
'typescript-buildinfo-name-unsupported':'TypeScript build-info file needs a plain name ending in .tsbuildinfo',
|
|
252
|
+
'typescript-project-reference-consumed':'TypeScript program uses files of a referenced project, which plain tsc does not build',
|
|
236
253
|
'typescript-config-outside-repository':'TypeScript configuration extends outside the observed repository',
|
|
237
254
|
'typescript-plugin-unsupported':'TypeScript compiler plugin needs explicit review',
|
|
238
255
|
'typescript-input-list-unavailable':'TypeScript input listing did not complete',
|
|
@@ -275,6 +292,7 @@ function probe(row,allowReuse=false,{launch=true}={}) {
|
|
|
275
292
|
}
|
|
276
293
|
values.push([argv,out.status,sha(cacheable?checkpoint.output:out.stdout),sha(out.stderr)]);
|
|
277
294
|
}
|
|
295
|
+
learn();
|
|
278
296
|
const value=sha(JSON.stringify(values));
|
|
279
297
|
if((row.probeHash!==null&&row.probeHash!==value)||row.probeError){
|
|
280
298
|
row.revision++;row.lastReason='declared state probe changed or recovered';}
|
|
@@ -319,7 +337,8 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
319
337
|
plan_id:row.plan.id,observed_at:lastObservation,
|
|
320
338
|
invocation:r?.invocation||null,target_provenance:r?.target||null,
|
|
321
339
|
...(r?.verificationRecipe?{verification_recipe:r.verificationRecipe}:{}),
|
|
322
|
-
coverage_at_run:r?.coverage||null,declared_inputs_match:null,reuse_eligible:false
|
|
340
|
+
coverage_at_run:r?.coverage||null,declared_inputs_match:null,reuse_eligible:false,
|
|
341
|
+
...(r?.runFacts?.length?{run_facts:r.runFacts.length}:{})};
|
|
323
342
|
if(receiptStateError){item.reason=receiptStateError;return item;}
|
|
324
343
|
if(!healthy||planning||recovering||index.unavailable){item.reason=
|
|
325
344
|
(planning||recovering)?'input observation reconciliation pending':
|
|
@@ -330,7 +349,8 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
330
349
|
if(pendingChecks.has(name)){item.reason='filesystem change pending content inspection';return item;}
|
|
331
350
|
if(r&&r.planId) item.declared_inputs_match=r.planId===row.plan.id&&
|
|
332
351
|
r.fingerprint===combinedSummary(name).fingerprint;
|
|
333
|
-
if(r
|
|
352
|
+
if(r&&!(r.fingerprintVersion===1||r.fingerprintVersion===2&&Array.isArray(r.runFacts))){
|
|
353
|
+
item.reason='receipt uses earlier input-resolution format';return item;}
|
|
334
354
|
if(row.plan.unresolved.length){
|
|
335
355
|
const checkpoint=item.declared_inputs_match===false?
|
|
336
356
|
r.planId!==row.plan.id?'; observed input plan changed':
|
|
@@ -340,6 +360,9 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
340
360
|
return item;
|
|
341
361
|
}
|
|
342
362
|
if(row.probeError){item.reason=row.probeError;return item;}
|
|
363
|
+
// The plan leaves out the build-info file it saw at start; the probe sees the current one.
|
|
364
|
+
if(row.probeCache?.layout&&(row.probeCache.layout.buildInfo||null)!==(row.plan.buildInfo||null)){
|
|
365
|
+
item.reason='the incremental build-info setting changed after the observer started; restart the observer';return item;}
|
|
343
366
|
// An expired probe or missing execution context cannot confirm CURRENT, but
|
|
344
367
|
// observed input content that already differs from the receipt is STALE.
|
|
345
368
|
let unconfirmed=!row.probeAt||Date.now()-row.probeAt>10000?'state probe observation expired':null;
|
|
@@ -396,11 +419,24 @@ function rows(contextHashes,synchronizedInstalled=false,synchronizedFilesystem=f
|
|
|
396
419
|
if(unconfirmed&&!(r.files&&row.staleInputs?.length)){item.freshness='UNVERIFIED';
|
|
397
420
|
item.reason=unconfirmed;item.changed_inputs=[];}
|
|
398
421
|
} else if(unconfirmed){item.freshness='UNVERIFIED';item.reason=unconfirmed;
|
|
422
|
+
// Lookups the run made outside the certificate are asked again for every
|
|
423
|
+
// answer, cached or synchronized, before it can say CURRENT.
|
|
424
|
+
// A direct run recorded before run facts may have made lookups no probe
|
|
425
|
+
// asks; only Windows, which never records direct runs, keeps reusing them.
|
|
426
|
+
} else if(r.fingerprintVersion===1&&r.verificationRecipe==='npm-direct-typescript@1'&&process.platform!=='win32'){
|
|
427
|
+
item.freshness='UNVERIFIED';item.reason='receipt predates run facts; run the check again';
|
|
428
|
+
} else if(r.runFacts&&(item.reason=runFactReason(r.runFacts))){item.changed_inputs=[];
|
|
399
429
|
} else {item.freshness='CURRENT';item.reason=result==='FAIL'?'last command failed':'declared inputs match';
|
|
400
430
|
item.reuse_eligible=result==='PASS';}
|
|
401
431
|
return item;
|
|
402
432
|
});
|
|
403
433
|
}
|
|
434
|
+
// Null when every run fact in a receipt answers as it did during the run.
|
|
435
|
+
function runFactReason(facts) {
|
|
436
|
+
const started=performance.now(),reason=runFactChanged(facts);
|
|
437
|
+
timing('daemon.run_facts',started,{facts:facts.length,changed:reason?1:0});
|
|
438
|
+
return reason;
|
|
439
|
+
}
|
|
404
440
|
// The indexed inputs already differ from the latest receipt, so no probe result
|
|
405
441
|
// can make this check CURRENT. A sync answers STALE without a full probe. A run
|
|
406
442
|
// probes, or builds the certificate from its recording (adopt), and a revert
|
|
@@ -439,7 +475,7 @@ function adopt(name,row,token) {
|
|
|
439
475
|
return done('inputs or certificate changed during the run');
|
|
440
476
|
const cache=pending.cache,argv=row.plan.probes[0];
|
|
441
477
|
let result;
|
|
442
|
-
try{result=recordedCertificate(text,cache,directProject(argv[2],argv[3],{verify:false}));}
|
|
478
|
+
try{result=recordedCertificate(text,cache,directProject(argv[2],argv[3],{verify:false}),buildInfoPath(row.plan));}
|
|
443
479
|
catch{return done('recording unreadable');}
|
|
444
480
|
if(result.issue)return done(result.issue);
|
|
445
481
|
const identities=new Map(cache.identities);
|
|
@@ -454,14 +490,11 @@ function status(contextHashes,synchronizedInstalled=false,synchronizedFilesystem
|
|
|
454
490
|
// Reload notifications accelerate visibility but are not required for it: a
|
|
455
491
|
// wrapper may persist its outcome even when its control connection is denied.
|
|
456
492
|
refreshReceipts();
|
|
457
|
-
const checks=rows(contextHashes,synchronizedInstalled,synchronizedFilesystem)
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const stateName=failed?'failed':unverified?'unverified':stale?'stale':'current';
|
|
463
|
-
const now=Date.now();return {schema:1,receipt_revision:receiptStateRevision,state:stateName,current,failed,observed_failed:observedFailed,
|
|
464
|
-
stale,unverified,checks,
|
|
493
|
+
const checks=rows(contextHashes,synchronizedInstalled,synchronizedFilesystem);
|
|
494
|
+
// A decision may run every check's probes in turn: callers wait for all of them.
|
|
495
|
+
const probing=[...(index?.checks.values()||[])].filter(row=>row.plan.probes.length);
|
|
496
|
+
const now=Date.now();return {schema:1,receipt_revision:receiptStateRevision,...statusSummary(checks),checks,
|
|
497
|
+
...(probing.length?{probe_deadline_ms:probing.reduce((sum,row)=>sum+fullProbeDeadline(row)*row.plan.probes.length,0)}:{}),
|
|
465
498
|
observation:{healthy:healthy&&!planning&&!recovering&&!index?.unavailable,
|
|
466
499
|
generation,observed_at:lastObservation,
|
|
467
500
|
events:(index?.eventCount||0)+[...external.values()].reduce((n,e)=>n+(e.index?.eventCount||0),0),
|
|
@@ -496,16 +529,10 @@ function hydrate(bundle,saved) {
|
|
|
496
529
|
}
|
|
497
530
|
next.eventCount=saved.eventCount;next.rehashedFiles=saved.rehashedFiles;
|
|
498
531
|
next.rehashedBytes=saved.rehashedBytes;next.profile=saved.profile;
|
|
532
|
+
next.racy=new Map(saved.racy||[]);
|
|
499
533
|
return next;
|
|
500
534
|
}
|
|
501
|
-
|
|
502
|
-
return {files:[...value.files],
|
|
503
|
-
aliasDependencies:[...value.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
|
|
504
|
-
rows:[...value.checks].map(([name,row])=>({name,files:[...row.files],
|
|
505
|
-
sum:row.sum.toString('hex'),revision:row.revision,lastReason:row.lastReason})),
|
|
506
|
-
eventCount:value.eventCount,rehashedFiles:value.rehashedFiles,
|
|
507
|
-
rehashedBytes:value.rehashedBytes,profile:value.profile};
|
|
508
|
-
}
|
|
535
|
+
const dehydrate=dehydrateIndex;
|
|
509
536
|
function retainIndex() {
|
|
510
537
|
if(!index||!healthy||planning||pending.size||planDirty||historyDisabled||
|
|
511
538
|
!fs.existsSync(snapshotPath))return;
|
|
@@ -958,7 +985,7 @@ async function request(message) {
|
|
|
958
985
|
mark('server.decision_start',{decision,pending:pending.size,planDirty,planning,recovering,healthy,historyDisabled,decisionRequests,historyDeadlineMs,reconciliationDeadlineMs});
|
|
959
986
|
timing('daemon.decision_reason',totalStarted,{decision,action:message.action,
|
|
960
987
|
plan:process.platform==='win32'?'validate_dependencies_before_reuse':'existing_plan',
|
|
961
|
-
probe:'reuse_only_with_identical_content_and_context',installed:'
|
|
988
|
+
probe:'reuse_only_with_identical_content_and_context',installed:'current_identity_required',
|
|
962
989
|
planPhase:metrics.planPhase||'initializing',pending:pending.size,
|
|
963
990
|
notifications:metrics.notifications,planRebuilds:metrics.planRebuilds});
|
|
964
991
|
if(process.platform==='win32'&&process.env.VSTATE_TEST_FAULTS==='1'&&
|
|
@@ -1065,6 +1092,20 @@ async function request(message) {
|
|
|
1065
1092
|
}
|
|
1066
1093
|
timing('daemon.request_probes',probeStarted,{checks:index?.checks.size||0});
|
|
1067
1094
|
lastObservation=Date.now();publish();
|
|
1095
|
+
// A run checkpoint never holds an installed identity from the racy window:
|
|
1096
|
+
// a same-size write in the same timestamp tick would keep it. Wait the
|
|
1097
|
+
// window out (at most 2 s), record those files again, and mark the
|
|
1098
|
+
// checkpoint unusable if one is still inside it.
|
|
1099
|
+
let racy=false;
|
|
1100
|
+
if(message.action==='snapshot'||message.action==='prelaunch'){
|
|
1101
|
+
const names=message.name?[message.name]:null;
|
|
1102
|
+
const indexes=()=>[index,...[...external.values()].map(e=>e.index)].filter(Boolean);
|
|
1103
|
+
const wait=Math.min(racyWindowMs,Math.max(0,...indexes().map(i=>i.racyUntil(names)))-Date.now());
|
|
1104
|
+
if(wait>0){metrics.racyWaits=(metrics.racyWaits||0)+1;await new Promise(resolve=>setTimeout(resolve,wait+5));}
|
|
1105
|
+
for(const i of indexes())i.settleRacy();
|
|
1106
|
+
racy=indexes().some(i=>i.racyUntil(names)>Date.now());
|
|
1107
|
+
if(racy)metrics.racyCheckpoints=(metrics.racyCheckpoints||0)+1;
|
|
1108
|
+
}
|
|
1068
1109
|
const applicabilityStarted=performance.now();
|
|
1069
1110
|
const data=status(message.contextHashes,synchronizedInstalled,
|
|
1070
1111
|
process.platform==='win32'&&healthy&&!planning&&!planDirty);
|
|
@@ -1075,18 +1116,16 @@ async function request(message) {
|
|
|
1075
1116
|
planGeneration:row.plan.id,
|
|
1076
1117
|
inputEventSerial:inputEventSerial.get(name)||0,
|
|
1077
1118
|
probeHash:row.probeHash,inputKey:inputKey(row),observationGaps:metrics.gaps,
|
|
1078
|
-
observationHealthy:healthy&&!planning&&!recovering&&applicableExternal(name).every(e=>e.healthy)};
|
|
1119
|
+
observationHealthy:healthy&&!planning&&!recovering&&!racy&&applicableExternal(name).every(e=>e.healthy)};
|
|
1079
1120
|
// Export only a currently guarded, check-specific discovery certificate.
|
|
1080
1121
|
// The caller compares it with the answers its own compile received. Schema 2
|
|
1081
1122
|
// certifies the realpath of every directory readDirectory visits. The
|
|
1082
1123
|
// snapshot stays the start of the observed interval, including validation.
|
|
1083
1124
|
if(message.runCertificate===1&&continuityBarrier()){
|
|
1084
1125
|
const row=index?.checks.get(message.name),cache=row?.probeCache;
|
|
1085
|
-
//
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
data.snapshots[message.name].recordingAccepted=true;
|
|
1089
|
-
else if(row?.plan.qualification==='npm-typescript-direct-v1'&&
|
|
1126
|
+
// Also after an accepted recording: the caller compares its record with
|
|
1127
|
+
// this certificate and finds its run facts with it.
|
|
1128
|
+
if(row?.plan.qualification==='npm-typescript-direct-v1'&&
|
|
1090
1129
|
!row.plan.unresolved.length&&cache?.schema===1&&cache.input===inputKey(row)&&
|
|
1091
1130
|
cache.hash===row.probeHash&&cache.guard===planGuard(index.bundle)&&
|
|
1092
1131
|
cache.output&&cache.queryData&&data.snapshots[message.name])
|
|
@@ -1095,6 +1134,15 @@ async function request(message) {
|
|
|
1095
1134
|
context:cache.context,output:cache.output,stderrHash:cache.stderrHash,
|
|
1096
1135
|
queryData:cache.queryData};
|
|
1097
1136
|
}
|
|
1137
|
+
// For a run checked by full probes: the last probe's questions, only to
|
|
1138
|
+
// find the run's lookups outside them. Never evidence for reuse.
|
|
1139
|
+
if(message.runFacts===1&&data.snapshots[message.name]){
|
|
1140
|
+
const row=index?.checks.get(message.name),cache=row?.probeCache;
|
|
1141
|
+
if(row?.plan.qualification==='npm-typescript-direct-v1'&&cache?.queryData&&
|
|
1142
|
+
typeof cache.context==='string'&&cache.input===inputKey(row)&&cache.hash===row.probeHash&&
|
|
1143
|
+
cache.guard===planGuard(index.bundle))
|
|
1144
|
+
data.snapshots[message.name].probeQuestions={context:cache.context,queryData:cache.queryData};
|
|
1145
|
+
}
|
|
1098
1146
|
const row=index?.checks.get(message.name);
|
|
1099
1147
|
if(adopting.has(message.name)&&row?.adoption&&data.snapshots[message.name])
|
|
1100
1148
|
data.snapshots[message.name].recordedCertificate=row.adoption.token;
|
package/src/declared-plan.mjs
CHANGED
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import {createHash} from 'node:crypto';
|
|
6
6
|
import {permittedRoots,resolveDeclaredInstalled} from './installed-inputs.mjs';
|
|
7
7
|
import {qualifyTypeScript} from './typescript-qualification.mjs';
|
|
8
|
+
import {directBuildInfo} from './direct-typescript.mjs';
|
|
8
9
|
import {yarnWorkspaceTypecheckInputs} from './yarn-workspace-inputs.mjs';
|
|
9
10
|
import {pnpmTypecheckInputs} from './pnpm-inputs.mjs';
|
|
10
11
|
import {observedRelative,withinPath,realObservedPath} from './path-identity.mjs';
|
|
@@ -16,6 +17,8 @@ const safePattern=value=>{
|
|
|
16
17
|
const raw=value.startsWith('!')?value.slice(1):value;
|
|
17
18
|
if(!raw||path.isAbsolute(raw)||raw.split(/[\\/]/).includes('..'))
|
|
18
19
|
throw Error(`unsafe repository-relative input pattern: ${value}`);
|
|
20
|
+
// The observer never watches .git directories; excluding one is fine.
|
|
21
|
+
if(raw===value&&raw.split(/[\\/]/).includes('.git'))throw Error(`input pattern names a .git directory, which is not observed: ${value}`);
|
|
19
22
|
return value;
|
|
20
23
|
};
|
|
21
24
|
|
|
@@ -65,14 +68,22 @@ export function discoverDeclared(config) {
|
|
|
65
68
|
catch(e){workspaceIssue=e.message;}
|
|
66
69
|
}
|
|
67
70
|
timing('plan.workspace_inputs',workspaceStarted,{available:workspace?1:0});
|
|
71
|
+
// An incremental direct check writes its build-info file during each run.
|
|
72
|
+
// It is compiler state, not an input, so the plan leaves it out. Read when
|
|
73
|
+
// the plan is built: a later change to it needs an observer restart, and
|
|
74
|
+
// until then the run reports the unexpected write and is not reusable.
|
|
75
|
+
let buildInfo=null;
|
|
76
|
+
if(selected.qualification==='npm-typescript-direct-v1')
|
|
77
|
+
try{buildInfo=directBuildInfo(root,selected.script);}catch{}
|
|
78
|
+
const excluded=buildInfo?['!'+buildInfo]:[];
|
|
68
79
|
const installedStarted=performance.now();
|
|
69
80
|
const installed=resolveDeclaredInstalled(root,
|
|
70
|
-
[...(selected.installedInputs||[]),...(workspace?.installed||[])],allowedRoots);
|
|
81
|
+
[...(selected.installedInputs||[]),...(workspace?.installed||[]),...excluded],allowedRoots);
|
|
71
82
|
timing('plan.installed_resolution',installedStarted,{mappings:installed.mappings.length});
|
|
72
83
|
const automatic=qualifyTypeScript(root,selected,
|
|
73
84
|
{workspace,issue:workspaceIssue});
|
|
74
85
|
const sourcePatterns=[...new Set([...selected.inputs,...(workspace?.source||[]),
|
|
75
|
-
...(workspace?.generated||[])])].sort();
|
|
86
|
+
...(workspace?.generated||[]),...excluded])].sort();
|
|
76
87
|
const additivePatterns=[...new Set([...(selected.generatedInputs||[]),
|
|
77
88
|
...installed.internalPatterns])].sort();
|
|
78
89
|
const unresolved=[...installed.unresolved,...(automatic?.issues||[]),
|
|
@@ -113,6 +124,7 @@ export function discoverDeclared(config) {
|
|
|
113
124
|
installedMappings:installed.mappings,effectiveEnvironmentGlobs:{},
|
|
114
125
|
probes:selected.probes||[],environment:selected.environment||{},
|
|
115
126
|
qualification:selected.qualification||null,
|
|
127
|
+
...(buildInfo?{buildInfo}:{}),
|
|
116
128
|
executionProvenance:'declared-command',
|
|
117
129
|
unresolved:[...new Set(unresolved)].sort()};
|
|
118
130
|
plan.resolutionCandidates=workspace?.resolutionCandidates||[];
|
|
@@ -10,12 +10,20 @@ import {timing} from './decision-profile.mjs';
|
|
|
10
10
|
import {directCompiler,directContextHash,directEnvironmentIssue,directProbeOutput,directProbeValue} from './direct-typescript.mjs';
|
|
11
11
|
import {withinPath} from './path-identity.mjs';
|
|
12
12
|
const hash=value=>createHash('sha256').update(value).digest('hex');
|
|
13
|
-
|
|
13
|
+
// Whether a direct run is recorded at all: on macOS and Linux, always, so its
|
|
14
|
+
// run facts are known even when full probes check it.
|
|
15
|
+
export function directRunRecordable(selected) {
|
|
14
16
|
return ['darwin','linux'].includes(process.platform)&&selected.qualification==='npm-typescript-direct-v1'&&
|
|
15
17
|
selected.probes?.length===1&&
|
|
16
|
-
selected.probes[0][1]===fileURLToPath(new URL('direct-typescript-probe.mjs',import.meta.url))
|
|
18
|
+
selected.probes[0][1]===fileURLToPath(new URL('direct-typescript-probe.mjs',import.meta.url));
|
|
19
|
+
}
|
|
20
|
+
// Whether a recorded run may replace the probes (tests can force full probes).
|
|
21
|
+
export function directRunCertificateAllowed(selected) {
|
|
22
|
+
return directRunRecordable(selected)&&
|
|
17
23
|
!(process.env.VSTATE_TEST_FAULTS==='1'&&process.env.VSTATE_TEST_FULL_RUN_PROBES==='1');
|
|
18
24
|
}
|
|
25
|
+
// The incremental build-info file the plan leaves out of the inputs, if any.
|
|
26
|
+
export const buildInfoPath=plan=>typeof plan?.buildInfo==='string'?path.join(plan.root,plan.buildInfo):null;
|
|
19
27
|
// The start snapshot's discovery certificate, checked without a child process.
|
|
20
28
|
// Returns what a recorded run is compared against, or null for full probes.
|
|
21
29
|
export function directRunCertificate(plan,selected,snapshot,root) {
|
|
@@ -26,7 +34,7 @@ export function directRunCertificate(plan,selected,snapshot,root) {
|
|
|
26
34
|
c.planId!==snapshot.planId||c.cwd!==plan.cwd||
|
|
27
35
|
c.probeDefinition!==hash(JSON.stringify(selected.probes))||
|
|
28
36
|
![c.output,c.context,c.stderrHash].every(s=>typeof s==='string'&&/^[a-f0-9]{64}$/.test(s))||
|
|
29
|
-
typeof c.queryData!=='string'||c.queryData.length>
|
|
37
|
+
typeof c.queryData!=='string'||c.queryData.length>1024*1024)
|
|
30
38
|
return fallback('certificate_unavailable');
|
|
31
39
|
// The compile runs in this process's environment; the daemon's is never substituted.
|
|
32
40
|
if(directEnvironmentIssue())return fallback('environment_unsupported');
|
|
@@ -41,9 +49,21 @@ export function directRunCertificate(plan,selected,snapshot,root) {
|
|
|
41
49
|
const queries=JSON.parse(inflateSync(Buffer.from(c.queryData,'base64'),{maxOutputLength:8*1024*1024}));
|
|
42
50
|
if(!Array.isArray(queries)||!queries.length)return fallback('certificate_invalid');
|
|
43
51
|
timing('cli.run_certificate',started,{outcome:'available',queries:queries.length});
|
|
44
|
-
return {value,queries,context:c.context,compiler};
|
|
52
|
+
return {value,queries,context:c.context,compiler,buildInfo:buildInfoPath(plan)};
|
|
45
53
|
}catch{return fallback('certificate_invalid');}
|
|
46
54
|
}
|
|
55
|
+
// The last probe's questions, sent for a run checked by full probes. They only
|
|
56
|
+
// show which of the run's lookups are run facts; the full probes, not these,
|
|
57
|
+
// establish the result.
|
|
58
|
+
export function probeQuestions(plan,selected,snapshot,root) {
|
|
59
|
+
const q=snapshot?.probeQuestions;
|
|
60
|
+
if(!directRunRecordable(selected)||typeof q?.queryData!=='string'||
|
|
61
|
+
q.queryData.length>1024*1024||typeof q.context!=='string')return null;
|
|
62
|
+
try{const queries=JSON.parse(inflateSync(Buffer.from(q.queryData,'base64'),{maxOutputLength:8*1024*1024}));
|
|
63
|
+
if(!Array.isArray(queries)||!queries.length)return null;
|
|
64
|
+
return {queries,context:q.context,compiler:directCompiler(root,{verify:false}),buildInfo:buildInfoPath(plan)};}
|
|
65
|
+
catch{return null;}
|
|
66
|
+
}
|
|
47
67
|
// Without a start certificate, the daemon may offer to build one from this run's
|
|
48
68
|
// recording at the end checkpoint. Returns its token, or null for full probes.
|
|
49
69
|
export function directRunRecording(plan,selected,snapshot,root) {
|
|
@@ -93,6 +113,18 @@ function statAgrees([kind,file,answer,options],target,result) {
|
|
|
93
113
|
}
|
|
94
114
|
const withinCompiler=compiler=>file=>withinPath(file,compiler.tsRoot)||
|
|
95
115
|
file!==compiler.entry&&file.toLowerCase()===compiler.entry.toLowerCase();
|
|
116
|
+
// The certificate question that asks again what a run's stat, realpath or
|
|
117
|
+
// package.json read answered, in queryAnswer's terms; null when it cannot be asked again.
|
|
118
|
+
function runFact(kind,file,answer) {
|
|
119
|
+
if(kind==='stat')return answer===null||answer==='f'?['fileExists',file,answer&&true]:
|
|
120
|
+
answer==='d'?['directoryExists',file,true]:null;
|
|
121
|
+
if(kind==='realpath')return answer===null||typeof answer==='string'?['realpath',file,answer]:null;
|
|
122
|
+
// Declaration checks read the package.json of each dependency they resolve.
|
|
123
|
+
// Its content hash is asked again like any other run fact; other reads are not.
|
|
124
|
+
if(kind==='read'&&path.basename(file)==='package.json')
|
|
125
|
+
return answer===null||typeof answer==='string'?['readFile',file,answer]:null;
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
96
128
|
// Null when every answer the run received agrees with the certified state,
|
|
97
129
|
// otherwise why not. The compiler is deterministic given its context and its
|
|
98
130
|
// answers, so a run whose answers all agree computed the certified result.
|
|
@@ -100,17 +132,36 @@ const withinCompiler=compiler=>file=>withinPath(file,compiler.tsRoot)||
|
|
|
100
132
|
// compiler tree are covered by the daemon's toolchain identity instead. tsc
|
|
101
133
|
// also stats a case-swapped copy of its own path to detect case sensitivity;
|
|
102
134
|
// only that one stat is exempt.
|
|
103
|
-
|
|
135
|
+
// With a `facts` array, a stat or realpath the probe never asked about is
|
|
136
|
+
// added to it instead (declaration diagnostics resolve module paths that
|
|
137
|
+
// building the program does not), as is a read of a package.json. The receipt
|
|
138
|
+
// keeps those run facts, and status asks them again before every CURRENT
|
|
139
|
+
// answer. Any other uncovered read, and any directory listing, is an issue.
|
|
140
|
+
// The record's facts without the build-info file an incremental run reads and
|
|
141
|
+
// writes, or {issue} when the run wrote anything else. That file is compiler
|
|
142
|
+
// state, not an input. A directory tsc created for it was missing when tsc
|
|
143
|
+
// checked it, so that check goes too; every other check stays.
|
|
144
|
+
export function withoutBuildInfo(facts,buildInfo) {
|
|
145
|
+
const created=new Set(facts.filter(([kind])=>kind==='mkdir').map(([,file])=>file));
|
|
146
|
+
for(const [kind,file] of facts){
|
|
147
|
+
if(kind==='write'&&file!==buildInfo)return {issue:`the compiler wrote ${file}`};
|
|
148
|
+
if(kind==='mkdir'&&!buildInfo?.startsWith(file+path.sep))return {issue:`the compiler created ${file}`};
|
|
149
|
+
}
|
|
150
|
+
return {facts:facts.filter(([kind,file])=>kind!=='write'&&kind!=='mkdir'&&file!==buildInfo&&
|
|
151
|
+
!(kind==='stat'&&created.has(file)))};
|
|
152
|
+
}
|
|
153
|
+
export function runRecordIssue(record,certificate,runFacts=null) {
|
|
104
154
|
if(record?.schema!==1||!Array.isArray(record.facts))return 'record unavailable';
|
|
105
155
|
if(record.unsupported)return `unsupported ${record.unsupported}`;
|
|
106
156
|
if(record.conflict)return `answer changed during the run: ${record.conflict}`;
|
|
107
|
-
const {compiler,queries}=certificate;
|
|
157
|
+
const {compiler,queries}=certificate,kept=withoutBuildInfo(record.facts,certificate.buildInfo||null);
|
|
158
|
+
if(kept.issue)return kept.issue;
|
|
108
159
|
if(record.main!==compiler.entry||!Array.isArray(record.node)||
|
|
109
160
|
directContextHash(compiler,record.node)!==certificate.context)return 'execution context changed';
|
|
110
161
|
const inCompiler=withinCompiler(compiler);
|
|
111
162
|
const facts=new Map();
|
|
112
163
|
for(const query of queries){if(!facts.has(query[1]))facts.set(query[1],[]);facts.get(query[1]).push(query);}
|
|
113
|
-
for(const [kind,file,answer] of
|
|
164
|
+
for(const [kind,file,answer] of kept.facts){
|
|
114
165
|
if(inCompiler(file))continue;
|
|
115
166
|
const known=facts.get(file)||[];
|
|
116
167
|
let checks=[];
|
|
@@ -123,40 +174,54 @@ export function runRecordIssue(record,certificate) {
|
|
|
123
174
|
else if(kind==='stat')checks=[...known,...(facts.get(path.dirname(file))||[])]
|
|
124
175
|
.map(q=>statAgrees(q,file,answer)).filter(value=>value!==null);
|
|
125
176
|
else return `unknown answer ${kind}`;
|
|
126
|
-
if(!checks.some(value=>value!==undefined))
|
|
177
|
+
if(!checks.some(value=>value!==undefined)){
|
|
178
|
+
const fact=runFacts&&runFact(kind,file,answer);
|
|
179
|
+
if(fact){runFacts.push(fact);continue;}
|
|
180
|
+
return `${kind} ${file} is not in the discovery certificate`;
|
|
181
|
+
}
|
|
127
182
|
if(checks.includes(false))return `${kind} ${file} differs from the discovery certificate`;
|
|
183
|
+
// Covered only by a listing of its directory: a later probe may keep the
|
|
184
|
+
// same program while that listing changes, so the run's own lookup stays
|
|
185
|
+
// a run fact.
|
|
186
|
+
if(kind==='stat'&&runFacts&&!known.length){const fact=runFact(kind,file,answer);if(fact)runFacts.push(fact);}
|
|
128
187
|
}
|
|
129
188
|
return null;
|
|
130
189
|
}
|
|
131
|
-
// Compares the record run-recorder.cjs wrote to fd 3.
|
|
132
|
-
export function validateRunRecord(text,certificate) {
|
|
190
|
+
// Compares the record run-recorder.cjs wrote to fd 3. Run facts go to `facts`.
|
|
191
|
+
export function validateRunRecord(text,certificate,facts=[]) {
|
|
133
192
|
const started=performance.now();
|
|
134
193
|
let record=null;
|
|
135
194
|
try{record=JSON.parse(text);}catch{}
|
|
136
195
|
let issue;
|
|
137
|
-
try{issue=runRecordIssue(record,certificate);}catch{issue='record unreadable';}
|
|
196
|
+
try{issue=runRecordIssue(record,certificate,facts);}catch{issue='record unreadable';}
|
|
138
197
|
timing('cli.run_recording',started,{outcome:issue?'rejected':'validated',
|
|
139
|
-
facts:record?.facts?.length||0,...(issue?{reason:issue}:{})});
|
|
198
|
+
facts:record?.facts?.length||0,runFacts:facts.length,...(issue?{reason:issue}:{})});
|
|
140
199
|
return issue;
|
|
141
200
|
}
|
|
142
201
|
// A certificate for the current inputs, built from a recorded run and the
|
|
143
202
|
// certificate of the last full probe, or {issue}. The run must read the same
|
|
144
203
|
// files, with the same configuration, and every answer it received must be
|
|
145
|
-
// covered by the old questions; only file contents may differ.
|
|
204
|
+
// covered by the old questions or be a run fact; only file contents may differ.
|
|
205
|
+
// Run facts stay out of this certificate: the receipt keeps them, and status
|
|
206
|
+
// asks them again for every answer. The program
|
|
146
207
|
// layout is then the same, so the output is rebuilt as the probe computes it.
|
|
147
208
|
// Limit: a file whose role changes (a package.json newly imported as a module)
|
|
148
209
|
// keeps its old role, so a later full probe can report the check STALE.
|
|
149
|
-
export function recordedCertificate(text,cache,compiler) {
|
|
210
|
+
export function recordedCertificate(text,cache,compiler,buildInfo=null) {
|
|
150
211
|
let record=null;
|
|
151
212
|
try{record=JSON.parse(text);}catch{}
|
|
152
213
|
if(record?.schema!==1||!Array.isArray(record.facts))return {issue:'record unavailable'};
|
|
153
214
|
if(!cache?.layout||!cache.queries?.length)return {issue:'no previous layout'};
|
|
215
|
+
const kept=withoutBuildInfo(record.facts,buildInfo);
|
|
216
|
+
if(kept.issue)return {issue:kept.issue};
|
|
154
217
|
const inCompiler=withinCompiler(compiler),reads=new Map();
|
|
155
|
-
|
|
218
|
+
const before=new Map(cache.queries.filter(q=>q[0]==='readFile'&&!inCompiler(q[1])).map(q=>[q[1],q[2]]));
|
|
219
|
+
for(const [kind,file,answer] of kept.facts)if(kind==='read'&&!inCompiler(file)){
|
|
156
220
|
if(answer!==null&&typeof answer!=='string')return {issue:`read ${file} failed`};
|
|
221
|
+
// A package.json only declaration checks read is a run fact, not a program read.
|
|
222
|
+
if(!before.has(file)&&path.basename(file)==='package.json')continue;
|
|
157
223
|
reads.set(file,answer);
|
|
158
224
|
}
|
|
159
|
-
const before=new Map(cache.queries.filter(q=>q[0]==='readFile'&&!inCompiler(q[1])).map(q=>[q[1],q[2]]));
|
|
160
225
|
if(reads.size!==before.size||[...before.keys()].some(file=>!reads.has(file)))
|
|
161
226
|
return {issue:'the run read different files'};
|
|
162
227
|
if(cache.layout.configs.some(([file])=>!reads.has(file)||reads.get(file)!==before.get(file)))
|
|
@@ -169,7 +234,9 @@ export function recordedCertificate(text,cache,compiler) {
|
|
|
169
234
|
return {issue:'the run loaded different lib files'};
|
|
170
235
|
const changed=new Set([...reads].filter(([file,answer])=>before.get(file)!==answer).map(([file])=>file));
|
|
171
236
|
const queries=cache.queries.map(q=>q[0]==='readFile'&&changed.has(q[1])?[q[0],q[1],reads.get(q[1]),...q.slice(3)]:q);
|
|
172
|
-
|
|
237
|
+
// Run facts belong to the receipt, which the caller builds; they do not
|
|
238
|
+
// change the program layout.
|
|
239
|
+
const issue=runRecordIssue({...record,facts:kept.facts},{compiler,queries,context:cache.context},[]);
|
|
173
240
|
if(issue)return {issue};
|
|
174
241
|
const files=[];
|
|
175
242
|
for(const [file,digest] of cache.layout.files){
|
|
@@ -15,8 +15,8 @@ try{
|
|
|
15
15
|
entry:compiler.entry,node:[realObservedPath(process.execPath),process.version]};
|
|
16
16
|
const contextHash=directContextHash(compiler,context.node);
|
|
17
17
|
if(!contextOnly(contextHash)){
|
|
18
|
-
const {queries,configs,files}=typeScriptInputFacts(root,compiler.tsRoot,compiler.entry);
|
|
19
|
-
const layout={root,script,configs,files,context};
|
|
18
|
+
const {queries,configs,files,buildInfo}=typeScriptInputFacts(root,compiler.tsRoot,compiler.entry);
|
|
19
|
+
const layout={root,script,configs,files,context,...(buildInfo?{buildInfo}:{})};
|
|
20
20
|
probeResult(directProbeOutput(layout),contextHash,queries,layout);
|
|
21
21
|
}
|
|
22
22
|
}catch(e){console.error(`VSTATE_REASON:${e.code&&/^[a-z-]+$/.test(e.code)?e.code:'typescript-contract-unavailable'}`);process.exitCode=2;}
|
|
@@ -10,7 +10,10 @@ const reviewed={
|
|
|
10
10
|
'5.5.2':{entry:'lib/tsc.js',digest:'953b4844b5f6edc745be844b6c367c227b59029b73c4182f508f90cc47e21816'},
|
|
11
11
|
'5.6.3':{entry:'lib/tsc.js',digest:'e674b5dc4dac50f6e91be30094aeea983a1f340231717e62b0b6199ae3049eb2'},
|
|
12
12
|
'5.9.3':{entry:'lib/_tsc.js',digest:'d1b32e75475710fdc85102b1365254d84ceb192d3639d962c53fe1355ce274ca'},
|
|
13
|
+
'6.0.2':{entry:'lib/_tsc.js',digest:'273e16620fe23362e085980d6dd822e1315d81ba792221c9b6008755fdafccdd'},
|
|
14
|
+
'6.0.3':{entry:'lib/_tsc.js',digest:'e0a6519f0d0a90125b7fd3bdbbcf700e167656beb3baf8aca8ed8bc037494537'},
|
|
13
15
|
};
|
|
16
|
+
export const reviewedTypeScriptVersions=Object.keys(reviewed);
|
|
14
17
|
const fail=code=>{throw Object.assign(Error(code),{code});};
|
|
15
18
|
const hash=bytes=>createHash('sha256').update(bytes).digest('hex');
|
|
16
19
|
export const directEnvironment={variables:['NODE_ENV','NODE_OPTIONS','NODE_PATH','NODE_COMPILE_CACHE','VSCODE_INSPECTOR_OPTIONS'],
|
|
@@ -78,9 +81,35 @@ export function directCompiler(root,{verify=true}={}){
|
|
|
78
81
|
// The direct probe's output: root, script, configuration and program files with
|
|
79
82
|
// their byte digests, and the execution context. A recorded run rebuilds it from
|
|
80
83
|
// the same layout (direct-run-certificate.mjs).
|
|
81
|
-
export function directProbeOutput({root,script,configs,files,context}){
|
|
84
|
+
export function directProbeOutput({root,script,configs,files,context,buildInfo}){
|
|
82
85
|
const named=list=>list.map(([file,digest])=>[hash(file),digest]);
|
|
83
|
-
return hash(JSON.stringify({root,script,configs:named(configs),files:named(files),...context
|
|
86
|
+
return hash(JSON.stringify({root,script,configs:named(configs),files:named(files),...context,
|
|
87
|
+
...(buildInfo?{buildInfo}:{})}));
|
|
88
|
+
}
|
|
89
|
+
// The build-info file an incremental root config makes tsc read and write,
|
|
90
|
+
// as {file} relative to the root, {} when not incremental, or {issue}. The
|
|
91
|
+
// probe and the plan both use this rule. It must be inside the checkout, end
|
|
92
|
+
// in .tsbuildinfo (the only write the run recorder allows) and contain no glob
|
|
93
|
+
// characters (the plan excludes it with a pattern). An option that makes the
|
|
94
|
+
// result depend on the build-info history is refused.
|
|
95
|
+
export function buildInfoFor(ts,options,root) {
|
|
96
|
+
if(!options?.incremental)return {};
|
|
97
|
+
if(options.assumeChangesOnlyAffectDirectDependencies)return {issue:'typescript-incremental-unsound'};
|
|
98
|
+
const file=ts.getTsBuildInfoEmitOutputFilePath?.(options);
|
|
99
|
+
if(!file||!withinPath(path.resolve(file),root))return {issue:'typescript-buildinfo-outside-repository'};
|
|
100
|
+
const relative=path.relative(root,path.resolve(file)).split(path.sep).join('/');
|
|
101
|
+
if(!relative.endsWith('.tsbuildinfo')||/[*?[\]{}()!+@,]/.test(relative))return {issue:'typescript-buildinfo-name-unsupported'};
|
|
102
|
+
return {file:relative};
|
|
103
|
+
}
|
|
104
|
+
// The plan's build-info file, or null. The compiler is loaded only when the
|
|
105
|
+
// root config sets incremental or extends another config.
|
|
106
|
+
export function directBuildInfo(root,script) {
|
|
107
|
+
const config=path.join(root,'tsconfig.json');
|
|
108
|
+
let raw;try{raw=fs.readFileSync(config,'utf8');}catch{return null;}
|
|
109
|
+
if(!/"(?:incremental|extends)"\s*:/.test(raw))return null;
|
|
110
|
+
const compiler=directProject(root,script,{verify:false}),ts=createRequire(import.meta.url)(compiler.tsRoot);
|
|
111
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(config,{},{...ts.sys,onUnRecoverableConfigFileDiagnostic(){}});
|
|
112
|
+
return parsed?.options?.composite?null:buildInfoFor(ts,parsed?.options,root).file??null;
|
|
84
113
|
}
|
|
85
114
|
// The probe value the daemon stores for one direct probe's checkpoint output.
|
|
86
115
|
export function directProbeValue(argv,output,stderrHash){
|