Files
deepseek-harness/packages/sdk/helper/src/package-managers/link-workspace.ts
imccyu 42b07a7022 feat(sdk): add developer project tooling
docs(rfc): propose SDK developer project tooling

feat: rename / docs

ci: fix windows gates

docs: revert
2026-07-15 23:17:29 +08:00

138 lines
5.4 KiB
TypeScript

/**
* Repository package discovery and NPM dependency-closure rewriting for live links.
*
* @module @deepseek-ai/dsh-helper/package-managers/link-workspace
*/
import { readFile, readdir } from 'node:fs/promises'
import { existsSync, realpathSync } from 'node:fs'
import { basename, dirname, join, relative, resolve, sep } from 'node:path'
import type { PackageJsonFile, PackageManifest } from '../documents/package-json-file.ts'
import { PnpmWorkspaceFile } from '../documents/pnpm-workspace-file.ts'
import type { ProjectFile } from '../documents/project-file.ts'
import type { PackageManager } from './package-manager.ts'
interface WorkspacePackage {
directory: string
manifest: PackageManifest
}
function posixPath(path: string): string {
return path.split(sep).join('/')
}
function canonicalPath(path: string): string {
let existing = resolve(path)
const suffix: string[] = []
while (!existsSync(existing)) {
const parent = dirname(existing)
/* v8 ignore next -- every absolute path reaches the existing filesystem root */
if (parent === existing) throw new Error(`cannot resolve an existing ancestor for ${path}`)
suffix.unshift(basename(existing))
existing = parent
}
return resolve(realpathSync(existing), ...suffix)
}
async function packageDirectories(root: string): Promise<string[]> {
const result: string[] = []
for (const vendor of await readdir(join(root, 'vendor'), { withFileTypes: true })) {
if (vendor.isDirectory()) result.push(join(root, 'vendor', vendor.name))
}
for (const group of await readdir(join(root, 'packages'), { withFileTypes: true })) {
if (!group.isDirectory()) continue
for (const pkg of await readdir(join(root, 'packages', group.name), { withFileTypes: true })) {
if (pkg.isDirectory()) result.push(join(root, 'packages', group.name, pkg.name))
}
}
return result
}
/** Index of repository packages used by `--link-workspace`. */
export class LinkWorkspace {
readonly root: string
private readonly packages: Map<string, WorkspacePackage>
private constructor(root: string, packages: Map<string, WorkspacePackage>) {
this.root = root
this.packages = packages
}
/** Scan vendor and package workspaces from a repository root. */
static async open(root: string): Promise<LinkWorkspace> {
const absolute = resolve(root)
const packages = new Map<string, WorkspacePackage>()
for (const directory of await packageDirectories(absolute)) {
let manifest: PackageManifest
try {
manifest = JSON.parse(await readFile(join(directory, 'package.json'), 'utf8')) as PackageManifest
} catch (error) {
throw new Error(`cannot read linked package at ${directory}: ${String(error)}`)
}
if (!manifest.name || typeof manifest.name !== 'string') continue
if (packages.has(manifest.name)) throw new Error(`duplicate linked package name: ${manifest.name}`)
packages.set(manifest.name, { directory, manifest })
}
if (!packages.has('cordis') || !packages.has('@deepseek-ai/dsh-scripts')) {
throw new Error(`not a DeepSeek Harness repository root: ${absolute}`)
}
return new LinkWorkspace(absolute, packages)
}
/** Expand direct NPM dependencies through all repository-local NPM dependency edges. */
closure(names: Iterable<string>): string[] {
const pending = [...names]
const result = new Set<string>()
while (pending.length > 0) {
const name = pending.pop()
if (!name || result.has(name)) continue
const pkg = this.packages.get(name)
/* v8 ignore next -- closure() only returns names present in this package map */
if (!pkg) continue
result.add(name)
const edges = {
...pkg.manifest.dependencies,
...pkg.manifest.peerDependencies as Record<string, string> | undefined,
}
for (const dependencyName of Object.keys(edges)) {
if (this.packages.has(dependencyName) && !result.has(dependencyName)) pending.push(dependencyName)
}
}
return [...result].sort()
}
/** Rewrite the full local closure to manager-specific live-link specs. */
apply(
projectRoot: string,
manifest: PackageJsonFile,
manager: PackageManager,
documents: readonly ProjectFile[],
): void {
const canonicalProjectRoot = canonicalPath(projectRoot)
const names = this.closure(manifest.npmDependencyNames())
for (const name of names) {
const pkg = this.packages.get(name)
/* v8 ignore next -- closure() only returns names present in this package map */
if (!pkg) continue
const relativePath = posixPath(relative(canonicalProjectRoot, realpathSync(pkg.directory)))
const spec = manager.linkSpec(relativePath)
const current = manifest.npmDependency(name)
manifest.setNpmDependency(current?.section ?? 'dependencies', name, spec)
if (manager.name === 'yarn') manifest.setResolution(name, spec)
}
if (manager.name === 'pnpm') {
const workspace = documents.find((item): item is PnpmWorkspaceFile => item instanceof PnpmWorkspaceFile)
if (!workspace) throw new Error('pnpm link mode requires pnpm-workspace.yaml')
workspace.disableAutoInstallPeers()
}
}
/** Resolve a package directory for diagnostics and tests. */
packageDirectory(name: string): string | undefined {
const directory = this.packages.get(name)?.directory
return directory
? resolve(dirname(directory), directory.split(sep).at(-1) as string)
: undefined
}
}