@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 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. One you may
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||npmDirect||testsDirect?['node_modules/**']:[]),
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
- if(details&&check)value={...value,checks:value.checks.filter(row=>row.name===check)};
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@redue/cli",
3
3
  "private": false,
4
- "version": "0.1.0-alpha.24",
4
+ "version": "0.1.0-alpha.25",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "tag": "alpha"
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
- const decisionDeadline=()=>Math.max(decisionBaseMs,publishedProbeDeadline()+10000);
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
- function call(message,timeout=5000) {
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 timer=setTimeout(()=>{mark('client.control_timeout');client.destroy();reject(Error('observer synchronization timeout'));},timeout);
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
- const started=Date.now(),remaining=()=>Math.max(1,timeout-(Date.now()-started));
136
- try{return await call(message,remaining());}
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}:{})},captureTimeout);}
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(captureTimeout,5000));}
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
- // The start checkpoint's probe may have raised the deadline.
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
- Number(process.env.VSTATE_SYNC_TIMEOUT_MS||decisionDeadline()));
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
- if(action==='sync')console.log(JSON.stringify(value));
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
- main().catch(e=>{console.error(`redue: ${e.message}`);process.exitCode=2;});
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.
@@ -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()||{};