@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.30

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.
Files changed (67) hide show
  1. package/CHANGELOG.md +806 -0
  2. package/README.md +157 -99
  3. package/bin/redue.mjs +267 -61
  4. package/docs/README.md +3 -1
  5. package/docs/user/agents.md +333 -15
  6. package/docs/user/configuration.md +28 -2
  7. package/docs/user/direct-typescript.md +219 -0
  8. package/docs/user/getting-started.md +49 -7
  9. package/docs/user/json.md +4 -2
  10. package/docs/user/node-script.md +177 -0
  11. package/docs/user/npm-launcher-notice.md +42 -0
  12. package/docs/user/pilot.md +146 -0
  13. package/docs/user/sandbox.md +312 -0
  14. package/docs/user/shell.md +3 -3
  15. package/docs/user/supported-projects.md +69 -23
  16. package/docs/user/tests-direct.md +143 -0
  17. package/package.json +4 -3
  18. package/src/agent-integration.mjs +467 -0
  19. package/src/agent-policy.md +28 -0
  20. package/src/cached-state.mjs +24 -1
  21. package/src/claude-hook.mjs +616 -0
  22. package/src/cli.mjs +449 -54
  23. package/src/daemon.mjs +442 -93
  24. package/src/decision-profile.mjs +15 -1
  25. package/src/decision-validation.mjs +12 -1
  26. package/src/declared-plan.mjs +38 -6
  27. package/src/direct-run-certificate.mjs +254 -0
  28. package/src/direct-typescript-probe.mjs +27 -0
  29. package/src/direct-typescript.mjs +205 -0
  30. package/src/glob.mjs +8 -0
  31. package/src/history-worker.mjs +21 -13
  32. package/src/index.mjs +108 -21
  33. package/src/installed-inputs.mjs +7 -1
  34. package/src/installed-observation.mjs +23 -0
  35. package/src/landlock.py +527 -0
  36. package/src/linux-inotify.py +39 -10
  37. package/src/node-onboarding.mjs +203 -54
  38. package/src/node-script.mjs +406 -0
  39. package/src/npm-integrity.mjs +124 -0
  40. package/src/npm-typecheck-environment.mjs +36 -0
  41. package/src/npmrc.mjs +16 -0
  42. package/src/observation.mjs +34 -12
  43. package/src/path-identity.mjs +8 -0
  44. package/src/plan-worker.mjs +17 -17
  45. package/src/pnpm-inputs.mjs +122 -57
  46. package/src/pnpm-shim-templates.json +180 -0
  47. package/src/pnpm-shims.mjs +54 -0
  48. package/src/pnpm-typecheck-probe.mjs +11 -6
  49. package/src/presentation.mjs +13 -4
  50. package/src/probe-deadline.mjs +37 -0
  51. package/src/probe-result.mjs +7 -3
  52. package/src/project-location.mjs +34 -0
  53. package/src/python-onboarding.mjs +89 -0
  54. package/src/run-recorder.cjs +91 -0
  55. package/src/sandbox.mjs +653 -0
  56. package/src/setup.mjs +205 -0
  57. package/src/stats.mjs +72 -0
  58. package/src/test-direct.mjs +608 -0
  59. package/src/test-recorder.cjs +435 -0
  60. package/src/typescript-contract-probe.mjs +4 -187
  61. package/src/typescript-input-facts.mjs +92 -0
  62. package/src/typescript-invocation.mjs +92 -0
  63. package/src/typescript-list.mjs +118 -13
  64. package/src/typescript-qualification.mjs +29 -49
  65. package/src/workspace-manifests.mjs +24 -0
  66. package/src/yarn-workspace-inputs.mjs +3 -0
  67. package/src/yarn-workspace-typecheck-probe.mjs +2 -1
@@ -1,4 +1,5 @@
1
1
  import fs from 'node:fs';
2
+ import {mark,traceEnvironment} from './decision-profile.mjs';
2
3
  import path from 'node:path';
3
4
  import {randomUUID} from 'node:crypto';
4
5
  import {fork} from 'node:child_process';
@@ -8,16 +9,39 @@ import {fileURLToPath} from 'node:url';
8
9
  // subscription is healthy; history may accelerate catch-up; a fresh scan is
9
10
  // the deterministic fallback. A failed history operation never updates cursor.
10
11
  const workerFile=path.join(path.dirname(fileURLToPath(import.meta.url)),'history-worker.mjs');
