fix(release): harden dependency verification
This commit is contained in:
parent
b46d36d3bf
commit
9162bc69bd
9 changed files with 287 additions and 53 deletions
|
|
@ -2,5 +2,5 @@
|
||||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||||
# after editing either side, bring the other along and re-record with:
|
# after editing either side, bring the other along and re-record with:
|
||||||
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-26-published-dependency-faces.md
|
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-08-26-published-dependency-faces.md
|
||||||
2026-08-26-published-dependency-faces.md: aa7714e5c83aaf1c68ef300bdb10006e534bf88d
|
2026-08-26-published-dependency-faces.md: 668d4c721446a87388ae50951156d1b24f91138c
|
||||||
2026-08-26-published-dependency-faces.zh.md: e0ba33ff25651d1adf378af87fd6065ed9e54d03
|
2026-08-26-published-dependency-faces.zh.md: b10a285a81490f8eb66d70d3e73c2f676ad1b35c
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ Every covered package keeps `@deepseek-ai/cordis` in matching `peerDependencies`
|
||||||
|
|
||||||
A workspace package reached by a runtime value import from the Host entry closure belongs only in `dependencies` when every imported runtime export appears in the policy's `safeHostDependencyExports` table. An export whose constructor identity or module state must be shared appears in `peerRequiredHostExports`; importing one such export keeps the whole package edge in matching `peerDependencies` and `devDependencies`. Each table key is an exact module specifier and each value is a reviewed export set. The verifier follows runtime local imports from the Host entry, records named and default imports and re-exports, and rejects exports present in neither table; namespace, dynamic, and side-effect imports remain unbounded and cannot enter either table.
|
A workspace package reached by a runtime value import from the Host entry closure belongs only in `dependencies` when every imported runtime export appears in the policy's `safeHostDependencyExports` table. An export whose constructor identity or module state must be shared appears in `peerRequiredHostExports`; importing one such export keeps the whole package edge in matching `peerDependencies` and `devDependencies`. Each table key is an exact module specifier and each value is a reviewed export set. The verifier follows runtime local imports from the Host entry, records named and default imports and re-exports, and rejects exports present in neither table; namespace, dynamic, and side-effect imports remain unbounded and cannot enter either table.
|
||||||
|
|
||||||
Workspace imports used by the Client bundle, type-only imports, module augmentations, `dsh.client.inject`, invariant companions, and existing metadata-only peers belong only in `devDependencies`. Existing third-party dependencies outside these managed relationships keep their declared section. Workspace references use `workspace:^`.
|
Workspace imports used by the Client bundle, type-only imports, module augmentations, `dsh.client.inject`, invariant companions, and existing metadata-only peers belong only in `devDependencies`. Ordinary third-party packages imported by the Host runtime belong in `dependencies`; other third-party relationships keep their declared section. Workspace references use `workspace:^`.
|
||||||
|
|
||||||
Some development relationships exist only in `dsh.client.inject` or TypeScript project references. The policy's `configurationOnlyDevDependencies` table names only those reviewed edges and keeps them in `devDependencies`.
|
Some development relationships exist only in `dsh.client.inject` or TypeScript project references. The policy's `configurationOnlyDevDependencies` table names only those reviewed edges and keeps them in `devDependencies`.
|
||||||
|
|
||||||
The verifier reads source manifests and source files, so it runs on a clean tree without built `lib/`. An unclassified Host runtime export is a policy violation that blocks all `--fix` writes; a maintainer must review the export and classify it, change the source relationship, or change the package selection. Once source safety passes, `--fix` performs only the section and range changes implied by the classification and removes stale peer metadata.
|
The verifier reads source manifests and source files, so it runs on a clean tree without built `lib/`. Every selected Host face must have `src/index.ts`. An unclassified Host runtime export is a policy violation that blocks all `--fix` writes; a maintainer must review the export and classify it, change the source relationship, or change the package selection. Once source safety passes, `--fix` performs only the section and range changes implied by the classification and removes stale peer metadata.
|
||||||
|
|
||||||
### Maintainer workflow
|
### Maintainer workflow
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ pnpm run verify-package-dependencies -- --fix
|
||||||
git diff -- packages pnpm-lock.yaml docs/module-graph.md docs/module-graph.zh.md docs/module-graph.i18n.yaml
|
git diff -- packages pnpm-lock.yaml docs/module-graph.md docs/module-graph.zh.md docs/module-graph.i18n.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
Measure the working-tree graph and a Git ref through the local metadata-only registry. Each run creates a fresh consumer and npm cache, executes `npm install --package-lock-only`, rejects archive downloads, and leaves the repository unchanged. `--runs` controls repetitions, `--timeout-ms` bounds each run, and optional `--max-ms` makes the command fail when the slowest run exceeds a threshold.
|
Measure the working-tree graph and a Git ref through the local metadata-only registry. Each run creates a fresh consumer and npm cache, replaces inherited npm configuration with explicit peer, hoisting, and registry settings, executes `npm install --package-lock-only`, rejects archive downloads, and leaves the repository unchanged. `--runs` controls repetitions, `--timeout-ms` terminates the npm process tree after its deadline, and optional `--max-ms` makes the command fail when the slowest run exceeds a threshold.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run benchmark:npm-resolution -- --runs=5 --timeout-ms=300000
|
pnpm run benchmark:npm-resolution -- --runs=5 --timeout-ms=300000
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,11 @@ Host-only 包通过另一份显式列表加入同一策略。该列表包含 `@d
|
||||||
|
|
||||||
Host 入口闭包中的运行期 value import 所到达的 workspace 包,只有在每个运行期导出都列入策略的 `safeHostDependencyExports` 表时才只属于 `dependencies`。constructor 身份或模块状态必须共享的导出列入 `peerRequiredHostExports`;一旦使用这类导出,整条包依赖边就保留在范围一致的 `peerDependencies` 与 `devDependencies` 中。表的每个 key 都是精确 module specifier,每个 value 都是经审查的导出集合。验证器从 Host 入口沿运行期本地 import 扫描,记录具名与默认 import 和 re-export,并拒绝两个表都未收录的导出;namespace、dynamic 和 side-effect import 无法限定导出范围,因此不能进入任一表。
|
Host 入口闭包中的运行期 value import 所到达的 workspace 包,只有在每个运行期导出都列入策略的 `safeHostDependencyExports` 表时才只属于 `dependencies`。constructor 身份或模块状态必须共享的导出列入 `peerRequiredHostExports`;一旦使用这类导出,整条包依赖边就保留在范围一致的 `peerDependencies` 与 `devDependencies` 中。表的每个 key 都是精确 module specifier,每个 value 都是经审查的导出集合。验证器从 Host 入口沿运行期本地 import 扫描,记录具名与默认 import 和 re-export,并拒绝两个表都未收录的导出;namespace、dynamic 和 side-effect import 无法限定导出范围,因此不能进入任一表。
|
||||||
|
|
||||||
Client bundle 使用的 workspace import、纯类型 import、模块扩充、`dsh.client.inject`、invariant companion 和仅有元数据的现存 peer 只属于 `devDependencies`。不属于这些受管关系的现有第三方 dependency 保持原区段。Workspace 引用使用 `workspace:^`。
|
Client bundle 使用的 workspace import、纯类型 import、模块扩充、`dsh.client.inject`、invariant companion 和仅有元数据的现存 peer 只属于 `devDependencies`。Host 运行时导入的普通第三方包属于 `dependencies`;其他第三方关系保持原区段。Workspace 引用使用 `workspace:^`。
|
||||||
|
|
||||||
部分开发期关系只存在于 `dsh.client.inject` 或 TypeScript project reference 中。策略的 `configurationOnlyDevDependencies` 表只列出这些已评审的依赖边,并将它们保留在 `devDependencies` 中。
|
部分开发期关系只存在于 `dsh.client.inject` 或 TypeScript project reference 中。策略的 `configurationOnlyDevDependencies` 表只列出这些已评审的依赖边,并将它们保留在 `devDependencies` 中。
|
||||||
|
|
||||||
验证器读取源码 manifest 和源码文件,因此可以在没有已构建 `lib/` 的干净工作树上运行。未分类的 Host 运行期导出属于策略违规,会阻止 `--fix` 的全部写入;维护者必须审查该导出,并选择分类该导出、修改源码关系或修改选包范围。源码安全检查通过后,`--fix` 只执行分类所确定的区段与范围变更,并删除失效的 peer 元数据。
|
验证器读取源码 manifest 和源码文件,因此可以在没有已构建 `lib/` 的干净工作树上运行。每个被选中的 Host face 都必须存在 `src/index.ts`。未分类的 Host 运行期导出属于策略违规,会阻止 `--fix` 的全部写入;维护者必须审查该导出,并选择分类该导出、修改源码关系或修改选包范围。源码安全检查通过后,`--fix` 只执行分类所确定的区段与范围变更,并删除失效的 peer 元数据。
|
||||||
|
|
||||||
### 维护流程
|
### 维护流程
|
||||||
|
|
||||||
|
|
@ -49,7 +49,7 @@ pnpm run verify-package-dependencies -- --fix
|
||||||
git diff -- packages pnpm-lock.yaml docs/module-graph.md docs/module-graph.zh.md docs/module-graph.i18n.yaml
|
git diff -- packages pnpm-lock.yaml docs/module-graph.md docs/module-graph.zh.md docs/module-graph.i18n.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
通过仅 metadata 的本地 registry 测量工作树依赖图与 Git ref。每轮都会创建全新 consumer 与 npm cache,执行 `npm install --package-lock-only`,拒绝下载包归档,并保持仓库不变。`--runs` 控制重复次数,`--timeout-ms` 限制单轮耗时,可选 `--max-ms` 会在最慢一轮超过阈值时让命令失败。
|
通过仅 metadata 的本地 registry 测量工作树依赖图与 Git ref。每轮都会创建全新 consumer 与 npm cache,用明确的 peer、hoisting 和 registry 设置替换继承的 npm 配置,执行 `npm install --package-lock-only`,拒绝下载包归档,并保持仓库不变。`--runs` 控制重复次数,`--timeout-ms` 会在期限到达后终止 npm 进程树,可选 `--max-ms` 会在最慢一轮超过阈值时让命令失败。
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm run benchmark:npm-resolution -- --runs=5 --timeout-ms=300000
|
pnpm run benchmark:npm-resolution -- --runs=5 --timeout-ms=300000
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ describe('next package benchmark graph', () => {
|
||||||
sourceLine: "import { runtimeValue } from '@f/runtime'",
|
sourceLine: "import { runtimeValue } from '@f/runtime'",
|
||||||
}],
|
}],
|
||||||
peerRequiredHostDependencies: new Set(),
|
peerRequiredHostDependencies: new Set(),
|
||||||
|
configurationOnlyDevDependencies: new Set(),
|
||||||
clientInject: new Set(),
|
clientInject: new Set(),
|
||||||
}
|
}
|
||||||
const index = new Map<string, Map<string, MutableRegistryManifest>>([
|
const index = new Map<string, Map<string, MutableRegistryManifest>>([
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
parseBenchmarkOptions,
|
parseBenchmarkOptions,
|
||||||
publishWorkspaceRange,
|
publishWorkspaceRange,
|
||||||
resolveNpmPackageLock,
|
resolveNpmPackageLock,
|
||||||
|
runCommandWithTimeout,
|
||||||
type RegistryIndex,
|
type RegistryIndex,
|
||||||
} from './benchmark-npm-resolution.ts'
|
} from './benchmark-npm-resolution.ts'
|
||||||
|
|
||||||
|
|
@ -104,4 +105,78 @@ describe('npm resolution benchmark', () => {
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('isolates peer resolution from inherited npm configuration', async () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'dsh-hostile-npm-config-'))
|
||||||
|
roots.push(root)
|
||||||
|
const userConfig = join(root, 'user.npmrc')
|
||||||
|
writeFileSync(userConfig, '@deepseek-ai:registry=http://127.0.0.1:1/\nlegacy-peer-deps=true\nomit=peer\n')
|
||||||
|
const previous = {
|
||||||
|
userConfig: process.env.npm_config_userconfig,
|
||||||
|
legacyPeerDeps: process.env.npm_config_legacy_peer_deps,
|
||||||
|
omit: process.env.npm_config_omit,
|
||||||
|
}
|
||||||
|
process.env.npm_config_userconfig = userConfig
|
||||||
|
process.env.npm_config_legacy_peer_deps = 'true'
|
||||||
|
process.env.npm_config_omit = 'peer'
|
||||||
|
try {
|
||||||
|
const index: RegistryIndex = new Map([
|
||||||
|
['@deepseek-ai/dsh', new Map([['0.1.0', {
|
||||||
|
name: '@deepseek-ai/dsh',
|
||||||
|
version: '0.1.0',
|
||||||
|
peerDependencies: { '@deepseek-ai/dsh-peer': '1.0.0' },
|
||||||
|
}]])],
|
||||||
|
['@deepseek-ai/dsh-peer', new Map([['1.0.0', {
|
||||||
|
name: '@deepseek-ai/dsh-peer',
|
||||||
|
version: '1.0.0',
|
||||||
|
}]])],
|
||||||
|
])
|
||||||
|
|
||||||
|
const result = await resolveNpmPackageLock(index, { '@deepseek-ai/dsh': '0.1.0' }, 10_000)
|
||||||
|
|
||||||
|
expect(result.archiveRequests).toBe(0)
|
||||||
|
expect(result.packageLock.packages['node_modules/@deepseek-ai/dsh-peer']?.version).toBe('1.0.0')
|
||||||
|
} finally {
|
||||||
|
if (previous.userConfig === undefined) delete process.env.npm_config_userconfig
|
||||||
|
else process.env.npm_config_userconfig = previous.userConfig
|
||||||
|
if (previous.legacyPeerDeps === undefined) delete process.env.npm_config_legacy_peer_deps
|
||||||
|
else process.env.npm_config_legacy_peer_deps = previous.legacyPeerDeps
|
||||||
|
if (previous.omit === undefined) delete process.env.npm_config_omit
|
||||||
|
else process.env.npm_config_omit = previous.omit
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it.skipIf(process.platform === 'win32')('force-kills a timed-out process tree', async () => {
|
||||||
|
const source = [
|
||||||
|
"const { spawn } = require('node:child_process')",
|
||||||
|
"process.on('SIGTERM', () => {})",
|
||||||
|
'const child = spawn(process.execPath, [\'-e\', "process.on(\'SIGTERM\', () => {}); setInterval(() => {}, 1000)"], { stdio: \'ignore\' })',
|
||||||
|
'console.log(child.pid)',
|
||||||
|
'setInterval(() => {}, 1000)',
|
||||||
|
].join(';')
|
||||||
|
let descendantPid: number | undefined
|
||||||
|
try {
|
||||||
|
const result = await runCommandWithTimeout(process.execPath, ['-e', source], {
|
||||||
|
cwd: process.cwd(),
|
||||||
|
env: process.env,
|
||||||
|
timeoutMs: 1_000,
|
||||||
|
terminationGraceMs: 100,
|
||||||
|
})
|
||||||
|
const reportedPid = Number.parseInt(result.output.trim(), 10)
|
||||||
|
if (!Number.isSafeInteger(reportedPid)) throw new Error(`child reported invalid pid ${result.output.trim()}`)
|
||||||
|
descendantPid = reportedPid
|
||||||
|
|
||||||
|
expect(result.timedOut).toBe(true)
|
||||||
|
expect(result.signal).toBe('SIGKILL')
|
||||||
|
expect(() => { process.kill(reportedPid, 0) }).toThrow()
|
||||||
|
} finally {
|
||||||
|
if (descendantPid !== undefined && Number.isSafeInteger(descendantPid)) {
|
||||||
|
try {
|
||||||
|
process.kill(descendantPid, 'SIGKILL')
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/** Benchmark npm's dependency-tree resolution against an all-local registry. */
|
/** Benchmark npm's dependency-tree resolution against an all-local registry. */
|
||||||
|
|
||||||
import { execFileSync, spawn } from 'node:child_process'
|
import { execFileSync, spawn, spawnSync, type ChildProcess } from 'node:child_process'
|
||||||
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
import { globSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||||
import { createServer, type Server } from 'node:http'
|
import { createServer, type Server } from 'node:http'
|
||||||
import { tmpdir } from 'node:os'
|
import { tmpdir } from 'node:os'
|
||||||
|
|
@ -10,6 +10,8 @@ import { parseArgs } from 'node:util'
|
||||||
|
|
||||||
const TARGET_PACKAGE = '@deepseek-ai/dsh'
|
const TARGET_PACKAGE = '@deepseek-ai/dsh'
|
||||||
const DEFAULT_TIMEOUT_MS = 300_000
|
const DEFAULT_TIMEOUT_MS = 300_000
|
||||||
|
const TERMINATION_GRACE_MS = 1_000
|
||||||
|
const FORCED_EXIT_TIMEOUT_MS = 5_000
|
||||||
const WORKSPACE_MANIFEST_GLOBS = [
|
const WORKSPACE_MANIFEST_GLOBS = [
|
||||||
'apps/*/package.json',
|
'apps/*/package.json',
|
||||||
'packages/*/*/package.json',
|
'packages/*/*/package.json',
|
||||||
|
|
@ -271,6 +273,92 @@ function npmExecutable(): string {
|
||||||
return process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
return process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function delay(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolveDelay => setTimeout(resolveDelay, ms))
|
||||||
|
}
|
||||||
|
|
||||||
|
function signalProcessTree(child: ChildProcess, signal: 'SIGTERM' | 'SIGKILL'): void {
|
||||||
|
if (child.pid === undefined) {
|
||||||
|
child.kill(signal)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const force = signal === 'SIGKILL' ? ['/F'] : []
|
||||||
|
const result = spawnSync('taskkill', ['/PID', String(child.pid), '/T', ...force], {
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: true,
|
||||||
|
})
|
||||||
|
if (result.error !== undefined) throw result.error
|
||||||
|
if (result.status !== 0 && child.exitCode === null && child.signalCode === null) child.kill(signal)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
process.kill(-child.pid, signal)
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as NodeJS.ErrnoException).code !== 'ESRCH') throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run one command with bounded process-tree termination after its deadline.
|
||||||
|
* @param command - Executable path or name.
|
||||||
|
* @param args - Arguments passed without shell interpolation on POSIX.
|
||||||
|
* @param options - Working directory, environment, timeout, and termination grace.
|
||||||
|
* @returns Exit facts, captured output, duration, and whether timeout handling began.
|
||||||
|
*/
|
||||||
|
export async function runCommandWithTimeout(
|
||||||
|
command: string,
|
||||||
|
args: readonly string[],
|
||||||
|
options: {
|
||||||
|
readonly cwd: string
|
||||||
|
readonly env: NodeJS.ProcessEnv
|
||||||
|
readonly timeoutMs: number
|
||||||
|
readonly terminationGraceMs?: number
|
||||||
|
},
|
||||||
|
): Promise<{ status: number | null; signal: NodeJS.Signals | null; durationMs: number; output: string; timedOut: boolean }> {
|
||||||
|
const started = performance.now()
|
||||||
|
const child = spawn(command, [...args], {
|
||||||
|
cwd: options.cwd,
|
||||||
|
detached: process.platform !== 'win32',
|
||||||
|
env: options.env,
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
})
|
||||||
|
let output = ''
|
||||||
|
child.stdout.setEncoding('utf8')
|
||||||
|
child.stderr.setEncoding('utf8')
|
||||||
|
child.stdout.on('data', (chunk) => { output += String(chunk) })
|
||||||
|
child.stderr.on('data', (chunk) => { output += String(chunk) })
|
||||||
|
const exited = new Promise<{ status: number | null; signal: NodeJS.Signals | null }>((resolveExit, reject) => {
|
||||||
|
child.once('error', reject)
|
||||||
|
child.once('close', (status, signal) => { resolveExit({ status, signal }) })
|
||||||
|
})
|
||||||
|
let timeout: NodeJS.Timeout | undefined
|
||||||
|
try {
|
||||||
|
const first = await Promise.race([
|
||||||
|
exited.then(outcome => ({ type: 'exit' as const, outcome })),
|
||||||
|
new Promise<{ type: 'timeout' }>((resolveTimeout) => {
|
||||||
|
timeout = setTimeout(() => { resolveTimeout({ type: 'timeout' }) }, options.timeoutMs)
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
if (first.type === 'exit') {
|
||||||
|
return { ...first.outcome, durationMs: performance.now() - started, output, timedOut: false }
|
||||||
|
}
|
||||||
|
|
||||||
|
signalProcessTree(child, 'SIGTERM')
|
||||||
|
await delay(options.terminationGraceMs ?? TERMINATION_GRACE_MS)
|
||||||
|
signalProcessTree(child, 'SIGKILL')
|
||||||
|
const forced = await Promise.race([
|
||||||
|
exited,
|
||||||
|
delay(FORCED_EXIT_TIMEOUT_MS).then(() => undefined),
|
||||||
|
])
|
||||||
|
if (forced === undefined) throw new Error('timed-out process tree did not exit after SIGKILL')
|
||||||
|
return { ...forced, durationMs: performance.now() - started, output, timedOut: true }
|
||||||
|
} finally {
|
||||||
|
if (timeout !== undefined) clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readNpmPackageLock(path: string): NpmPackageLock {
|
function readNpmPackageLock(path: string): NpmPackageLock {
|
||||||
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
const parsed: unknown = JSON.parse(readFileSync(path, 'utf8'))
|
||||||
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||||
|
|
@ -289,50 +377,39 @@ async function runNpm(
|
||||||
registry: string,
|
registry: string,
|
||||||
timeoutMs: number,
|
timeoutMs: number,
|
||||||
): Promise<{ durationMs: number; output: string; timedOut: boolean }> {
|
): Promise<{ durationMs: number; output: string; timedOut: boolean }> {
|
||||||
const started = performance.now()
|
const npmrc = join(cwd, '.npmrc')
|
||||||
const child = spawn(npmExecutable(), [
|
const globalNpmrc = join(cwd, '.npmrc-global')
|
||||||
|
writeFileSync(npmrc, `registry=${registry}\n@deepseek-ai:registry=${registry}\n`)
|
||||||
|
writeFileSync(globalNpmrc, '')
|
||||||
|
const inheritedEnvironment = Object.fromEntries(Object.entries(process.env)
|
||||||
|
.filter(([name]) => !name.toLowerCase().startsWith('npm_config_')))
|
||||||
|
const result = await runCommandWithTimeout(npmExecutable(), [
|
||||||
'install',
|
'install',
|
||||||
'--package-lock-only',
|
'--package-lock-only',
|
||||||
'--ignore-scripts',
|
'--ignore-scripts',
|
||||||
'--no-audit',
|
'--no-audit',
|
||||||
'--no-fund',
|
'--no-fund',
|
||||||
'--loglevel=error',
|
'--loglevel=error',
|
||||||
|
'--include=peer',
|
||||||
|
'--install-strategy=hoisted',
|
||||||
|
'--legacy-peer-deps=false',
|
||||||
`--registry=${registry}`,
|
`--registry=${registry}`,
|
||||||
], {
|
], {
|
||||||
cwd,
|
cwd,
|
||||||
// Windows resolves npm through a .cmd shim, which spawn() refuses
|
|
||||||
// without a shell since the CVE-2024-27980 hardening.
|
|
||||||
shell: process.platform === 'win32',
|
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...inheritedEnvironment,
|
||||||
npm_config_cache: join(cwd, '.npm-cache'),
|
npm_config_cache: join(cwd, '.npm-cache'),
|
||||||
|
npm_config_globalconfig: globalNpmrc,
|
||||||
|
npm_config_userconfig: npmrc,
|
||||||
npm_config_update_notifier: 'false',
|
npm_config_update_notifier: 'false',
|
||||||
},
|
},
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
timeoutMs,
|
||||||
})
|
})
|
||||||
let output = ''
|
if (result.timedOut) return result
|
||||||
child.stdout.setEncoding('utf8')
|
if (result.status !== 0) {
|
||||||
child.stderr.setEncoding('utf8')
|
throw new Error(`npm install exited ${String(result.status)} after ${result.durationMs.toFixed(0)} ms\n${result.output.trim()}`)
|
||||||
child.stdout.on('data', (chunk) => { output += String(chunk) })
|
|
||||||
child.stderr.on('data', (chunk) => { output += String(chunk) })
|
|
||||||
const outcome = await new Promise<{ status: number | null; timedOut: boolean }>((resolveExit, reject) => {
|
|
||||||
let timeoutReached = false
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
timeoutReached = true
|
|
||||||
child.kill('SIGTERM')
|
|
||||||
}, timeoutMs)
|
|
||||||
child.once('error', reject)
|
|
||||||
child.once('exit', (status) => {
|
|
||||||
clearTimeout(timer)
|
|
||||||
resolveExit({ status, timedOut: timeoutReached })
|
|
||||||
})
|
|
||||||
})
|
|
||||||
const durationMs = performance.now() - started
|
|
||||||
if (outcome.timedOut) return { durationMs, output, timedOut: true }
|
|
||||||
if (outcome.status !== 0) {
|
|
||||||
throw new Error(`npm install exited ${String(outcome.status)} after ${durationMs.toFixed(0)} ms\n${output.trim()}`)
|
|
||||||
}
|
}
|
||||||
return { durationMs, output, timedOut: false }
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -403,6 +480,7 @@ export async function resolveNpmPackageLock(
|
||||||
packageLock: readNpmPackageLock(join(consumer, 'package-lock.json')),
|
packageLock: readNpmPackageLock(join(consumer, 'package-lock.json')),
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
server.closeAllConnections()
|
||||||
await close(server)
|
await close(server)
|
||||||
rmSync(consumer, { recursive: true, force: true })
|
rmSync(consumer, { recursive: true, force: true })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,14 @@ const CONFIGURATION_ONLY_DEV_DEPENDENCIES = {
|
||||||
const SAFE_HOST_DEPENDENCY_EXPORTS = {
|
const SAFE_HOST_DEPENDENCY_EXPORTS = {
|
||||||
'@deepseek-ai/dsh-api-session-controller/remote-events': ['SESSION_CONTROLLER_REMOTE_EVENTS'],
|
'@deepseek-ai/dsh-api-session-controller/remote-events': ['SESSION_CONTROLLER_REMOTE_EVENTS'],
|
||||||
'@deepseek-ai/dsh-credentials': ['credentialKey'],
|
'@deepseek-ai/dsh-credentials': ['credentialKey'],
|
||||||
'@deepseek-ai/dsh-host-apiproxy': ['toFetchHandler'],
|
|
||||||
'@deepseek-ai/dsh-host-apiproxy/api': ['RpcId', 'clientRequestSchema'],
|
|
||||||
'@deepseek-ai/dsh-llm': ['MessageId', 'callConfigEquals', 'deepFreeze', 'freezeMessage'],
|
'@deepseek-ai/dsh-llm': ['MessageId', 'callConfigEquals', 'deepFreeze', 'freezeMessage'],
|
||||||
'@deepseek-ai/dsh-llm/brand': ['ToolCallId'],
|
'@deepseek-ai/dsh-llm/brand': ['ToolCallId'],
|
||||||
'@deepseek-ai/dsh-session': ['isJsonValue'],
|
'@deepseek-ai/dsh-session': ['isJsonValue'],
|
||||||
|
'@deepseek-ai/dsh-session/types': ['SessionId'],
|
||||||
'@deepseek-ai/dsh-settings': ['settingsNamespace'],
|
'@deepseek-ai/dsh-settings': ['settingsNamespace'],
|
||||||
'@deepseek-ai/dsh-system-prompt': ['FIRST_PARTY_SECTION_ORDER'],
|
'@deepseek-ai/dsh-system-prompt': ['FIRST_PARTY_SECTION_ORDER'],
|
||||||
'@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
|
'@deepseek-ai/dsh-timeout': ['MAX_TIMER_DELAY_MS'],
|
||||||
|
'@deepseek-ai/dsh-typert-protocol': ['RemoteError', 'remoteErrorOf'],
|
||||||
'@deepseek-ai/dsh-util-crypto': ['randomUUID'],
|
'@deepseek-ai/dsh-util-crypto': ['randomUUID'],
|
||||||
'@deepseek-ai/schemastery': ['default'],
|
'@deepseek-ai/schemastery': ['default'],
|
||||||
} as const satisfies HostDependencyExports
|
} as const satisfies HostDependencyExports
|
||||||
|
|
@ -50,7 +50,7 @@ const SAFE_HOST_DEPENDENCY_EXPORTS = {
|
||||||
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
|
/** Runtime exports that require every consumer to resolve the provider's shared peer instance. */
|
||||||
const PEER_REQUIRED_HOST_EXPORTS = {
|
const PEER_REQUIRED_HOST_EXPORTS = {
|
||||||
'@deepseek-ai/dsh-scope': ['carrierKeyOf', 'scopeOf', 'scopeTarget'],
|
'@deepseek-ai/dsh-scope': ['carrierKeyOf', 'scopeOf', 'scopeTarget'],
|
||||||
'@deepseek-ai/dsh-typert-protocol': ['TypertLookupFailure', 'TypertRemoteFailure', 'remoteMethods'],
|
'@deepseek-ai/dsh-typert-protocol': ['Remote', 'TypertRemoteService', 'remoteMethods'],
|
||||||
} as const satisfies HostDependencyExports
|
} as const satisfies HostDependencyExports
|
||||||
|
|
||||||
/** Exact import specifier to reviewed runtime exports. */
|
/** Exact import specifier to reviewed runtime exports. */
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,7 @@ function facts(manifest: PackageDependencyManifest): PackageDependencyFacts {
|
||||||
sourceLine: "import { runtimeValue } from '@deepseek-ai/dsh-runtime'",
|
sourceLine: "import { runtimeValue } from '@deepseek-ai/dsh-runtime'",
|
||||||
}],
|
}],
|
||||||
peerRequiredHostDependencies: new Set(),
|
peerRequiredHostDependencies: new Set(),
|
||||||
|
configurationOnlyDevDependencies: new Set(),
|
||||||
clientInject: new Set(),
|
clientInject: new Set(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -109,11 +110,17 @@ describe('package dependency scope', () => {
|
||||||
'@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
|
'@deepseek-ai/dsh-client-ui-tool': ['@deepseek-ai/dsh-api-remotes'],
|
||||||
})
|
})
|
||||||
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
|
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/schemastery']).toEqual(['default'])
|
||||||
|
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-session/types']).toEqual([
|
||||||
|
'SessionId',
|
||||||
|
])
|
||||||
|
expect(PACKAGE_DEPENDENCY_POLICY.safeHostDependencyExports['@deepseek-ai/dsh-typert-protocol']).toEqual([
|
||||||
|
'RemoteError', 'remoteErrorOf',
|
||||||
|
])
|
||||||
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-scope']).toEqual([
|
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-scope']).toEqual([
|
||||||
'carrierKeyOf', 'scopeOf', 'scopeTarget',
|
'carrierKeyOf', 'scopeOf', 'scopeTarget',
|
||||||
])
|
])
|
||||||
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toEqual([
|
expect(PACKAGE_DEPENDENCY_POLICY.peerRequiredHostExports['@deepseek-ai/dsh-typert-protocol']).toEqual([
|
||||||
'TypertLookupFailure', 'TypertRemoteFailure', 'remoteMethods',
|
'Remote', 'TypertRemoteService', 'remoteMethods',
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -187,6 +194,7 @@ describe('package dependency scope', () => {
|
||||||
sourceLine: "import { safeValue } from '@f/provider/api'",
|
sourceLine: "import { safeValue } from '@f/provider/api'",
|
||||||
}],
|
}],
|
||||||
peerRequiredHostDependencies: new Set(),
|
peerRequiredHostDependencies: new Set(),
|
||||||
|
configurationOnlyDevDependencies: new Set(),
|
||||||
clientInject: new Set(),
|
clientInject: new Set(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -211,6 +219,15 @@ describe('package dependency scope', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('face-aware source classification', () => {
|
describe('face-aware source classification', () => {
|
||||||
|
it('fails when a managed Host package has no Host entry', () => {
|
||||||
|
const root = mkdtempSync(join(tmpdir(), 'dsh-package-missing-host-'))
|
||||||
|
roots.push(root)
|
||||||
|
const subject = pkg('@f/host', 'packages/g/host/package.json')
|
||||||
|
|
||||||
|
expect(() => readPackageDependencyFacts(root, subject, 'configured-host', new Set([subject.name])))
|
||||||
|
.toThrow('packages/g/host/package.json: Host runtime entry packages/g/host/src/index.ts does not exist')
|
||||||
|
})
|
||||||
|
|
||||||
it('counts Host values as dependencies and Client values as development inputs', () => {
|
it('counts Host values as dependencies and Client values as development inputs', () => {
|
||||||
const root = mkdtempSync(join(tmpdir(), 'dsh-package-faces-'))
|
const root = mkdtempSync(join(tmpdir(), 'dsh-package-faces-'))
|
||||||
roots.push(root)
|
roots.push(root)
|
||||||
|
|
@ -234,9 +251,12 @@ describe('face-aware source classification', () => {
|
||||||
}
|
}
|
||||||
const found = readPackageDependencyFacts(root, subject, 'client-host', new Set([
|
const found = readPackageDependencyFacts(root, subject, 'client-host', new Set([
|
||||||
CORDIS, '@f/runtime', '@f/types', '@f/nested', '@f/hidden', '@f/browser', '@f/injected',
|
CORDIS, '@f/runtime', '@f/types', '@f/nested', '@f/hidden', '@f/browser', '@f/injected',
|
||||||
]))
|
]), policy({
|
||||||
|
configurationOnlyDevDependencies: { '@f/dual': ['@f/injected'] },
|
||||||
|
}))
|
||||||
|
|
||||||
expect([...found.hostRuntimeSourceUses.keys()].sort()).toEqual(['@f/nested', '@f/runtime'])
|
expect([...found.hostRuntimeSourceUses.keys()].sort()).toEqual(['@f/nested', '@f/runtime'])
|
||||||
|
expect([...found.configurationOnlyDevDependencies]).toEqual(['@f/injected'])
|
||||||
expect(found.hostRuntimeExportUses).toEqual([
|
expect(found.hostRuntimeExportUses).toEqual([
|
||||||
{
|
{
|
||||||
packageName: '@f/nested',
|
packageName: '@f/nested',
|
||||||
|
|
@ -294,6 +314,57 @@ describe('face-aware source classification', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('dependency sections', () => {
|
describe('dependency sections', () => {
|
||||||
|
it('does not leak repository configuration into captured dependency facts', () => {
|
||||||
|
const manifest: PackageDependencyManifest = {
|
||||||
|
name: '@deepseek-ai/dsh-client-locale',
|
||||||
|
dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
|
||||||
|
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^' },
|
||||||
|
peerDependencies: { [CORDIS]: 'workspace:^' },
|
||||||
|
}
|
||||||
|
const base = facts(manifest)
|
||||||
|
const subject: PackageDependencyFacts = {
|
||||||
|
...base,
|
||||||
|
workspaceNames: new Set([...base.workspaceNames, '@deepseek-ai/dsh-api-remotes']),
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(collectPackageDependencyViolations({
|
||||||
|
facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
|
||||||
|
})).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('requires non-workspace Host runtime imports in dependencies', () => {
|
||||||
|
const manifest: PackageDependencyManifest = {
|
||||||
|
name: '@deepseek-ai/dsh-probe',
|
||||||
|
dependencies: { '@deepseek-ai/dsh-runtime': 'workspace:^' },
|
||||||
|
devDependencies: { [CORDIS]: 'workspace:^', '@deepseek-ai/dsh-types': 'workspace:^', external: '^1.0.0' },
|
||||||
|
peerDependencies: { [CORDIS]: 'workspace:^' },
|
||||||
|
}
|
||||||
|
const subject: PackageDependencyFacts = {
|
||||||
|
...facts(manifest),
|
||||||
|
hostRuntimeSourceUses: new Map([
|
||||||
|
['@deepseek-ai/dsh-runtime', ['packages/core/probe/src/index.ts']],
|
||||||
|
['external', ['packages/core/probe/src/index.ts']],
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
const state = {
|
||||||
|
facts: [subject], packages: [], policyViolations: [], workspaceNames: subject.workspaceNames,
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(collectPackageDependencyViolations(state)).toContain(
|
||||||
|
'packages/core/probe/package.json: external (packages/core/probe/src/index.ts) '
|
||||||
|
+ 'must be dependencies-only; found devDependencies',
|
||||||
|
)
|
||||||
|
repairPackageDependencyManifest(subject)
|
||||||
|
expect(manifest.dependencies?.external).toBe('^1.0.0')
|
||||||
|
expect(manifest.devDependencies?.external).toBeUndefined()
|
||||||
|
|
||||||
|
delete manifest.dependencies?.external
|
||||||
|
expect(collectPackageDependencyViolations(state)).toContain(
|
||||||
|
'packages/core/probe/package.json: external (packages/core/probe/src/index.ts) '
|
||||||
|
+ 'must be dependencies-only; found no dependency section',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('accepts Host dependencies, development-only inputs, and shared Cordis', () => {
|
it('accepts Host dependencies, development-only inputs, and shared Cordis', () => {
|
||||||
const manifest: PackageDependencyManifest = {
|
const manifest: PackageDependencyManifest = {
|
||||||
name: '@deepseek-ai/dsh-probe',
|
name: '@deepseek-ai/dsh-probe',
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,7 @@ export interface PackageDependencyFacts {
|
||||||
readonly hostRuntimeSourceUses: ReadonlyMap<string, readonly string[]>
|
readonly hostRuntimeSourceUses: ReadonlyMap<string, readonly string[]>
|
||||||
readonly hostRuntimeExportUses: readonly HostRuntimeExportUse[]
|
readonly hostRuntimeExportUses: readonly HostRuntimeExportUse[]
|
||||||
readonly peerRequiredHostDependencies: ReadonlySet<string>
|
readonly peerRequiredHostDependencies: ReadonlySet<string>
|
||||||
|
readonly configurationOnlyDevDependencies: ReadonlySet<string>
|
||||||
readonly clientInject: ReadonlySet<string>
|
readonly clientInject: ReadonlySet<string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -296,7 +297,7 @@ function readHostRuntimeUses(root: string, pkg: WorkspacePackageManifest): {
|
||||||
const seen = new Set<string>()
|
const seen = new Set<string>()
|
||||||
const visit = (path: string): void => {
|
const visit = (path: string): void => {
|
||||||
const normalized = normalize(path)
|
const normalized = normalize(path)
|
||||||
if (seen.has(normalized) || !existsSync(normalized)) return
|
if (seen.has(normalized)) return
|
||||||
seen.add(normalized)
|
seen.add(normalized)
|
||||||
const source = readFileSync(normalized, 'utf8')
|
const source = readFileSync(normalized, 'utf8')
|
||||||
const displayPath = normalizePath(relative(root, normalized))
|
const displayPath = normalizePath(relative(root, normalized))
|
||||||
|
|
@ -312,7 +313,11 @@ function readHostRuntimeUses(root: string, pkg: WorkspacePackageManifest): {
|
||||||
if (target !== undefined) visit(target)
|
if (target !== undefined) visit(target)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visit(resolve(root, pkg.dir, 'src/index.ts'))
|
const entry = resolve(root, pkg.dir, 'src/index.ts')
|
||||||
|
if (!existsSync(entry)) {
|
||||||
|
throw new Error(`${pkg.manifestPath}: Host runtime entry ${normalizePath(relative(root, entry))} does not exist`)
|
||||||
|
}
|
||||||
|
visit(entry)
|
||||||
return {
|
return {
|
||||||
packageUses,
|
packageUses,
|
||||||
exportUses: [...exportUses.values()].sort((left, right) =>
|
exportUses: [...exportUses.values()].sort((left, right) =>
|
||||||
|
|
@ -358,6 +363,9 @@ export function readPackageDependencyFacts(
|
||||||
peerRequiredHostDependencies: new Set(hostRuntime.exportUses
|
peerRequiredHostDependencies: new Set(hostRuntime.exportUses
|
||||||
.filter(use => policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true)
|
.filter(use => policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true)
|
||||||
.map(use => use.packageName)),
|
.map(use => use.packageName)),
|
||||||
|
configurationOnlyDevDependencies: new Set(
|
||||||
|
policy.configurationOnlyDevDependencies[pkg.manifest.name ?? ''] ?? [],
|
||||||
|
),
|
||||||
clientInject: new Set(inject.map(packageNameOf).filter(name => name !== undefined)),
|
clientInject: new Set(inject.map(packageNameOf).filter(name => name !== undefined)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -405,7 +413,7 @@ export function collectHostDependencyExportPolicyViolations(
|
||||||
for (const fact of facts) {
|
for (const fact of facts) {
|
||||||
for (const use of fact.hostRuntimeExportUses) {
|
for (const use of fact.hostRuntimeExportUses) {
|
||||||
if (use.packageName === fact.manifest.name || use.packageName === CORDIS) continue
|
if (use.packageName === fact.manifest.name || use.packageName === CORDIS) continue
|
||||||
if (!workspaceNames.has(use.packageName) && fact.manifest.peerDependencies?.[use.packageName] === undefined) continue
|
if (!workspaceNames.has(use.packageName)) continue
|
||||||
if (policy.safeHostDependencyExports[use.specifier]?.includes(use.exportName) === true) continue
|
if (policy.safeHostDependencyExports[use.specifier]?.includes(use.exportName) === true) continue
|
||||||
if (policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true) continue
|
if (policy.peerRequiredHostExports[use.specifier]?.includes(use.exportName) === true) continue
|
||||||
violations.push(
|
violations.push(
|
||||||
|
|
@ -427,12 +435,16 @@ export function readPackageDependencyState(
|
||||||
const discovered = discoverPackageDependencyScope(packages.release, policy)
|
const discovered = discoverPackageDependencyScope(packages.release, policy)
|
||||||
const facts = discovered.selected.map(pkg =>
|
const facts = discovered.selected.map(pkg =>
|
||||||
readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy))
|
readPackageDependencyFacts(root, pkg, pkg.role, workspaceNames, policy))
|
||||||
|
const selectedNames = new Set(facts.map(fact => fact.manifest.name))
|
||||||
return {
|
return {
|
||||||
facts,
|
facts,
|
||||||
packages: packages.release,
|
packages: packages.release,
|
||||||
policyViolations: [
|
policyViolations: [
|
||||||
...discovered.violations,
|
...discovered.violations,
|
||||||
...collectHostDependencyExportPolicyViolations(facts, workspaceNames, policy),
|
...collectHostDependencyExportPolicyViolations(facts, workspaceNames, policy),
|
||||||
|
...Object.keys(policy.configurationOnlyDevDependencies)
|
||||||
|
.filter(name => !selectedNames.has(name))
|
||||||
|
.map(name => `configurationOnlyDevDependencies names unmanaged package ${name}`),
|
||||||
].sort(),
|
].sort(),
|
||||||
workspaceNames,
|
workspaceNames,
|
||||||
}
|
}
|
||||||
|
|
@ -441,7 +453,6 @@ export function readPackageDependencyState(
|
||||||
/** Derive the required npm section for each relationship owned by the policy. */
|
/** Derive the required npm section for each relationship owned by the policy. */
|
||||||
export function expectedPackageDependencies(
|
export function expectedPackageDependencies(
|
||||||
facts: PackageDependencyFacts,
|
facts: PackageDependencyFacts,
|
||||||
policy: PackageDependencyPolicy = PACKAGE_DEPENDENCY_POLICY,
|
|
||||||
): ReadonlyMap<string, ExpectedPackageDependency> {
|
): ReadonlyMap<string, ExpectedPackageDependency> {
|
||||||
const expected = new Map<string, { section: ExpectedPackageDependency['section']; origins: Set<string> }>()
|
const expected = new Map<string, { section: ExpectedPackageDependency['section']; origins: Set<string> }>()
|
||||||
const add = (name: string, sectionName: ExpectedPackageDependency['section'], origin: string): void => {
|
const add = (name: string, sectionName: ExpectedPackageDependency['section'], origin: string): void => {
|
||||||
|
|
@ -463,18 +474,16 @@ export function expectedPackageDependencies(
|
||||||
for (const name of facts.clientInject) {
|
for (const name of facts.clientInject) {
|
||||||
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'dsh.client.inject')
|
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'dsh.client.inject')
|
||||||
}
|
}
|
||||||
for (const name of PACKAGE_DEPENDENCY_POLICY.configurationOnlyDevDependencies[facts.manifest.name ?? ''] ?? []) {
|
for (const name of facts.configurationOnlyDevDependencies) {
|
||||||
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'configured development-only relationship')
|
|
||||||
}
|
|
||||||
for (const name of policy.configurationOnlyDevDependencies[facts.manifest.name ?? ''] ?? []) {
|
|
||||||
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'configured development-only relationship')
|
if (facts.workspaceNames.has(name)) add(name, 'devDependencies', 'configured development-only relationship')
|
||||||
}
|
}
|
||||||
for (const name of Object.keys(facts.manifest.peerDependencies ?? {})) {
|
for (const name of Object.keys(facts.manifest.peerDependencies ?? {})) {
|
||||||
if (name !== CORDIS) add(name, 'devDependencies', 'existing non-Cordis peer')
|
if (name !== CORDIS) add(name, 'devDependencies', 'existing non-Cordis peer')
|
||||||
}
|
}
|
||||||
for (const [name, paths] of facts.hostRuntimeSourceUses) {
|
for (const [name, paths] of facts.hostRuntimeSourceUses) {
|
||||||
if (!facts.workspaceNames.has(name) && facts.manifest.peerDependencies?.[name] === undefined) continue
|
const expectedSection = facts.workspaceNames.has(name) && facts.peerRequiredHostDependencies.has(name)
|
||||||
const expectedSection = facts.peerRequiredHostDependencies.has(name) ? 'peer-dev' : 'dependencies'
|
? 'peer-dev'
|
||||||
|
: 'dependencies'
|
||||||
for (const path of paths) add(name, expectedSection, path)
|
for (const path of paths) add(name, expectedSection, path)
|
||||||
}
|
}
|
||||||
return new Map([...expected].map(([name, rule]) => [name, {
|
return new Map([...expected].map(([name, rule]) => [name, {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue