mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
fix(client): harden bootstrap invariants
This commit is contained in:
@@ -14,9 +14,11 @@ const CLIENT_MANIFEST_GLOB = 'packages/client/*/package.json'
|
||||
const MANIFEST_GLOBS = ['packages/*/*/package.json', 'apps/*/package.json', 'vendor/*/package.json']
|
||||
const CONFIG_GLOB = 'packages/*/*/tsdown.config.ts'
|
||||
const PLATFORM_SOURCE = 'packages/client/web/src/platform.ts'
|
||||
const PARSER_PRELOAD_SOURCE = 'packages/client/modules/src/index.ts'
|
||||
const STATIC_PRESET_SOURCE = 'packages/client/tsdown.client.ts'
|
||||
const CORDIS = '@deepseek-ai/cordis'
|
||||
const DSH_PREFIX = '@deepseek-ai/dsh-'
|
||||
const CLIENT_WEB = '@deepseek-ai/dsh-client-web'
|
||||
|
||||
/** One workspace package's browser-module declaration. */
|
||||
export interface ClientDeclaration {
|
||||
@@ -38,6 +40,8 @@ export interface ClientPackage extends ClientDeclaration {
|
||||
readonly staticLinked: boolean
|
||||
/** Production source locations grouped by imported package name. */
|
||||
readonly sourceUses: Readonly<Record<string, readonly string[]>>
|
||||
/** Production source locations grouped by runtime-imported package name. */
|
||||
readonly runtimeSourceUses: Readonly<Record<string, readonly string[]>>
|
||||
/** Installed implementation dependencies. */
|
||||
readonly dependencies: Readonly<Record<string, string>>
|
||||
/** Consumer-supplied dependencies. */
|
||||
@@ -58,6 +62,8 @@ export interface ClientPackageFacts {
|
||||
readonly platformModules: readonly string[]
|
||||
/** Dynamic factories the HTML parser loads before shell boot. */
|
||||
readonly preloadedExternals: readonly string[]
|
||||
/** Package rows whose bundles the HTML parser executes before shell boot. */
|
||||
readonly parserPreloadIds: readonly string[]
|
||||
/** Manifest field errors found while reading declarations. */
|
||||
readonly malformed: readonly string[]
|
||||
}
|
||||
@@ -78,10 +84,38 @@ export interface ClientDeclarations {
|
||||
*/
|
||||
export function collectSourcePackageUses(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFilePackageUses(sourceFile)
|
||||
return collectSourceFilePackageUses(sourceFile, false)
|
||||
}
|
||||
|
||||
function collectSourceFilePackageUses(sourceFile: ts.SourceFile): Set<string> {
|
||||
/**
|
||||
* Collect bare packages whose values one production source file reaches at runtime.
|
||||
* @param path - File path used to select TypeScript's parser mode.
|
||||
* @param source - Source text to inspect.
|
||||
* @returns Bare package names retained by runtime imports, exports, requires, or JSX.
|
||||
*/
|
||||
export function collectRuntimeSourcePackageUses(path: string, source: string): Set<string> {
|
||||
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
|
||||
return collectSourceFilePackageUses(sourceFile, true)
|
||||
}
|
||||
|
||||
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
|
||||
const clause = node.importClause
|
||||
if (clause === undefined) return true
|
||||
if (clause.phaseModifier === ts.SyntaxKind.TypeKeyword) return false
|
||||
if (clause.name !== undefined) return true
|
||||
const bindings = clause.namedBindings
|
||||
if (bindings === undefined || ts.isNamespaceImport(bindings)) return true
|
||||
return bindings.elements.length === 0 || bindings.elements.some(element => !element.isTypeOnly)
|
||||
}
|
||||
|
||||
function exportCarriesRuntimeValue(node: ts.ExportDeclaration): boolean {
|
||||
if (node.isTypeOnly) return false
|
||||
const clause = node.exportClause
|
||||
if (clause === undefined || ts.isNamespaceExport(clause)) return true
|
||||
return clause.elements.length === 0 || clause.elements.some(element => !element.isTypeOnly)
|
||||
}
|
||||
|
||||
function collectSourceFilePackageUses(sourceFile: ts.SourceFile, runtimeOnly: boolean): Set<string> {
|
||||
const uses = new Set<string>()
|
||||
|
||||
const add = (specifier: ts.Expression | undefined): void => {
|
||||
@@ -89,17 +123,19 @@ function collectSourceFilePackageUses(sourceFile: ts.SourceFile): Set<string> {
|
||||
uses.add(packageNameOf(specifier.text))
|
||||
}
|
||||
const visit = (node: ts.Node): void => {
|
||||
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
|
||||
add(node.moduleSpecifier)
|
||||
if (ts.isImportDeclaration(node)) {
|
||||
if (!runtimeOnly || importCarriesRuntimeValue(node)) add(node.moduleSpecifier)
|
||||
} else if (ts.isExportDeclaration(node)) {
|
||||
if (!runtimeOnly || exportCarriesRuntimeValue(node)) add(node.moduleSpecifier)
|
||||
} else if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)) {
|
||||
add(node.moduleReference.expression)
|
||||
} else if (ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
||||
if (!runtimeOnly || !node.isTypeOnly) add(node.moduleReference.expression)
|
||||
} else if (!runtimeOnly && ts.isImportTypeNode(node) && ts.isLiteralTypeNode(node.argument)) {
|
||||
add(node.argument.literal)
|
||||
} else if (ts.isCallExpression(node)
|
||||
&& (node.expression.kind === ts.SyntaxKind.ImportKeyword
|
||||
|| ts.isIdentifier(node.expression) && node.expression.text === 'require')) {
|
||||
add(node.arguments[0])
|
||||
} else if (ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
} else if (!runtimeOnly && ts.isModuleDeclaration(node) && ts.isStringLiteral(node.name)) {
|
||||
add(node.name)
|
||||
} else if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node) || ts.isJsxFragment(node)) {
|
||||
uses.add('react')
|
||||
@@ -193,9 +229,11 @@ export function fixClientPackageManifests(root: string, facts: ClientPackageFact
|
||||
for (const [name, rule] of expected) {
|
||||
const range = preferredRange(target.manifest, name, rule.kind, inferredRanges)
|
||||
if (range === undefined) continue
|
||||
target.changed = rule.kind === 'dev'
|
||||
? ensureDevOnly(target.manifest, name, range) || target.changed
|
||||
: ensurePeerDev(target.manifest, name, range) || target.changed
|
||||
target.changed = rule.kind === 'dependency'
|
||||
? ensureDependencyOnly(target.manifest, name, range) || target.changed
|
||||
: rule.kind === 'dev'
|
||||
? ensureDevOnly(target.manifest, name, range) || target.changed
|
||||
: ensurePeerDev(target.manifest, name, range) || target.changed
|
||||
}
|
||||
|
||||
if (pkg.dynamic) {
|
||||
@@ -263,6 +301,12 @@ function ensureDevOnly(manifest: Manifest, name: string, range: string): boolean
|
||||
return setDependency(manifest, 'devDependencies', name, range) || changed
|
||||
}
|
||||
|
||||
function ensureDependencyOnly(manifest: Manifest, name: string, range: string): boolean {
|
||||
let changed = deleteDependency(manifest, 'peerDependencies', name)
|
||||
changed = deleteDependency(manifest, 'devDependencies', name) || changed
|
||||
return setDependency(manifest, 'dependencies', name, range) || changed
|
||||
}
|
||||
|
||||
function ensurePeerDev(manifest: Manifest, name: string, range: string): boolean {
|
||||
let changed = deleteDependency(manifest, 'dependencies', name)
|
||||
changed = setDependency(manifest, 'peerDependencies', name, range) || changed
|
||||
@@ -301,9 +345,11 @@ function preferredRange(
|
||||
kind: ExpectedRule['kind'],
|
||||
inferred: ReadonlyMap<string, ReadonlySet<string>>,
|
||||
): string | undefined {
|
||||
const order: readonly DependencySection[] = kind === 'dev'
|
||||
? ['devDependencies', 'peerDependencies', 'dependencies']
|
||||
: ['peerDependencies', 'devDependencies', 'dependencies']
|
||||
const order: readonly DependencySection[] = kind === 'dependency'
|
||||
? ['dependencies', 'devDependencies', 'peerDependencies']
|
||||
: kind === 'dev'
|
||||
? ['devDependencies', 'peerDependencies', 'dependencies']
|
||||
: ['peerDependencies', 'devDependencies', 'dependencies']
|
||||
for (const field of order) {
|
||||
const range = section(manifest, field)[name]
|
||||
if (range !== undefined) return range
|
||||
@@ -373,17 +419,24 @@ function collectModeViolations(facts: ClientPackageFacts): string[] {
|
||||
|
||||
const rows = rowNames(facts.declarations)
|
||||
for (const specifier of facts.preloadedExternals) {
|
||||
if (rowPackageOf(specifier, rows) !== undefined) continue
|
||||
violations.push(
|
||||
PLATFORM_SOURCE + ': parser-preloaded external ' + JSON.stringify(specifier)
|
||||
+ ' has no dynamic dsh.client row',
|
||||
)
|
||||
if (rowPackageOf(specifier, rows) === undefined) {
|
||||
violations.push(
|
||||
PLATFORM_SOURCE + ': parser-preloaded external ' + JSON.stringify(specifier)
|
||||
+ ' has no dynamic dsh.client row',
|
||||
)
|
||||
}
|
||||
if (!facts.parserPreloadIds.includes(stripClientSuffix(specifier))) {
|
||||
violations.push(
|
||||
PLATFORM_SOURCE + ': parser-preloaded external ' + JSON.stringify(specifier)
|
||||
+ ' has no matching PARSER_PRELOAD_IDS row in ' + PARSER_PRELOAD_SOURCE,
|
||||
)
|
||||
}
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
interface ExpectedRule {
|
||||
readonly kind: 'dev' | 'peer-dev'
|
||||
readonly kind: 'dependency' | 'dev' | 'peer-dev'
|
||||
readonly origins: Set<string>
|
||||
}
|
||||
|
||||
@@ -399,6 +452,15 @@ function collectDependencyViolations(facts: ClientPackageFacts): string[] {
|
||||
const expected = expectedSections(pkg, staticInputs)
|
||||
for (const [name, rule] of [...expected].sort(([left], [right]) => left.localeCompare(right))) {
|
||||
const actual = declaredSections(pkg, name)
|
||||
if (rule.kind === 'dependency') {
|
||||
if (actual.length === 1 && actual[0] === 'dependencies') continue
|
||||
violations.push(
|
||||
pkg.manifest + ': ' + name + ' (' + describeOrigins(rule.origins) + ') is a runtime import'
|
||||
+ ' retained by a statically linked artifact; declare it only in dependencies, found '
|
||||
+ describeSections(actual),
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (rule.kind === 'dev') {
|
||||
if (actual.length === 1 && actual[0] === 'devDependencies') continue
|
||||
violations.push(
|
||||
@@ -457,7 +519,14 @@ function expectedSections(pkg: ClientPackage, staticInputs: ReadonlySet<string>)
|
||||
const expected = new Map<string, ExpectedRule>([
|
||||
[CORDIS, { kind: 'peer-dev', origins: new Set(['client package baseline']) }],
|
||||
])
|
||||
if (!pkg.dynamic) return expected
|
||||
if (!pkg.dynamic) {
|
||||
if (pkg.name === CLIENT_WEB) return expected
|
||||
for (const [name, locations] of Object.entries(pkg.runtimeSourceUses)) {
|
||||
if (name === pkg.name || name === CORDIS || isInternalDsh(name)) continue
|
||||
expected.set(name, { kind: 'dependency', origins: new Set(locations) })
|
||||
}
|
||||
return expected
|
||||
}
|
||||
|
||||
const add = (name: string, origin: string): void => {
|
||||
if (name === pkg.name) return
|
||||
@@ -483,7 +552,6 @@ interface ModuleEdge {
|
||||
function collectModuleViolations(facts: ClientPackageFacts): string[] {
|
||||
const violations: string[] = []
|
||||
const baseline = new Set([...facts.platformModules, ...facts.preloadedExternals])
|
||||
const staticModules = new Set(facts.platformModules)
|
||||
const rows = rowNames(facts.declarations)
|
||||
const byName = new Map(facts.declarations.map(entry => [entry.name, entry]))
|
||||
const edges: ModuleEdge[] = []
|
||||
@@ -509,7 +577,6 @@ function collectModuleViolations(facts: ClientPackageFacts): string[] {
|
||||
)
|
||||
continue
|
||||
}
|
||||
if (staticModules.has(specifier)) continue
|
||||
const supplier = rowPackageOf(specifier, rows)
|
||||
if (supplier === pkg.name) {
|
||||
violations.push(pkg.manifest + ': dsh.client.external names its own row ' + JSON.stringify(specifier))
|
||||
@@ -657,28 +724,43 @@ async function readStaticLinkedRoster(root: string): Promise<Set<string>> {
|
||||
return roster
|
||||
}
|
||||
|
||||
function readStringLiteralArray(root: string, name: string): string[] {
|
||||
const path = resolve(root, PLATFORM_SOURCE)
|
||||
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
||||
let current = expression
|
||||
while (ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isParenthesizedExpression(current)) {
|
||||
current = current.expression
|
||||
}
|
||||
return current
|
||||
}
|
||||
|
||||
function readStringLiteralArray(root: string, sourcePath: string, name: string): string[] {
|
||||
const path = resolve(root, sourcePath)
|
||||
const source = ts.createSourceFile(path, readFileSync(path, 'utf8'), ts.ScriptTarget.Latest, false, ts.ScriptKind.TS)
|
||||
const constants = new Map<string, string>()
|
||||
for (const statement of source.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(declaration.name) || declaration.initializer === undefined) continue
|
||||
const initializer = unwrapExpression(declaration.initializer)
|
||||
if (ts.isStringLiteral(initializer)) constants.set(declaration.name.text, initializer.text)
|
||||
}
|
||||
}
|
||||
for (const statement of source.statements) {
|
||||
if (!ts.isVariableStatement(statement)) continue
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== name) continue
|
||||
const expression = declaration.initializer !== undefined && ts.isAsExpression(declaration.initializer)
|
||||
? declaration.initializer.expression
|
||||
: declaration.initializer
|
||||
const expression = declaration.initializer === undefined ? undefined : unwrapExpression(declaration.initializer)
|
||||
if (expression === undefined || !ts.isArrayLiteralExpression(expression)) {
|
||||
throw new Error(GATE + ': ' + name + ' in ' + PLATFORM_SOURCE + ' must be an array literal')
|
||||
throw new Error(GATE + ': ' + name + ' in ' + sourcePath + ' must be an array literal')
|
||||
}
|
||||
return expression.elements.map((element) => {
|
||||
if (!ts.isStringLiteral(element)) {
|
||||
throw new Error(GATE + ': ' + name + ' in ' + PLATFORM_SOURCE + ' must contain only string literals')
|
||||
}
|
||||
return element.text
|
||||
const value = unwrapExpression(element)
|
||||
if (ts.isStringLiteral(value)) return value.text
|
||||
if (ts.isIdentifier(value) && constants.has(value.text)) return constants.get(value.text) as string
|
||||
throw new Error(GATE + ': ' + name + ' in ' + sourcePath + ' must contain only string constants')
|
||||
})
|
||||
}
|
||||
}
|
||||
throw new Error(GATE + ': ' + PLATFORM_SOURCE + ' declares no ' + name)
|
||||
throw new Error(GATE + ': ' + sourcePath + ' declares no ' + name)
|
||||
}
|
||||
|
||||
async function readFacts(root: string): Promise<ClientPackageFacts> {
|
||||
@@ -694,17 +776,23 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8')) as Manifest
|
||||
if (typeof manifest.name !== 'string') throw new Error(GATE + ': ' + manifestPath + ' has no package name')
|
||||
const sourceUses = new Map<string, Set<string>>()
|
||||
const runtimeSourceUses = new Map<string, Set<string>>()
|
||||
const packageDirectory = dirname(manifestPath)
|
||||
const sourcePrefix = packageDirectory + '/src/'
|
||||
for (const sourceFile of project.sourceFiles()) {
|
||||
if (sourceFile.isDeclarationFile) continue
|
||||
const file = project.relativePath(sourceFile)
|
||||
if (!file.startsWith(sourcePrefix)) continue
|
||||
for (const name of collectSourceFilePackageUses(sourceFile)) {
|
||||
for (const name of collectSourceFilePackageUses(sourceFile, false)) {
|
||||
const locations = sourceUses.get(name) ?? new Set<string>()
|
||||
locations.add(file)
|
||||
sourceUses.set(name, locations)
|
||||
}
|
||||
for (const name of collectSourceFilePackageUses(sourceFile, true)) {
|
||||
const locations = runtimeSourceUses.get(name) ?? new Set<string>()
|
||||
locations.add(file)
|
||||
runtimeSourceUses.set(name, locations)
|
||||
}
|
||||
}
|
||||
packages.push({
|
||||
...declaration,
|
||||
@@ -713,6 +801,10 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
|
||||
[...sourceUses].sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([name, locations]) => [name, [...locations].sort()]),
|
||||
),
|
||||
runtimeSourceUses: Object.fromEntries(
|
||||
[...runtimeSourceUses].sort(([left], [right]) => left.localeCompare(right))
|
||||
.map(([name, locations]) => [name, [...locations].sort()]),
|
||||
),
|
||||
dependencies: manifest.dependencies ?? {},
|
||||
peerDependencies: manifest.peerDependencies ?? {},
|
||||
devDependencies: manifest.devDependencies ?? {},
|
||||
@@ -723,8 +815,9 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
|
||||
packages,
|
||||
declarations,
|
||||
staticLinkedPackages,
|
||||
platformModules: readStringLiteralArray(root, 'PLATFORM_MODULES'),
|
||||
preloadedExternals: readStringLiteralArray(root, 'PRELOADED_CLIENT_EXTERNALS'),
|
||||
platformModules: readStringLiteralArray(root, PLATFORM_SOURCE, 'PLATFORM_MODULES'),
|
||||
preloadedExternals: readStringLiteralArray(root, PLATFORM_SOURCE, 'PRELOADED_CLIENT_EXTERNALS'),
|
||||
parserPreloadIds: readStringLiteralArray(root, PARSER_PRELOAD_SOURCE, 'PARSER_PRELOAD_IDS'),
|
||||
malformed,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user