rvlib-shipkit 0.38.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +379 -16
- package/README.md +26 -14
- package/package.json +2 -2
- package/src/assets/gen/comfyts-gen.ts +27 -10
- package/src/assets/gen/derived.ts +1 -1
- package/src/assets/gen/generate-roll.ts +1 -1
- package/src/assets/kit-config.ts +6 -6
- package/src/assets/run-assets.ts +14 -10
- package/src/assets/server/studio-server.ts +6 -5
- package/src/check.ts +343 -0
- package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
- package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
- package/src/checks/SK003-claude-md-shape.check.ts +66 -0
- package/src/checks/SK004-gitignore-policy.check.ts +309 -0
- package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
- package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
- package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
- package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
- package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
- package/src/checks/SK010-undeclared-unit.check.ts +111 -0
- package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
- package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
- package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
- package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
- package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
- package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
- package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
- package/src/checks/SK018-test-files-run.check.ts +28 -0
- package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
- package/src/checks/SK020-self-spawning-check.check.ts +147 -0
- package/src/checks/contracts.check.ts +20 -0
- package/src/cli/cmd_Asks.ts +3 -2
- package/src/cli/cmd_Assets.ts +2 -2
- package/src/cli/cmd_Bump.ts +1 -1
- package/src/cli/cmd_Check.ts +6 -6
- package/src/cli/cmd_Checks.ts +95 -0
- package/src/cli/cmd_Ci.ts +48 -13
- package/src/cli/cmd_Commands.ts +2 -6
- package/src/cli/cmd_ConfigKeys.ts +69 -0
- package/src/cli/cmd_Contracts.ts +1 -1
- package/src/cli/cmd_Fleet.ts +16 -3
- package/src/cli/cmd_Hooks.ts +1 -1
- package/src/cli/cmd_Keys.ts +43 -18
- package/src/cli/cmd_Mv.ts +2 -2
- package/src/cli/cmd_Prose.ts +10 -1
- package/src/cli/cmd_Releases.ts +2 -2
- package/src/cli/cmd_Serve.ts +1 -1
- package/src/cli/cmd_Start.ts +6 -6
- package/src/cli/cmd_Status.ts +25 -0
- package/src/cli/cmd_Tui.ts +2 -2
- package/src/cli.ts +7 -4
- package/src/collections/actions.ts +4 -4
- package/src/collections/checks.ts +257 -0
- package/src/collections/collection-types.ts +254 -1
- package/src/collections/comfy.ts +23 -0
- package/src/collections/md-body.ts +14 -0
- package/src/collections/pings.ts +38 -12
- package/src/collections/posts.ts +19 -3
- package/src/collections/providers.ts +198 -91
- package/src/collections/reflections.ts +41 -0
- package/src/collections/state-rules.ts +73 -0
- package/src/collections/test-time.ts +288 -0
- package/src/collections/tests.ts +181 -0
- package/src/collections/wikis.ts +66 -0
- package/src/comfy/comfy-panel.ts +292 -0
- package/src/contracts/apply-contracts.ts +17 -12
- package/src/contracts/contract-types.ts +16 -4
- package/src/contracts/contracts-engine.ts +78 -42
- package/src/contracts/manifests/discord.contract.ts +6 -6
- package/src/contracts/manifests/identity.contract.ts +3 -3
- package/src/contracts/manifests/itch.contract.ts +12 -15
- package/src/contracts/manifests/social.contract.ts +5 -5
- package/src/contracts/manifests/steam.contract.ts +16 -16
- package/src/contracts/run-contracts.ts +4 -3
- package/src/core/attach.ts +96 -0
- package/src/core/canonical-scripts.ts +25 -0
- package/src/core/check-ctx.ts +204 -14
- package/src/core/check-file-names.ts +112 -0
- package/src/core/check-pass.ts +84 -0
- package/src/core/check-runner.ts +796 -0
- package/src/core/config-keys.ts +256 -0
- package/src/core/config-rewrite.ts +77 -0
- package/src/core/discover-checks.ts +166 -90
- package/src/core/discover-legacy-checks.ts +26 -0
- package/src/core/doc-block.ts +13 -0
- package/src/core/floor-resolve.ts +264 -0
- package/src/core/floor.ts +20 -137
- package/src/core/git-bin.ts +24 -0
- package/src/core/git-root.ts +108 -0
- package/src/core/instance.ts +195 -0
- package/src/core/jobs.ts +118 -0
- package/src/core/layout.ts +52 -0
- package/src/core/legacy-shapes.ts +207 -0
- package/src/core/lib-root.ts +5 -0
- package/src/core/locked-write.ts +110 -39
- package/src/core/machine-commit.ts +5 -4
- package/src/core/migrate-layout.ts +616 -0
- package/src/core/motherhome.ts +2 -8
- package/src/core/nesting.ts +4 -12
- package/src/core/one-way-in-policy.ts +73 -17
- package/src/core/outcome-words.ts +45 -0
- package/src/core/package-manager.ts +88 -0
- package/src/core/parallel.ts +21 -0
- package/src/core/process-tree.ts +44 -0
- package/src/core/raw-access.ts +93 -0
- package/src/core/read-set.ts +152 -0
- package/src/core/run-check.ts +155 -213
- package/src/core/run-ci.ts +152 -189
- package/src/core/run-fix.ts +99 -0
- package/src/core/run-fixers.ts +64 -33
- package/src/core/self-update.ts +11 -3
- package/src/core/slots.ts +83 -0
- package/src/core/spawn-timeout.ts +34 -3
- package/src/core/when-block.ts +401 -0
- package/src/core/when.ts +135 -0
- package/src/core/write-status.ts +5 -2
- package/src/fleet/scan-fleet.ts +16 -0
- package/src/hooks/leak-guard.ts +20 -9
- package/src/hooks/one-way-in-guard.ts +13 -5
- package/src/hooks/root-identity.ts +2 -1
- package/src/hooks/run-hooks.ts +38 -26
- package/src/init-repo.ts +39 -23
- package/src/ledger/check-stats.ts +155 -0
- package/src/ledger/check-table.ts +179 -0
- package/src/ledger/db.ts +255 -0
- package/src/ledger/gate-checks.ts +433 -0
- package/src/ledger/git-key.ts +116 -77
- package/src/ledger/git-scan.ts +96 -35
- package/src/ledger/ledger.ts +116 -42
- package/src/ledger/live-runs.ts +111 -0
- package/src/ledger/live-timeline.ts +97 -0
- package/src/ledger/repo-view.ts +73 -0
- package/src/ledger/run-log.ts +448 -0
- package/src/ledger/run-timeline.ts +173 -0
- package/src/ledger/store.ts +23 -12
- package/src/ledger/suite-log.ts +161 -0
- package/src/ledger/toolchain-identity.ts +7 -1
- package/src/ledger/world-cache.ts +111 -0
- package/src/mv/mv-md.ts +16 -6
- package/src/problems/extract.ts +182 -0
- package/src/problems/problem-board.ts +37 -0
- package/src/problems/problem-line.ts +77 -0
- package/src/prose/prose-view.ts +90 -0
- package/src/prose/run-prose.ts +60 -19
- package/src/prose/ste.ts +140 -24
- package/src/reflections/reflections-queue.ts +81 -0
- package/src/release/git-facts.ts +15 -9
- package/src/release/run-bump.ts +6 -5
- package/src/release/run-release.ts +25 -11
- package/src/release/run-releases.ts +2 -2
- package/src/release/unit-gate.ts +7 -54
- package/src/release/unit-resolve.ts +282 -0
- package/src/release/units.ts +33 -217
- package/src/repo-config.ts +157 -88
- package/src/rules/rule.ts +36 -96
- package/src/rules/untrack-generated.ts +5 -4
- package/src/scaffold/files/vscode-tasks.json +10 -31
- package/src/scaffold/jsonc-spans.ts +113 -0
- package/src/scaffold/project-scaffold.ts +16 -43
- package/src/scan.ts +98 -0
- package/src/serve/gate-trigger.ts +148 -0
- package/src/serve/health-schedule.ts +102 -0
- package/src/serve/hub-app/chart-view.ts +179 -0
- package/src/serve/hub-app/check-view.tsx +345 -0
- package/src/serve/hub-app/check-words.ts +164 -0
- package/src/serve/hub-app/checks-svc.ts +280 -0
- package/src/serve/hub-app/clock-svc.ts +33 -0
- package/src/serve/hub-app/col-card.tsx +23 -39
- package/src/serve/hub-app/cols-svc.ts +44 -20
- package/src/serve/hub-app/comfy-svc.ts +171 -0
- package/src/serve/hub-app/comfy-view.tsx +81 -0
- package/src/serve/hub-app/dur.ts +41 -0
- package/src/serve/hub-app/hash-route.ts +121 -0
- package/src/serve/hub-app/header-bar.tsx +117 -0
- package/src/serve/hub-app/health-svc.ts +23 -28
- package/src/serve/hub-app/help.tsx +29 -0
- package/src/serve/hub-app/hub-app.tsx +122 -119
- package/src/serve/hub-app/hub-st.ts +115 -42
- package/src/serve/hub-app/hub-types.ts +117 -2
- package/src/serve/hub-app/hub.css +534 -192
- package/src/serve/hub-app/index.html +2 -0
- package/src/serve/hub-app/live-svc.ts +174 -0
- package/src/serve/hub-app/md.ts +15 -0
- package/src/serve/hub-app/media-svc.ts +4 -1
- package/src/serve/hub-app/media-view.tsx +15 -0
- package/src/serve/hub-app/next-run.ts +68 -0
- package/src/serve/hub-app/project-gutter.css +65 -0
- package/src/serve/hub-app/project-gutter.tsx +128 -0
- package/src/serve/hub-app/projects-svc.ts +226 -0
- package/src/serve/hub-app/prose-hits-st.ts +124 -0
- package/src/serve/hub-app/prose-hits-view.tsx +176 -0
- package/src/serve/hub-app/prose-hits.css +21 -0
- package/src/serve/hub-app/run-strip.ts +47 -0
- package/src/serve/hub-app/runs-tabs.ts +24 -0
- package/src/serve/hub-app/test-view.tsx +107 -0
- package/src/serve/hub-app/tests-svc.ts +62 -0
- package/src/serve/hub-app/tests-view.tsx +238 -0
- package/src/serve/hub-app/time-ago.tsx +34 -0
- package/src/serve/hub-app/timeline-chart.tsx +274 -0
- package/src/serve/hub-app/timeline-geometry.ts +314 -0
- package/src/serve/hub-app/timeline-svc.ts +62 -0
- package/src/serve/hub-app/timeline-view.tsx +245 -0
- package/src/serve/hub-app/tree-build.ts +410 -0
- package/src/serve/hub-app/tree-row.tsx +42 -0
- package/src/serve/hub-app/tree-svc.ts +174 -0
- package/src/serve/hub-app/wikis-view.tsx +53 -0
- package/src/serve/other-repos.ts +79 -0
- package/src/serve/project-gutter-route.ts +350 -0
- package/src/serve/project-gutter.ts +449 -0
- package/src/serve/run-serve.ts +245 -16
- package/src/serve/same-origin.ts +14 -0
- package/src/serve/serve-hub.ts +213 -18
- package/src/shipkit.ts +19 -12
- package/src/smoke/run-smoke.ts +9 -4
- package/src/start-repo.ts +75 -36
- package/src/suite/judge-suites.ts +209 -15
- package/src/suite/kept-timings.ts +58 -0
- package/src/suite/runner-shapes.ts +88 -0
- package/src/suite/suite-cli.ts +147 -24
- package/src/suite/suite-cpu-preload.ts +92 -0
- package/src/suite/test-units.ts +243 -0
- package/src/ts/config.ts +4 -69
- package/src/ts/jsonc.ts +69 -0
- package/src/ts/lex.ts +143 -0
- package/src/ts/rename.ts +73 -10
- package/src/ts/source-graph.ts +28 -8
- package/src/ts/tsconfig-projects.ts +6 -97
- package/src/ts/tsconfig-resolve.ts +108 -0
- package/src/tui/build-steps-panel.ts +61 -0
- package/src/tui/checks-panel.ts +24 -16
- package/src/tui/run-tui.ts +2 -2
- package/src/tui/tui-panel.ts +5 -4
- package/src/wiki/graph-colors.ts +68 -0
- package/src/wiki/graph-mount.ts +7 -9
- package/src/wiki/graph-page.ts +42 -25
- package/src/wiki/graph-positions.ts +105 -0
- package/src/wiki/run-wiki.ts +1 -1
- package/src/wiki/scan-md.ts +1 -1
- package/src/wiki/serve-graph.ts +33 -3
- package/src/cli/cmd_Rules.ts +0 -51
- package/src/cli/cmd_Units.ts +0 -82
- package/src/collections/graphs.ts +0 -32
- package/src/contracts/contracts-once.ts +0 -43
- package/src/core/check-suite.ts +0 -302
- package/src/core/repair-contract-dirs.ts +0 -136
- package/src/core/task-runner.ts +0 -339
- package/src/ledger/gate-units.ts +0 -138
- package/src/ledger/unit-table.ts +0 -105
- package/src/rules/SK002-contracts-fulfillness.ts +0 -24
- package/src/rules/SK003-claude-md-shape.ts +0 -70
- package/src/rules/SK004-gitignore-policy.ts +0 -238
- package/src/rules/SK010-undeclared-release-unit.ts +0 -127
- package/src/rules/rule-registry.ts +0 -64
- package/src/rules/run-fix.ts +0 -72
- package/src/tui/tasks-panel.ts +0 -60
package/src/release/unit-gate.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
// what a single unit must prove before its version is written or published
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// tsconfig
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
// what a single unit must prove before its version is written or published,
|
|
2
|
+
// beside the release gate. Its TESTS are not here: every unit has its own test
|
|
3
|
+
// check, `test:<unit id>`, and the release gate runs it in the full tier
|
|
4
|
+
// (run-release.ts runReleaseGate). Nor is its typecheck: `typecheck:<unit id>`
|
|
5
|
+
// compiles it under its own tsconfig in that same gate. What stays is what no
|
|
6
|
+
// check can say: the tree under the unit is committed, and a package bound for
|
|
7
|
+
// npm has a tsconfig at all.
|
|
8
8
|
import { dirtyPaths } from './git-facts.ts'
|
|
9
|
-
import { resolveTypecheckProjects } from '../ts/tsconfig-projects.ts'
|
|
10
9
|
import type { ReleaseUnit } from './units.ts'
|
|
11
10
|
import type { RepoConfig } from '../repo-config.ts'
|
|
12
11
|
|
|
13
|
-
/** the unit's own tsc: its node_modules, then the repo's, then PATH */
|
|
14
|
-
export function resolveTsc(root: string, unit: ReleaseUnit): string | null {
|
|
15
|
-
for (const c of [join(unit.dir, 'node_modules/.bin/tsc'), join(root, 'node_modules/.bin/tsc')]) if (existsSync(c)) return c
|
|
16
|
-
return Bun.which('tsc')
|
|
17
|
-
}
|
|
18
|
-
|
|
19
12
|
/** the publish argv. `--access public` is not a default: npm restricts a
|
|
20
13
|
* scoped package unless it is asked, and a restricted publish looks
|
|
21
14
|
* successful right up until a consumer cannot install it. */
|
|
@@ -23,13 +16,6 @@ export function npmPublishArgv(o: { npmrc?: string | null } = {}): string[] {
|
|
|
23
16
|
return ['npm', 'publish', '--access', 'public', ...(o.npmrc != null ? ['--userconfig', o.npmrc] : [])]
|
|
24
17
|
}
|
|
25
18
|
|
|
26
|
-
/** count of passing tests in a `bun test` run, or null when the output does
|
|
27
|
-
* not say — a suite whose result cannot be read is not a proven suite */
|
|
28
|
-
export const passCount = (out: string): number | null => {
|
|
29
|
-
const m = out.match(/(\d+) pass/)
|
|
30
|
-
return m?.[1] != null ? Number(m[1]) : null
|
|
31
|
-
}
|
|
32
|
-
|
|
33
19
|
/** everything that must hold for this unit, checked before anything is
|
|
34
20
|
* written. Empty means the unit is releasable as far as the machine can tell. */
|
|
35
21
|
export function unitBlockers(root: string, config: RepoConfig, unit: ReleaseUnit): string[] {
|
|
@@ -43,38 +29,5 @@ export function unitBlockers(root: string, config: RepoConfig, unit: ReleaseUnit
|
|
|
43
29
|
}
|
|
44
30
|
if (unit.targets.npm && unit.tsconfig == null)
|
|
45
31
|
out.push(`unit '${unit.id}': publishes to npm but has no tsconfig.json — a package nobody typechecks standalone is a package that breaks on install`)
|
|
46
|
-
if (unit.id === 'root') return out
|
|
47
|
-
|
|
48
|
-
if (Bun.which('bun') == null) return [...out, 'bun is not on PATH']
|
|
49
|
-
const t = Bun.spawnSync(['bun', 'test'], { cwd: unit.dir })
|
|
50
|
-
const tOut = `${t.stdout.toString()}${t.stderr.toString()}`
|
|
51
|
-
if (t.exitCode !== 0) out.push(`unit '${unit.id}': its own test suite is red (exit ${t.exitCode}) — bun test in ${unit.rel}`)
|
|
52
|
-
else {
|
|
53
|
-
const pass = passCount(tOut)
|
|
54
|
-
if (pass == null || pass < 1) out.push(`unit '${unit.id}': its test suite ran 0 tests — a vacuous suite proves nothing, write a real one`)
|
|
55
|
-
}
|
|
56
|
-
if (unit.tsconfig != null) {
|
|
57
|
-
const tsc = resolveTsc(root, unit)
|
|
58
|
-
if (tsc == null) out.push(`unit '${unit.id}': tsc not resolvable — bun add -d typescript at the repo root`)
|
|
59
|
-
else {
|
|
60
|
-
// a solution-style tsconfig (`files: []` plus `references`) makes
|
|
61
|
-
// tsc compile ZERO files and exit 0, which would publish a package
|
|
62
|
-
// whose only proof of soundness was that nobody looked
|
|
63
|
-
let projects: ReturnType<typeof resolveTypecheckProjects> | null = null
|
|
64
|
-
try { projects = resolveTypecheckProjects(unit.tsconfig) }
|
|
65
|
-
catch (e) { out.push(`unit '${unit.id}': ${String((e as Error)?.message ?? e)}`) }
|
|
66
|
-
for (const m of projects?.missing ?? [])
|
|
67
|
-
out.push(`unit '${unit.id}': its tsconfig references '${m}', which does not exist, so whatever that project covered is unchecked`)
|
|
68
|
-
if (projects != null && projects.vacuous)
|
|
69
|
-
out.push(`unit '${unit.id}': its tsconfig compiles no files of its own and reaches no project that does, so a typecheck over it proves nothing — it cannot be published`)
|
|
70
|
-
for (const p of projects?.projects ?? []) {
|
|
71
|
-
const r = Bun.spawnSync([tsc, '--noEmit', '-p', p], { cwd: root })
|
|
72
|
-
if (r.exitCode !== 0) {
|
|
73
|
-
const lines = `${r.stdout.toString()}${r.stderr.toString()}`.trim().split('\n').slice(0, 8).join('\n')
|
|
74
|
-
out.push(`unit '${unit.id}': does not typecheck under ${p} — it cannot be published\n${lines}`)
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
32
|
return out
|
|
80
33
|
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// unit RESOLUTION, with no disk access of its own: every read goes through the
|
|
2
|
+
// reader it is handed. release/units.ts hands it plain fs, a check hands it the
|
|
3
|
+
// check context, so a check reading units stays keyed on what it read.
|
|
4
|
+
import { basename, isAbsolute, join, relative } from 'path'
|
|
5
|
+
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
6
|
+
import type { RepoConfig } from '../repo-config.ts'
|
|
7
|
+
import type { TestsConfig } from '../suite/judge-suites.ts'
|
|
8
|
+
|
|
9
|
+
/** how resolution reads the disk, by repo-relative path. Plain fs by default,
|
|
10
|
+
* the check context inside a check so every read lands in its read set */
|
|
11
|
+
export type UnitReader = {
|
|
12
|
+
text: (rel: string) => string | null
|
|
13
|
+
exists: (rel: string, kind?: 'file' | 'dir') => boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** the check context as a reader. A path outside the repo (a config pointing
|
|
17
|
+
* a tsconfig at `../`) is not the repo's to read: it reads as absent, and the
|
|
18
|
+
* answer is marked untracked rather than stored on reads that never saw it */
|
|
19
|
+
export function checkUnitReader(c: CheckCtx): UnitReader {
|
|
20
|
+
const outside = (rel: string): boolean => isAbsolute(rel) || rel === '..' || rel.startsWith('../')
|
|
21
|
+
return {
|
|
22
|
+
text: (rel) => (outside(rel) ? (c.untrack(`read ${rel}, outside the repo`), null) : c.read(rel)),
|
|
23
|
+
// the root itself is the one path that cannot move under a check
|
|
24
|
+
exists: (rel, kind) => (rel === '' ? kind !== 'file' : outside(rel) ? (c.untrack(`probed ${rel}, outside the repo`), false) : c.exists(rel, kind)),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type UnitDecl = {
|
|
29
|
+
/** repo-relative directory holding the unit's package.json — the only required key */
|
|
30
|
+
dir: string
|
|
31
|
+
/** default: basename(dir) */
|
|
32
|
+
id?: string
|
|
33
|
+
/** tag template, `{name}` and `{version}` substituted (default '{name}@{version}') */
|
|
34
|
+
tag?: string
|
|
35
|
+
/** repo-relative changelog (default '<dir>/CHANGELOG.md') */
|
|
36
|
+
changelog?: string
|
|
37
|
+
/** publish this sub-unit to npm. REQUIRED unless its package.json says
|
|
38
|
+
* `private: true`: the npm target is declared, never inferred, and the
|
|
39
|
+
* ambiguous package REFUSES rather than defaulting to the quiet answer.
|
|
40
|
+
* `true` on a private package.json is a resolution FAILURE */
|
|
41
|
+
npm?: boolean
|
|
42
|
+
/** cut a GitHub release for this unit's tag (default false for sub-units) */
|
|
43
|
+
githubRelease?: boolean
|
|
44
|
+
/** repo-relative tsconfig for the unit's own typecheck, `typecheck:<unit id>`
|
|
45
|
+
* and the release gate (default '<dir>/tsconfig.json' when it exists) */
|
|
46
|
+
tsconfig?: string
|
|
47
|
+
/** the unit's own suite budget, paths relative to the unit dir (suite/judge-suites.ts) */
|
|
48
|
+
tests?: TestsConfig
|
|
49
|
+
/** `false` = a unit that never releases and exists to scope its own checks:
|
|
50
|
+
* a bench, a fixture app, an examples folder. Its tests run as
|
|
51
|
+
* `test:<unit id>` in its own dir (own package.json script, bunfig,
|
|
52
|
+
* preload, `tests` budget), its tsconfig as `typecheck:<unit id>`, and it
|
|
53
|
+
* claims its dir for rule SK010, but `shipkit releases`, `bump`,
|
|
54
|
+
* `release`, the npm checks and the per unit release gate never see it. Only
|
|
55
|
+
* `dir`, `id`, `tests`, `tsconfig` and `deps` mean anything beside it */
|
|
56
|
+
release?: false
|
|
57
|
+
/** checks the unit's generated checks (`test:<unit id>`, `typecheck:<unit id>`)
|
|
58
|
+
* wait for, by id: a check that says whether the world they need is
|
|
59
|
+
* there (a local server binary, installed dependencies). A dep that warns
|
|
60
|
+
* or fails SKIPS them with its reason, so a missing prerequisite reads as
|
|
61
|
+
* a visible skip, never as a green or a red */
|
|
62
|
+
deps?: string[]
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const UNIT_DECL_KEYS: readonly string[] = ['dir', 'id', 'tag', 'changelog', 'npm', 'githubRelease', 'tsconfig', 'tests', 'release', 'deps']
|
|
66
|
+
|
|
67
|
+
/** the only keys a `release: false` unit reads. Any other one describes a
|
|
68
|
+
* release that never happens, so it is refused rather than ignored */
|
|
69
|
+
export const TEST_ONLY_UNIT_KEYS: readonly string[] = ['dir', 'id', 'tests', 'tsconfig', 'deps', 'release']
|
|
70
|
+
|
|
71
|
+
/** a unit declared `release: false`: it scopes tests and claims its dir, and
|
|
72
|
+
* no release surface ever lists it */
|
|
73
|
+
export type TestOnlyUnit = {
|
|
74
|
+
id: string
|
|
75
|
+
/** absolute */
|
|
76
|
+
dir: string
|
|
77
|
+
/** repo-relative */
|
|
78
|
+
rel: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** a unit, fully resolved: no optionals, no defaults left to re-apply downstream */
|
|
82
|
+
export type ReleaseUnit = {
|
|
83
|
+
id: string
|
|
84
|
+
/** absolute */
|
|
85
|
+
dir: string
|
|
86
|
+
/** repo-relative git pathspec, '' for the root */
|
|
87
|
+
rel: string
|
|
88
|
+
name: string
|
|
89
|
+
/** package.json version — THE only version source */
|
|
90
|
+
version: string
|
|
91
|
+
/** absolute path to the versioned changelog */
|
|
92
|
+
changelog: string
|
|
93
|
+
/** tag template, `{name}` and `{version}` unsubstituted — bump renders the NEXT tag from it */
|
|
94
|
+
tagTemplate: string
|
|
95
|
+
/** rendered tag for the current version */
|
|
96
|
+
tag: string
|
|
97
|
+
/** the tag template with `{version}` replaced by `*` — feeds the stranded-tag probe */
|
|
98
|
+
tagGlob: string
|
|
99
|
+
targets: { npm: boolean; tag: boolean; githubRelease: boolean }
|
|
100
|
+
/** the unit's OWN package.json `private: true`, never the repo's visibility */
|
|
101
|
+
isPrivate: boolean
|
|
102
|
+
/** absolute path, or null when the unit has no tsconfig of its own */
|
|
103
|
+
tsconfig: string | null
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** a unit that could not be described, and why. Scoped by id because the two
|
|
107
|
+
* cases differ: a broken DECLARED unit is a repo-wide declaration bug (rule
|
|
108
|
+
* SK010), while a root package that is not release-shaped only matters the
|
|
109
|
+
* day someone releases it. */
|
|
110
|
+
export type UnitError = { id: string; message: string }
|
|
111
|
+
|
|
112
|
+
export type UnitResolution = {
|
|
113
|
+
/** every unit that resolved — a broken sibling never hides the working ones */
|
|
114
|
+
units: ReleaseUnit[]
|
|
115
|
+
/** non-empty means no release of the named unit may proceed */
|
|
116
|
+
errors: UnitError[]
|
|
117
|
+
/** the units declared `release: false`, kept apart so no release surface
|
|
118
|
+
* can list one by accident, and so rule SK010 still counts their dirs */
|
|
119
|
+
testOnly: TestOnlyUnit[]
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const formatUnitError = (e: UnitError): string => `unit '${e.id}': ${e.message}`
|
|
123
|
+
|
|
124
|
+
const ROOT_TAG = 'v{version}'
|
|
125
|
+
const SUB_TAG = '{name}@{version}'
|
|
126
|
+
|
|
127
|
+
export const render = (template: string, p: { name: string; version: string }): string =>
|
|
128
|
+
template.replaceAll('{name}', p.name).replaceAll('{version}', p.version)
|
|
129
|
+
|
|
130
|
+
export type Pkg = {
|
|
131
|
+
name?: string
|
|
132
|
+
version?: string
|
|
133
|
+
private?: boolean
|
|
134
|
+
scripts?: Record<string, string>
|
|
135
|
+
dependencies?: Record<string, string>
|
|
136
|
+
peerDependencies?: Record<string, string>
|
|
137
|
+
optionalDependencies?: Record<string, string>
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function readPkg(root: string, dir: string, io: UnitReader): { pkg: Pkg } | { error: string } {
|
|
141
|
+
const p = join(dir, 'package.json')
|
|
142
|
+
const text = io.text(relative(root, p))
|
|
143
|
+
if (text == null) return { error: `no package.json in ${dir}` }
|
|
144
|
+
try { return { pkg: JSON.parse(text) as Pkg } }
|
|
145
|
+
catch (e) { return { error: `${p} is unparseable (${String(e).slice(0, 120)})` } }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** every releasable unit, root first then the declared sub-units in declared
|
|
149
|
+
* order. Release ORDER is different (releaseOrder below) — the root ships
|
|
150
|
+
* last because it depends on its sub-units, never the reverse. */
|
|
151
|
+
export function resolveUnits(root: string, config: RepoConfig, io: UnitReader): UnitResolution {
|
|
152
|
+
const errors: UnitError[] = []
|
|
153
|
+
const units: ReleaseUnit[] = []
|
|
154
|
+
const testOnly: TestOnlyUnit[] = []
|
|
155
|
+
const rc = config.release ?? {}
|
|
156
|
+
|
|
157
|
+
const rootPkg = readPkg(root, root, io)
|
|
158
|
+
if ('error' in rootPkg) errors.push({ id: 'root', message: `${rootPkg.error} — release reads name and version from it` })
|
|
159
|
+
else {
|
|
160
|
+
const u = build({
|
|
161
|
+
root,
|
|
162
|
+
id: 'root',
|
|
163
|
+
dir: root,
|
|
164
|
+
pkg: rootPkg.pkg,
|
|
165
|
+
tagTemplate: rc.tag ?? ROOT_TAG,
|
|
166
|
+
changelogRel: rc.changelog ?? 'CHANGELOG.md',
|
|
167
|
+
npm: rc.npm,
|
|
168
|
+
// the root IS the repo: its package.json private flag is the whole
|
|
169
|
+
// declaration, and a public root package publishes by default
|
|
170
|
+
npmWhenUndeclared: 'from-private-flag',
|
|
171
|
+
githubRelease: rc.githubRelease ?? true,
|
|
172
|
+
tsconfigRel: rc.tsconfig,
|
|
173
|
+
checkFolderName: false,
|
|
174
|
+
io,
|
|
175
|
+
})
|
|
176
|
+
if ('error' in u) errors.push(...u.error)
|
|
177
|
+
else units.push(u.unit)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const seen = new Set<string>(['root'])
|
|
181
|
+
for (const decl of config.units ?? []) {
|
|
182
|
+
const id = decl.id ?? (typeof decl.dir === 'string' ? basename(decl.dir) : '?')
|
|
183
|
+
const bad = Object.keys(decl).filter((k) => !UNIT_DECL_KEYS.includes(k))
|
|
184
|
+
if (bad.length > 0) { errors.push({ id, message: `unknown key(s) ${bad.join(', ')} (known: ${UNIT_DECL_KEYS.join(', ')})` }); continue }
|
|
185
|
+
if (typeof decl.dir !== 'string' || decl.dir === '') { errors.push({ id, message: 'every units[] entry needs a `dir`' }); continue }
|
|
186
|
+
const dir = join(root, decl.dir)
|
|
187
|
+
if (!io.exists(relative(root, dir), 'dir')) { errors.push({ id, message: `'${decl.dir}' is not a directory` }); continue }
|
|
188
|
+
if (seen.has(id)) { errors.push({ id, message: `duplicate unit id — give '${decl.dir}' an explicit id` }); continue }
|
|
189
|
+
seen.add(id)
|
|
190
|
+
if ('release' in decl) {
|
|
191
|
+
if (decl.release !== false) { errors.push({ id, message: `\`release\` takes only \`false\` (a unit that never releases); drop the key for a unit that does` }); continue }
|
|
192
|
+
const idle = Object.keys(decl).filter((k) => !TEST_ONLY_UNIT_KEYS.includes(k))
|
|
193
|
+
if (idle.length > 0) { errors.push({ id, message: `release: false, so ${idle.join(', ')} describe(s) a release that never happens — drop them (a test only unit reads ${TEST_ONLY_UNIT_KEYS.join(', ')})` }); continue }
|
|
194
|
+
testOnly.push({ id, dir, rel: relative(root, dir) })
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
const pkg = readPkg(root, dir, io)
|
|
198
|
+
if ('error' in pkg) { errors.push({ id, message: pkg.error }); continue }
|
|
199
|
+
const u = build({
|
|
200
|
+
root,
|
|
201
|
+
id,
|
|
202
|
+
dir,
|
|
203
|
+
pkg: pkg.pkg,
|
|
204
|
+
tagTemplate: decl.tag ?? SUB_TAG,
|
|
205
|
+
changelogRel: decl.changelog ?? `${decl.dir}/CHANGELOG.md`,
|
|
206
|
+
npm: decl.npm,
|
|
207
|
+
npmWhenUndeclared: 'refuse',
|
|
208
|
+
githubRelease: decl.githubRelease ?? false,
|
|
209
|
+
tsconfigRel: decl.tsconfig,
|
|
210
|
+
// only the DEFAULT tag renders the package name into a git tag, so
|
|
211
|
+
// only there does the folder have to agree with it
|
|
212
|
+
checkFolderName: decl.tag == null,
|
|
213
|
+
io,
|
|
214
|
+
})
|
|
215
|
+
if ('error' in u) errors.push(...u.error)
|
|
216
|
+
else units.push(u.unit)
|
|
217
|
+
}
|
|
218
|
+
return { units, errors, testOnly }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function build(p: {
|
|
222
|
+
root: string
|
|
223
|
+
id: string
|
|
224
|
+
dir: string
|
|
225
|
+
pkg: Pkg
|
|
226
|
+
tagTemplate: string
|
|
227
|
+
changelogRel: string
|
|
228
|
+
npm: boolean | undefined
|
|
229
|
+
/** how an UNDECLARED npm target resolves. The root IS the repo, so its
|
|
230
|
+
* package.json private flag is the whole declaration. A sub-unit has no
|
|
231
|
+
* such declaration, so a non-private package with no `npm` key is
|
|
232
|
+
* ambiguous and REFUSES. */
|
|
233
|
+
npmWhenUndeclared: 'from-private-flag' | 'refuse'
|
|
234
|
+
githubRelease: boolean
|
|
235
|
+
tsconfigRel: string | undefined
|
|
236
|
+
checkFolderName: boolean
|
|
237
|
+
io: UnitReader
|
|
238
|
+
}): { unit: ReleaseUnit } | { error: UnitError[] } {
|
|
239
|
+
const errors: UnitError[] = []
|
|
240
|
+
const at = (message: string): UnitError => ({ id: p.id, message })
|
|
241
|
+
const name = p.pkg.name ?? ''
|
|
242
|
+
const version = p.pkg.version ?? ''
|
|
243
|
+
if (name === '' || version === '') errors.push(at('package.json needs both name and version'))
|
|
244
|
+
// under the default template the package NAME becomes the git tag, so the
|
|
245
|
+
// folder it lives in must be findable from that tag. A scope satisfies it
|
|
246
|
+
// (`@acme/thing` in `thing/`), and an explicit `tag` opts out entirely.
|
|
247
|
+
const base = basename(p.dir)
|
|
248
|
+
if (p.checkFolderName && name !== '' && name !== base && !name.endsWith(`/${base}`))
|
|
249
|
+
errors.push(at(`package name '${name}' must equal the folder name '${base}' (a scope is fine: '@scope/${base}') — or declare an explicit \`tag\` for this unit`))
|
|
250
|
+
const isPrivate = p.pkg.private === true
|
|
251
|
+
// npm's own unconditional refusal. Declaring npm:true on top of it is a
|
|
252
|
+
// contradiction, not a preference — surface it instead of silently winning
|
|
253
|
+
if (p.npm === true && isPrivate)
|
|
254
|
+
errors.push(at('npm: true but package.json says private: true — drop one, npm refuses the publish either way'))
|
|
255
|
+
// "declared, never inferred" cuts BOTH ways: the package nobody decided
|
|
256
|
+
// about must refuse, not quietly resolve to no-publish. A silent false
|
|
257
|
+
// here is a lib that bumps, tags and cuts a release while publishing
|
|
258
|
+
// nothing, and the only trace is one note line in a long log.
|
|
259
|
+
if (p.npm == null && !isPrivate && p.npmWhenUndeclared === 'refuse')
|
|
260
|
+
errors.push(at('package.json is not private and this units[] entry does not say `npm` — add `npm: true` to publish it, or `private: true` to its package.json to keep it internal'))
|
|
261
|
+
if (errors.length > 0) return { error: errors }
|
|
262
|
+
const declaredNpm = p.npm ?? (p.npmWhenUndeclared === 'from-private-flag' && !isPrivate)
|
|
263
|
+
const tsconfig = p.tsconfigRel != null ? join(p.root, p.tsconfigRel) : join(p.dir, 'tsconfig.json')
|
|
264
|
+
return {
|
|
265
|
+
unit: {
|
|
266
|
+
id: p.id,
|
|
267
|
+
dir: p.dir,
|
|
268
|
+
rel: relative(p.root, p.dir),
|
|
269
|
+
name,
|
|
270
|
+
version,
|
|
271
|
+
changelog: join(p.root, p.changelogRel),
|
|
272
|
+
tagTemplate: p.tagTemplate,
|
|
273
|
+
tag: render(p.tagTemplate, { name, version }),
|
|
274
|
+
tagGlob: render(p.tagTemplate, { name, version: '*' }),
|
|
275
|
+
// the `!isPrivate` is a belt over the declaration: a private
|
|
276
|
+
// package never publishes, whatever anything else says
|
|
277
|
+
targets: { npm: declaredNpm && !isPrivate, tag: true, githubRelease: p.githubRelease },
|
|
278
|
+
isPrivate,
|
|
279
|
+
tsconfig: p.io.exists(relative(p.root, tsconfig)) ? tsconfig : null,
|
|
280
|
+
},
|
|
281
|
+
}
|
|
282
|
+
}
|
package/src/release/units.ts
CHANGED
|
@@ -1,227 +1,35 @@
|
|
|
1
|
-
// a repo is N
|
|
1
|
+
// a repo is N units, and every one releases unless it says `release: false`. The ROOT unit is mandatory, never declared,
|
|
2
2
|
// and configured by `release`. Sub-units are optional, exist only as entries
|
|
3
3
|
// in `units[]`, and are configured by their own entry. Nothing is scanned
|
|
4
4
|
// into existence: an undeclared package is caught by rule SK010, so
|
|
5
5
|
// resolution stays deterministic and a forgotten package still cannot hide.
|
|
6
|
+
// A `release: false` entry resolves into `testOnly`, never into `units`, so
|
|
7
|
+
// no release surface can list one: it only scopes tests (suite/test-units.ts).
|
|
6
8
|
//
|
|
7
9
|
// This module is THE source of unit facts. Every surface that used to
|
|
8
10
|
// rediscover packages (drift probes, per-lib tests, per-lib typecheck, the
|
|
9
11
|
// stranded-tag probe) reads it instead of walking the tree with its own rules.
|
|
10
|
-
import {
|
|
11
|
-
import
|
|
12
|
-
import { basename, join, relative } from 'path'
|
|
12
|
+
import { readFileSync, statSync } from 'fs'
|
|
13
|
+
import { join } from 'path'
|
|
13
14
|
import type { RepoConfig } from '../repo-config.ts'
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
tsconfig?: string
|
|
33
|
-
/** the unit's own suite budget, paths relative to the unit dir (suite/judge-suites.ts) */
|
|
34
|
-
tests?: TestsConfig
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const UNIT_DECL_KEYS: readonly string[] = ['dir', 'id', 'tag', 'changelog', 'npm', 'githubRelease', 'tsconfig', 'tests']
|
|
38
|
-
|
|
39
|
-
/** a unit, fully resolved: no optionals, no defaults left to re-apply downstream */
|
|
40
|
-
export type ReleaseUnit = {
|
|
41
|
-
id: string
|
|
42
|
-
/** absolute */
|
|
43
|
-
dir: string
|
|
44
|
-
/** repo-relative git pathspec, '' for the root */
|
|
45
|
-
rel: string
|
|
46
|
-
name: string
|
|
47
|
-
/** package.json version — THE only version source */
|
|
48
|
-
version: string
|
|
49
|
-
/** absolute path to the versioned changelog */
|
|
50
|
-
changelog: string
|
|
51
|
-
/** tag template, `{name}` and `{version}` unsubstituted — bump renders the NEXT tag from it */
|
|
52
|
-
tagTemplate: string
|
|
53
|
-
/** rendered tag for the current version */
|
|
54
|
-
tag: string
|
|
55
|
-
/** the tag template with `{version}` replaced by `*` — feeds the stranded-tag probe */
|
|
56
|
-
tagGlob: string
|
|
57
|
-
targets: { npm: boolean; tag: boolean; githubRelease: boolean }
|
|
58
|
-
/** the unit's OWN package.json `private: true`, never the repo's visibility */
|
|
59
|
-
isPrivate: boolean
|
|
60
|
-
/** absolute path, or null when the unit has no tsconfig of its own */
|
|
61
|
-
tsconfig: string | null
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** a unit that could not be described, and why. Scoped by id because the two
|
|
65
|
-
* cases differ: a broken DECLARED unit is a repo-wide declaration bug (rule
|
|
66
|
-
* SK010), while a root package that is not release-shaped only matters the
|
|
67
|
-
* day someone releases it. */
|
|
68
|
-
export type UnitError = { id: string; message: string }
|
|
69
|
-
|
|
70
|
-
export type UnitResolution = {
|
|
71
|
-
/** every unit that resolved — a broken sibling never hides the working ones */
|
|
72
|
-
units: ReleaseUnit[]
|
|
73
|
-
/** non-empty means no release of the named unit may proceed */
|
|
74
|
-
errors: UnitError[]
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
export const formatUnitError = (e: UnitError): string => `unit '${e.id}': ${e.message}`
|
|
78
|
-
|
|
79
|
-
const ROOT_TAG = 'v{version}'
|
|
80
|
-
const SUB_TAG = '{name}@{version}'
|
|
81
|
-
|
|
82
|
-
const render = (template: string, p: { name: string; version: string }): string =>
|
|
83
|
-
template.replaceAll('{name}', p.name).replaceAll('{version}', p.version)
|
|
84
|
-
|
|
85
|
-
type Pkg = {
|
|
86
|
-
name?: string
|
|
87
|
-
version?: string
|
|
88
|
-
private?: boolean
|
|
89
|
-
scripts?: Record<string, string>
|
|
90
|
-
dependencies?: Record<string, string>
|
|
91
|
-
peerDependencies?: Record<string, string>
|
|
92
|
-
optionalDependencies?: Record<string, string>
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function readPkg(dir: string): { pkg: Pkg } | { error: string } {
|
|
96
|
-
const p = join(dir, 'package.json')
|
|
97
|
-
if (!existsSync(p)) return { error: `no package.json in ${dir}` }
|
|
98
|
-
try { return { pkg: JSON.parse(readFileSync(p, 'utf8')) as Pkg } }
|
|
99
|
-
catch (e) { return { error: `${p} is unparseable (${String(e).slice(0, 120)})` } }
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** every releasable unit, root first then the declared sub-units in declared
|
|
103
|
-
* order. Release ORDER is different (releaseOrder below) — the root ships
|
|
104
|
-
* last because it depends on its sub-units, never the reverse. */
|
|
105
|
-
export function resolveUnits(root: string, config: RepoConfig): UnitResolution {
|
|
106
|
-
const errors: UnitError[] = []
|
|
107
|
-
const units: ReleaseUnit[] = []
|
|
108
|
-
const rc = config.release ?? {}
|
|
109
|
-
|
|
110
|
-
const rootPkg = readPkg(root)
|
|
111
|
-
if ('error' in rootPkg) errors.push({ id: 'root', message: `${rootPkg.error} — release reads name and version from it` })
|
|
112
|
-
else {
|
|
113
|
-
const u = build({
|
|
114
|
-
root,
|
|
115
|
-
id: 'root',
|
|
116
|
-
dir: root,
|
|
117
|
-
pkg: rootPkg.pkg,
|
|
118
|
-
tagTemplate: rc.tag ?? ROOT_TAG,
|
|
119
|
-
changelogRel: rc.changelog ?? 'CHANGELOG.md',
|
|
120
|
-
npm: rc.npm,
|
|
121
|
-
// the root IS the repo: its package.json private flag is the whole
|
|
122
|
-
// declaration, and a public root package publishes by default
|
|
123
|
-
npmWhenUndeclared: 'from-private-flag',
|
|
124
|
-
githubRelease: rc.githubRelease ?? true,
|
|
125
|
-
tsconfigRel: rc.tsconfig,
|
|
126
|
-
checkFolderName: false,
|
|
127
|
-
})
|
|
128
|
-
if ('error' in u) errors.push(...u.error)
|
|
129
|
-
else units.push(u.unit)
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const seen = new Set<string>(['root'])
|
|
133
|
-
for (const decl of config.units ?? []) {
|
|
134
|
-
const id = decl.id ?? (typeof decl.dir === 'string' ? basename(decl.dir) : '?')
|
|
135
|
-
const bad = Object.keys(decl).filter((k) => !UNIT_DECL_KEYS.includes(k))
|
|
136
|
-
if (bad.length > 0) { errors.push({ id, message: `unknown key(s) ${bad.join(', ')} (known: ${UNIT_DECL_KEYS.join(', ')})` }); continue }
|
|
137
|
-
if (typeof decl.dir !== 'string' || decl.dir === '') { errors.push({ id, message: 'every units[] entry needs a `dir`' }); continue }
|
|
138
|
-
const dir = join(root, decl.dir)
|
|
139
|
-
if (!existsSync(dir) || !statSync(dir).isDirectory()) { errors.push({ id, message: `'${decl.dir}' is not a directory` }); continue }
|
|
140
|
-
if (seen.has(id)) { errors.push({ id, message: `duplicate unit id — give '${decl.dir}' an explicit id` }); continue }
|
|
141
|
-
seen.add(id)
|
|
142
|
-
const pkg = readPkg(dir)
|
|
143
|
-
if ('error' in pkg) { errors.push({ id, message: pkg.error }); continue }
|
|
144
|
-
const u = build({
|
|
145
|
-
root,
|
|
146
|
-
id,
|
|
147
|
-
dir,
|
|
148
|
-
pkg: pkg.pkg,
|
|
149
|
-
tagTemplate: decl.tag ?? SUB_TAG,
|
|
150
|
-
changelogRel: decl.changelog ?? `${decl.dir}/CHANGELOG.md`,
|
|
151
|
-
npm: decl.npm,
|
|
152
|
-
npmWhenUndeclared: 'refuse',
|
|
153
|
-
githubRelease: decl.githubRelease ?? false,
|
|
154
|
-
tsconfigRel: decl.tsconfig,
|
|
155
|
-
// only the DEFAULT tag renders the package name into a git tag, so
|
|
156
|
-
// only there does the folder have to agree with it
|
|
157
|
-
checkFolderName: decl.tag == null,
|
|
158
|
-
})
|
|
159
|
-
if ('error' in u) errors.push(...u.error)
|
|
160
|
-
else units.push(u.unit)
|
|
161
|
-
}
|
|
162
|
-
return { units, errors }
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function build(p: {
|
|
166
|
-
root: string
|
|
167
|
-
id: string
|
|
168
|
-
dir: string
|
|
169
|
-
pkg: Pkg
|
|
170
|
-
tagTemplate: string
|
|
171
|
-
changelogRel: string
|
|
172
|
-
npm: boolean | undefined
|
|
173
|
-
/** how an UNDECLARED npm target resolves. The root IS the repo, so its
|
|
174
|
-
* package.json private flag is the whole declaration. A sub-unit has no
|
|
175
|
-
* such declaration, so a non-private package with no `npm` key is
|
|
176
|
-
* ambiguous and REFUSES. */
|
|
177
|
-
npmWhenUndeclared: 'from-private-flag' | 'refuse'
|
|
178
|
-
githubRelease: boolean
|
|
179
|
-
tsconfigRel: string | undefined
|
|
180
|
-
checkFolderName: boolean
|
|
181
|
-
}): { unit: ReleaseUnit } | { error: UnitError[] } {
|
|
182
|
-
const errors: UnitError[] = []
|
|
183
|
-
const at = (message: string): UnitError => ({ id: p.id, message })
|
|
184
|
-
const name = p.pkg.name ?? ''
|
|
185
|
-
const version = p.pkg.version ?? ''
|
|
186
|
-
if (name === '' || version === '') errors.push(at('package.json needs both name and version'))
|
|
187
|
-
// under the default template the package NAME becomes the git tag, so the
|
|
188
|
-
// folder it lives in must be findable from that tag. A scope satisfies it
|
|
189
|
-
// (`@acme/thing` in `thing/`), and an explicit `tag` opts out entirely.
|
|
190
|
-
const base = basename(p.dir)
|
|
191
|
-
if (p.checkFolderName && name !== '' && name !== base && !name.endsWith(`/${base}`))
|
|
192
|
-
errors.push(at(`package name '${name}' must equal the folder name '${base}' (a scope is fine: '@scope/${base}') — or declare an explicit \`tag\` for this unit`))
|
|
193
|
-
const isPrivate = p.pkg.private === true
|
|
194
|
-
// npm's own unconditional refusal. Declaring npm:true on top of it is a
|
|
195
|
-
// contradiction, not a preference — surface it instead of silently winning
|
|
196
|
-
if (p.npm === true && isPrivate)
|
|
197
|
-
errors.push(at('npm: true but package.json says private: true — drop one, npm refuses the publish either way'))
|
|
198
|
-
// "declared, never inferred" cuts BOTH ways: the package nobody decided
|
|
199
|
-
// about must refuse, not quietly resolve to no-publish. A silent false
|
|
200
|
-
// here is a lib that bumps, tags and cuts a release while publishing
|
|
201
|
-
// nothing, and the only trace is one note line in a long log.
|
|
202
|
-
if (p.npm == null && !isPrivate && p.npmWhenUndeclared === 'refuse')
|
|
203
|
-
errors.push(at('package.json is not private and this units[] entry does not say `npm` — add `npm: true` to publish it, or `private: true` to its package.json to keep it internal'))
|
|
204
|
-
if (errors.length > 0) return { error: errors }
|
|
205
|
-
const declaredNpm = p.npm ?? (p.npmWhenUndeclared === 'from-private-flag' && !isPrivate)
|
|
206
|
-
const tsconfig = p.tsconfigRel != null ? join(p.root, p.tsconfigRel) : join(p.dir, 'tsconfig.json')
|
|
207
|
-
return {
|
|
208
|
-
unit: {
|
|
209
|
-
id: p.id,
|
|
210
|
-
dir: p.dir,
|
|
211
|
-
rel: relative(p.root, p.dir),
|
|
212
|
-
name,
|
|
213
|
-
version,
|
|
214
|
-
changelog: join(p.root, p.changelogRel),
|
|
215
|
-
tagTemplate: p.tagTemplate,
|
|
216
|
-
tag: render(p.tagTemplate, { name, version }),
|
|
217
|
-
tagGlob: render(p.tagTemplate, { name, version: '*' }),
|
|
218
|
-
// the `!isPrivate` is a belt over the declaration: a private
|
|
219
|
-
// package never publishes, whatever anything else says
|
|
220
|
-
targets: { npm: declaredNpm && !isPrivate, tag: true, githubRelease: p.githubRelease },
|
|
221
|
-
isPrivate,
|
|
222
|
-
tsconfig: existsSync(tsconfig) ? tsconfig : null,
|
|
223
|
-
},
|
|
224
|
-
}
|
|
15
|
+
import { readPkg, render, resolveUnits as resolveWith, type Pkg, type ReleaseUnit, type UnitReader, type UnitResolution } from './unit-resolve.ts'
|
|
16
|
+
|
|
17
|
+
export * from './unit-resolve.ts'
|
|
18
|
+
|
|
19
|
+
export const fsUnitReader = (root: string): UnitReader => ({
|
|
20
|
+
text: (rel) => { try { return readFileSync(join(root, rel), 'utf8') } catch { return null } },
|
|
21
|
+
exists: (rel, kind) => {
|
|
22
|
+
try {
|
|
23
|
+
const st = statSync(join(root, rel))
|
|
24
|
+
return kind == null || (kind === 'dir' ? st.isDirectory() : st.isFile())
|
|
25
|
+
} catch { return false }
|
|
26
|
+
},
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/** every releasable unit, read from the disk (release/unit-resolve.ts owns the
|
|
30
|
+
* resolution and its order) */
|
|
31
|
+
export function resolveUnits(root: string, config: RepoConfig, io: UnitReader = fsUnitReader(root)): UnitResolution {
|
|
32
|
+
return resolveWith(root, config, io)
|
|
225
33
|
}
|
|
226
34
|
|
|
227
35
|
/** publish order: sub-units first, the root last. The root depends on what
|
|
@@ -240,7 +48,7 @@ export function releaseOrder(units: ReleaseUnit[]): ReleaseUnit[] {
|
|
|
240
48
|
const deps = new Map<string, Set<string>>()
|
|
241
49
|
for (const u of subs) {
|
|
242
50
|
const need = new Set<string>()
|
|
243
|
-
const read = readPkg(u.dir)
|
|
51
|
+
const read = readPkg(u.dir, u.dir, fsUnitReader(u.dir))
|
|
244
52
|
const manifest: Pkg = 'error' in read ? {} : read.pkg
|
|
245
53
|
for (const block of ['dependencies', 'peerDependencies', 'optionalDependencies'] as const)
|
|
246
54
|
for (const dep of Object.keys(manifest[block] ?? {})) {
|
|
@@ -271,6 +79,13 @@ export function releaseOrder(units: ReleaseUnit[]): ReleaseUnit[] {
|
|
|
271
79
|
|
|
272
80
|
export const unitById = (units: ReleaseUnit[], id: string): ReleaseUnit | undefined => units.find((u) => u.id === id)
|
|
273
81
|
|
|
82
|
+
/** why `id` names no releasing unit. A `release: false` unit says so, rather
|
|
83
|
+
* than reading as a typo */
|
|
84
|
+
export function unknownUnit(r: UnitResolution, id: string): string {
|
|
85
|
+
if (r.testOnly.some((u) => u.id === id)) return `unit '${id}' is declared release: false, so it never releases (its tests run as test:${id})`
|
|
86
|
+
return `no unit '${id}' — releasing units: ${r.units.map((u) => u.id).join(', ')}`
|
|
87
|
+
}
|
|
88
|
+
|
|
274
89
|
/** the tag this unit WOULD carry at another version — bump needs it before it
|
|
275
90
|
* writes, to refuse on a tag that already exists */
|
|
276
91
|
export const renderTag = (unit: ReleaseUnit, version: string): string => render(unit.tagTemplate, { name: unit.name, version })
|
|
@@ -290,6 +105,7 @@ export function prepublishScripts(scripts: Record<string, string>): string[] {
|
|
|
290
105
|
/** the unit's own prepublish hooks, read from ITS package.json (a sub-unit's
|
|
291
106
|
* hooks are invisible to the root's) */
|
|
292
107
|
export function unitPrepublishScripts(unit: ReleaseUnit): string[] {
|
|
293
|
-
const p = readPkg(unit.dir)
|
|
108
|
+
const p = readPkg(unit.dir, unit.dir, fsUnitReader(unit.dir))
|
|
294
109
|
return 'error' in p ? [] : prepublishScripts(p.pkg.scripts ?? {})
|
|
295
110
|
}
|
|
111
|
+
|