12
+ // Each query runs in a fresh helper: a reused native history session can take
13
+ // ten times longer. The next helper is forked once a query settles, so its
14
+ // startup is off the decision path. A failed or late helper is killed.
15
+ let spare=null,queue=Promise.resolve(),closed=false;
16
+ // After the observer's last query no helper is kept or started.
17
+ export function closeHistory() {closed=true;spare?.kill('SIGKILL');spare=null;}
18
+ function startHelper() {
19
+ const child=fork(workerFile,[],{stdio:['ignore','ignore','ignore','ipc'],env:traceEnvironment()});
20
+ child.abandoned=new Set();child.on('error',()=>{});
21
+ child.once('exit',()=>{if(spare===child)spare=null;
22
+ child.reply?.({ok:false,lost:Error(`history helper exited ${child.exitCode??child.signalCode}`)});
23
+ for(const next of child.abandoned)try{fs.unlinkSync(next);}catch{}});
24
+ return child;
25
+ }
11
26
  export function history(root,cursor,backend,deadlineMs,faultFile) {
27
+ const run=()=>query(root,cursor,backend,deadlineMs,faultFile);
28
+ const result=queue.then(run,run);queue=result.catch(()=>{});return result;
29
+ }
30
+ function query(root,cursor,backend,deadlineMs,faultFile) {
12
31
  const next=cursor+'.'+randomUUID()+'.next';
13
32
  return new Promise((resolve,reject)=>{
14
- const worker=fork(workerFile,[root,fs.existsSync(cursor)?cursor:'',next,backend,
15
- faultFile||''],{stdio:['ignore','ignore','ignore','ipc']});
16
- let settled=false,abandoned=false;
33
+ mark('history.submit',{deadlineMs});
34
+ const worker=spare??startHelper();spare=null;
35
+ worker.ref();worker.channel?.ref();
36
+ let settled=false;
17
37
  const finish=(error,value)=>{
18
- if(settled)return;settled=true;clearTimeout(timer);
38
+ if(settled)return;settled=true;worker.reply=null;clearTimeout(timer);
39
+ mark('history.complete',{childPid:worker.pid,outcome:error?.code||error?.name||'ok',events:value?.length});
40
+ if(error)worker.abandoned.add(next);
19
41
  worker.kill('SIGKILL');
20
- if(error){abandoned=true;try{fs.unlinkSync(next);}catch{}
42
+ // A failed query disables history for this observer; keep no helper.
43
+ if(!closed&&!error){spare=startHelper();spare.unref();spare.channel?.unref();}
44
+ if(error){try{fs.unlinkSync(next);}catch{}
21
45
  reject(error);return;}
22
46
  try{fs.renameSync(next,cursor);resolve(value);}
23
47
  catch(e){try{fs.unlinkSync(next);}catch{}reject(e);}
@@ -26,12 +50,10 @@ export function history(root,cursor,backend,deadlineMs,faultFile) {
26
50
  const e=Error(`history query did not settle within ${deadlineMs} ms`);
27
51
  e.code='ETIMEDOUT';finish(e);
28
52
  },deadlineMs);
29
- worker.on('message',message=>message.ok?finish(null,message.changes):
30
- finish(Object.assign(Error(message.error),{code:message.code})));
31
- worker.once('error',e=>finish(e));
32
- worker.once('exit',(code,signal)=>{
33
- if(abandoned)try{fs.unlinkSync(next);}catch{}
34
- if(!settled)finish(Error(`history helper exited ${code??signal}`));
35
- });
53
+ worker.reply=message=>message.lost?finish(message.lost):message.ok?finish(null,message.changes):
54
+ finish(Object.assign(Error(message.error),{code:message.code}));
55
+ worker.on('message',message=>worker.reply?.(message));
56
+ try{worker.send({root,previous:fs.existsSync(cursor)?cursor:'',next,backend,faultFile:faultFile||''});}
57
+ catch(e){finish(e);}
36
58
  });
37
59
  }
@@ -41,3 +41,11 @@ export function validInputRelative(rel,platform=process.platform) {
41
41
  rel.includes('\\')))return false;
42
42
  return true;
43
43
  }
44
+
45
+ // File identity fields. Any write changes ctime, but timestamps tick in
46
+ // milliseconds: a file changed within the racy window could be rewritten in the
47
+ // same tick at the same size without changing them (git's racy rule).
48
+ export const identityFields=['dev','ino','mode','nlink','size','mtimeNs','ctimeNs'];
49
+ export const racyWindowMs=2000;
50
+ // ctime, which no ordinary tool can set ahead, ends the window.
51
+ export const racyUntil=st=>Number(st.ctimeNs/1000000n)+racyWindowMs;
@@ -3,11 +3,14 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import {createHash} from 'node:crypto';
5
5
  import {discover} from './plan.mjs';
