mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
collectPackageNames repeated collectPackageAliases' directory walk verbatim, which jscpd reported as a 7-line clone and which failed the lint-and-duplication gate on both the Linux and Windows lanes. Both now read one workspacePackages() iterator: the alias collector keeps only packages named after their directory, and the coverage collector keeps every one of them.
249 lines
10 KiB
TypeScript
249 lines
10 KiB
TypeScript
/**
|
|
* Expand the workspace path aliases that a wildcard would otherwise resolve by
|
|
* probing every package group in turn.
|
|
*
|
|
* `tsconfig.base.json` is the resolution facade for the whole repository, and
|
|
* two of its aliases used one key per *group* rather than per package:
|
|
* `@deepseek-ai/dsh-*` listed 49 candidate globs and `@deepseek-ai/dsh-*\/invariant`
|
|
* listed 45. TypeScript and tsx try those candidates in order, so a specifier
|
|
* whose package sits late in the list pays for every earlier miss. Under tsx's
|
|
* ESM hook each miss is an `ERR_MODULE_NOT_FOUND` that Node decorates with a
|
|
* full CommonJS resolution walk, which dominated source-launch boot.
|
|
*
|
|
* This generator writes one explicit entry per package into a marked region of
|
|
* `tsconfig.base.json`, leaving every hand-written alias and comment outside
|
|
* that region untouched. `--check` reports drift instead of writing, so a new
|
|
* package that needs an alias fails a gate rather than silently resolving
|
|
* through a fallback that no longer exists.
|
|
*
|
|
* @module scripts/gen-tsconfig-paths
|
|
*/
|
|
|
|
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
import { join, resolve } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
const CONFIG = join(ROOT, 'tsconfig.base.json')
|
|
const BEGIN = ' // BEGIN generated package aliases — pnpm run gen-tsconfig-paths'
|
|
const END = ' // END generated package aliases'
|
|
|
|
/** Package-name prefix the expanded aliases cover. */
|
|
const PREFIX = '@deepseek-ai/dsh-'
|
|
|
|
/** One workspace package the generated region maps. */
|
|
interface PackageAlias {
|
|
/** Bare specifier, e.g. `@deepseek-ai/dsh-session`. */
|
|
readonly specifier: string
|
|
/** Repository-relative source directory, e.g. `./packages/session/session/src`. */
|
|
readonly source: string
|
|
/** Whether the package carries `src/invariant.ts`, which earns a second alias. */
|
|
readonly hasInvariant: boolean
|
|
}
|
|
|
|
/**
|
|
* Read a workspace manifest's declared name.
|
|
* @param manifest - absolute path to a `package.json`.
|
|
* @returns The declared name, or undefined when the file is absent or nameless.
|
|
*/
|
|
function packageName(manifest: string): string | undefined {
|
|
let parsed: unknown
|
|
try {
|
|
parsed = JSON.parse(readFileSync(manifest, 'utf8'))
|
|
} catch (_absentOrUnreadableManifest) {
|
|
return undefined
|
|
}
|
|
if (typeof parsed !== 'object' || parsed === null) return undefined
|
|
const name: unknown = (parsed as { name?: unknown }).name
|
|
return typeof name === 'string' ? name : undefined
|
|
}
|
|
|
|
/** One workspace package directory and the name its manifest declares. */
|
|
interface WorkspacePackage {
|
|
readonly group: string
|
|
readonly directory: string
|
|
readonly packageDir: string
|
|
readonly name: string
|
|
}
|
|
|
|
/**
|
|
* Walk `packages/<group>/<directory>` once, in a stable order.
|
|
* @returns Every directory whose manifest names a `@deepseek-ai/dsh-` package and that carries `src`.
|
|
*/
|
|
function workspacePackages(): WorkspacePackage[] {
|
|
const packages = join(ROOT, 'packages')
|
|
const found: WorkspacePackage[] = []
|
|
for (const group of readdirSync(packages).sort()) {
|
|
const groupDir = join(packages, group)
|
|
if (!statSync(groupDir).isDirectory()) continue
|
|
for (const directory of readdirSync(groupDir).sort()) {
|
|
const packageDir = join(groupDir, directory)
|
|
const name = packageName(join(packageDir, 'package.json'))
|
|
if (name === undefined || !name.startsWith(PREFIX)) continue
|
|
if (existsSync(join(packageDir, 'src'))) found.push({ group, directory, packageDir, name })
|
|
}
|
|
}
|
|
return found
|
|
}
|
|
|
|
/**
|
|
* Collect every package the removed wildcards could resolve.
|
|
*
|
|
* A wildcard substituted the specifier's suffix into `packages/<group>/<suffix>/src`,
|
|
* so it only ever resolved a package whose declared name is exactly
|
|
* `@deepseek-ai/dsh-<directory>`. Packages named after something other than
|
|
* their directory already carry a hand-written alias and are skipped here.
|
|
*
|
|
* @returns Aliases sorted by specifier.
|
|
* @throws When two package directories claim one specifier, which the removed
|
|
* wildcards resolved by group order and an explicit map cannot express.
|
|
*/
|
|
export function collectPackageAliases(): PackageAlias[] {
|
|
const bySpecifier = new Map<string, PackageAlias & { directory: string }>()
|
|
for (const { group, directory, packageDir, name } of workspacePackages()) {
|
|
if (name !== `${PREFIX}${directory}`) continue
|
|
const previous = bySpecifier.get(name)
|
|
if (previous !== undefined) {
|
|
throw new Error(
|
|
`gen-tsconfig-paths: ${name} is claimed by packages/${previous.directory} and packages/${group}/${directory}; `
|
|
+ 'an explicit alias cannot express the group-order tiebreak the wildcard used.',
|
|
)
|
|
}
|
|
bySpecifier.set(name, {
|
|
specifier: name,
|
|
source: `./packages/${group}/${directory}/src`,
|
|
hasInvariant: existsSync(join(packageDir, 'src', 'invariant.ts')),
|
|
directory: `${group}/${directory}`,
|
|
})
|
|
}
|
|
return [...bySpecifier.values()]
|
|
.map(({ specifier, source, hasInvariant }) => ({ specifier, source, hasInvariant }))
|
|
.sort((left, right) => left.specifier.localeCompare(right.specifier))
|
|
}
|
|
|
|
/**
|
|
* Collect every workspace package the aliases must cover.
|
|
*
|
|
* Unlike {@link collectPackageAliases} this keeps packages whose name does not
|
|
* match their directory. The generator cannot map those — only a hand-written
|
|
* alias can — but they still have to be mapped by something, because deleting
|
|
* the group wildcards removed the fallback that used to catch them.
|
|
*
|
|
* @returns Declared names of every `@deepseek-ai/dsh-` package carrying a `src` directory.
|
|
*/
|
|
export function collectPackageNames(): string[] {
|
|
return workspacePackages()
|
|
.map(({ name }) => name)
|
|
.sort((left, right) => left.localeCompare(right))
|
|
}
|
|
|
|
/**
|
|
* Read the bare package specifiers a config maps, generated region included.
|
|
* @param text - `tsconfig.base.json` contents.
|
|
* @returns Specifiers mapped without a subpath.
|
|
*/
|
|
export function mappedSpecifiers(text: string): Set<string> {
|
|
const keys = new Set<string>()
|
|
for (const match of text.matchAll(/^\s*"(@deepseek-ai\/dsh-[^"/]+)":/gm)) {
|
|
const key = match[1]
|
|
if (key !== undefined) keys.add(key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
/**
|
|
* Report packages that no alias maps.
|
|
*
|
|
* A package missing from `paths` still resolves — through the workspace symlink
|
|
* and the package's own `exports` — but to built `lib/` output rather than to
|
|
* source, which is the artifact-plane leak the explicit aliases exist to avoid.
|
|
* Naming it here turns that into a gate failure instead of a silent difference.
|
|
*
|
|
* @param packages - every workspace package that needs an alias.
|
|
* @param mapped - bare specifiers the config maps.
|
|
* @returns Unmapped package names, in the order given.
|
|
*/
|
|
export function uncoveredPackages(
|
|
packages: readonly string[],
|
|
mapped: ReadonlySet<string>,
|
|
): string[] {
|
|
return packages.filter(name => !mapped.has(name))
|
|
}
|
|
|
|
/**
|
|
* Render the generated region's alias lines.
|
|
* @param aliases - packages to map, in emission order.
|
|
* @param handWritten - specifiers already mapped outside the region; a duplicate key would shadow one silently.
|
|
* @returns The region body, one JSON member per line.
|
|
*/
|
|
export function renderAliases(aliases: readonly PackageAlias[], handWritten: ReadonlySet<string>): string {
|
|
const lines: string[] = []
|
|
for (const alias of aliases) {
|
|
if (!handWritten.has(alias.specifier)) {
|
|
lines.push(` ${JSON.stringify(alias.specifier)}: [${JSON.stringify(alias.source)}]`)
|
|
}
|
|
const invariant = `${alias.specifier}/invariant`
|
|
if (alias.hasInvariant && !handWritten.has(invariant)) {
|
|
lines.push(` ${JSON.stringify(invariant)}: [${JSON.stringify(`${alias.source}/invariant.ts`)}]`)
|
|
}
|
|
}
|
|
// The region closes `paths`, so the last member carries no trailing comma.
|
|
return lines.join(',\n')
|
|
}
|
|
|
|
/**
|
|
* Replace the generated region of a config's text.
|
|
* @param text - current `tsconfig.base.json` contents.
|
|
* @param body - rendered alias lines.
|
|
* @returns The updated contents.
|
|
* @throws When the markers are missing or out of order.
|
|
*/
|
|
export function writeRegion(text: string, body: string): string {
|
|
const begin = text.indexOf(BEGIN)
|
|
const end = text.indexOf(END)
|
|
if (begin < 0 || end < begin) {
|
|
throw new Error(`gen-tsconfig-paths: ${CONFIG} is missing the generated-region markers.`)
|
|
}
|
|
return `${text.slice(0, begin)}${BEGIN}\n${body}\n${END}${text.slice(end + END.length)}`
|
|
}
|
|
|
|
/**
|
|
* Parse the config's `paths` keys, ignoring the generated region.
|
|
* @param text - current `tsconfig.base.json` contents.
|
|
* @returns Specifiers mapped by hand.
|
|
*/
|
|
function handWrittenSpecifiers(text: string): Set<string> {
|
|
const begin = text.indexOf(BEGIN)
|
|
const end = text.indexOf(END)
|
|
const outside = begin < 0 || end < begin ? text : text.slice(0, begin) + text.slice(end)
|
|
const keys = new Set<string>()
|
|
for (const match of outside.matchAll(/^\s*"(@deepseek-ai\/[^"]+)":/gm)) {
|
|
const key = match[1]
|
|
if (key !== undefined) keys.add(key)
|
|
}
|
|
return keys
|
|
}
|
|
|
|
if (process.argv[1] && import.meta.filename === resolve(process.argv[1])) {
|
|
const check = process.argv.includes('--check')
|
|
const current = readFileSync(CONFIG, 'utf8')
|
|
const next = writeRegion(current, renderAliases(collectPackageAliases(), handWrittenSpecifiers(current)))
|
|
const uncovered = uncoveredPackages(collectPackageNames(), mappedSpecifiers(next))
|
|
if (uncovered.length > 0) {
|
|
console.error(
|
|
'gen-tsconfig-paths: no alias maps '
|
|
+ `${uncovered.join(', ')}; add a hand-written entry, because a package named after `
|
|
+ 'something other than its directory cannot be generated.',
|
|
)
|
|
process.exitCode = 1
|
|
} else if (current === next) {
|
|
console.log('gen-tsconfig-paths: tsconfig.base.json package aliases are current.')
|
|
} else if (check) {
|
|
console.error('gen-tsconfig-paths: tsconfig.base.json is stale; run `pnpm run gen-tsconfig-paths`.')
|
|
process.exitCode = 1
|
|
} else {
|
|
writeFileSync(CONFIG, next)
|
|
console.log('gen-tsconfig-paths: rewrote tsconfig.base.json package aliases.')
|
|
}
|
|
}
|