/** * Workspace package invariant checks for package-manager-independent quality * gates. * * Run: `tsx scripts/check-workspace-constraints.ts`. */ import { existsSync, readdirSync, readFileSync } from 'node:fs' import { join, relative, resolve } from 'node:path' const root = resolve(import.meta.dirname, '..') // vendor/* is single-level; packages// nests one level deeper // (the group dirs — core/llm/bash/… — are pure containers with no manifest). const workspaceGlobs = [ { dir: 'vendor', depth: 1 }, { dir: 'packages', depth: 2 }, ] as const const vendoredPackages = new Set([ 'cordis', 'cosmokit', 'schemastery', '@cordisjs/plugin-loader', '@cordisjs/plugin-include', '@cordisjs/plugin-group', '@cordisjs/plugin-timer', '@cordisjs/plugin-hmr', '@cordisjs/plugin-logger-console', ]) const localArtifactDirs = new Set(['node_modules']) /** The subset of package.json fields this constraint check cares about. */ interface PackageManifest { name?: string version?: string private?: boolean type?: string main?: string types?: string bin?: string | Record exports?: Record< string, | string | { types?: string default?: string } | null | undefined > files?: string[] peerDependencies?: Record devDependencies?: Record } /** One workspace manifest and its repo-relative path. */ interface WorkspaceManifest { dir: string manifest: PackageManifest } function readJson(path: string): PackageManifest { return JSON.parse(readFileSync(path, 'utf8')) as PackageManifest } const rootManifest = readJson(join(root, 'package.json')) const repositoryVersion = rootManifest.version /** Repo-relative dirs holding a package.json, walked to the configured depth. */ function packageDirs(base: string, depth: number): string[] { if (depth === 1) { return readdirSync(join(root, base), { withFileTypes: true }) .filter(entry => entry.isDirectory()) .filter(entry => !localArtifactDirs.has(entry.name)) .filter(entry => existsSync(join(root, base, entry.name, 'package.json'))) .map(entry => join(base, entry.name)) } return readdirSync(join(root, base), { withFileTypes: true }) .filter(entry => entry.isDirectory()) .filter(entry => !localArtifactDirs.has(entry.name)) .flatMap(group => packageDirs(join(base, group.name), depth - 1)) } function workspaceManifests(): WorkspaceManifest[] { const manifests: WorkspaceManifest[] = [ { dir: '.', manifest: rootManifest }, ] for (const { dir: base, depth } of workspaceGlobs) { for (const dir of packageDirs(base, depth)) { manifests.push({ dir, manifest: readJson(join(root, dir, 'package.json')) }) } } return manifests } const packageFileExtras: Readonly> = { '@deepseek-ai/dsh-helper': ['lib/assets'], '@deepseek-ai/dsh-tui': ['lib/prompt.js'], '@deepseek-ai/dsh-scripts': [ 'lib/dev/tsdown-config.js', 'lib/local-plugin-loader-hooks.js', 'lib/assets', ], } function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean { return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index]) } function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] { const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : [] return [ 'lib/index.js', // Every package publishes its invariant ownership companion as a separate // bundle; the package-invariant gate validates the companion itself. 'lib/invariant.js', ...manifest.bin ? ['lib/bin.js'] : [], ...manifest.exports?.['./worker'] ? ['lib/worker.cjs'] : [], // UI plugin packages ship their browser bundle beside the node lib // (single-artifact ruling: dist/ retired, ./client resolves lib/client.js). // Keyed on the artifact path, not the subpath name: apiproxy's ./client is // a browser-safe source channel, not a bundle. ...exportDefault(manifest, './client') === './lib/client.js' ? ['lib/client.js'] : [], // runtime's shell-held loader subpath ships as its own bundle beside the client half. ...exportDefault(manifest, './loader') === './lib/loader.js' ? ['lib/loader.js'] : [], // web-react's store subpath ships its own bundle (single-entry builds; no shared chunk). ...exportDefault(manifest, './store') === './lib/store/index.js' ? ['lib/store/index.js'] : [], ...extras, // Subpaths whose runtime default is the tsc-emitted tree (lib/types/*.js — // browser-safe source channels rehomed off src so plain Node can import // them without type stripping) publish the emitted JS alongside the // declarations. ...usesEmittedTreeDefaults(manifest) ? ['lib/types/**/*.js'] : [], 'lib/types/**/*.d.ts', 'lib/types/**/*.d.ts.map', 'src', ] } /** Runtime target of an export entry: conditional `default`, or the bare-string shorthand. */ function exportDefault(manifest: PackageManifest, subpath: string): string | undefined { const entry = manifest.exports?.[subpath] if (typeof entry === 'string') return entry if (typeof entry === 'object' && entry !== null) return entry.default return undefined } /** Whether any export's runtime default points into the tsc-emitted lib/types tree. */ function usesEmittedTreeDefaults(manifest: PackageManifest): boolean { return Object.keys(manifest.exports ?? {}).some(subpath => exportDefault(manifest, subpath)?.startsWith('./lib/types/') === true) } function checkWorkspace({ dir, manifest }: WorkspaceManifest): string[] { const errors: string[] = [] const label = manifest.name ?? dir if (manifest.private !== true) { errors.push(`${label}: package.json must set "private": true`) } if (manifest.name && vendoredPackages.has(manifest.name)) { return errors } if (manifest.name?.startsWith('@deepseek-ai/dsh-') && manifest.name !== '@deepseek-ai/dsh-root') { const peer = manifest.peerDependencies?.cordis const dev = manifest.devDependencies?.cordis if (!peer) errors.push(`${label}: cordis must be a peerDependency`) if (!dev) errors.push(`${label}: cordis must also be a devDependency`) if (peer && dev && peer !== dev) { errors.push(`${label}: cordis peer (${peer}) and dev (${dev}) ranges must match`) } if (manifest.version !== repositoryVersion) { errors.push(`${label}: package.json version must match root version ${repositoryVersion ?? '(missing)'}`) } if (manifest.type !== 'module') { errors.push(`${label}: package.json must set "type": "module"`) } if (manifest.main !== 'lib/index.js') { errors.push(`${label}: package.json must set "main": "lib/index.js"`) } if (manifest.types !== 'lib/types/index.d.ts') { errors.push(`${label}: package.json must set "types": "lib/types/index.d.ts"`) } const rootExport = manifest.exports?.['.'] const rootEntry = typeof rootExport === 'object' && rootExport !== null ? rootExport : undefined if (rootEntry?.types !== './lib/types/index.d.ts') { errors.push(`${label}: package.json exports["."].types must be "./lib/types/index.d.ts"`) } if (rootEntry?.default !== './lib/index.js') { errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`) } const invariantRaw = manifest.exports?.['./invariant'] const invariantExport = typeof invariantRaw === 'object' && invariantRaw !== null ? invariantRaw : undefined if (invariantExport?.types !== undefined && invariantExport.types !== './lib/types/invariant.d.ts') { errors.push(`${label}: package.json exports["./invariant"].types must be "./lib/types/invariant.d.ts"`) } if (invariantExport?.default !== undefined && invariantExport.default !== './lib/invariant.js') { errors.push(`${label}: package.json exports["./invariant"].default must be "./lib/invariant.js"`) } if (invariantExport && (invariantExport.types === undefined || invariantExport.default === undefined)) { errors.push(`${label}: package.json exports["./invariant"] must declare both types and default targets`) } const expectedFiles = expectedDshPackageFiles(manifest) if (!sameStringList(manifest.files, expectedFiles)) { errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`) } } return errors.map(error => `${relative(root, join(root, dir, 'package.json'))}: ${error}`) } /** * Enforce `packages//`: groups are open-named containers without a * package.json, and packages may be neither flat nor more deeply nested. */ function checkHierarchyShape(): string[] { const errors: string[] = [] const packagesRoot = join(root, 'packages') for (const group of readdirSync(packagesRoot, { withFileTypes: true })) { if (!group.isDirectory()) continue const groupRel = join('packages', group.name) if (existsSync(join(packagesRoot, group.name, 'package.json'))) { errors.push(`${groupRel}: a group dir must not contain a package.json — packages live at packages//, not directly under packages/`) continue } for (const pkg of readdirSync(join(packagesRoot, group.name), { withFileTypes: true })) { if (!pkg.isDirectory()) continue if (localArtifactDirs.has(pkg.name)) continue const pkgRel = join(groupRel, pkg.name) if (!existsSync(join(packagesRoot, group.name, pkg.name, 'package.json'))) { errors.push(`${pkgRel}: expected a package here (no package.json found) — the hierarchy is exactly packages//, no deeper nesting`) } } } return errors } function checkRepositoryVersion(): string[] { if (repositoryVersion && /^\d+\.\d+\.\d+$/.test(repositoryVersion)) return [] return ['package.json: version must be stable X.Y.Z'] } const errors = [ ...checkRepositoryVersion(), ...workspaceManifests().flatMap(checkWorkspace), ...checkHierarchyShape(), ] if (errors.length > 0) { console.error(errors.join('\n')) process.exitCode = 1 }