@redue/cli 0.1.0-alpha.16 → 0.1.0-alpha.18

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.
@@ -3,16 +3,17 @@ import {testProject,testRecipeScript,testCheckInputs,testsQualification} from '.
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import {spawnSync} from 'node:child_process';
6
- import micromatch from './glob.mjs';
7
6
  import {yarnWorkspaceTypecheckInputs} from './yarn-workspace-inputs.mjs';
8
7
  import {pnpmInstallInfo,pnpmProjectInfo,pnpmTypecheckInputs} from './pnpm-inputs.mjs';
9
8
  import {realObservedPath} from './path-identity.mjs';
10
9
  import {findExecutable} from './executable-lookup.mjs';
10
+ import {workspaceManifests} from './workspace-manifests.mjs';
11
+ import {partName,tscRuns} from './typescript-invocation.mjs';
11
12
 
12
13
  const exists=file=>{try{fs.accessSync(file);return true;}catch{return false;}};
13
14
  const readJson=file=>JSON.parse(fs.readFileSync(file,'utf8'));
14
15
  const scriptNames={
15
- typecheck:['typecheck','type-check','check:types','types:check'],
16
+ typecheck:['typecheck','type-check','check:types','types:check','check-types'],
16
17
  test:['test','test:unit','test:check'],
17
18
  lint:['lint','lint:check'],
18
19
  build:['build'],
@@ -82,6 +83,28 @@ function manager(root,pkg) {
82
83
  return {name,version,layout:'node-modules/classic',issue:null,lockfiles:locks};
83
84
  }
84
85
  const exactTypeScript=script=>script.trim()==='tsc --noEmit';
86
+ // A script of tsc runs that each pass --noEmit (typescript-invocation.mjs).
87
+ const noEmitRuns=script=>Boolean(tscRuns(script)?.every(run=>run.noEmit));
88
+ // Ready checks for a typecheck, one per tsc run: the script's runs (a chain
89
+ // gives NAME.1 ... NAME.n), or with no script one proposed `tsc --noEmit`.
90
+ // `make(run)` returns a run's command, cwd and inputs, or throws why its
91
+ // contract does not hold.
92
+ function tscChecks(name,script,scripts,make){
93
+ const runs=script===null?[{args:['--noEmit']}]:tscRuns(scripts[script]);
94
+ if(!runs)throw Error('the script is not tsc runs in a supported form');
95
+ return runs.map(({args},i)=>({name:partName(name,i+1,runs.length),
96
+ ...(script===null?{}:{script}),...make({script,args,count:runs.length})}));
97
+ }
98
+ // A proposed `tsc --noEmit` only where nothing typechecks already: a script
99
+ // that runs any TypeScript checker in another form (tsc -b, vue-tsc,
100
+ // svelte-check, tsgo, also quoted or wrapped by concurrently or run-p) is the
101
+ // project's own typecheck, which plain tsc would not match.
102
+ const typeChecker=/(?:^|[^\w-])(?:tsc|vue-tsc|tsgo|svelte-check|tsc-watch)(?=$|[^\w-])|\bastro check\b|\bnuxi typecheck\b/;
103
+ const typechecks=scripts=>Object.values(scripts||{}).some(text=>typeof text==='string'&&typeChecker.test(text));
104
+ const proposable=(dir,scripts)=>exists(path.join(dir,'tsconfig.json'))&&!typechecks(scripts);
105
+ // The inputs a TypeScript contract adds to its check's config: no exclusions,
106
+ // which would outlive the tsconfig they came from; the plan adds the current ones.
107
+ const contractInputs=contract=>[...contract.configs,...contract.sourcePatterns.filter(p=>!p.startsWith('!'))];
85
108
  function sourceLinkIssue(root) {
86
109
  const stack=likelyDirs.filter(name=>exists(path.join(root,name)));
87
110
  let inspected=0;
@@ -139,27 +162,25 @@ function selectScript(kind,names,scripts) {
139
162
  const candidates=Object.keys(scripts).filter(name=>{
140
163
  const value=scripts[name]?.trim();
141
164
  if(typeof value!=='string')return false;
142
- if(kind==='typecheck')return exactTypeScript(value);
165
+ if(kind==='typecheck')return noEmitRuns(value);
143
166
  if(kind==='test')return /^(?:vitest|jest)(?:\s|$)/.test(value);
144
167
  if(kind==='lint')return /^eslint(?:\s|$)/.test(value);
145
168
  return /^vite\s+build(?:\s|$)/.test(value);
146
169
  });
170
+ // A lone plain `tsc --noEmit` wins over other tsc shapes beside it (a
171
+ // project's `typescript` next to `typescript:tests:raw`).
172
+ const exact=kind==='typecheck'?candidates.filter(name=>exactTypeScript(scripts[name])):[];
173
+ if(exact.length===1)return {script:exact[0]};
147
174
  return candidates.length===1?{script:candidates[0]}:
148
175
  candidates.length>1?{ambiguous:candidates}:{};
149
176
  }
150
- function workspaceManifests(root,patterns) {
151
- const listing=spawnSync('git',['ls-files','--cached','--others','--exclude-standard',
152
- '-z','--',':(glob)**/package.json'],{cwd:root,encoding:'utf8',timeout:5000,
153
- maxBuffer:32*1024*1024,stdio:['ignore','pipe','ignore'],
154
- env:{...process.env,GIT_OPTIONAL_LOCKS:'0'}});
155
- if(listing.status!==0||listing.error)throw Error('workspace package manifests cannot be inspected');
156
- const matched=listing.stdout.split('\0').filter(Boolean).filter(file=>{
157
- const dir=path.posix.dirname(file);
158
- return dir!=='.'&&!dir.split('/').includes('node_modules')&&
159
- micromatch.isMatch(dir,patterns,{dot:true});
160
- }).sort();
161
- if(matched.length>50)throw Error('more than 50 workspace packages found; select checks explicitly');
162
- return matched.map(file=>({file,pkg:readJson(path.join(root,file))}));
177
+ // A pnpm workspace package's TypeScript run, as its check's command, cwd and inputs.
178
+ function pnpmCheck(root,name,dir,manifest,run){
179
+ const contract=pnpmTypecheckInputs(root,name,run);
180
+ if(contract.limitations.length)throw Error(contract.limitations.join('; '));
181
+ return {cwd:dir,command:['@node',`@typescript-bin:${dir}`,...run.args],
182
+ inputs:[...new Set(['package.json','pnpm-lock.yaml','pnpm-workspace.yaml',manifest,
183
+ ...contractInputs(contract)])]};
163
184
  }
164
185
  // Workspace packages as init finds them, with their scripts; [] without workspaces.
165
186
  export function workspacePackages(root){
@@ -192,24 +213,33 @@ export function discoverNodeProject(root,{recipe=null}={}) {
192
213
  for(const [kind,names] of Object.entries(scriptNames)) {
193
214
  const selected=selectScript(kind,names,scripts),script=selected.script;
194
215
  if(selected.ambiguous)ambiguous.push({kind,scripts:selected.ambiguous});
195
- if(!script)continue;
216
+ // With no typecheck script, a root tsconfig.json gets a proposed
217
+ // `tsc --noEmit` check; package.json stays unchanged. It is kept only
218
+ // when a contract makes it ready.
219
+ const proposed=!script&&!selected.ambiguous&&kind==='typecheck'&&!observationIssue&&
220
+ proposable(root,scripts);
221
+ if(!script&&!proposed)continue;
196
222
  let classification=observationIssue?{level:'unsupported',reason:observationIssue}:
197
- classify(kind,script,scripts[script],pkg,pm,root);
223
+ script?classify(kind,script,scripts[script],pkg,pm,root):{level:'recording',reason:'no typecheck script'};
224
+ let parts=null;
198
225
  if(workspaces&&classification.qualification==='typescript-noemit-v1')classification={level:'recording',
199
226
  reason:'workspace task and installed-input closure need explicit qualification'};
200
227
  const check={name:kind,script,kind,inputs:sourceInputs(root,kind,pm.lockfiles||[])};
201
228
  if(recipe==='typescript-direct'&&kind==='typecheck'){
202
229
  try{
203
230
  if(pm.name!=='npm'||workspaces||observationIssue||pm.issue)throw Error('direct recipe requires a supported single-package npm checkout');
204
- const compiler=directProject(root,script);
205
- check.command=['@node','@typescript-compiler:.','--noEmit'];check.cwd='.';
231
+ let version;
232
+ parts=tscChecks('typecheck',script??null,scripts,run=>{
233
+ version=directProject(root,run).version;
234
+ return {command:['@node','@typescript-compiler:.',...run.args],cwd:'.'};});
206
235
  classification={level:'ready',qualification:directQualification,
207
- reason:`explicit local TypeScript ${compiler.version} compilation; npm launcher/lifecycle scripts are not executed; package.json stays unchanged`};
236
+ reason:`explicit local TypeScript ${version} compilation; npm launcher/lifecycle scripts are not executed; package.json stays unchanged`};
208
237
  }catch(e){classification={level:'recording',reason:`Direct recipe unavailable (${e.code||e.message}); original npm script retained, without bypassing required behavior`};}
209
238
  }
210
239
  if(recipe==='tests-direct'&&kind==='test'){
211
240
  try{
212
- if(pm.name!=='npm'||workspaces||observationIssue||pm.issue)throw Error('test recipe requires a supported single-package npm checkout');
241
+ if(!['npm','pnpm','yarn'].includes(pm.name)||workspaces||observationIssue||pm.issue)
242
+ throw Error('test recipe requires a supported single-package npm, pnpm or Yarn checkout');
213
243
  const chosen=testRecipeScript(scripts);
214
244
  if(chosen.ambiguous)throw Error(`several scripts run Vitest or Jest directly (${chosen.ambiguous.join(', ')})`);
215
245
  if(!chosen.script)throw Error(chosen.issue||'no script runs Vitest or Jest directly');
@@ -220,20 +250,24 @@ export function discoverNodeProject(root,{recipe=null}={}) {
220
250
  reason:`explicit local ${project.runner} ${project.version} run of script ${JSON.stringify(chosen.script)}; the whole checkout is its input; npm and lifecycle scripts are not executed`};
221
251
  }catch(e){classification={level:'recording',reason:`Test recipe unavailable (${e.code||e.message}); original npm script retained`};}
222
252
  }
223
- if(pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&
224
- /^(?:tsc --noEmit|tsc -p (?:\.|tsconfig\.json)(?: --noEmit)?)$/.test(scripts[script].trim())){
225
- try{const contract=pnpmTypecheckInputs(root,'.',script);
226
- if(contract.limitations.length)throw Error(contract.limitations.join('; '));
227
- check.command=['@node','@typescript-bin:.',
228
- ...scripts[script].trim().split(/\s+/).slice(1)];
229
- check.cwd='.';
230
- check.inputs=['package.json','pnpm-lock.yaml',
231
- ...(exists(path.join(root,'pnpm-workspace.yaml'))?['pnpm-workspace.yaml']:[]),
232
- 'tsconfig.json','src/**'];
233
- classification={level:'ready',reason:'pinned pnpm TypeScript inputs and installation; checked again before reuse',
253
+ if(pm.name==='pnpm'&&kind==='typecheck'&&!workspaces&&!pm.issue&&(script===undefined||tscRuns(scripts[script]))){
254
+ try{
255
+ parts=tscChecks('typecheck',script??null,scripts,run=>{
256
+ const contract=pnpmTypecheckInputs(root,'.',run);
257
+ if(contract.limitations.length)throw Error(contract.limitations.join('; '));
258
+ return {command:['@node','@typescript-bin:.',...run.args],cwd:'.',
259
+ inputs:[...new Set(['package.json','pnpm-lock.yaml',
260
+ ...(exists(path.join(root,'pnpm-workspace.yaml'))?['pnpm-workspace.yaml']:[]),
261
+ ...contractInputs(contract)])]};});
262
+ classification={level:'ready',reason:'pnpm TypeScript inputs and installation; checked again before reuse',
234
263
  qualification:'pnpm-tsc-v1'};}
235
264
  catch(e){classification={level:'recording',reason:`pnpm TypeScript contract needs review: ${e.message}`};}
236
265
  }
266
+ if(!script&&classification.level!=='ready')continue;
267
+ if(parts&&classification.level==='ready'){
268
+ for(const part of parts)checks.push({config:{...check,...part,qualification:classification.qualification},...classification});
269
+ continue;
270
+ }
237
271
  if(classification.qualification)check.qualification=classification.qualification;
238
272
  checks.push({config:check,...classification});
239
273
  }
@@ -251,6 +285,16 @@ export function discoverNodeProject(root,{recipe=null}={}) {
251
285
  const selected=selectScript(kind,names,member.scripts||{});
252
286
  if(selected.ambiguous){ambiguous.push({kind:`${name}:${kind}`,
253
287
  scripts:selected.ambiguous});continue;}
288
+ // A pnpm package with a tsconfig.json and no typecheck script gets a
289
+ // proposed `tsc --noEmit` check when the pnpm contract holds.
290
+ if(!selected.script&&kind==='typecheck'&&pm.name==='pnpm'&&!pm.issue&&
291
+ proposable(path.join(root,dir),member.scripts)&&!typechecks(scripts)){
292
+ try{for(const part of tscChecks(`${name}:typecheck`,null,{},run=>pnpmCheck(root,name,dir,file,run)))
293
+ checks.push({config:{kind,workspace:name,qualification:'pnpm-tsc-v1',...part},level:'ready',
294
+ reason:'proposed tsc --noEmit for the package\'s tsconfig.json; pnpm workspace TypeScript inputs; checked again before reuse'});}
295
+ catch{}
296
+ continue;
297
+ }
254
298
  if(!selected.script)continue;
255
299
  const command=pm.name==='npm'?['@which:npm','run',selected.script,
256
300
  '--workspace',name]:pm.name==='yarn'?
@@ -271,22 +315,21 @@ export function discoverNodeProject(root,{recipe=null}={}) {
271
315
  level='ready';reason='pinned Yarn workspace tsc and installed inputs; checked again before reuse';
272
316
  }catch(e){reason=`workspace TypeScript contract needs review: ${e.message}`;}
273
317
  }
274
- if(!pm.issue&&pm.name==='pnpm'&&kind==='typecheck'&&
275
- /^(?:tsc --noEmit|tsc -p (?:\.|tsconfig\.json)(?: --noEmit)?)$/.test(
276
- (member.scripts||{})[selected.script]?.trim()||'')&&
277
- !member.scripts?.['pre'+selected.script]&&!member.scripts?.['post'+selected.script]){
278
- try{const contract=pnpmTypecheckInputs(root,name,selected.script);
279
- if(contract.limitations.length)throw Error(contract.limitations.join('; '));
280
- check.workspace=name;check.qualification='pnpm-tsc-v1';
281
- check.script=selected.script;
282
- check.cwd=dir;
283
- check.command=['@node',`@typescript-bin:${dir}`,
284
- ...member.scripts[selected.script].trim().split(/\s+/).slice(1)];
285
- check.inputs=['package.json','pnpm-lock.yaml','pnpm-workspace.yaml',file,
286
- `${dir}/src/**`];
287
- level='ready';reason='pinned pnpm workspace TypeScript inputs; checked again before reuse';
318
+ if(!pm.issue&&pm.name==='pnpm'&&kind==='typecheck'&&tscRuns((member.scripts||{})[selected.script])){
319
+ try{const parts=tscChecks(`${name}:typecheck`,selected.script,member.scripts,run=>pnpmCheck(root,name,dir,file,run));
320
+ for(const part of parts)checks.push({config:{kind,workspace:name,qualification:'pnpm-tsc-v1',...part},level:'ready',
321
+ reason:'pnpm workspace TypeScript inputs; checked again before reuse'});
322
+ continue;
288
323
  }catch(e){reason=`pnpm TypeScript contract needs review: ${e.message}`;}
289
324
  }
325
+ if(recipe==='tests-direct'&&kind==='test'&&!pm.issue){
326
+ try{const project=testProject(root,selected.script,dir);
327
+ Object.assign(check,{workspace:name,script:selected.script,cwd:dir,
328
+ command:['@node',`@test-runner:${project.runner}`,...project.args],...testCheckInputs(project),
329
+ qualification:testsQualification});
330
+ level='ready';reason=`explicit local ${project.runner} ${project.version} run of ${name}'s script ${JSON.stringify(selected.script)}; the whole checkout is its input; the package manager and lifecycle scripts are not executed`;
331
+ }catch(e){reason=`Test recipe unavailable (${e.code||e.message}); original script retained`;}
332
+ }
290
333
  checks.push({config:check,level,reason});
291
334
  }
292
335
  }
@@ -0,0 +1,124 @@
1
+ // An installed npm package checked against what the lockfile recorded: the
2
+ // tarball npm cached under the lockfile's integrity (sha512, else sha1),
3
+ // unpacked in memory and compared file by file. Nothing is fetched.
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import zlib from 'node:zlib';
8
+ import {createHash} from 'node:crypto';
9
+ import {npmrc} from './npmrc.mjs';
10
+ import {racyUntil} from './path-identity.mjs';
11
+
12
+ const fail=code=>{throw Object.assign(Error(code),{code});};
13
+ // npm's cache directory, as npm picks it: npm_config_cache, else `cache` in
14
+ // the project's .npmrc, then the user's (npm_config_userconfig or
15
+ // ~/.npmrc), else npm's default. A leading ~ is the home directory. The
16
+ // global npmrc is not read.
17
+ export function npmCacheDir(root=null,env=process.env,platform=process.platform,home=os.homedir()){
18
+ const variable=name=>Object.entries(env).find(([key])=>key.toLowerCase()===name)?.[1];
19
+ const expand=value=>path.resolve(value.replace(/^~(?=$|[\/])/,home));
20
+ const configured=variable('npm_config_cache');
21
+ if(configured)return expand(configured);
22
+ for(const file of [root&&path.join(root,'.npmrc'),variable('npm_config_userconfig')||path.join(home,'.npmrc')].filter(Boolean)){
23
+ let cache;try{cache=npmrc(file).cache;}catch{}
24
+ if(typeof cache==='string'&&cache)return expand(cache);
25
+ }
26
+ return platform==='win32'?path.join(env.LOCALAPPDATA||path.join(home,'AppData','Local'),'npm-cache'):path.join(home,'.npm');
27
+ }
28
+ // The package-lock.json entry for an installed top-level package:
29
+ // {version, integrity, resolved, alias}. npm-shrinkwrap.json, which npm reads
30
+ // instead, is not modeled. Parsed entries are kept per process by the
31
+ // lockfile's identity: the daemon asks on every reuse check.
32
+ const lockfiles=new Map();
33
+ export function lockedPackage(root,name){
34
+ if(fs.existsSync(path.join(root,'npm-shrinkwrap.json')))fail('npm-shrinkwrap-unsupported');
35
+ const file=path.join(root,'package-lock.json');
36
+ let st;try{st=fs.statSync(file,{bigint:true});}catch{fail('npm-lockfile-integrity-missing');}
37
+ const identity=[st.dev,st.ino,st.size,st.mtimeNs,st.ctimeNs].join(':');
38
+ let packages=lockfiles.get(file)?.identity===identity?lockfiles.get(file).packages:null;
39
+ if(!packages){
40
+ try{packages=JSON.parse(fs.readFileSync(file,'utf8'))?.packages;}catch{fail('npm-lockfile-unreadable');}
41
+ // A lockfile changed within the racy window is read again next time.
42
+ if(racyUntil(st)<=Date.now())lockfiles.set(file,{identity,packages});
43
+ }
44
+ const entry=packages?.[`node_modules/${name}`];
45
+ if(!entry||typeof entry.version!=='string'||typeof entry.integrity!=='string')fail('npm-lockfile-integrity-missing');
46
+ return {version:entry.version,integrity:entry.integrity,resolved:entry.resolved??null,alias:entry.name??null};
47
+ }
48
+ // Whether a lockfile entry is the registry's own package NAME at VERSION:
49
+ // no alias, and a tarball URL .../NAME/-/NAME-VERSION.tgz over https.
50
+ export const registryPackage=(entry,name,version)=>entry.alias===null&&typeof entry.resolved==='string'&&
51
+ /^https:\/\//.test(entry.resolved)&&entry.resolved.endsWith(`/${name}/-/${name}-${version}.tgz`);
52
+ // The digest an SRI string names, as {algorithm, hex}: sha512, else sha1
53
+ // (some registries record only sha1; a collision needs a malicious party,
54
+ // which is outside REDUE's threat model). null for anything else.
55
+ export function sriDigest(integrity){
56
+ const items=String(integrity).split(/\s+/);
57
+ for(const [algorithm,length] of [['sha512',64],['sha1',20]]){
58
+ const value=items.map(item=>new RegExp(`^${algorithm}-([A-Za-z0-9+/]+={0,2})$`).exec(item)?.[1]).find(Boolean);
59
+ const bytes=value&&Buffer.from(value,'base64');
60
+ if(bytes?.length===length)return {algorithm,hex:bytes.toString('hex')};
61
+ }
62
+ return null;
63
+ }
64
+ // The cached tarball for an integrity, checked against it.
65
+ export function cachedTarball(cache,integrity){
66
+ const digest=sriDigest(integrity);
67
+ if(!digest)fail('npm-lockfile-integrity-unsupported');
68
+ const {algorithm,hex}=digest;
69
+ let bytes;
70
+ try{bytes=fs.readFileSync(path.join(cache,'_cacache','content-v2',algorithm,hex.slice(0,2),hex.slice(2,4),hex.slice(4)));}
71
+ catch{fail('npm-tarball-uncached');}
72
+ if(createHash(algorithm).update(bytes).digest('hex')!==hex)fail('npm-tarball-corrupt');
73
+ return bytes;
74
+ }
75
+ // The regular files of a gzipped npm tarball, as Map(path below the top
76
+ // directory -> bytes). Links and special files are refused.
77
+ export function tarballFiles(gzipped){
78
+ let tar;try{tar=zlib.gunzipSync(gzipped,{maxOutputLength:512*1024*1024});}catch{fail('npm-tarball-invalid');}
79
+ const files=new Map();
80
+ const text=(block,from,to)=>block.subarray(from,to).toString('utf8').replace(/\0[\s\S]*$/,'');
81
+ let offset=0,longName=null,paxPath=null;
82
+ while(offset+512<=tar.length){
83
+ const header=tar.subarray(offset,offset+512);
84
+ if(header.every(byte=>byte===0))break;
85
+ const size=parseInt(text(header,124,136).trim()||'0',8),type=String.fromCharCode(header[156]||48);
86
+ if(!Number.isSafeInteger(size)||size<0)fail('npm-tarball-invalid');
87
+ const body=tar.subarray(offset+512,offset+512+size);
88
+ offset+=512+Math.ceil(size/512)*512;
89
+ if(type==='x'){
90
+ // pax records: "LEN key=value\n"
91
+ for(const record of body.toString('utf8').split('\n')){const match=/^\d+ path=(.*)$/.exec(record);if(match)paxPath=match[1];}
92
+ continue;
93
+ }
94
+ if(type==='g')continue;
95
+ if(type==='L'){longName=body.toString('utf8').replace(/\0[\s\S]*$/,'');continue;}
96
+ const prefix=text(header,345,500);
97
+ const name=paxPath??longName??(prefix?`${prefix}/${text(header,0,100)}`:text(header,0,100));
98
+ paxPath=null;longName=null;
99
+ if(type==='5')continue;
100
+ if(!['0','7'].includes(type))fail('npm-tarball-link-unsupported');
101
+ // npm strips the top directory (usually package/).
102
+ const relative=name.split('/').slice(1).join('/');
103
+ if(!relative||relative.split('/').some(part=>part===''||part==='.'||part==='..'))fail('npm-tarball-invalid');
104
+ files.set(relative,body);
105
+ }
106
+ return files;
107
+ }
108
+ // Whether `dir` holds exactly the tarball's files, byte for byte; throws
109
+ // otherwise. About 120 ms for TypeScript 6 (140 files, 24 MB).
110
+ export function matchTarball(dir,files){
111
+ let count=0;
112
+ const walk=(folder,relative='')=>{
113
+ let entries;try{entries=fs.readdirSync(folder,{withFileTypes:true});}catch{fail('direct-typescript-implementation-unreviewed');}
114
+ for(const entry of entries){
115
+ const name=relative?`${relative}/${entry.name}`:entry.name,full=path.join(folder,entry.name);
116
+ if(entry.isDirectory()){walk(full,name);continue;}
117
+ const expected=files.get(name);
118
+ let same=false;try{same=entry.isFile()&&Boolean(expected)&&fs.readFileSync(full).equals(expected);}catch{}
119
+ if(!same)fail('direct-typescript-implementation-unreviewed');
120
+ count++;
121
+ }
122
+ };walk(dir);
123
+ if(count!==files.size)fail('direct-typescript-implementation-unreviewed');
124
+ }
package/src/npmrc.mjs ADDED
@@ -0,0 +1,16 @@
1
+ // An .npmrc file read as npm reads ini: comments, a BOM, `key = value`, a bare
2
+ // key as true, quotes stripped, true/false as booleans. {} when it is absent.
3
+ import fs from 'node:fs';
4
+
5
+ export function npmrc(file){
6
+ const settings={};
7
+ if(!fs.existsSync(file))return settings;
8
+ for(const line of fs.readFileSync(file,'utf8').replace(/^\uFEFF/,'').split(/\r?\n/)){
9
+ const trimmed=line.trim();if(!trimmed||/^[#;]/.test(trimmed))continue;
10
+ const at=trimmed.indexOf('='),key=(at<0?trimmed:trimmed.slice(0,at)).trim();
11
+ let value=at<0?true:trimmed.slice(at+1).trim();
12
+ if(typeof value==='string'&&/^(["']).*\1$/.test(value))value=value.slice(1,-1);
13
+ settings[key]=value==='true'?true:value==='false'?false:value;
14
+ }
15
+ return settings;
16
+ }
@@ -5,6 +5,7 @@ import {createHash} from 'node:crypto';
5
5
  import {discover} from './plan.mjs';
6
6
  import {InputIndex,dehydrateIndex} from './index.mjs';
7
7
  import {directProject,directQualification} from './direct-typescript.mjs';
8
+ import {checkRun} from './typescript-invocation.mjs';
8
9
  import {queriesMatch} from './typescript-list.mjs';
9
10
  import {planGuard,inputKey} from './decision-validation.mjs';
10
11
  import {timing,traceBase} from './decision-profile.mjs';
@@ -25,8 +26,10 @@ try {
25
26
  const queryStarted=performance.now();
26
27
  // Cached membership queries load the compiler API; establish its reviewed
27
28
  // implementation before that optimization can execute project code.
29
+ // Every direct check (a chain has one per run) uses the root's compiler:
30
+ // its bytes are verified once.
28
31
  const directRoots=new Set(config.checks.filter(check=>check.qualification===directQualification)
29
- .map(check=>directProject(config.root,check.script).tsRoot));
32
+ .map((check,i)=>directProject(config.root,checkRun(check),{verify:i===0}).tsRoot));
30
33
  if(directRoots.size&&(workerData.reuse.queries||[]).some(([kind,,,options])=>
31
34
  kind==='readDirectory'&&!directRoots.has(options?.tsRoot)))throw Error('retained compiler implementation changed');
32
35
  const queriesValid=guard===workerData.reuse.guard&&