6
- import {InputIndex} from './index.mjs';
6
+ import {InputIndex,dehydrateIndex} from './index.mjs';
7
+ import {directProject,directQualification} from './direct-typescript.mjs';
8
+ import {checkRun} from './typescript-invocation.mjs';
7
9
  import {queriesMatch} from './typescript-list.mjs';
8
10
  import {planGuard,inputKey} from './decision-validation.mjs';
9
- import {timing} from './decision-profile.mjs';
11
+ import {timing,traceBase} from './decision-profile.mjs';
10
12
 
13
+ traceBase(workerData.diagnostic);
11
14
  try {
12
15
  const totalStarted=performance.now();
13
16
  const configStarted=performance.now();
@@ -21,7 +24,16 @@ try {
21
24
  try{
22
25
  guard=planGuard(bundle);
23
26
  const queryStarted=performance.now();
24
- const queriesValid=!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries);
27
+ // Cached membership queries load the compiler API; establish its reviewed
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.
31
+ const directRoots=new Set(config.checks.filter(check=>check.qualification===directQualification)
32
+ .map((check,i)=>directProject(config.root,checkRun(check),{verify:i===0}).tsRoot));
33
+ if(directRoots.size&&(workerData.reuse.queries||[]).some(([kind,,,options])=>
34
+ kind==='readDirectory'&&!directRoots.has(options?.tsRoot)))throw Error('retained compiler implementation changed');
35
+ const queriesValid=guard===workerData.reuse.guard&&
36
+ (!workerData.reuse.queries?.length||queriesMatch(workerData.reuse.queries));
25
37
  timing('worker.compiler_queries',queryStarted,{queries:workerData.reuse.queries?.length||0,matched:queriesValid?1:0});
26
38
  if(guard!==workerData.reuse.guard||!queriesValid)reuseReason='configuration_or_resolution_changed';
27
39
  else {
@@ -104,26 +116,14 @@ try {
104
116
  const outside=new InputIndex({root:boundary,plans},{trackGit:false,includeInstalled:true});
105
117
  outside.coldScan();
106
118
  if(outside.unavailable)throw Error(outside.unavailable);
107
- external[boundary]={files:[...outside.files],
108
- aliasDependencies:[...outside.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
109
- rows:[...outside.checks]
110
- .map(([name,row])=>({name,files:[...row.files],sum:row.sum.toString('hex'),
111
- revision:row.revision,lastReason:row.lastReason})),
112
- eventCount:outside.eventCount,rehashedFiles:outside.rehashedFiles,
113
- rehashedBytes:outside.rehashedBytes,profile:outside.profile};
119
+ external[boundary]=dehydrateIndex(outside);
114
120
  }
115
121
  timing('worker.total',totalStarted,{mode:'cold',files:index.files.size});
116
122
  parentPort.postMessage({ok:true,bundle,discoveryMs,indexingMs,guard,planReused,
117
123
  inputKeys:Object.fromEntries([...index.checks].map(([name,row])=>[name,inputKey(row)])),
118
124
  restartMode:'cold-index',
119
125
  external,
120
- index:{files:[...index.files],
121
- aliasDependencies:[...index.aliasDependencies].map(([name,refs])=>[name,[...refs]]),
122
- rows:[...index.checks].map(([name,row])=>({
123
- name,files:[...row.files],sum:row.sum.toString('hex'),revision:row.revision,
124
- lastReason:row.lastReason})),
125
- eventCount:index.eventCount,rehashedFiles:index.rehashedFiles,
126
- rehashedBytes:index.rehashedBytes,profile:index.profile}});
126
+ index:dehydrateIndex(index)});
127
127
  }
128
128
  } catch(error) {
129
129
  parentPort.postMessage({ok:false,error:error?.message||String(error),
@@ -1,21 +1,27 @@
1
- // Pinned pnpm 12 node-modules installation provider. The engine sees only
1
+ // pnpm 9-12 node-modules installation provider. The engine sees only
2
2
  // repository-relative input sets and resolution relationships, never pnpm
3
3
  // virtual-store names as semantic identities.
4
- import {compilerFiles} from './typescript-list.mjs';
4
+ import {probeTimeLeft} from './probe-deadline.mjs';
5
+ import {compilerApiIssue,compilerFiles} from './typescript-list.mjs';
5
6
  import fs from 'node:fs';
6
7
  import path from 'node:path';
7
8
  import {createRequire} from 'node:module';
8
9
  import {execFileSync,spawnSync} from 'node:child_process';
9
- import micromatch from './glob.mjs';
10
- import YAML from 'yaml';
10
+ import micromatch,{patternMatcher} from './glob.mjs';
11
11
  import {resolveLinks} from './installed-inputs.mjs';
12
12
  import {withinPath as within,realObservedPath,samePath} from './path-identity.mjs';
13
13
  import {timing} from './decision-profile.mjs';
14
+ import {npmrc} from './npmrc.mjs';
15
+ import {runConfig,tscRun,tscRuns} from './typescript-invocation.mjs';
14
16
 
15
17
  const rel=(root,file)=>path.relative(root,file).split(path.sep).join('/');
16
18
  const read=file=>JSON.parse(fs.readFileSync(file,'utf8'));
17
19
  const exists=file=>{try{fs.lstatSync(file);return true;}catch{return false;}};
18
- function yaml(file){
20
+ // Loaded on first use: the parser takes about 40 ms to load, and every
21
+ // command imports this module.
22
+ let YAML=null;
23
+ export function yaml(file){
24
+ YAML??=createRequire(import.meta.url)('yaml');
19
25
  const doc=YAML.parseDocument(fs.readFileSync(file,'utf8'),{uniqueKeys:true});
20
26
  if(doc.errors.length)throw Error(`${path.basename(file)} cannot be interpreted safely`);
21
27
  return doc.toJS()||{};
@@ -26,40 +32,67 @@ function safePatterns(values){
26
32
  throw Error('pnpm workspace patterns are unsupported');
27
33
  return values;
28
34
  }
35
+ // The supported pnpm majors. pnpm 9 reads settings from .npmrc, pnpm 10 from
36
+ // pnpm-workspace.yaml first and then .npmrc, pnpm 11 and 12 from
37
+ // pnpm-workspace.yaml only.
38
+ export const pnpmMajors=[9,10,11,12];
39
+ // Settings that move or copy installed files, or run code during install,
40
+ // where the contract does not look; by pnpm-workspace.yaml and .npmrc name.
41
+ // Other settings change only what gets installed, and the installed files are
42
+ // what the contract observes. These are refused when either file sets them,
43
+ // whichever file the installed pnpm reads. Only the project's files are read:
44
+ // user or global rc files and npm_config_* variables at install time are not.
45
+ const refused=[['modulesDir','modules-dir'],['virtualStoreDir','virtual-store-dir'],['lockfileDir','lockfile-dir'],
46
+ ['enableGlobalVirtualStore','enable-global-virtual-store'],['injectWorkspacePackages','inject-workspace-packages'],
47
+ ['sharedWorkspaceLockfile','shared-workspace-lockfile'],['pnpmfile','pnpmfile'],['globalPnpmfile','global-pnpmfile'],
48
+ ['configDependencies','config-dependencies']];
49
+ // Whether a refused setting is set: present at all, except that only
50
+ // sharedWorkspaceLockfile=false and injectWorkspacePackages=true move files.
51
+ const setting=(key,value)=>value!==undefined&&
52
+ (key==='sharedWorkspaceLockfile'?value===false:key==='injectWorkspacePackages'?value===true:true);
29
53
  export function pnpmProjectInfo(root){
30
54
  root=realObservedPath(root);
31
55
  const pkg=read(path.join(root,'package.json'));
32
- const pin=/^pnpm@(12\.\d+\.\d+)(?:\+[^\s]+)?$/.exec(pkg.packageManager||'');
33
- if(!pin)throw Error('an exact pnpm 12 packageManager pin is required');
56
+ // A pin is optional; when present it names pnpm 9-12, and the install must match it.
57
+ const pin=pkg.packageManager===undefined?null:/^pnpm@((\d+)\.\d+\.\d+)(?:\+[^\s]+)?$/.exec(pkg.packageManager);
58
+ if(pkg.packageManager!==undefined&&(!pin||!pnpmMajors.includes(Number(pin[2]))))
59
+ throw Error(`packageManager ${JSON.stringify(String(pkg.packageManager).split('+')[0])} is outside pnpm ${pnpmMajors[0]}-${pnpmMajors.at(-1)}`);
34
60
  const workspaceFile=path.join(root,'pnpm-workspace.yaml');
35
- const settings=exists(workspaceFile)?yaml(workspaceFile):{};
36
- const patterns=safePatterns(settings.packages||[]);
37
- const linker=settings.nodeLinker||'isolated';
38
- return {version:pin[1],linker,patterns,settings};
61
+ const workspace=exists(workspaceFile)?yaml(workspaceFile):{},rc=npmrc(path.join(root,'.npmrc'));
62
+ const patterns=safePatterns(workspace.packages||[]);
63
+ // The linker the given pnpm major reads; without a major, the one any reads.
64
+ const linkerFor=major=>(major===9?rc['node-linker']:major>=11?workspace.nodeLinker:
65
+ workspace.nodeLinker??rc['node-linker'])||'isolated';
66
+ const major=pin?Number(pin[2]):null;
67
+ return {version:pin?.[1]??null,linker:linkerFor(major),linkerFor,patterns,settings:workspace,
68
+ refused:refused.filter(([key,name])=>setting(key,workspace[key])||setting(key,rc[name])).map(([key])=>key)};
39
69
  }
40
70
  export function pnpmInstallInfo(root){
41
71
  const started=performance.now();
42
72
  root=realObservedPath(root);
43
- const pkg=read(path.join(root,'package.json'));
44
- const {version,linker,patterns,settings}=pnpmProjectInfo(root);
73
+ const {version,linkerFor,patterns,settings,refused}=pnpmProjectInfo(root);
45
74
  const workspaceFile=path.join(root,'pnpm-workspace.yaml');
46
- if(!['isolated','hoisted'].includes(linker))throw Error(`pnpm ${linker} linker is recording-only`);
47
- if(settings.modulesDir||settings.virtualStoreDir||settings.enableGlobalVirtualStore)
48
- throw Error('custom pnpm modules or virtual-store location is recording-only');
49
- if(settings.injectWorkspacePackages)
50
- throw Error('injected workspace dependencies are recording-only');
51
- if(settings.sharedWorkspaceLockfile===false)
52
- throw Error('separate workspace lockfiles are recording-only');
53
- if(exists(path.join(root,'.pnpmfile.cjs'))||exists(path.join(root,'.pnpmfile.mjs')))
75
+ if(refused.some(key=>['pnpmfile','globalPnpmfile','configDependencies'].includes(key))||
76
+ exists(path.join(root,'.pnpmfile.cjs'))||exists(path.join(root,'.pnpmfile.mjs')))
54
77
  throw Error('custom pnpm installation hooks need review');
78
+ if(refused.some(key=>['modulesDir','virtualStoreDir','lockfileDir','enableGlobalVirtualStore'].includes(key)))
79
+ throw Error('custom pnpm modules or virtual-store location is recording-only');
80
+ if(refused.includes('injectWorkspacePackages'))throw Error('injected workspace dependencies are recording-only');
81
+ if(refused.includes('sharedWorkspaceLockfile'))throw Error('separate workspace lockfiles are recording-only');
55
82
  const modulesFile=path.join(root,'node_modules','.modules.yaml');
56
83
  if(!exists(modulesFile))throw Error('installed pnpm module metadata is missing');
57
84
  const modules=yaml(modulesFile);
58
- if(modules.nodeLinker!==linker||modules.packageManager!==pkg.packageManager)
85
+ const installed=/^pnpm@((\d+)\.\d+\.\d+)$/.exec(String(modules.packageManager));
86
+ if(!installed||!pnpmMajors.includes(Number(installed[2])))
87
+ throw Error(`installed by ${JSON.stringify(String(modules.packageManager))}, outside pnpm ${pnpmMajors[0]}-${pnpmMajors.at(-1)}`);
88
+ const linker=linkerFor(Number(installed[2]));
89
+ if(!['isolated','hoisted'].includes(linker))throw Error(`pnpm ${linker} linker is recording-only`);
90
+ if(modules.nodeLinker!==linker||version!==null&&installed[1]!==version)
59
91
  throw Error('installed pnpm linker or version differs from project configuration');
60
92
  const virtualStore=path.join(root,'node_modules','.pnpm');
61
- // pnpm 12 on Windows can omit virtualStoreDir when using its project-local
62
- // default. Accept only the observed, ordinary local directory in that case.
93
+ // pnpm can omit virtualStoreDir when using its project-local default
94
+ // (seen with pnpm 12 on Windows). Accept only the observed, ordinary local
95
+ // directory in that case.
63
96
  const declared=modules.virtualStoreDir;
64
97
  const selected=declared===undefined?virtualStore:
65
98
  path.resolve(path.dirname(modulesFile),declared);
@@ -71,18 +104,8 @@ export function pnpmInstallInfo(root){
71
104
  throw Error('nonstandard or incomplete project-local pnpm virtual store');
72
105
  if(!exists(path.join(root,'pnpm-lock.yaml')))
73
106
  throw Error('pnpm lockfile is missing');
74
- if(exists(path.join(root,'.npmrc'))){
75
- // pnpm 12 reads project settings from pnpm-workspace.yaml. Registry/auth
76
- // entries affect acquisition, not a completed local TypeScript invocation.
77
- for(const line of fs.readFileSync(path.join(root,'.npmrc'),'utf8').split(/\r?\n/)){
78
- const trimmed=line.trim();if(!trimmed||/^[#;]/.test(trimmed))continue;
79
- const key=trimmed.split('=')[0].trim();
80
- if(key!=='registry'&&!/^\/\/[^\s]+\/:_authToken$/.test(key))
81
- throw Error('project .npmrc has execution-relevant pnpm settings requiring review');
82
- }
83
- }
84
107
  timing('pnpm.install_metadata',started);
85
- return {version,linker,patterns,settings,modulesFile,
108
+ return {version:installed[1],linker,patterns,settings,modulesFile,
86
109
  configurationInputs:['package.json','pnpm-lock.yaml',
87
110
  ...(exists(workspaceFile)?['pnpm-workspace.yaml']:[]),
88
111
  ...(exists(path.join(root,'.npmrc'))?['.npmrc']:[]),
@@ -96,7 +119,7 @@ export function pnpmWorkspacePackages(root,patterns){
96
119
  const files=execFileSync('git',['ls-files','--cached','--others','--exclude-standard',
97
120
  '-z','--',':(glob)**/package.json'],{cwd:root,
98
121
  env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},maxBuffer:32*1024*1024,
99
- timeout:5000}).toString().split('\0').filter(Boolean);
122
+ timeout:probeTimeLeft(5000)}).toString().split('\0').filter(Boolean);
100
123
  for(const file of files){
101
124
  const dir=path.posix.dirname(file);
102
125
  if(dir==='.'||dir.split('/').includes('node_modules')||
@@ -172,16 +195,37 @@ function packageAt(root,requester,name){
172
195
  }
173
196
  return {physical:null,candidates};
174
197
  }
175
- function parsedTypeScript(root,dir,ts){
176
- const config=path.join(root,dir,'tsconfig.json'),reads=new Set();
198
+ // Files TypeScript may consume from its own include: each directory its
199
+ // include patterns watch for new files (TypeScript's own
200
+ // wildcardDirectories), and each file it lists. node_modules below them is
201
+ // left out, as TypeScript's wildcards leave it out; installed files a program
202
+ // reads are installed inputs. `exclude` entries stay inputs: an import can
203
+ // still reach an excluded file.
204
+ const sourceFiles='*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json}';
205
+ function sourcePatterns(root,parsed){
206
+ const patterns=new Set(),excluded=new Set();
207
+ for(const [folder,flags] of Object.entries(parsed.wildcardDirectories||{})){
208
+ // TypeScript 4 lowercases these keys on case-insensitive file systems;
209
+ // the native realpath gives the directory's own spelling.
210
+ let at;try{at=fs.realpathSync.native(folder);}catch{at=path.resolve(folder);}
211
+ if(!within(at,root))throw Error('TypeScript source membership escapes the checkout');
212
+ const prefix=rel(root,at)?rel(root,at)+'/':'';
213
+ if(flags&1){patterns.add(`${prefix}**/${sourceFiles}`);excluded.add(`!${prefix}**/node_modules/**`);}
214
+ else patterns.add(prefix+sourceFiles);
215
+ }
216
+ const included=patternMatcher([...patterns,...excluded]);
217
+ for(const file of parsed.fileNames.map(name=>path.resolve(name))){
218
+ if(!within(file,root))throw Error('TypeScript source membership escapes the checkout');
219
+ if(!included(rel(root,file)))patterns.add(rel(root,file));
220
+ }
221
+ return [...patterns,...excluded];
222
+ }
223
+ function parsedTypeScript(root,dir,ts,configName='tsconfig.json'){
224
+ const config=path.join(root,dir,configName),reads=new Set();
177
225
  const host={...ts.sys,readFile(file){reads.add(path.resolve(file));return ts.sys.readFile(file);},
178
226
  onUnRecoverableConfigFileDiagnostic(){}};
179
227
  const parsed=ts.getParsedCommandLineOfConfigFile(config,{},host);
180
228
  if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
181
- const raw=ts.readConfigFile(config,ts.sys.readFile);
182
- if(raw.error||!Array.isArray(raw.config?.include)||raw.config.include.length!==1||
183
- !/^src(?:$|\/)/.test(raw.config.include[0]))
184
- throw Error('TypeScript source file membership is outside the supported src include');
185
229
  if(parsed.options.plugins?.length||parsed.options.incremental||parsed.options.composite||
186
230
  parsed.projectReferences?.length||parsed.options.paths||parsed.options.baseUrl||
187
231
  parsed.options.rootDirs?.length||parsed.options.typeRoots?.length||
@@ -193,16 +237,17 @@ function parsedTypeScript(root,dir,ts){
193
237
  throw Error('TypeScript module resolver is outside the supported Node contract');
194
238
  for(const file of reads)if(!within(realObservedPath(file),root))
195
239
  throw Error('TypeScript configuration escapes the checkout');
196
- return {parsed,configs:[...reads].map(file=>rel(root,file)).sort()};
240
+ return {parsed,configs:[...reads].map(file=>rel(root,file)).sort(),
241
+ sourcePatterns:sourcePatterns(root,parsed)};
197
242
  }
198
- function listedTypeScriptFiles(root,selected,tsc){
243
+ function listedTypeScriptFiles(root,selected,tsc,configName){
199
244
  // --listFilesOnly resolves the compiler's effective module/file set but
200
245
  // performs no typecheck and emits no project outputs.
201
- const out=spawnSync(process.execPath,[tsc,'-p','tsconfig.json','--listFilesOnly'],
246
+ const out=spawnSync(process.execPath,[tsc,'-p',configName,'--listFilesOnly'],
202
247
  {cwd:path.join(root,selected.dir),env:process.env,encoding:'utf8',
203
248
  // The native NTFS public workspace takes 3-5 s to list ~770 inputs;
204
249
  // 5 s sometimes expired despite bounded, progressing compiler work.
205
- timeout:10000,maxBuffer:32*1024*1024,stdio:['ignore','pipe','pipe']});
250
+ timeout:probeTimeLeft(10000),maxBuffer:32*1024*1024,stdio:['ignore','pipe','pipe']});
206
251
  if(out.error||out.signal||out.status!==0)
207
252
  throw Error('TypeScript input listing is unavailable');
208
253
  const files=out.stdout.trim().split(/\r?\n/).filter(Boolean)
@@ -235,26 +280,35 @@ function sharedHardlinkIn(folder){
235
280
  }
236
281
  return false;
237
282
  }
238
- export function pnpmTypecheckInputs(root,workspace,script,{captureQueries=false}={}){
283
+ // `run`: the check's {script, part, args}, or a script name for its only run.
284
+ export function pnpmTypecheckInputs(root,workspace,run,{captureQueries=false}={}){
239
285
  const totalStarted=performance.now();
240
286
  root=realObservedPath(root);
241
287
  const install=pnpmInstallInfo(root),members=pnpmWorkspacePackages(root,install.patterns);
242
288
  const selected=workspace==='.'?{name:'.',dir:'.',manifest:'package.json',
243
289
  pkg:read(path.join(root,'package.json'))}:members.get(workspace);
244
290
  if(!selected)throw Error(`workspace ${workspace} is not declared`);
245
- if(!/^(?:tsc --noEmit|tsc -p (?:\.|tsconfig\.json)(?: --noEmit)?)$/.test(
246
- selected.pkg.scripts?.[script]?.trim()||'')||
247
- selected.pkg.scripts?.['pre'+script]||selected.pkg.scripts?.['post'+script])
248
- throw Error('typecheck must be one standalone TypeScript invocation');
291
+ // A script name stands for its only run.
292
+ if(typeof run==='string'){const runs=tscRuns(selected.pkg.scripts?.[run]);run={script:run,args:runs?.length===1?runs[0].args:[]};}
293
+ const {run:own,issue}=tscRun(selected.pkg.scripts,run);
294
+ if(issue)throw Error(`typecheck must be standalone TypeScript runs: ${issue}`);
295
+ // `tsc -p .` without --noEmit predates the other forms.
296
+ if(!own.noEmit&&!['-p .','-p tsconfig.json'].includes(run.args.join(' ')))
297
+ throw Error('typecheck must pass --noEmit');
298
+ const configName=runConfig(path.join(root,selected.dir),run.args);
299
+ if(!configName)throw Error('TypeScript project is outside the package');
249
300
  const selectedDir=path.join(root,selected.dir),lookup=createRequire(path.join(selectedDir,'package.json'));
250
301
  let tsPackage;try{tsPackage=realObservedPath(lookup.resolve('typescript/package.json'));}
251
302
  catch{throw Error('installed TypeScript compiler cannot be resolved');}
252
303
  if(!within(tsPackage,root))throw Error('TypeScript compiler is outside the observed checkout');
253
- const tsRoot=path.dirname(tsPackage),ts=lookup('typescript');
304
+ const tsRoot=path.dirname(tsPackage),apiIssue=compilerApiIssue(tsRoot);
305
+ if(apiIssue)throw Error(apiIssue);
306
+ const ts=lookup('typescript');
254
307
  const tsc=path.join(tsRoot,'bin','tsc');
255
308
  if(!exists(tsc))throw Error('installed TypeScript executable is missing');
256
309
  const configStarted=performance.now();
257
- const {configs}=parsedTypeScript(root,selected.dir,ts);
310
+ const {configs,sourcePatterns:sources}=parsedTypeScript(root,selected.dir,ts,configName);
311
+ const isSource=patternMatcher(sources);
258
312
  timing('pnpm.ts_config',configStarted,{configs:configs.length});
259
313
  const closure=new Map(),installed=new Set(),generated=new Set(),
260
314
  links=new Set(),candidates=new Set(),absences=[],instances=new Map();
@@ -304,12 +358,12 @@ export function pnpmTypecheckInputs(root,workspace,script,{captureQueries=false}
304
358
  resolutionMs:Math.round(resolutionMs),workspaceLinks:links.size,
305
359
  candidates:candidates.size});
306
360
  const listingStarted=performance.now();
307
- const captured=captureQueries?compilerFiles(tsRoot,path.join(root,selected.dir)):null;
308
- const files=captured?captured.files.map(realObservedPath):listedTypeScriptFiles(root,selected,tsc);
361
+ const captured=captureQueries?compilerFiles(tsRoot,path.join(root,selected.dir),configName):null;
362
+ const files=captured?captured.files.map(realObservedPath):listedTypeScriptFiles(root,selected,tsc,configName);
309
363
  timing('pnpm.ts_input_listing',listingStarted,{files:files.length});
310
364
  const mappingStarted=performance.now();
311
365
  for(const file of files){
312
- if(within(file,path.join(selectedDir,'src'))||
366
+ if(isSource(rel(root,file))||
313
367
  [...closure.values()].some(member=>within(file,path.join(root,member.dir))))continue;
314
368
  const packageRoot=installedPackageRoot(root,file);
315
369
  if(!packageRoot)throw Error(`TypeScript consumes a file outside the observed pnpm contract: ${rel(root,file)}`);
@@ -320,6 +374,16 @@ export function pnpmTypecheckInputs(root,workspace,script,{captureQueries=false}
320
374
  }
321
375
  timing('pnpm.input_mapping',mappingStarted,{instances:instances.size,
322
376
  installedPatterns:installed.size});
377
+ // A config extended from an installed package is an installed input too:
378
+ // source patterns leave node_modules out.
379
+ for(const config of configs){
380
+ const file=realObservedPath(path.join(root,config));
381
+ if(!within(file,path.join(root,'node_modules')))continue;
382
+ const packageRoot=installedPackageRoot(root,file);
383
+ if(!packageRoot)throw Error(`TypeScript configuration ${config} is outside an installed package`);
384
+ installed.add(`${rel(root,packageRoot)}/**`);
385
+ installed.add(`!${rel(root,packageRoot)}/node_modules/**`);
386
+ }
323
387
  installed.add(`${rel(root,tsRoot)}/**`);
324
388
  installed.add(`!${rel(root,tsRoot)}/node_modules/**`);
325
389
  if(!instances.has(tsRoot))instances.set(tsRoot,{name:'typescript',physicalRoot:rel(root,tsRoot)});
@@ -328,11 +392,12 @@ export function pnpmTypecheckInputs(root,workspace,script,{captureQueries=false}
328
392
  timing('pnpm.hardlink_inspection',sharedStarted,{instances:instances.size,
329
393
  shared:shared.length});
330
394
  const source=[...new Set([...install.configurationInputs,selected.manifest,
331
- ...configs,`${selected.dir==='.'?'':selected.dir+'/'}src/**`,
395
+ ...configs,...sources,
332
396
  ...closure.values().map(member=>member.manifest),...generated])];
333
397
  timing('pnpm.total_discovery',totalStarted,{instances:instances.size,
334
398
  inputFiles:files.length});
335
399
  return {install,workspace:selected,closure:[...closure.values()],source,
400
+ sourcePatterns:sources,args:run.args,
336
401
  installed:[...installed],generated:[...generated],
337
402
  // The read-only state probe consumes this same resolved compiler file set.
338
403
  // It is deliberately omitted from the persisted user-facing input plan.