2026-06-16 14:55:37 +08:00
|
|
|
/**
|
|
|
|
|
* Workspace package invariant checks for package-manager-independent quality
|
|
|
|
|
* gates.
|
|
|
|
|
*
|
|
|
|
|
* Run: `tsx scripts/check-workspace-constraints.ts`.
|
|
|
|
|
*/
|
|
|
|
|
|
2026-06-20 23:12:14 +08:00
|
|
|
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
2026-06-16 14:55:37 +08:00
|
|
|
import { join, relative, resolve } from 'node:path'
|
|
|
|
|
|
|
|
|
|
const root = resolve(import.meta.dirname, '..')
|
Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.
Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.
Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.
WIP: doc cross-links and package/RFC docs still to update.
2026-06-20 22:55:20 +08:00
|
|
|
// vendor/* is single-level; packages/<group>/<pkg> 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
|
2026-06-16 14:55:37 +08:00
|
|
|
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',
|
|
|
|
|
])
|
|
|
|
|
|
2026-07-04 00:57:35 +08:00
|
|
|
const localArtifactDirs = new Set(['node_modules'])
|
|
|
|
|
|
2026-06-16 14:55:37 +08:00
|
|
|
/** The subset of package.json fields this constraint check cares about. */
|
|
|
|
|
interface PackageManifest {
|
|
|
|
|
name?: string
|
|
|
|
|
version?: string
|
|
|
|
|
private?: boolean
|
|
|
|
|
type?: string
|
2026-06-20 00:21:57 +08:00
|
|
|
main?: string
|
|
|
|
|
types?: string
|
2026-06-22 01:01:08 +08:00
|
|
|
bin?: string | Record<string, string>
|
|
|
|
|
exports?: Record<
|
|
|
|
|
string,
|
|
|
|
|
| {
|
2026-06-20 00:21:57 +08:00
|
|
|
types?: string
|
|
|
|
|
default?: string
|
|
|
|
|
}
|
2026-06-22 01:01:08 +08:00
|
|
|
| undefined
|
|
|
|
|
>
|
2026-06-20 00:21:57 +08:00
|
|
|
files?: string[]
|
2026-06-16 14:55:37 +08:00
|
|
|
peerDependencies?: Record<string, string>
|
|
|
|
|
devDependencies?: Record<string, string>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** 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
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 17:49:01 +08:00
|
|
|
const rootManifest = readJson(join(root, 'package.json'))
|
|
|
|
|
const repositoryVersion = rootManifest.version
|
|
|
|
|
|
Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.
Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.
Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.
WIP: doc cross-links and package/RFC docs still to update.
2026-06-20 22:55:20 +08:00
|
|
|
/** 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())
|
2026-07-04 00:57:35 +08:00
|
|
|
.filter(entry => !localArtifactDirs.has(entry.name))
|
|
|
|
|
.filter(entry => existsSync(join(root, base, entry.name, 'package.json')))
|
Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.
Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.
Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.
WIP: doc cross-links and package/RFC docs still to update.
2026-06-20 22:55:20 +08:00
|
|
|
.map(entry => join(base, entry.name))
|
|
|
|
|
}
|
|
|
|
|
return readdirSync(join(root, base), { withFileTypes: true })
|
|
|
|
|
.filter(entry => entry.isDirectory())
|
2026-07-04 00:57:35 +08:00
|
|
|
.filter(entry => !localArtifactDirs.has(entry.name))
|
Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.
Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.
Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.
WIP: doc cross-links and package/RFC docs still to update.
2026-06-20 22:55:20 +08:00
|
|
|
.flatMap(group => packageDirs(join(base, group.name), depth - 1))
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:55:37 +08:00
|
|
|
function workspaceManifests(): WorkspaceManifest[] {
|
|
|
|
|
const manifests: WorkspaceManifest[] = [
|
2026-07-13 17:49:01 +08:00
|
|
|
{ dir: '.', manifest: rootManifest },
|
2026-06-16 14:55:37 +08:00
|
|
|
]
|
|
|
|
|
|
Reorganize packages into a modular hierarchy
Move the 18 flat packages/<name> packages into role-grouped dirs:
core/, llm/, bash/, session-persistence/, ui/, support/. Group dirs are
pure containers; each package keeps its @deepseek-ai/dsh-* name.
Collapse the per-package tsconfig paths maps (base + typecheck) into one
@deepseek-ai/dsh-* wildcard with a candidate per group, and derive the
publint list from the hierarchy. Update all depth-coupled globs/configs
(workspace, tsdown, vitest, eslint, knip, tsconfig includes/refs,
per-package tsconfigs, generators, doc-script scopes, type-equiv manifest)
and the cross-package/script relative imports in tests.
Fix doc-typecheck's workspacePaths() to parse tsconfig JSONC via the
TypeScript API instead of a regex comment-strip, which corrupted the
new wildcard `/*/` path candidates.
WIP: doc cross-links and package/RFC docs still to update.
2026-06-20 22:55:20 +08:00
|
|
|
for (const { dir: base, depth } of workspaceGlobs) {
|
|
|
|
|
for (const dir of packageDirs(base, depth)) {
|
2026-06-16 14:55:37 +08:00
|
|
|
manifests.push({ dir, manifest: readJson(join(root, dir, 'package.json')) })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return manifests
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 00:21:57 +08:00
|
|
|
const dshPackageFiles = [
|
|
|
|
|
'lib/index.js',
|
2026-06-20 00:26:02 +08:00
|
|
|
'lib/types/**/*.d.ts',
|
|
|
|
|
'lib/types/**/*.d.ts.map',
|
2026-06-20 00:21:57 +08:00
|
|
|
'src',
|
|
|
|
|
] as const
|
|
|
|
|
|
2026-06-22 01:01:08 +08:00
|
|
|
const dshBinPackageFiles = [
|
|
|
|
|
'lib/index.js',
|
|
|
|
|
'lib/bin.js',
|
|
|
|
|
'lib/types/**/*.d.ts',
|
|
|
|
|
'lib/types/**/*.d.ts.map',
|
|
|
|
|
'src',
|
|
|
|
|
] as const
|
|
|
|
|
|
workflow: swap the engine's internals to node:worker_threads
In-place port of dsh-workflow-vm from the in-process node:vm execution
to one worker thread per run (the workflow-workerthread engine of
PR #215, adopted as THE engine): the script's vm context moves inside
the worker, agent() bridges to ctx.subagents over the message port
(host.ts/protocol.ts/session.ts/worker.ts are new; runtime.ts loses the
abandon channel — the host's grace timer force-settles and TERMINATES
instead), start() pre-parses the body host-side to keep the seam's
synchronous SCRIPT_PARSE throw, and a ready→go handshake keeps a run
cancelled before start from ever executing the body. start() no longer
blocks the host, termination is real, and the value boundary is
serialization by construction. The package keeps its name until the
follow-up rename commit; scripts see the identical hook surface, and
the seam-contract tests hardened ahead of this swap pass unchanged.
The run and child-RPC surfaces are class-shaped rather than literal
bundles: WorkerRun IMPLEMENTS the seam's WorkflowRun (id/meta are its
own clone, separate from event payloads') and start() returns the
instance directly — interface parity with the seam is compiler-checked;
worker-side, ChildRpcBridge (implements ChildPort; callId allocation +
pending book-keeping settled by onChild* entry points) and
RpcChildHandle (every member an RPC keyed by its callId) carry names in
stacks. ChildPort's method is startAgent — it names what it starts,
matching the script-side agent() hook and the agentsStarted /
workflow/agent-* vocabulary; the Child* type names deliberately stay
(the worker side is cordis- and subagent-free; these are reduced JSON
projections, not the seam's types).
Review findings from the reference PR are folded in rather than
re-introduced:
- cancel() drives BOTH child-cancel channels host-side: the request
signal aborts AND each registered child's explicit cancel() is
called — a worker wedged in a synchronous spin cannot relay its own
ChildCancel RPCs (regression: cancel-only provider + wedged worker).
- All host warn paths render through the total renderThrown; a child
dispose() rejecting a value whose coercion throws still acks
ChildDisposed instead of wedging the script's finally (regression).
- built-worker.e2e.ts is wired into builtBinSmokeGate and the AGENTS.md
CI sequence — the built lib/worker.js resolution contract now runs in
an automated gate.
- workflow/end payload pinned on the worker-death path (with the
cancelled and grace-force-settle pins riding the ported spec).
- Real-Worker scripted timing budgets widened (50-300ms → 150-1000ms)
for starved CI hosts.
Workspace plumbing: the "./worker" subpath export sanctions the second
runtime bundle (check-workspace-constraints), tsdown builds two
single-entry passes, tsx becomes a devDependency for the unbuilt worker
spawn.
2026-07-09 18:39:31 +08:00
|
|
|
const dshWorkerPackageFiles = [
|
|
|
|
|
'lib/index.js',
|
2026-07-13 20:55:47 +08:00
|
|
|
'lib/worker.cjs',
|
workflow: swap the engine's internals to node:worker_threads
In-place port of dsh-workflow-vm from the in-process node:vm execution
to one worker thread per run (the workflow-workerthread engine of
PR #215, adopted as THE engine): the script's vm context moves inside
the worker, agent() bridges to ctx.subagents over the message port
(host.ts/protocol.ts/session.ts/worker.ts are new; runtime.ts loses the
abandon channel — the host's grace timer force-settles and TERMINATES
instead), start() pre-parses the body host-side to keep the seam's
synchronous SCRIPT_PARSE throw, and a ready→go handshake keeps a run
cancelled before start from ever executing the body. start() no longer
blocks the host, termination is real, and the value boundary is
serialization by construction. The package keeps its name until the
follow-up rename commit; scripts see the identical hook surface, and
the seam-contract tests hardened ahead of this swap pass unchanged.
The run and child-RPC surfaces are class-shaped rather than literal
bundles: WorkerRun IMPLEMENTS the seam's WorkflowRun (id/meta are its
own clone, separate from event payloads') and start() returns the
instance directly — interface parity with the seam is compiler-checked;
worker-side, ChildRpcBridge (implements ChildPort; callId allocation +
pending book-keeping settled by onChild* entry points) and
RpcChildHandle (every member an RPC keyed by its callId) carry names in
stacks. ChildPort's method is startAgent — it names what it starts,
matching the script-side agent() hook and the agentsStarted /
workflow/agent-* vocabulary; the Child* type names deliberately stay
(the worker side is cordis- and subagent-free; these are reduced JSON
projections, not the seam's types).
Review findings from the reference PR are folded in rather than
re-introduced:
- cancel() drives BOTH child-cancel channels host-side: the request
signal aborts AND each registered child's explicit cancel() is
called — a worker wedged in a synchronous spin cannot relay its own
ChildCancel RPCs (regression: cancel-only provider + wedged worker).
- All host warn paths render through the total renderThrown; a child
dispose() rejecting a value whose coercion throws still acks
ChildDisposed instead of wedging the script's finally (regression).
- built-worker.e2e.ts is wired into builtBinSmokeGate and the AGENTS.md
CI sequence — the built lib/worker.js resolution contract now runs in
an automated gate.
- workflow/end payload pinned on the worker-death path (with the
cancelled and grace-force-settle pins riding the ported spec).
- Real-Worker scripted timing budgets widened (50-300ms → 150-1000ms)
for starved CI hosts.
Workspace plumbing: the "./worker" subpath export sanctions the second
runtime bundle (check-workspace-constraints), tsdown builds two
single-entry passes, tsx becomes a devDependency for the unbuilt worker
spawn.
2026-07-09 18:39:31 +08:00
|
|
|
'lib/types/**/*.d.ts',
|
|
|
|
|
'lib/types/**/*.d.ts.map',
|
|
|
|
|
'src',
|
|
|
|
|
] as const
|
|
|
|
|
|
2026-07-15 18:17:38 +08:00
|
|
|
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
|
|
|
|
'@deepseek-ai/dsh-helper': ['lib/assets'],
|
|
|
|
|
'@deepseek-ai/dsh-scripts': [
|
|
|
|
|
'lib/dev/tsdown-config.js',
|
|
|
|
|
'lib/local-plugin-loader-hooks.js',
|
|
|
|
|
'lib/assets',
|
|
|
|
|
],
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 00:21:57 +08:00
|
|
|
function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean {
|
|
|
|
|
return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-22 01:01:08 +08:00
|
|
|
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
2026-07-15 18:17:38 +08:00
|
|
|
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
|
|
|
|
|
if (extras.length > 0) {
|
|
|
|
|
return [
|
|
|
|
|
'lib/index.js',
|
|
|
|
|
...manifest.bin ? ['lib/bin.js'] : [],
|
|
|
|
|
...extras,
|
|
|
|
|
'lib/types/**/*.d.ts',
|
|
|
|
|
'lib/types/**/*.d.ts.map',
|
|
|
|
|
'src',
|
|
|
|
|
]
|
|
|
|
|
}
|
workflow: swap the engine's internals to node:worker_threads
In-place port of dsh-workflow-vm from the in-process node:vm execution
to one worker thread per run (the workflow-workerthread engine of
PR #215, adopted as THE engine): the script's vm context moves inside
the worker, agent() bridges to ctx.subagents over the message port
(host.ts/protocol.ts/session.ts/worker.ts are new; runtime.ts loses the
abandon channel — the host's grace timer force-settles and TERMINATES
instead), start() pre-parses the body host-side to keep the seam's
synchronous SCRIPT_PARSE throw, and a ready→go handshake keeps a run
cancelled before start from ever executing the body. start() no longer
blocks the host, termination is real, and the value boundary is
serialization by construction. The package keeps its name until the
follow-up rename commit; scripts see the identical hook surface, and
the seam-contract tests hardened ahead of this swap pass unchanged.
The run and child-RPC surfaces are class-shaped rather than literal
bundles: WorkerRun IMPLEMENTS the seam's WorkflowRun (id/meta are its
own clone, separate from event payloads') and start() returns the
instance directly — interface parity with the seam is compiler-checked;
worker-side, ChildRpcBridge (implements ChildPort; callId allocation +
pending book-keeping settled by onChild* entry points) and
RpcChildHandle (every member an RPC keyed by its callId) carry names in
stacks. ChildPort's method is startAgent — it names what it starts,
matching the script-side agent() hook and the agentsStarted /
workflow/agent-* vocabulary; the Child* type names deliberately stay
(the worker side is cordis- and subagent-free; these are reduced JSON
projections, not the seam's types).
Review findings from the reference PR are folded in rather than
re-introduced:
- cancel() drives BOTH child-cancel channels host-side: the request
signal aborts AND each registered child's explicit cancel() is
called — a worker wedged in a synchronous spin cannot relay its own
ChildCancel RPCs (regression: cancel-only provider + wedged worker).
- All host warn paths render through the total renderThrown; a child
dispose() rejecting a value whose coercion throws still acks
ChildDisposed instead of wedging the script's finally (regression).
- built-worker.e2e.ts is wired into builtBinSmokeGate and the AGENTS.md
CI sequence — the built lib/worker.js resolution contract now runs in
an automated gate.
- workflow/end payload pinned on the worker-death path (with the
cancelled and grace-force-settle pins riding the ported spec).
- Real-Worker scripted timing budgets widened (50-300ms → 150-1000ms)
for starved CI hosts.
Workspace plumbing: the "./worker" subpath export sanctions the second
runtime bundle (check-workspace-constraints), tsdown builds two
single-entry passes, tsx becomes a devDependency for the unbuilt worker
spawn.
2026-07-09 18:39:31 +08:00
|
|
|
if (manifest.bin) return dshBinPackageFiles
|
|
|
|
|
// A declared "./worker" subpath export sanctions the one extra runtime
|
|
|
|
|
// bundle a worker-thread entry needs (and NodeNext/publint then validate
|
|
|
|
|
// that subpath's targets like any other export).
|
|
|
|
|
if (manifest.exports?.['./worker']) return dshWorkerPackageFiles
|
|
|
|
|
return dshPackageFiles
|
2026-06-22 01:01:08 +08:00
|
|
|
}
|
|
|
|
|
|
2026-06-16 14:55:37 +08:00
|
|
|
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`)
|
|
|
|
|
}
|
2026-07-13 17:49:01 +08:00
|
|
|
if (manifest.version !== repositoryVersion) {
|
|
|
|
|
errors.push(`${label}: package.json version must match root version ${repositoryVersion ?? '(missing)'}`)
|
2026-06-16 14:55:37 +08:00
|
|
|
}
|
|
|
|
|
if (manifest.type !== 'module') {
|
|
|
|
|
errors.push(`${label}: package.json must set "type": "module"`)
|
|
|
|
|
}
|
2026-06-20 00:21:57 +08:00
|
|
|
if (manifest.main !== 'lib/index.js') {
|
|
|
|
|
errors.push(`${label}: package.json must set "main": "lib/index.js"`)
|
|
|
|
|
}
|
2026-06-20 00:26:02 +08:00
|
|
|
if (manifest.types !== 'lib/types/index.d.ts') {
|
|
|
|
|
errors.push(`${label}: package.json must set "types": "lib/types/index.d.ts"`)
|
2026-06-20 00:21:57 +08:00
|
|
|
}
|
2026-06-20 00:26:02 +08:00
|
|
|
if (manifest.exports?.['.']?.types !== './lib/types/index.d.ts') {
|
|
|
|
|
errors.push(`${label}: package.json exports["."].types must be "./lib/types/index.d.ts"`)
|
2026-06-20 00:21:57 +08:00
|
|
|
}
|
|
|
|
|
if (manifest.exports?.['.']?.default !== './lib/index.js') {
|
|
|
|
|
errors.push(`${label}: package.json exports["."].default must be "./lib/index.js"`)
|
|
|
|
|
}
|
2026-06-22 01:01:08 +08:00
|
|
|
const expectedFiles = expectedDshPackageFiles(manifest)
|
|
|
|
|
if (!sameStringList(manifest.files, expectedFiles)) {
|
|
|
|
|
errors.push(`${label}: package.json files must be ${JSON.stringify(expectedFiles)}`)
|
2026-06-20 00:21:57 +08:00
|
|
|
}
|
2026-06-16 14:55:37 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return errors.map(error => `${relative(root, join(root, dir, 'package.json'))}: ${error}`)
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-20 23:12:14 +08:00
|
|
|
/**
|
2026-07-13 23:27:00 +08:00
|
|
|
* Enforce `packages/<group>/<pkg>`: groups are open-named containers without a
|
|
|
|
|
* package.json, and packages may be neither flat nor more deeply nested.
|
2026-06-20 23:12:14 +08:00
|
|
|
*/
|
|
|
|
|
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/<group>/<pkg>, not directly under packages/`)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
for (const pkg of readdirSync(join(packagesRoot, group.name), { withFileTypes: true })) {
|
|
|
|
|
if (!pkg.isDirectory()) continue
|
2026-07-04 00:57:35 +08:00
|
|
|
if (localArtifactDirs.has(pkg.name)) continue
|
2026-06-20 23:12:14 +08:00
|
|
|
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/<group>/<pkg>, no deeper nesting`)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return errors
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-13 17:49:01 +08:00
|
|
|
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(),
|
|
|
|
|
]
|
2026-06-16 14:55:37 +08:00
|
|
|
if (errors.length > 0) {
|
|
|
|
|
console.error(errors.join('\n'))
|
|
|
|
|
process.exitCode = 1
|
|
|
|
|
}
|