@redue/cli 0.1.0-alpha.24 → 0.1.0-alpha.25
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 +26 -0
- package/README.md +1 -4
- package/bin/redue.mjs +17 -5
- package/docs/user/direct-typescript.md +8 -0
- package/package.json +1 -1
- package/src/cli.mjs +35 -16
- package/src/daemon.mjs +1 -0
- package/src/direct-run-certificate.mjs +3 -1
- package/src/direct-typescript-probe.mjs +4 -1
- package/src/direct-typescript.mjs +23 -3
- package/src/pnpm-inputs.mjs +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.25 — 2026-10-29
|
|
4
|
+
|
|
5
|
+
Running your tests no longer makes a typecheck STALE, and `status --sync`
|
|
6
|
+
answers about twice as fast.
|
|
7
|
+
|
|
8
|
+
- **Vitest and the direct TypeScript recipe.** A Vitest run writes its
|
|
9
|
+
results under `node_modules/.vite/vitest`, which the recipe counted as an
|
|
10
|
+
installed input, so the typecheck turned STALE after every test run. Those
|
|
11
|
+
files are no longer inputs. Everything else under `node_modules/.vite`
|
|
12
|
+
still is. A program that asks the compiler about anything under
|
|
13
|
+
`node_modules/.vite`, or lists `node_modules`, is UNVERIFIED instead. A
|
|
14
|
+
check set up by an earlier alpha reports STALE ("input plan changed") once
|
|
15
|
+
after the upgrade.
|
|
16
|
+
- **Faster status.** `status --sync`, `detail` and `explain` no longer start
|
|
17
|
+
a second Node process on macOS and Linux, the YAML parser loads only for
|
|
18
|
+
pnpm projects, and each call to the observer no longer waits on a fresh
|
|
19
|
+
read of its status file. On the 800-file bench, warm `status --sync` adds
|
|
20
|
+
27 ms over cached status on a GitHub Linux runner (alpha.24: 120 ms) and
|
|
21
|
+
36 ms on a macOS runner (alpha.24: 137 ms).
|
|
22
|
+
- **Releases are benched.** The release smoke now benches the candidate
|
|
23
|
+
against the last release on macOS and Linux runners; a slower candidate,
|
|
24
|
+
or one over the warm-sync or run-overhead budget, blocks the release.
|
|
25
|
+
- Known limit: on the maintainer's Mac, which runs Microsoft Defender,
|
|
26
|
+
`redue run` still adds about 15-20% to a 2.7 s typecheck (3% on a GitHub
|
|
27
|
+
Linux runner).
|
|
28
|
+
|
|
3
29
|
## 0.1.0-alpha.24 — 2026-10-28
|
|
4
30
|
|
|
5
31
|
Checks keep working after a normal `npm install`, and agents that chain an
|
package/README.md
CHANGED
|
@@ -76,10 +76,7 @@ inputs. 30 repositories had at least one such check:
|
|
|
76
76
|
Everything else stays UNVERIFIED, with a reason you can read in
|
|
77
77
|
`redue explain`. The usual ones: a script starts a program REDUE cannot see
|
|
78
78
|
into (esbuild, turbo, nx, oxlint's tsgolint, TypeScript 7's native compiler),
|
|
79
|
-
the project uses Bun, or a test suite writes into its own inputs.
|
|
80
|
-
meet right after setup: a Vitest run writes its results under
|
|
81
|
-
`node_modules/.vite`, which the direct TypeScript recipe counts as an
|
|
82
|
-
installed input, so the typecheck turns STALE until it runs again.
|
|
79
|
+
the project uses Bun, or a test suite writes into its own inputs.
|
|
83
80
|
[Supported projects](docs/user/supported-projects.md) has the details.
|
|
84
81
|
|
|
85
82
|
## Measured with Claude Code
|
package/bin/redue.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import {mark,traceBase,traceContext,traceEnvironment,traceId} from '../src/decision-profile.mjs';
|
|
4
4
|
traceBase({...traceContext(),request:traceId(),work:'client'});
|
|
5
5
|
mark('client.loaded');
|
|
6
|
-
import {directCompiler,directQualification,directEnvironment,directInterpretation} from '../src/direct-typescript.mjs';
|
|
6
|
+
import {directCompiler,directQualification,directEnvironment,directInstalledInputs,directInterpretation} from '../src/direct-typescript.mjs';
|
|
7
7
|
import {runArgv,tscRun} from '../src/typescript-invocation.mjs';
|
|
8
8
|
import {testEnvironment,testRunnerAt,testsInterpretation,testsQualification} from '../src/test-direct.mjs';
|
|
9
9
|
import {scriptEnvironment,scriptInterpretation,scriptKinds,scriptQualification} from '../src/node-script.mjs';
|
|
@@ -337,7 +337,7 @@ const checks=publicConfig.checks.map(check=>{
|
|
|
337
337
|
typeof check.cwd==='string'&&check.cwd&&!path.isAbsolute(check.cwd)&&!check.cwd.split('/').includes('..')?check.cwd:'.'),
|
|
338
338
|
cwd:check.cwd,inputs:check.inputs||[],generatedInputs:check.generatedInputs||[],
|
|
339
339
|
installedInputs:check.installedInputs||(
|
|
340
|
-
npmAutomatic||
|
|
340
|
+
npmDirect?[...directInstalledInputs]:npmAutomatic||testsDirect?['node_modules/**']:[]),
|
|
341
341
|
allowedExternalRoots:check.allowedExternalRoots||[],
|
|
342
342
|
probes:[...(check.probes||[]),...autoProbes]
|
|
343
343
|
.map((p,i)=>argv(p,`${check.name}.probes[${i}]`)),
|
|
@@ -424,6 +424,15 @@ if(command==='run'&&process.platform!=='win32'){
|
|
|
424
424
|
process.off('SIGINT',interrupt);process.off('SIGTERM',terminate);
|
|
425
425
|
if(outcome.error)error(outcome.error.message);
|
|
426
426
|
process.exit(outcome.status??(outcome.signal?128+os.constants.signals[outcome.signal]:1));
|
|
427
|
+
}else if(internal==='sync'&&process.platform!=='win32'){
|
|
428
|
+
// As for run, the synchronized status engine runs in this process.
|
|
429
|
+
process.argv=[process.argv[0],engine,runtimeFile,internal,'','--value'];
|
|
430
|
+
mark('client.engine_submit',{inProcess:true});
|
|
431
|
+
const synchronized=await import(pathToFileURL(engine).href);
|
|
432
|
+
await synchronized.finished;
|
|
433
|
+
mark('client.engine_complete',{exit:process.exitCode??0});
|
|
434
|
+
if(synchronized.syncValue)showSynchronized(synchronized.syncValue);
|
|
435
|
+
await exit(process.exitCode??0);
|
|
427
436
|
}else{
|
|
428
437
|
mark('client.engine_submit');
|
|
429
438
|
const result=spawnSync(process.execPath,[engine,runtimeFile,internal,...(check?[check]:[])],
|
|
@@ -434,10 +443,13 @@ if(command==='run'&&process.platform!=='win32'){
|
|
|
434
443
|
if(result.stdout){
|
|
435
444
|
if(['status','detail','explain'].includes(command)){
|
|
436
445
|
let value;try{value=JSON.parse(result.stdout);}catch{error('status response could not be read; applicability is unknown');}
|
|
437
|
-
|
|
438
|
-
if(json)console.log(JSON.stringify(value));
|
|
439
|
-
else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
|
|
446
|
+
showSynchronized(value);
|
|
440
447
|
}else printLifecycle(result.stdout,command,json);
|
|
441
448
|
}
|
|
442
449
|
await exit(result.status??1);
|
|
443
450
|
}
|
|
451
|
+
function showSynchronized(value){
|
|
452
|
+
if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
|
|
453
|
+
if(json)console.log(JSON.stringify(value));
|
|
454
|
+
else console.log(details?renderExplain(value,check):renderStatus(value,{short}));
|
|
455
|
+
}
|
|
@@ -141,6 +141,14 @@ changes invalidate or withdraw applicability normally. Installed files are obser
|
|
|
141
141
|
by identity, so reinstalling identical packages or restoring an installed file's
|
|
142
142
|
bytes also needs one rerun (see [configuration](configuration.md)).
|
|
143
143
|
|
|
144
|
+
Vitest writes its results under `node_modules/.vite/vitest` at every run.
|
|
145
|
+
They are not inputs, so a test run leaves the typecheck CURRENT. Everything
|
|
146
|
+
else under `node_modules/.vite` is still an input. A program that asks about
|
|
147
|
+
anything under `node_modules/.vite` (by any spelling or through a link that
|
|
148
|
+
resolves there), or lists `node_modules`, is UNVERIFIED: REDUE does not
|
|
149
|
+
observe what Vitest writes there. A check configured before 0.1.0-alpha.25 gets the new inputs on
|
|
150
|
+
its own and reports STALE once after the upgrade.
|
|
151
|
+
|
|
144
152
|
A root tsconfig with `incremental: true` is supported. tsc reads and writes
|
|
145
153
|
its build-info file (`tsBuildInfoFile`, or the default next to the config) on
|
|
146
154
|
each run, so REDUE leaves that file out of the inputs and lets the run write
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -55,7 +55,17 @@ function publishedProbeDeadline() {
|
|
|
55
55
|
try{return publishedDeadline(JSON.parse(fs.readFileSync(path.join(state,'status.json'),'utf8')).probe_deadline_ms);}
|
|
56
56
|
catch{return 0;}
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
// The wait for a decision, read when it starts: the base at once, and the
|
|
59
|
+
// published deadline when status.json has been read. That file is rewritten
|
|
60
|
+
// at every decision, and a first read of a fresh file is slow where a scanner
|
|
61
|
+
// checks files as they are opened, so it is read off the main thread while
|
|
62
|
+
// the call runs. On Windows it is read before the call, as before: the
|
|
63
|
+
// observer replaces it during the call, and a replacement fails there while a
|
|
64
|
+
// reader holds the file open. An override is exact.
|
|
65
|
+
const decisionWait=override=>override?{timeout:Number(override),deadline:null}:
|
|
66
|
+
process.platform==='win32'?{timeout:Math.max(decisionBaseMs,publishedProbeDeadline()+10000),deadline:null}:{timeout:decisionBaseMs,
|
|
67
|
+
deadline:fs.promises.readFile(path.join(state,'status.json'),'utf8')
|
|
68
|
+
.then(text=>Math.max(decisionBaseMs,publishedDeadline(JSON.parse(text).probe_deadline_ms)+10000),()=>decisionBaseMs)};
|
|
59
69
|
const digest=value=>createHash('sha256').update(value).digest('hex');
|
|
60
70
|
function nodeIdentity() {const real=fs.realpathSync(process.execPath),st=fs.statSync(real,{bigint:true});
|
|
61
71
|
return digest(JSON.stringify([real,process.version,...['dev','ino','mode','size','mtimeNs','ctimeNs']
|
|
@@ -120,20 +130,26 @@ function owner(){
|
|
|
120
130
|
atomicJson(ownerFile,expected);
|
|
121
131
|
}
|
|
122
132
|
}
|
|
123
|
-
|
|
133
|
+
// deadline, when given, resolves to the whole wait in ms, which replaces
|
|
134
|
+
// timeout when it is longer.
|
|
135
|
+
function call(message,timeout=5000,deadline=null) {
|
|
124
136
|
return new Promise((resolve,reject)=>{
|
|
125
137
|
let data='',done=false;mark('client.control_connect',{deadlineMs:timeout});const client=net.createConnection(socket);
|
|
126
|
-
const
|
|
138
|
+
const started=Date.now(),expire=()=>{mark('client.control_timeout');client.destroy();reject(Error('observer synchronization timeout'));};
|
|
139
|
+
let timer=setTimeout(expire,timeout);
|
|
140
|
+
deadline?.then(total=>{if(done||total<=timeout)return;mark('client.control_deadline',{deadlineMs:total});
|
|
141
|
+
clearTimeout(timer);timer=setTimeout(expire,Math.max(0,total-(Date.now()-started)));});
|
|
127
142
|
client.on('connect',()=>{mark('client.control_submit');client.write(JSON.stringify({...message,...(profiling?{diagnostic:traceContext()}: {})})+'\n');});
|
|
128
143
|
client.on('data',chunk=>data+=chunk);
|
|
129
144
|
client.on('end',()=>{mark('client.control_complete',{bytes:data.length});clearTimeout(timer);if(done)return;done=true;
|
|
130
145
|
try{resolve(JSON.parse(data));}catch(e){reject(e);}});
|
|
131
|
-
client.on('error',e=>{mark('client.control_error',{code:e.code});clearTimeout(timer);reject(e);});
|
|
146
|
+
client.on('error',e=>{mark('client.control_error',{code:e.code});clearTimeout(timer);done=true;reject(e);});
|
|
132
147
|
});
|
|
133
148
|
}
|
|
134
|
-
async function decisionCall(message,timeout) {
|
|
135
|
-
|
|
136
|
-
|
|
149
|
+
async function decisionCall(message,{timeout,deadline}) {
|
|
150
|
+
let total=timeout;deadline?.then(value=>{total=Math.max(total,value);});
|
|
151
|
+
const started=Date.now(),remaining=()=>Math.max(1,total-(Date.now()-started));
|
|
152
|
+
try{return await call(message,timeout,deadline);}
|
|
137
153
|
catch(error){
|
|
138
154
|
if(process.platform!=='win32'||!['ENOENT','ECONNREFUSED','ECONNRESET'].includes(error.code))throw error;
|
|
139
155
|
const pidFile=path.join(state,'observer.lock','pid');
|
|
@@ -233,8 +249,7 @@ async function execute() {
|
|
|
233
249
|
const environmentBefore=contextHashes()[name];
|
|
234
250
|
const observedContext=selected.qualification===directQualification?{interpretation:directInterpretation,npmProxyDigest:observedNpmContext()}:null;
|
|
235
251
|
timing('cli.context_before',contextStarted);
|
|
236
|
-
const
|
|
237
|
-
captureTimeout=Number(process.env.VSTATE_CAPTURE_TIMEOUT_MS||decisionDeadline());
|
|
252
|
+
const captureWait=decisionWait(process.env.VSTATE_CAPTURE_TIMEOUT_MS);
|
|
238
253
|
let before=null,after=null;
|
|
239
254
|
// Expensive state probes can take seconds. Keep the filesystem checkpoint
|
|
240
255
|
// adjacent to process launch so a late notification from earlier work
|
|
@@ -244,7 +259,7 @@ async function execute() {
|
|
|
244
259
|
let probeBefore=callerCertificate?null:probeHash(probePlan);
|
|
245
260
|
const startSnapshotStarted=performance.now();
|
|
246
261
|
try{before=await call({action:'snapshot',name,contextHashes:contextHashes(),
|
|
247
|
-
...(callerCertificate?{runCertificate:1}:{})},
|
|
262
|
+
...(callerCertificate?{runCertificate:1}:{})},captureWait.timeout,captureWait.deadline);}
|
|
248
263
|
catch(error){captureIssues.push('start observation unavailable'+
|
|
249
264
|
(typeof error.code==='string'&&/^[A-Z0-9_]+$/.test(error.code)?` (${error.code})`:''));}
|
|
250
265
|
timing('cli.start_checkpoint',startSnapshotStarted,{available:before?.snapshots?.[name]?1:0});
|
|
@@ -307,7 +322,7 @@ async function execute() {
|
|
|
307
322
|
await new Promise(resolve=>setTimeout(resolve,100));
|
|
308
323
|
const prelaunchStarted=performance.now();
|
|
309
324
|
try{before=await call({action:'prelaunch',name,
|
|
310
|
-
contextHashes:contextHashes()},Math.min(
|
|
325
|
+
contextHashes:contextHashes()},Math.min(captureWait.timeout,5000));}
|
|
311
326
|
catch{before=null;captureIssues.push('prelaunch observation unavailable');}
|
|
312
327
|
timing('cli.prelaunch_checkpoint',prelaunchStarted,
|
|
313
328
|
{available:before?.snapshots?.[name]?1:0});
|
|
@@ -393,11 +408,12 @@ async function execute() {
|
|
|
393
408
|
if(recordFile)try{fs.writeFileSync(recordFile,recordText,{mode:0o600});}catch{}
|
|
394
409
|
const endSnapshotStarted=performance.now();
|
|
395
410
|
if(!unobservedDirectRun){
|
|
411
|
+
// The start checkpoint's probe may have raised the deadline.
|
|
412
|
+
const endWait=decisionWait(process.env.VSTATE_CAPTURE_TIMEOUT_MS);
|
|
396
413
|
try{after=await call({action:'snapshot',name,contextHashes:contextHashes(),
|
|
397
414
|
...(testRun&&Number.isSafeInteger(snap?.inputEventSerial)?{sinceSerial:snap.inputEventSerial}:{}),
|
|
398
415
|
...(recording?{runCertificate:1,adopt:recording}:recorded&&!certificate?{runCertificate:1,runFacts:1}:{})},
|
|
399
|
-
|
|
400
|
-
Number(process.env.VSTATE_CAPTURE_TIMEOUT_MS||decisionDeadline()));}
|
|
416
|
+
endWait.timeout,endWait.deadline);}
|
|
401
417
|
catch{captureIssues.push('end observation unavailable');}
|
|
402
418
|
finally{if(recordFile)fs.rmSync(recordFile,{force:true});}
|
|
403
419
|
timing('cli.end_checkpoint',endSnapshotStarted,{available:after?.snapshots?.[name]?1:0});
|
|
@@ -601,7 +617,7 @@ async function main(){
|
|
|
601
617
|
observedNpmDigest:observedNpmContext(),proxyShapes});
|
|
602
618
|
}
|
|
603
619
|
let value;try{value=await decisionCall({action:'sync',contextHashes:contextHashes()},
|
|
604
|
-
|
|
620
|
+
decisionWait(process.env.VSTATE_SYNC_TIMEOUT_MS));
|
|
605
621
|
// A client upgrade can encounter an older running observer. Its successful
|
|
606
622
|
// control response does not establish that it evaluated the latest receipt.
|
|
607
623
|
if(value.receipt_revision===null){
|
|
@@ -615,7 +631,9 @@ async function main(){
|
|
|
615
631
|
}}
|
|
616
632
|
catch(e){value=unavailableCached(state,e.message,config.checks);process.exitCode=2;}
|
|
617
633
|
timing('cli.synchronized_status',started,{available:value.observation?.healthy?1:0});
|
|
618
|
-
|
|
634
|
+
// In the front's process (bin/redue.mjs), the value is handed back.
|
|
635
|
+
if(action==='sync'&&process.argv.includes('--value'))syncValue=value;
|
|
636
|
+
else if(action==='sync')console.log(JSON.stringify(value));
|
|
619
637
|
else for(const row of value.checks)console.log(`${row.name}: ${row.freshness}/${row.result||'NO RESULT'} — ${row.reason}`);
|
|
620
638
|
return;
|
|
621
639
|
}
|
|
@@ -623,4 +641,5 @@ async function main(){
|
|
|
623
641
|
if(action==='run'){await execute();return;}
|
|
624
642
|
throw Error(`unknown action ${action}`);
|
|
625
643
|
}
|
|
626
|
-
|
|
644
|
+
export let syncValue=null;
|
|
645
|
+
export const finished=main().catch(e=>{console.error(`redue: ${e.message}`);process.exitCode=2;});
|
package/src/daemon.mjs
CHANGED
|
@@ -269,6 +269,7 @@ function probe(row,allowReuse=false,{launch=true}={}) {
|
|
|
269
269
|
'typescript-plugin-unsupported':'TypeScript compiler plugin needs explicit review',
|
|
270
270
|
'typescript-input-list-unavailable':'TypeScript input listing did not complete',
|
|
271
271
|
'typescript-input-outside-repository':'TypeScript consumes files outside the observed repository',
|
|
272
|
+
'typescript-vite-cache-consumed':'TypeScript asks about node_modules/.vite or lists node_modules; Vitest rewrites files there and this recipe does not observe them',
|
|
272
273
|
'npm-installation-layout-unsupported':'npm executable or installation layout is unsupported',
|
|
273
274
|
'npm-installation-unobservable':'npm installation contains unobservable entries',
|
|
274
275
|
'npm-configuration-unsupported':'npm script configuration needs review',
|
|
@@ -7,7 +7,7 @@ import {createHash} from 'node:crypto';
|
|
|
7
7
|
import {inflateSync} from 'node:zlib';
|
|
8
8
|
import {fileURLToPath} from 'node:url';
|
|
9
9
|
import {timing} from './decision-profile.mjs';
|
|
10
|
-
import {directCompiler,directContextHash,directEnvironmentIssue,directProbeOutput,directProbeValue} from './direct-typescript.mjs';
|
|
10
|
+
import {directCompiler,directContextHash,directEnvironmentIssue,directProbeOutput,directProbeValue,viteCacheQuestion} 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
|
|
@@ -158,6 +158,8 @@ export function runRecordIssue(record,certificate,runFacts=null) {
|
|
|
158
158
|
if(kept.issue)return kept.issue;
|
|
159
159
|
if(record.main!==compiler.entry||!Array.isArray(record.node)||
|
|
160
160
|
directContextHash(compiler,record.node)!==certificate.context)return 'execution context changed';
|
|
161
|
+
const vite=viteCacheQuestion(compiler.root,kept.facts);
|
|
162
|
+
if(vite)return `the compiler asked about ${vite}, which Vitest rewrites and REDUE does not observe`;
|
|
161
163
|
const inCompiler=withinCompiler(compiler);
|
|
162
164
|
const facts=new Map();
|
|
163
165
|
for(const query of queries){if(!facts.has(query[1]))facts.set(query[1],[]);facts.get(query[1]).push(query);}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {directProject,directInterpretation,directContextHash,directProbeOutput} from './direct-typescript.mjs';
|
|
2
|
+
import {directProject,directInterpretation,directContextHash,directProbeOutput,viteCacheQuestion} from './direct-typescript.mjs';
|
|
3
3
|
import {typeScriptInputFacts} from './typescript-input-facts.mjs';
|
|
4
4
|
import {contextOnly,probeResult} from './probe-result.mjs';
|
|
5
5
|
import {realObservedPath} from './path-identity.mjs';
|
|
@@ -18,6 +18,9 @@ try{
|
|
|
18
18
|
const contextHash=directContextHash(compiler,context.node);
|
|
19
19
|
if(!contextOnly(contextHash)){
|
|
20
20
|
const {queries,configs,files,buildInfo}=typeScriptInputFacts(root,compiler.tsRoot,compiler.entry,compiler.config);
|
|
21
|
+
// Captured questions include every config and program file read.
|
|
22
|
+
if(viteCacheQuestion(root,queries||[...configs,...files].map(([file])=>['readFile',file])))
|
|
23
|
+
throw Object.assign(Error('typescript-vite-cache-consumed'),{code:'typescript-vite-cache-consumed'});
|
|
21
24
|
const layout={root,script,configs,files,context,...(buildInfo?{buildInfo}:{})};
|
|
22
25
|
probeResult(directProbeOutput(layout),contextHash,queries,layout);
|
|
23
26
|
}
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import {createHash} from 'node:crypto';
|
|
5
5
|
import {createRequire} from 'node:module';
|
|
6
|
-
import {racyUntil,realObservedPath,withinPath} from './path-identity.mjs';
|
|
6
|
+
import {normalizedPath,racyUntil,realObservedPath,withinPath} from './path-identity.mjs';
|
|
7
7
|
import {cachedTarball,lockedPackage,matchTarball,npmCacheDir,registryPackage,tarballFiles} from './npm-integrity.mjs';
|
|
8
8
|
import {runConfig,tscRun} from './typescript-invocation.mjs';
|
|
9
9
|
export const directInterpretation='npm-direct-typescript@1';
|
|
@@ -79,7 +79,7 @@ export function directCompiler(root,{verify=true}={}){
|
|
|
79
79
|
for(const [name,file] of files.sort(([a],[b])=>a<b?-1:a>b?1:0))digest.update(name+'\0'+hash(fs.readFileSync(file))+'\0');
|
|
80
80
|
if(digest.digest('hex')!==spec.digest)fail('direct-typescript-implementation-unreviewed');
|
|
81
81
|
}
|
|
82
|
-
return {tsRoot,version,entry:path.join(tsRoot,spec.entry),digest:spec.digest};
|
|
82
|
+
return {root,tsRoot,version,entry:path.join(tsRoot,spec.entry),digest:spec.digest};
|
|
83
83
|
}
|
|
84
84
|
// A TypeScript 5.x or 6.0 the lockfile records: the installed package must be
|
|
85
85
|
// the registry's `typescript` at that version, holding exactly the files of
|
|
@@ -115,7 +115,7 @@ function lockedCompiler(root,tsRoot,version,verify){
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
const entry=fs.existsSync(path.join(tsRoot,'lib','_tsc.js'))?'lib/_tsc.js':'lib/tsc.js';
|
|
118
|
-
return {tsRoot,version,entry:path.join(tsRoot,entry),digest:hash('npm-integrity\0'+locked.integrity)};
|
|
118
|
+
return {root,tsRoot,version,entry:path.join(tsRoot,entry),digest:hash('npm-integrity\0'+locked.integrity)};
|
|
119
119
|
}
|
|
120
120
|
// The direct probe's output: root, script, configuration and program files with
|
|
121
121
|
// their byte digests, and the execution context. A recorded run rebuilds it from
|
|
@@ -162,6 +162,26 @@ export function directContextHash(compiler,node){
|
|
|
162
162
|
return hash(JSON.stringify({interpretation:directInterpretation,compiler:compiler.digest,
|
|
163
163
|
entry:compiler.entry,node}));
|
|
164
164
|
}
|
|
165
|
+
// Vitest writes its results under node_modules/.vite/vitest at every run. The
|
|
166
|
+
// recipe does not observe them. A compiler that asks about anything under
|
|
167
|
+
// node_modules/.vite, is told a path there (realpath), or lists node_modules,
|
|
168
|
+
// could see them, so its result is UNVERIFIED.
|
|
169
|
+
export const directInstalledInputs=Object.freeze(['node_modules/**','!node_modules/.vite/vitest/**']);
|
|
170
|
+
const listings=new Set(['readdir','listing','directories','entries','readDirectory']);
|
|
171
|
+
// The first path of [kind, file, answer] questions that does, or null. Kinds
|
|
172
|
+
// are a recorded run's (readdir) or a probe's. Compared without case: a
|
|
173
|
+
// case-insensitive mount reaches .vite under any spelling.
|
|
174
|
+
export function viteCacheQuestion(root,questions){
|
|
175
|
+
const fold=value=>normalizedPath(value).toLowerCase();
|
|
176
|
+
const modules=fold(path.join(root,'node_modules')),cache=path.join(modules,'.vite');
|
|
177
|
+
for(const [kind,file,answer] of questions){
|
|
178
|
+
if(typeof file!=='string')continue;
|
|
179
|
+
const at=fold(file);
|
|
180
|
+
if(withinPath(at,cache)||at===modules&&listings.has(kind))return file;
|
|
181
|
+
if(kind==='realpath'&&typeof answer==='string'&&withinPath(fold(answer),cache))return answer;
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
165
185
|
// The compiler for one check run {script, part, args}: script null for a
|
|
166
186
|
// check init proposed without one. The run must pass --noEmit and name a
|
|
167
187
|
// config inside the root.
|
package/src/pnpm-inputs.mjs
CHANGED
|
@@ -8,7 +8,6 @@ import path from 'node:path';
|
|
|
8
8
|
import {createRequire} from 'node:module';
|
|
9
9
|
import {execFileSync,spawnSync} from 'node:child_process';
|
|
10
10
|
import micromatch,{patternMatcher} from './glob.mjs';
|
|
11
|
-
import YAML from 'yaml';
|
|
12
11
|
import {resolveLinks} from './installed-inputs.mjs';
|
|
13
12
|
import {withinPath as within,realObservedPath,samePath} from './path-identity.mjs';
|
|
14
13
|
import {timing} from './decision-profile.mjs';
|
|
@@ -18,7 +17,11 @@ import {runConfig,tscRun,tscRuns} from './typescript-invocation.mjs';
|
|
|
18
17
|
const rel=(root,file)=>path.relative(root,file).split(path.sep).join('/');
|
|
19
18
|
const read=file=>JSON.parse(fs.readFileSync(file,'utf8'));
|
|
20
19
|
const exists=file=>{try{fs.lstatSync(file);return true;}catch{return false;}};
|
|
20
|
+
// Loaded on first use: the parser takes about 40 ms to load, and every
|
|
21
|
+
// command imports this module.
|
|
22
|
+
let YAML=null;
|
|
21
23
|
export function yaml(file){
|
|
24
|
+
YAML??=createRequire(import.meta.url)('yaml');
|
|
22
25
|
const doc=YAML.parseDocument(fs.readFileSync(file,'utf8'),{uniqueKeys:true});
|
|
23
26
|
if(doc.errors.length)throw Error(`${path.basename(file)} cannot be interpreted safely`);
|
|
24
27
|
return doc.toJS()||{};
|