@redue/cli 0.1.0-alpha.3 → 0.1.0-alpha.31
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 +831 -0
- package/README.md +157 -99
- package/bin/redue.mjs +322 -66
- package/docs/README.md +3 -1
- package/docs/user/agents.md +333 -15
- package/docs/user/configuration.md +28 -2
- package/docs/user/direct-typescript.md +219 -0
- package/docs/user/getting-started.md +50 -7
- package/docs/user/json.md +4 -2
- package/docs/user/node-script.md +177 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +146 -0
- package/docs/user/sandbox.md +326 -0
- package/docs/user/shell.md +3 -3
- package/docs/user/supported-projects.md +69 -23
- package/docs/user/tests-direct.md +143 -0
- package/package.json +4 -3
- package/src/agent-integration.mjs +467 -0
- package/src/agent-policy.md +28 -0
- package/src/cached-state.mjs +24 -1
- package/src/claude-hook.mjs +616 -0
- package/src/cli.mjs +449 -54
- package/src/daemon.mjs +442 -93
- package/src/decision-profile.mjs +15 -1
- package/src/decision-validation.mjs +12 -1
- package/src/declared-plan.mjs +38 -6
- package/src/direct-run-certificate.mjs +254 -0
- package/src/direct-typescript-probe.mjs +27 -0
- package/src/direct-typescript.mjs +205 -0
- package/src/glob.mjs +8 -0
- package/src/history-worker.mjs +21 -13
- package/src/index.mjs +108 -21
- package/src/installed-inputs.mjs +7 -1
- package/src/installed-observation.mjs +23 -0
- package/src/landlock.py +527 -0
- package/src/linux-inotify.py +39 -10
- package/src/node-onboarding.mjs +203 -54
- package/src/node-script.mjs +406 -0
- package/src/npm-integrity.mjs +124 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npmrc.mjs +16 -0
- package/src/observation.mjs +34 -12
- package/src/path-identity.mjs +8 -0
- package/src/plan-worker.mjs +17 -17
- package/src/pnpm-inputs.mjs +122 -57
- package/src/pnpm-shim-templates.json +180 -0
- package/src/pnpm-shims.mjs +54 -0
- package/src/pnpm-typecheck-probe.mjs +11 -6
- package/src/presentation.mjs +13 -4
- package/src/probe-deadline.mjs +37 -0
- package/src/probe-result.mjs +7 -3
- package/src/project-location.mjs +34 -0
- package/src/python-onboarding.mjs +89 -0
- package/src/run-recorder.cjs +91 -0
- package/src/sandbox.mjs +659 -0
- package/src/setup.mjs +224 -0
- package/src/stats.mjs +72 -0
- package/src/test-direct.mjs +608 -0
- package/src/test-recorder.cjs +435 -0
- package/src/typescript-contract-probe.mjs +4 -187
- package/src/typescript-input-facts.mjs +92 -0
- package/src/typescript-invocation.mjs +92 -0
- package/src/typescript-list.mjs +118 -13
- package/src/typescript-qualification.mjs +29 -49
- package/src/workspace-manifests.mjs +24 -0
- package/src/yarn-workspace-inputs.mjs +3 -0
- package/src/yarn-workspace-typecheck-probe.mjs +2 -1
package/src/observation.mjs
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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.
|
|
30
|
-
finish(Object.assign(Error(message.error),{code:message.code}))
|
|
31
|
-
worker.
|
|
32
|
-
worker.
|
|
33
|
-
|
|
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
|
}
|
package/src/path-identity.mjs
CHANGED
|
@@ -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;
|
package/src/plan-worker.mjs
CHANGED
|
@@ -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
|
-
|
|
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]=
|
|
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:
|
|
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),
|
package/src/pnpm-inputs.mjs
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
//
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
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
|
|
36
|
-
const patterns=safePatterns(
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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(
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
62
|
-
//
|
|
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
|
-
|
|
176
|
-
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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),
|
|
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(
|
|
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
|
|
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.
|