@redue/cli 0.0.0-stage → 0.1.0-alpha.10
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 +128 -0
- package/LICENSE +180 -0
- package/README.md +162 -2
- package/bin/redue.mjs +355 -0
- package/bin/vstate.mjs +3 -0
- package/docs/README.md +21 -0
- package/docs/user/agents.md +207 -0
- package/docs/user/configuration.md +60 -0
- package/docs/user/direct-typescript.md +96 -0
- package/docs/user/getting-started.md +148 -0
- package/docs/user/json.md +59 -0
- package/docs/user/npm-launcher-notice.md +42 -0
- package/docs/user/pilot.md +142 -0
- package/docs/user/receipt-selection-notice.md +60 -0
- package/docs/user/shell.md +25 -0
- package/docs/user/supported-projects.md +90 -0
- package/examples/redue.config.json +8 -0
- package/package.json +47 -4
- package/src/agent-integration.mjs +248 -0
- package/src/agent-policy.md +70 -0
- package/src/cached-state.mjs +42 -0
- package/src/cli.mjs +459 -0
- package/src/control-endpoint.mjs +11 -0
- package/src/daemon.mjs +1082 -0
- package/src/decision-profile.mjs +32 -0
- package/src/decision-validation.mjs +41 -0
- package/src/declared-plan.mjs +125 -0
- package/src/direct-run-certificate.mjs +40 -0
- package/src/direct-typescript-probe.mjs +56 -0
- package/src/direct-typescript.mjs +76 -0
- package/src/executable-lookup.mjs +18 -0
- package/src/glob.mjs +9 -0
- package/src/history-worker.mjs +23 -0
- package/src/index.mjs +306 -0
- package/src/installed-inputs.mjs +110 -0
- package/src/linux-inotify.py +228 -0
- package/src/linux-observer.mjs +79 -0
- package/src/node-onboarding.mjs +274 -0
- package/src/npm-typecheck-environment.mjs +36 -0
- package/src/npm-typecheck-probe.mjs +120 -0
- package/src/observation.mjs +39 -0
- package/src/owned-state.mjs +32 -0
- package/src/path-identity.mjs +43 -0
- package/src/plan-worker.mjs +140 -0
- package/src/plan.mjs +10 -0
- package/src/platform-observation.mjs +17 -0
- package/src/platform-state.mjs +14 -0
- package/src/pnpm-inputs.mjs +355 -0
- package/src/pnpm-typecheck-probe.mjs +47 -0
- package/src/presentation.mjs +53 -0
- package/src/probe-result.mjs +21 -0
- package/src/process-liveness.mjs +11 -0
- package/src/project-location.mjs +34 -0
- package/src/run-lock.mjs +77 -0
- package/src/state-store.mjs +73 -0
- package/src/toolchain-probe.mjs +21 -0
- package/src/typescript-contract-probe.mjs +5 -0
- package/src/typescript-input-facts.mjs +65 -0
- package/src/typescript-list.mjs +72 -0
- package/src/typescript-qualification.mjs +77 -0
- package/src/windows-command.mjs +16 -0
- package/src/windows-process.mjs +15 -0
- package/src/yarn-workspace-inputs.mjs +148 -0
- package/src/yarn-workspace-typecheck-probe.mjs +107 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Internal supported-provider protocol. Default stdout is unchanged, so
|
|
2
|
+
// ordinary run checkpoints keep the same probe/result identity. The daemon
|
|
3
|
+
// may reuse a complete result only after revalidating its input key AND this
|
|
4
|
+
// independently checked execution/toolchain context. No TTL grants trust.
|
|
5
|
+
import {deflateSync} from 'node:zlib';
|
|
6
|
+
export function contextOnly(context) {
|
|
7
|
+
if(process.argv.includes('--redue-context')){
|
|
8
|
+
process.stdout.write(context);return true;
|
|
9
|
+
}
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
export function probeResult(output,context,queries=null) {
|
|
13
|
+
if(!process.argv.includes('--redue-checkpoint')){process.stdout.write(output);return;}
|
|
14
|
+
const raw=queries?JSON.stringify(queries):null;
|
|
15
|
+
let value=JSON.stringify({output,context,queryData:raw&&Buffer.byteLength(raw)<=8*1024*1024?
|
|
16
|
+
deflateSync(raw).toString('base64'):null});
|
|
17
|
+
// Optimization metadata must not overflow the ordinary probe channel.
|
|
18
|
+
// Larger contracts keep the normal complete probe, without result reuse.
|
|
19
|
+
if(Buffer.byteLength(value)>512*1024)value=JSON.stringify({output,context,queries:null});
|
|
20
|
+
process.stdout.write(value);
|
|
21
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
|
|
3
|
+
export function processAlive(pid) {
|
|
4
|
+
if(!Number.isSafeInteger(pid)||pid<1)return true;
|
|
5
|
+
if(process.platform==='linux')try{
|
|
6
|
+
const stat=fs.readFileSync(`/proc/${pid}/stat`,'utf8');
|
|
7
|
+
if(['Z','X'].includes(stat.slice(stat.lastIndexOf(')')+2)[0]))return false;
|
|
8
|
+
}catch(e){if(e.code!=='ENOENT'&&e.code!=='ESRCH')return true;}
|
|
9
|
+
try{process.kill(pid,0);return true;}
|
|
10
|
+
catch(e){return e.code!=='ESRCH';}
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Shared project selection only; evidence and caller-context semantics stay in the kernel.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import {createHash} from 'node:crypto';
|
|
6
|
+
import {realObservedPath} from './path-identity.mjs';
|
|
7
|
+
import {stateIdentity,userStateBase} from './platform-state.mjs';
|
|
8
|
+
|
|
9
|
+
export function discoverConfig(cwd=process.cwd()) {
|
|
10
|
+
let dir=path.resolve(cwd);const found=[];
|
|
11
|
+
for(;;){
|
|
12
|
+
for(const name of ['redue.config.json','vstate.config.json']) {
|
|
13
|
+
const file=path.join(dir,name);
|
|
14
|
+
if(fs.existsSync(file))found.push(file);
|
|
15
|
+
}
|
|
16
|
+
if(fs.existsSync(path.join(dir,'.git'))||path.dirname(dir)===dir)break;
|
|
17
|
+
dir=path.dirname(dir);
|
|
18
|
+
}
|
|
19
|
+
if(found.length>1)throw Error('multiple REDUE configurations apply; select the intended one with --config FILE');
|
|
20
|
+
return found[0]||path.resolve(cwd,'redue.config.json');
|
|
21
|
+
}
|
|
22
|
+
export function canonicalConfig(configFile) {
|
|
23
|
+
return path.join(realObservedPath(path.dirname(configFile)),path.basename(configFile));
|
|
24
|
+
}
|
|
25
|
+
export function projectState(root,configFile) {
|
|
26
|
+
return path.join(userStateBase(process.platform,process.env,os.homedir()),'vstate',
|
|
27
|
+
`vstate-${createHash('sha256').update(stateIdentity(root)+'\0'+stateIdentity(canonicalConfig(configFile))).digest('hex').slice(0,16)}`);
|
|
28
|
+
}
|
|
29
|
+
export function readProject(configFile) {
|
|
30
|
+
const config=JSON.parse(fs.readFileSync(configFile,'utf8'));
|
|
31
|
+
if(config.schema!==1||!Array.isArray(config.checks)||!config.checks.length)
|
|
32
|
+
throw Error('config needs schema 1 and at least one check');
|
|
33
|
+
return {config,root:realObservedPath(path.resolve(path.dirname(configFile),config.root||'.'))};
|
|
34
|
+
}
|
package/src/run-lock.mjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import {processAlive} from './process-liveness.mjs';
|
|
5
|
+
|
|
6
|
+
const alive=processAlive;
|
|
7
|
+
const write=(file,value)=>{
|
|
8
|
+
const temp=file+`.${process.pid}.tmp`;
|
|
9
|
+
fs.writeFileSync(temp,JSON.stringify(value),{mode:0o600});
|
|
10
|
+
fs.renameSync(temp,file);
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function inspectRunLock(state) {
|
|
14
|
+
const lock=path.join(state,'run.lock');
|
|
15
|
+
if(!fs.existsSync(lock))return {state:'absent'};
|
|
16
|
+
let owner;
|
|
17
|
+
try{owner=JSON.parse(fs.readFileSync(path.join(lock,'owner.json'),'utf8'));}
|
|
18
|
+
catch{return {state:'uncertain',reason:'lock owner record missing or unreadable'};}
|
|
19
|
+
if(owner.schema!==1||!owner.token||!Number.isSafeInteger(owner.pid))
|
|
20
|
+
return {state:'uncertain',reason:'lock owner record invalid'};
|
|
21
|
+
if(alive(owner.pid))return {state:'live',reason:'wrapper process exists',owner};
|
|
22
|
+
if(owner.phase==='running'&&Number.isSafeInteger(owner.childPid)&&alive(owner.childPid))
|
|
23
|
+
return {state:'uncertain',reason:'recorded check process is still running',owner};
|
|
24
|
+
if(['launching','running','finished'].includes(owner.phase))
|
|
25
|
+
return {state:'uncertain',reason:'a descendant check process may survive; operator review required',owner};
|
|
26
|
+
if(owner.phase!=='prepared')
|
|
27
|
+
return {state:'uncertain',reason:'unknown wrapper phase',owner};
|
|
28
|
+
return {state:'abandoned',reason:'wrapper died before child launch',owner};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function reclaim(state,confirmed=false){
|
|
32
|
+
const lock=path.join(state,'run.lock'),guard=path.join(state,'run-recovery.lock');
|
|
33
|
+
fs.mkdirSync(guard);
|
|
34
|
+
try{
|
|
35
|
+
const status=inspectRunLock(state);
|
|
36
|
+
if(status.state!=='abandoned'){
|
|
37
|
+
const owner=status.owner;
|
|
38
|
+
const candidate=confirmed&&status.state==='uncertain'&&owner&&
|
|
39
|
+
!alive(owner.pid)&&(!Number.isSafeInteger(owner.childPid)||!alive(owner.childPid));
|
|
40
|
+
if(!candidate)throw Error(`run lock ${status.state}: ${status.reason}; inspect ${lock}`);
|
|
41
|
+
}
|
|
42
|
+
const entries=fs.readdirSync(lock);
|
|
43
|
+
if(entries.length!==1||entries[0]!=='owner.json')
|
|
44
|
+
throw Error('run lock has unexpected files; operator inspection required');
|
|
45
|
+
const quarantined=path.join(state,`abandoned-run-${randomUUID()}`);
|
|
46
|
+
fs.renameSync(lock,quarantined);
|
|
47
|
+
fs.unlinkSync(path.join(quarantined,'owner.json'));fs.rmdirSync(quarantined);
|
|
48
|
+
}finally{fs.rmdirSync(guard);}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function recoverRunLock(state,confirmed=false){
|
|
52
|
+
if(!confirmed)throw Error('explicit --confirm-check-stopped required');
|
|
53
|
+
reclaim(state,true);
|
|
54
|
+
return {recovered:true};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function acquireRunLock(state) {
|
|
58
|
+
const lock=path.join(state,'run.lock');
|
|
59
|
+
const token=randomUUID();
|
|
60
|
+
try{fs.mkdirSync(lock);}
|
|
61
|
+
catch(e){
|
|
62
|
+
if(e.code!=='EEXIST')throw e;
|
|
63
|
+
reclaim(state);
|
|
64
|
+
fs.mkdirSync(lock);
|
|
65
|
+
}
|
|
66
|
+
const owner={schema:1,token,pid:process.pid,phase:'prepared',childPid:null,
|
|
67
|
+
startedAt:Date.now()};
|
|
68
|
+
const file=path.join(lock,'owner.json');write(file,owner);
|
|
69
|
+
return {
|
|
70
|
+
phase(value,childPid=null){owner.phase=value;owner.childPid=childPid;write(file,owner);},
|
|
71
|
+
release(){
|
|
72
|
+
const current=JSON.parse(fs.readFileSync(file,'utf8'));
|
|
73
|
+
if(current.token!==token)throw Error('run lock ownership changed; refusing removal');
|
|
74
|
+
fs.unlinkSync(file);fs.rmdirSync(lock);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const STATE_SCHEMA=1;
|
|
5
|
+
|
|
6
|
+
export function atomicJson(file,value){
|
|
7
|
+
const temp=file+`.`+process.pid+`.tmp`;
|
|
8
|
+
fs.writeFileSync(temp,JSON.stringify(value),{mode:0o600});
|
|
9
|
+
// Windows readers and AV can briefly hold the replaced name open. Keep the
|
|
10
|
+
// old complete file until replacement succeeds; never delete it first.
|
|
11
|
+
for(let attempt=0;;attempt++)try{fs.renameSync(temp,file);break;}catch(e){
|
|
12
|
+
if(process.platform!=='win32'||!['EPERM','EACCES','EBUSY'].includes(e.code)||attempt===9)
|
|
13
|
+
throw e;
|
|
14
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)),0,0,10*(attempt+1));
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function readReceipts(state){
|
|
19
|
+
let value;
|
|
20
|
+
try{value=JSON.parse(fs.readFileSync(path.join(state,'receipts-v1.json'),'utf8'));}
|
|
21
|
+
catch(e){if(e.code==='ENOENT')return {schema:STATE_SCHEMA,checks:{}};throw e;}
|
|
22
|
+
if(!value||typeof value!=='object'||Array.isArray(value)||value.schema!==STATE_SCHEMA||
|
|
23
|
+
!value.checks||typeof value.checks!=='object'||Array.isArray(value.checks))
|
|
24
|
+
throw Error(`incompatible receipt state in ${state}; inspect or remove only this vstate state directory`);
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// This identity detects replacement of REDUE's owned, atomically written receipt
|
|
29
|
+
// selector. It is not an input-content fingerprint or permission to reuse inputs.
|
|
30
|
+
export function receiptRevision(state){
|
|
31
|
+
// A failed/crashed owned writer may have persisted a new immutable outcome
|
|
32
|
+
// without replacing the selector. Never certify its older selection.
|
|
33
|
+
try {fs.lstatSync(path.join(state,'receipt-selection-pending-v1.json'));
|
|
34
|
+
throw Error('receipt selection update incomplete; inspect the failed run, then record a new execution to recover');
|
|
35
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
36
|
+
try {const stat=fs.statSync(path.join(state,'receipts-v1.json'),{bigint:true});
|
|
37
|
+
if(!stat.isFile())throw Error('receipt selector is not a file');
|
|
38
|
+
return JSON.stringify(['dev','ino','size','mtimeNs','ctimeNs'].map(key=>String(stat[key])));
|
|
39
|
+
}catch(error){if(error.code==='ENOENT')return 'absent';throw error;}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function readReceiptSnapshot(state){
|
|
43
|
+
const revision=receiptRevision(state),value=readReceipts(state);
|
|
44
|
+
if(receiptRevision(state)!==revision)
|
|
45
|
+
throw Error('receipt selection changed during read; retry status');
|
|
46
|
+
return {revision,value};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function commitReceipt(state,name,receipt){
|
|
50
|
+
const prior=readReceipts(state),runId=receipt.invocation?.runId;
|
|
51
|
+
if(!/^[0-9a-f-]{36}$/.test(runId||''))throw Error('receipt lacks a valid run ID');
|
|
52
|
+
const runs=path.join(state,'runs-v1');
|
|
53
|
+
fs.mkdirSync(runs,{recursive:true,mode:0o700});
|
|
54
|
+
// The caller holds the state run lock. Leave this marker on any write failure;
|
|
55
|
+
// only a later successful owned commit may clear it. No journal scan guesses
|
|
56
|
+
// which incomplete execution should become the selected evidence.
|
|
57
|
+
const pending=path.join(state,'receipt-selection-pending-v1.json');
|
|
58
|
+
let updates={schema:STATE_SCHEMA,checks:{}};
|
|
59
|
+
try {updates=JSON.parse(fs.readFileSync(pending,'utf8'));
|
|
60
|
+
if(updates?.schema!==STATE_SCHEMA||!updates.checks||typeof updates.checks!=='object'||Array.isArray(updates.checks))
|
|
61
|
+
throw Error('incompatible incomplete receipt update; inspect owned state before recording');
|
|
62
|
+
}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
63
|
+
updates.checks[name]={runId};
|
|
64
|
+
atomicJson(pending,updates);
|
|
65
|
+
// One immutable outcome per invocation. Never overwrite an existing run.
|
|
66
|
+
fs.writeFileSync(path.join(runs,runId+'.json'),
|
|
67
|
+
JSON.stringify({schema:STATE_SCHEMA,check:name,receipt}),{flag:'wx',mode:0o600});
|
|
68
|
+
prior.checks[name]=receipt;
|
|
69
|
+
atomicJson(path.join(state,'receipts-v1.json'),prior);
|
|
70
|
+
delete updates.checks[name];
|
|
71
|
+
if(Object.keys(updates.checks).length)atomicJson(pending,updates);
|
|
72
|
+
else fs.unlinkSync(pending);
|
|
73
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Read-only execution identity for explicitly named local tools. Output contains
|
|
2
|
+
// hashes and metadata only; the verifier hashes the output again for its receipt.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import {createHash} from 'node:crypto';
|
|
5
|
+
import {fileURLToPath} from 'node:url';
|
|
6
|
+
|
|
7
|
+
const sha=value=>createHash('sha256').update(value).digest('hex');
|
|
8
|
+
function identity(file) {
|
|
9
|
+
const real=fs.realpathSync(file),stat=fs.statSync(real,{bigint:true});
|
|
10
|
+
if(!stat.isFile())throw Error('toolchain identity requires a regular file');
|
|
11
|
+
const facts=[sha(real),...['dev','ino','mode','size','mtimeNs','ctimeNs']
|
|
12
|
+
.map(key=>String(stat[key]))];
|
|
13
|
+
// Hash small scripts/bundles on each probe. For large native executables,
|
|
14
|
+
// version plus identity/metadata keeps synchronized reads inexpensive.
|
|
15
|
+
if(stat.size<=16n*1024n*1024n)facts.push(sha(fs.readFileSync(real)));
|
|
16
|
+
return facts;
|
|
17
|
+
}
|
|
18
|
+
const own=fileURLToPath(import.meta.url);
|
|
19
|
+
process.stdout.write(JSON.stringify({schema:1,nodeVersion:process.version,
|
|
20
|
+
node:identity(process.execPath),probe:identity(own),
|
|
21
|
+
tools:process.argv.slice(2).map(identity)}));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Retired npm-launcher interpretation. Do not return even a context-only digest:
|
|
3
|
+
// old receipts and cached compiler certificates cannot qualify this obligation.
|
|
4
|
+
console.error('VSTATE_REASON:npm-launcher-unobserved-inputs');
|
|
5
|
+
process.exitCode=2;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Extracted from the existing npm TypeScript probe; shared compiler input logic.
|
|
2
|
+
import fs from 'node:fs';import path from 'node:path';import {createHash} from 'node:crypto';
|
|
3
|
+
import {createRequire} from 'node:module';import {spawnSync} from 'node:child_process';
|
|
4
|
+
import {compilerFiles} from './typescript-list.mjs';
|
|
5
|
+
import {withinPath as within,realObservedPath} from './path-identity.mjs';
|
|
6
|
+
import {timing} from './decision-profile.mjs';
|
|
7
|
+
const require=createRequire(import.meta.url),hash=value=>createHash('sha256').update(value).digest('hex');
|
|
8
|
+
const digestFile=file=>hash(fs.readFileSync(file));
|
|
9
|
+
const fail=code=>{throw Object.assign(Error(code),{code});};
|
|
10
|
+
export function typeScriptInputFacts(root,tsRoot,tsc){
|
|
11
|
+
const configReads=new Set(),visited=new Set();
|
|
12
|
+
let ts;try{ts=require(tsRoot);}catch{}
|
|
13
|
+
if(typeof ts?.getParsedCommandLineOfConfigFile==='function'){
|
|
14
|
+
const host={...ts.sys,readFile(file){configReads.add(path.resolve(file));
|
|
15
|
+
return ts.sys.readFile(file);},onUnRecoverableConfigFileDiagnostic(){}};
|
|
16
|
+
const parseConfig=file=>{
|
|
17
|
+
const absolute=path.resolve(file);
|
|
18
|
+
if(!within(absolute,root))fail('typescript-config-outside-repository');
|
|
19
|
+
if(visited.has(absolute))return;visited.add(absolute);
|
|
20
|
+
if(visited.size>64)fail('typescript-config-graph-too-large');
|
|
21
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(absolute,{},host);
|
|
22
|
+
if(!parsed||parsed.errors?.length)fail('typescript-config-unavailable');
|
|
23
|
+
if(parsed.options?.plugins?.length)fail('typescript-plugin-unsupported');
|
|
24
|
+
if(parsed.options?.incremental||parsed.options?.composite)
|
|
25
|
+
fail('typescript-incremental-unsupported');
|
|
26
|
+
for(const reference of parsed.projectReferences||[])
|
|
27
|
+
parseConfig(reference.path.endsWith('.json')?reference.path:
|
|
28
|
+
path.join(reference.path,'tsconfig.json'));
|
|
29
|
+
};
|
|
30
|
+
parseConfig(path.join(root,'tsconfig.json'));
|
|
31
|
+
}else{
|
|
32
|
+
// TypeScript 7's native compiler does not expose the previous config API.
|
|
33
|
+
// Its CLI reports the effective config; restrict this path to one direct
|
|
34
|
+
// config rather than guessing about extends/reference provenance.
|
|
35
|
+
const config=path.join(root,'tsconfig.json'),raw=fs.readFileSync(config,'utf8');
|
|
36
|
+
if(/"(?:extends|references|plugins|incremental|composite)"\s*:/.test(raw))
|
|
37
|
+
fail('typescript-config-graph-unsupported');
|
|
38
|
+
const shown=spawnSync(process.execPath,[tsc,'--showConfig'],{cwd:root,
|
|
39
|
+
env:process.env,encoding:'utf8',timeout:4000,maxBuffer:16*1024*1024,
|
|
40
|
+
stdio:['ignore','pipe','pipe']});
|
|
41
|
+
if(shown.error||shown.signal||shown.status!==0)fail('typescript-config-unavailable');
|
|
42
|
+
const effective=JSON.parse(shown.stdout);
|
|
43
|
+
if(effective.compilerOptions?.plugins?.length||
|
|
44
|
+
effective.compilerOptions?.incremental||effective.compilerOptions?.composite)
|
|
45
|
+
fail('typescript-config-graph-unsupported');
|
|
46
|
+
configReads.add(config);
|
|
47
|
+
}
|
|
48
|
+
for(const file of configReads)if(!within(realObservedPath(file),root))
|
|
49
|
+
fail('typescript-config-outside-repository');
|
|
50
|
+
const listingStarted=performance.now();
|
|
51
|
+
const captured=process.argv.includes('--redue-checkpoint')?compilerFiles(tsRoot,root):null;
|
|
52
|
+
const listed=captured?{status:0,stdout:captured.files.join('\n')}:spawnSync(process.execPath,[tsc,'--noEmit','--listFilesOnly'],
|
|
53
|
+
{cwd:root,env:process.env,encoding:'utf8',timeout:process.platform==='win32'?10000:4000,
|
|
54
|
+
maxBuffer:16*1024*1024,
|
|
55
|
+
stdio:['ignore','pipe','pipe']});
|
|
56
|
+
timing('typescript_contract.list_files',listingStarted,{status:listed.status??null,
|
|
57
|
+
error:listed.error?.code||null});
|
|
58
|
+
if(listed.error||listed.signal||listed.status!==0)fail('typescript-input-list-unavailable');
|
|
59
|
+
const files=listed.stdout.trim().split(/\r?\n/).filter(Boolean)
|
|
60
|
+
.map(file=>path.resolve(file));
|
|
61
|
+
if(!files.length||files.some(file=>!within(realObservedPath(file),root)))
|
|
62
|
+
fail('typescript-input-outside-repository');
|
|
63
|
+
return {configs:[...configReads].sort().map(file=>[hash(file),digestFile(file)]),
|
|
64
|
+
files:[...new Set(files)].sort().map(file=>[hash(file),digestFile(file)]),queries:captured?.queries};
|
|
65
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// TypeScript's compiler host exposes the filesystem questions used to resolve
|
|
2
|
+
// a program. Capture those answers, not runtime application accesses. Reuse is
|
|
3
|
+
// allowed only while every answer and the observed input content still matches.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import {createHash} from 'node:crypto';
|
|
7
|
+
import {createRequire} from 'node:module';
|
|
8
|
+
const require=createRequire(import.meta.url);
|
|
9
|
+
const hash=value=>createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
10
|
+
export function queryAnswer(kind,file,options) {
|
|
11
|
+
try{
|
|
12
|
+
if(kind==='readDirectory')return require(options.tsRoot).sys.readDirectory(file,
|
|
13
|
+
...options.args.map((value,i)=>options.undefinedArguments?.includes(i)?undefined:value)).sort();
|
|
14
|
+
if(kind==='readFile')return hash(fs.readFileSync(file,'utf8'));
|
|
15
|
+
if(kind==='fileExists')return fs.statSync(file).isFile();
|
|
16
|
+
if(kind==='directoryExists')return fs.statSync(file).isDirectory();
|
|
17
|
+
if(kind==='realpath')return fs.realpathSync(file);
|
|
18
|
+
if(kind==='directories')return fs.readdirSync(file,{withFileTypes:true})
|
|
19
|
+
.filter(entry=>entry.isDirectory()||entry.isSymbolicLink()&&
|
|
20
|
+
fs.statSync(path.join(file,entry.name)).isDirectory()).map(entry=>entry.name).sort();
|
|
21
|
+
// Directory entries include names/types, including newly created candidates.
|
|
22
|
+
if(kind==='entries')return fs.readdirSync(file,{withFileTypes:true})
|
|
23
|
+
.map(entry=>[entry.name,entry.isDirectory()?'d':entry.isSymbolicLink()?'l':'f']).sort();
|
|
24
|
+
throw Error('unknown compiler filesystem question');
|
|
25
|
+
}catch(error){if(['ENOENT','ENOTDIR'].includes(error.code))return null;throw error;}
|
|
26
|
+
}
|
|
27
|
+
export function queriesMatch(queries) {
|
|
28
|
+
if(!queries?.length)return false;
|
|
29
|
+
return queries.every(([kind,file,answer,options])=>
|
|
30
|
+
JSON.stringify(queryAnswer(kind,file,options))===JSON.stringify(answer));
|
|
31
|
+
}
|
|
32
|
+
export function compilerFiles(tsRoot,cwd) {
|
|
33
|
+
const require=createRequire(import.meta.url),ts=require(tsRoot);
|
|
34
|
+
if(typeof ts.createProgram!=='function'||typeof ts.createCompilerHost!=='function')return null;
|
|
35
|
+
const queries=new Map();
|
|
36
|
+
const remember=(kind,file)=>{
|
|
37
|
+
const full=path.resolve(file),answer=queryAnswer(kind,full);
|
|
38
|
+
queries.set(kind+'\0'+full,[kind,full,answer]);return answer;
|
|
39
|
+
};
|
|
40
|
+
const readFile=file=>{remember('readFile',file);return ts.sys.readFile(file);};
|
|
41
|
+
const fileExists=file=>{remember('fileExists',file);return ts.sys.fileExists(file);};
|
|
42
|
+
const directoryExists=file=>{remember('directoryExists',file);return ts.sys.directoryExists(file);};
|
|
43
|
+
const getDirectories=file=>{remember('directories',file);return ts.sys.getDirectories(file);};
|
|
44
|
+
const readDirectory=(dir,...args)=>{
|
|
45
|
+
const result=ts.sys.readDirectory(dir,...args);
|
|
46
|
+
// JSON turns undefined array entries into null. TypeScript distinguishes
|
|
47
|
+
// them (an omitted exclude list is not a null list), so retain that fact.
|
|
48
|
+
const full=path.resolve(dir),options={tsRoot,args,argsVersion:1,
|
|
49
|
+
undefinedArguments:args.flatMap((value,i)=>value===undefined?[i]:[])};
|
|
50
|
+
queries.set('readDirectory\0'+full+JSON.stringify(args),
|
|
51
|
+
['readDirectory',full,[...result].sort(),options]);
|
|
52
|
+
return result;
|
|
53
|
+
};
|
|
54
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(path.join(cwd,'tsconfig.json'),{},
|
|
55
|
+
{...ts.sys,readFile,fileExists,directoryExists,getDirectories,readDirectory,
|
|
56
|
+
onUnRecoverableConfigFileDiagnostic(){}});
|
|
57
|
+
if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
|
|
58
|
+
const host=ts.createCompilerHost(parsed.options);
|
|
59
|
+
Object.assign(host,{readFile,fileExists,directoryExists,getDirectories,
|
|
60
|
+
realpath(file){remember('realpath',file);return ts.sys.realpath(file);},
|
|
61
|
+
getSourceFile(file,languageVersion,onError){
|
|
62
|
+
const text=readFile(file);return text===undefined?undefined:
|
|
63
|
+
ts.createSourceFile(file,text,languageVersion);
|
|
64
|
+
}});
|
|
65
|
+
const program=ts.createProgram({rootNames:parsed.fileNames,options:parsed.options,
|
|
66
|
+
projectReferences:parsed.projectReferences,host});
|
|
67
|
+
const files=program.getSourceFiles().map(file=>path.resolve(file.fileName));
|
|
68
|
+
// Stable answers are necessary even at first capture; a concurrent edit
|
|
69
|
+
// cannot become the basis for a reusable discovery result.
|
|
70
|
+
const facts=[...queries.values()];
|
|
71
|
+
return {files,queries:queriesMatch(facts)?facts:null};
|
|
72
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import {directProject,directQualification,directEnvironment} from './direct-typescript.mjs';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import {realObservedPath} from './path-identity.mjs';
|
|
5
|
+
|
|
6
|
+
// This is a machine-enforced contract, not a claim inferred from a script name.
|
|
7
|
+
// Runtime validation checks effective compiler membership, configuration and
|
|
8
|
+
// toolchain identity. Supported discovery certificates still validate those
|
|
9
|
+
// filesystem queries; caller context is independently compared at the decision.
|
|
10
|
+
export function qualifyTypeScript(root,selected,discovered={}) {
|
|
11
|
+
if(selected.qualification===directQualification){
|
|
12
|
+
const issues=[];
|
|
13
|
+
try{
|
|
14
|
+
const compiler=directProject(root,selected.script);
|
|
15
|
+
if(selected.command?.length!==3||selected.command[0]!==process.execPath||
|
|
16
|
+
selected.command[1]!==compiler.entry||selected.command[2]!=='--noEmit'||
|
|
17
|
+
selected.cwd&&selected.cwd!=='.')issues.push('direct compiler invocation changed');
|
|
18
|
+
}catch(e){issues.push(e.code||'direct compiler prerequisites unavailable');}
|
|
19
|
+
if(!selected.inputs?.includes('**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs,json,jsonc}')||
|
|
20
|
+
!selected.inputs?.includes('package.json')||!selected.inputs?.includes('tsconfig.json')||
|
|
21
|
+
!selected.installedInputs?.includes('node_modules/**'))issues.push('direct compiler source/config/installed inputs incomplete');
|
|
22
|
+
if(!selected.environment?.executableIdentity||
|
|
23
|
+
!directEnvironment.variables.every(key=>selected.environment?.variables?.includes(key))||
|
|
24
|
+
!directEnvironment.prefixes.every(key=>selected.environment?.prefixes?.includes(key)))
|
|
25
|
+
issues.push('direct compiler execution context incomplete');
|
|
26
|
+
if(selected.probes?.length!==1||!selected.probes?.some(argv=>path.basename(argv[1]||'')==='direct-typescript-probe.mjs'&&
|
|
27
|
+
argv[0]===process.execPath&&argv[2]===root&&argv[3]===selected.script))issues.push('direct compiler probe missing');
|
|
28
|
+
return {qualified:issues.length===0,issues};
|
|
29
|
+
}
|
|
30
|
+
if(selected.qualification==='pnpm-tsc-v1'){
|
|
31
|
+
const issues=[];
|
|
32
|
+
const workspace=selected.workspace||'.';
|
|
33
|
+
const script=selected.script||selected.command?.at(-1);
|
|
34
|
+
try{
|
|
35
|
+
if(discovered.issue)throw Error(discovered.issue);
|
|
36
|
+
const contract=discovered.workspace;
|
|
37
|
+
if(!contract)throw Error('pnpm TypeScript input discovery unavailable');
|
|
38
|
+
issues.push(...contract.limitations);
|
|
39
|
+
const declared=contract.workspace.pkg.scripts[script].trim().split(/\s+/).slice(1);
|
|
40
|
+
if(path.resolve(root,selected.cwd||'.')!==path.join(root,contract.workspace.dir)||
|
|
41
|
+
selected.command?.[0]!==process.execPath||
|
|
42
|
+
selected.command?.[1]!==contract.tsc||
|
|
43
|
+
JSON.stringify(selected.command.slice(2))!==JSON.stringify(declared))
|
|
44
|
+
issues.push('standalone TypeScript invocation changed');
|
|
45
|
+
}catch(e){issues.push(e.message);}
|
|
46
|
+
if(!selected.probes?.some(argv=>path.basename(argv[1]||'')==='pnpm-typecheck-probe.mjs'))
|
|
47
|
+
issues.push('pnpm TypeScript input probe missing');
|
|
48
|
+
if(!selected.environment?.executableIdentity||
|
|
49
|
+
!['node'].every(name=>selected.environment?.pathExecutables?.includes(name))||
|
|
50
|
+
!['PNPM_','COREPACK_','npm_config_','DYLD_','TSGO_'].every(name=>
|
|
51
|
+
selected.environment?.prefixes?.includes(name)))
|
|
52
|
+
issues.push('pnpm execution context declaration incomplete');
|
|
53
|
+
return {qualified:issues.length===0,issues};
|
|
54
|
+
}
|
|
55
|
+
if(selected.qualification==='yarn-workspace-tsc-v1'){
|
|
56
|
+
const issues=[];
|
|
57
|
+
try{
|
|
58
|
+
if(!selected.workspace||selected.command?.length!==5||
|
|
59
|
+
selected.command[1]!=='workspace'||selected.command[2]!==selected.workspace||
|
|
60
|
+
selected.command[3]!=='run')issues.push('workspace command changed');
|
|
61
|
+
else if(discovered.issue)throw Error(discovered.issue);
|
|
62
|
+
else if(!discovered.workspace)throw Error('Yarn workspace input discovery unavailable');
|
|
63
|
+
}catch(e){issues.push(e.message);}
|
|
64
|
+
if(!selected.probes?.some(argv=>path.basename(argv[1]||'')==='yarn-workspace-typecheck-probe.mjs'))
|
|
65
|
+
issues.push('workspace TypeScript input probe missing');
|
|
66
|
+
if(!selected.environment?.executableIdentity||
|
|
67
|
+
!['node','yarn'].every(name=>selected.environment?.pathExecutables?.includes(name))||
|
|
68
|
+
!['YARN_','COREPACK_','npm_config_','DYLD_','TSGO_'].every(name=>
|
|
69
|
+
selected.environment?.prefixes?.includes(name)))
|
|
70
|
+
issues.push('Yarn execution context declaration incomplete');
|
|
71
|
+
return {qualified:issues.length===0,issues};
|
|
72
|
+
}
|
|
73
|
+
if(selected.qualification!=='typescript-noemit-v1')return null;
|
|
74
|
+
// Retired automatic launcher contract: npm's notifier consumes unobserved
|
|
75
|
+
// remote/cache/time state. A successful execution is still an outcome.
|
|
76
|
+
return {qualified:false,issues:['npm launcher applicability is unqualified: update notification uses unobserved remote/cache/time inputs; run the npm script when needed']};
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// Windows cannot CreateProcess a .cmd/.bat file directly. Keep the logical
|
|
4
|
+
// argv in the receipt, and launch only the well-defined, quoted batch subset
|
|
5
|
+
// through cmd.exe. Unrepresentable arguments fail instead of being altered.
|
|
6
|
+
export function windowsLaunch(command,{platform=process.platform,
|
|
7
|
+
comspec=process.env.ComSpec||process.env.COMSPEC||'C:\\Windows\\System32\\cmd.exe'}={}) {
|
|
8
|
+
if(platform!=='win32')return {file:command[0],args:command.slice(1),options:{}};
|
|
9
|
+
if(!/\.(?:cmd|bat)$/i.test(path.win32.extname(command[0])))
|
|
10
|
+
return {file:command[0],args:command.slice(1),options:{}};
|
|
11
|
+
if(command.some(value=>/[\r\n"%!^&|<>]/.test(value)))
|
|
12
|
+
throw Error('Windows batch command contains characters that cannot be passed safely; use an executable or Node script as exact argv');
|
|
13
|
+
const line='"'+command.map(value=>'"'+value+'"').join(' ')+'"';
|
|
14
|
+
return {file:comspec,args:['/d','/s','/c',line],
|
|
15
|
+
options:{windowsVerbatimArguments:true,windowsHide:false},mechanism:'cmd-batch'};
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {spawn} from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
// taskkill /T is the native narrow process-tree operation available without
|
|
4
|
+
// elevation. It cannot own deliberately detached descendants.
|
|
5
|
+
export function terminateWindowsTree(pid,{start=spawn,timeoutMs=5000}={}) {
|
|
6
|
+
if(!Number.isSafeInteger(pid)||pid<1)return Promise.reject(Error('invalid check PID'));
|
|
7
|
+
return new Promise((resolve,reject)=>{
|
|
8
|
+
const child=start('taskkill.exe',['/PID',String(pid),'/T','/F'],
|
|
9
|
+
{stdio:'ignore',windowsHide:true});
|
|
10
|
+
const timer=setTimeout(()=>{child.kill();reject(Error('taskkill deadline exceeded'));},timeoutMs);
|
|
11
|
+
child.once('error',error=>{clearTimeout(timer);reject(error);});
|
|
12
|
+
child.once('close',code=>{clearTimeout(timer);code===0?resolve():
|
|
13
|
+
reject(Error(`taskkill exited ${code}`));});
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {execFileSync} from 'node:child_process';
|
|
4
|
+
import {createRequire} from 'node:module';
|
|
5
|
+
import micromatch from './glob.mjs';
|
|
6
|
+
import {withinPath as within,realObservedPath} from './path-identity.mjs';
|
|
7
|
+
|
|
8
|
+
export function yarnLocalTsc(root){
|
|
9
|
+
const expected=path.join(root,'node_modules','typescript','bin','tsc');
|
|
10
|
+
if(process.platform==='win32'){
|
|
11
|
+
const shim=path.join(root,'node_modules','.bin','tsc.cmd');
|
|
12
|
+
const body=fs.readFileSync(shim,'utf8').replace(/\r\n/g,'\n').trim();
|
|
13
|
+
const standard=`@IF EXIST "%~dp0\\node.exe" (
|
|
14
|
+
"%~dp0\\node.exe" "%~dp0\\..\\typescript\\bin\\tsc" %*
|
|
15
|
+
) ELSE (
|
|
16
|
+
@SETLOCAL
|
|
17
|
+
@SET PATHEXT=%PATHEXT:;.JS;=;%
|
|
18
|
+
node "%~dp0\\..\\typescript\\bin\\tsc" %*
|
|
19
|
+
)`;
|
|
20
|
+
if(body!==standard)throw Error('local TypeScript executable is not the installed compiler');
|
|
21
|
+
return realObservedPath(expected);
|
|
22
|
+
}
|
|
23
|
+
const tsc=realObservedPath(path.join(root,'node_modules','.bin','tsc'));
|
|
24
|
+
if(tsc!==expected)throw Error('local TypeScript executable is not the installed compiler');
|
|
25
|
+
return tsc;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const posix=file=>file.split(path.sep).join('/');
|
|
29
|
+
const read=file=>JSON.parse(fs.readFileSync(file,'utf8'));
|
|
30
|
+
const relative=(root,file)=>posix(path.relative(root,file));
|
|
31
|
+
const safeDirectory=(root,dir)=>{
|
|
32
|
+
const full=path.resolve(root,dir);
|
|
33
|
+
if(!within(full,root))throw Error('workspace path escapes checkout');
|
|
34
|
+
return full;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export function yarnWorkspacePackages(root){
|
|
38
|
+
root=realObservedPath(root);
|
|
39
|
+
const pkg=read(path.join(root,'package.json'));
|
|
40
|
+
const patterns=Array.isArray(pkg.workspaces)?pkg.workspaces:pkg.workspaces?.packages;
|
|
41
|
+
if(!Array.isArray(patterns)||!patterns.length||patterns.some(value=>
|
|
42
|
+
typeof value!=='string'||path.isAbsolute(value)||value.split('/').includes('..')))
|
|
43
|
+
throw Error('workspace declarations are unsupported');
|
|
44
|
+
const files=execFileSync('git',['ls-files','--cached','--others','--exclude-standard',
|
|
45
|
+
'-z','--',':(glob)**/package.json'],{cwd:root,env:{...process.env,GIT_OPTIONAL_LOCKS:'0'},
|
|
46
|
+
maxBuffer:32*1024*1024,timeout:5000}).toString().split('\0').filter(Boolean);
|
|
47
|
+
const found=new Map();
|
|
48
|
+
for(const file of files){
|
|
49
|
+
const dir=path.posix.dirname(file);
|
|
50
|
+
if(dir==='.'||dir.split('/').includes('node_modules')||
|
|
51
|
+
!micromatch.isMatch(dir,patterns,{dot:true}))continue;
|
|
52
|
+
const member=read(path.join(root,file));
|
|
53
|
+
if(!member.name||found.has(member.name))throw Error('workspace package name missing or duplicated');
|
|
54
|
+
found.set(member.name,{name:member.name,dir,manifest:file,pkg:member});
|
|
55
|
+
}
|
|
56
|
+
if(found.size>50)throw Error('workspace count exceeds narrow Node onboarding contract');
|
|
57
|
+
return found;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function parsedTypeScript(root,dir){
|
|
61
|
+
const require=createRequire(path.join(root,'package.json'));
|
|
62
|
+
const ts=require(path.join(root,'node_modules','typescript'));
|
|
63
|
+
const config=path.join(root,dir,'tsconfig.json'),reads=new Set();
|
|
64
|
+
const host={...ts.sys,readFile(file){reads.add(path.resolve(file));return ts.sys.readFile(file);},
|
|
65
|
+
onUnRecoverableConfigFileDiagnostic(){}};
|
|
66
|
+
const parsed=ts.getParsedCommandLineOfConfigFile(config,{},host);
|
|
67
|
+
if(!parsed||parsed.errors?.length)throw Error('TypeScript configuration unavailable');
|
|
68
|
+
const raw=ts.readConfigFile(config,ts.sys.readFile);
|
|
69
|
+
if(raw.error||!Array.isArray(raw.config?.include)||raw.config.include.length!==1||
|
|
70
|
+
!['src','src/**/*','src/**'].includes(raw.config.include[0]))
|
|
71
|
+
throw Error('workspace TypeScript file membership is outside the supported src include');
|
|
72
|
+
if(parsed.options.plugins?.length||parsed.options.incremental||parsed.options.composite)
|
|
73
|
+
throw Error('TypeScript plugins or incremental/project builds are outside this contract');
|
|
74
|
+
if(parsed.options.paths||parsed.options.baseUrl||parsed.options.rootDirs?.length||
|
|
75
|
+
parsed.options.typeRoots?.length||parsed.options.customConditions?.length||
|
|
76
|
+
parsed.options.moduleSuffixes?.length||
|
|
77
|
+
parsed.options.moduleResolution!==undefined&&
|
|
78
|
+
parsed.options.moduleResolution!==ts.ModuleResolutionKind.Node10)
|
|
79
|
+
throw Error('custom TypeScript module resolution needs explicit input coverage');
|
|
80
|
+
if(parsed.projectReferences?.length)throw Error('TypeScript project references need a separate contract');
|
|
81
|
+
for(const file of reads)if(!within(realObservedPath(file),root))
|
|
82
|
+
throw Error(`TypeScript configuration escapes the checkout: ${path.basename(file)}`);
|
|
83
|
+
return {parsed,configs:[...reads].map(file=>relative(root,file)).sort()};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function yarnWorkspaceTypecheckInputs(root,workspace,script){
|
|
87
|
+
root=realObservedPath(root);
|
|
88
|
+
const rootPkg=read(path.join(root,'package.json'));
|
|
89
|
+
if(!/^yarn@4\./.test(rootPkg.packageManager||''))throw Error('pinned Yarn 4 required');
|
|
90
|
+
const yarnrc=fs.readFileSync(path.join(root,'.yarnrc.yml'),'utf8');
|
|
91
|
+
const linker=yarnrc.match(/^\s*nodeLinker\s*:\s*['"]?([\w-]+)/m)?.[1];
|
|
92
|
+
const mode=yarnrc.match(/^\s*nmMode\s*:\s*['"]?([\w-]+)/m)?.[1]||'classic';
|
|
93
|
+
if(linker!=='node-modules'||mode!=='classic')
|
|
94
|
+
throw Error('Yarn node-modules classic installation required');
|
|
95
|
+
if(/^\s*(?:yarnPath|plugins)\s*:/m.test(yarnrc))
|
|
96
|
+
throw Error('custom Yarn executable or plugins need explicit toolchain coverage');
|
|
97
|
+
const members=yarnWorkspacePackages(root),selected=members.get(workspace);
|
|
98
|
+
if(!selected)throw Error(`workspace ${workspace} is not declared`);
|
|
99
|
+
if(selected.pkg.scripts?.[script]?.trim()!=='tsc -p .'||
|
|
100
|
+
selected.pkg.scripts?.['pre'+script]||selected.pkg.scripts?.['post'+script])
|
|
101
|
+
throw Error('workspace typecheck must be standalone tsc -p .');
|
|
102
|
+
const packageDir=safeDirectory(root,selected.dir);
|
|
103
|
+
if(fs.existsSync(path.join(packageDir,'node_modules','.bin','tsc'))||
|
|
104
|
+
fs.existsSync(path.join(packageDir,'node_modules','.bin','tsc.cmd')))
|
|
105
|
+
throw Error('workspace-local TypeScript executable needs separate toolchain coverage');
|
|
106
|
+
const {parsed,configs}=parsedTypeScript(root,selected.dir);
|
|
107
|
+
const outDir=parsed.options.outDir&&within(path.resolve(parsed.options.outDir),root)?
|
|
108
|
+
relative(root,path.resolve(parsed.options.outDir)):null;
|
|
109
|
+
if(!parsed.options.noEmit&&!outDir)
|
|
110
|
+
throw Error('emitting TypeScript check needs an in-checkout outDir');
|
|
111
|
+
if(outDir&&within(path.join(root,outDir),path.join(packageDir,'src')))
|
|
112
|
+
throw Error('TypeScript output overlaps observed source inputs');
|
|
113
|
+
const closure=new Map(),visit=member=>{
|
|
114
|
+
for(const name of Object.keys({...member.pkg.dependencies,...member.pkg.devDependencies,
|
|
115
|
+
...member.pkg.peerDependencies,...member.pkg.optionalDependencies})){
|
|
116
|
+
const dep=members.get(name);if(!dep||dep.name===selected.name||closure.has(name))continue;
|
|
117
|
+
closure.set(name,dep);visit(dep);
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
visit(selected);
|
|
121
|
+
const source=[...new Set(['package.json','yarn.lock','.yarnrc.yml',
|
|
122
|
+
selected.manifest,...configs,`${selected.dir}/src/**`])];
|
|
123
|
+
const installed=['node_modules/**',`${selected.dir}/node_modules/**`];
|
|
124
|
+
const generated=[];
|
|
125
|
+
const linkTriggers=[];
|
|
126
|
+
for(const member of members.values()){
|
|
127
|
+
const logical=`node_modules/${member.name}`;
|
|
128
|
+
installed.push(`!${logical}`,`!${logical}/**`);
|
|
129
|
+
if(!closure.has(member.name))continue;
|
|
130
|
+
installed.push(`${member.dir}/node_modules/**`);
|
|
131
|
+
const declaration=member.pkg.types||member.pkg.typings;
|
|
132
|
+
if(!declaration||path.isAbsolute(declaration)||declaration.split('/').includes('..'))
|
|
133
|
+
throw Error(`${member.name} has no supported declaration-output location`);
|
|
134
|
+
const output=path.posix.dirname(declaration);
|
|
135
|
+
const outputDir=output==='.'?member.dir:`${member.dir}/${output}`;
|
|
136
|
+
generated.push(`${outputDir}/**`);
|
|
137
|
+
source.push(member.manifest);
|
|
138
|
+
// The logical link is part of resolution identity even though its target
|
|
139
|
+
// contents are indexed at the physical workspace output location.
|
|
140
|
+
linkTriggers.push(logical);
|
|
141
|
+
}
|
|
142
|
+
yarnLocalTsc(root);
|
|
143
|
+
return {workspace:selected,closure:[...closure.values()],source:[...new Set(source)],
|
|
144
|
+
installed,generated:[...new Set(generated)],configs,
|
|
145
|
+
configurationInputs:[...new Set(['package.json','yarn.lock','.yarnrc.yml',
|
|
146
|
+
...configs,...members.values().map(row=>row.manifest),
|
|
147
|
+
'node_modules/.yarn-state.yml'])],linkTriggers,outDir};
|
|
148
|
+
}
|