rvlib-shipkit 0.38.1 → 1.0.0

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.
Files changed (252) hide show
  1. package/CHANGELOG.md +367 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/generate-roll.ts +1 -1
  6. package/src/assets/kit-config.ts +6 -6
  7. package/src/assets/run-assets.ts +14 -10
  8. package/src/assets/server/studio-server.ts +6 -5
  9. package/src/check.ts +343 -0
  10. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  11. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  12. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  13. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  14. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
  15. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  16. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  17. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  18. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  19. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  20. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  21. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  22. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  23. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  24. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  25. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  26. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  27. package/src/checks/SK018-test-files-run.check.ts +28 -0
  28. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  29. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  30. package/src/checks/contracts.check.ts +20 -0
  31. package/src/cli/cmd_Asks.ts +3 -2
  32. package/src/cli/cmd_Assets.ts +2 -2
  33. package/src/cli/cmd_Bump.ts +1 -1
  34. package/src/cli/cmd_Check.ts +6 -6
  35. package/src/cli/cmd_Checks.ts +95 -0
  36. package/src/cli/cmd_Ci.ts +48 -13
  37. package/src/cli/cmd_Commands.ts +2 -6
  38. package/src/cli/cmd_ConfigKeys.ts +69 -0
  39. package/src/cli/cmd_Contracts.ts +1 -1
  40. package/src/cli/cmd_Fleet.ts +6 -2
  41. package/src/cli/cmd_Hooks.ts +1 -1
  42. package/src/cli/cmd_Keys.ts +43 -18
  43. package/src/cli/cmd_Mv.ts +2 -2
  44. package/src/cli/cmd_Prose.ts +10 -1
  45. package/src/cli/cmd_Releases.ts +2 -2
  46. package/src/cli/cmd_Serve.ts +1 -1
  47. package/src/cli/cmd_Start.ts +6 -6
  48. package/src/cli/cmd_Status.ts +25 -0
  49. package/src/cli/cmd_Tui.ts +2 -2
  50. package/src/cli.ts +7 -4
  51. package/src/collections/actions.ts +4 -4
  52. package/src/collections/checks.ts +258 -0
  53. package/src/collections/collection-types.ts +254 -1
  54. package/src/collections/comfy.ts +23 -0
  55. package/src/collections/md-body.ts +14 -0
  56. package/src/collections/pings.ts +38 -12
  57. package/src/collections/posts.ts +19 -3
  58. package/src/collections/providers.ts +198 -91
  59. package/src/collections/reflections.ts +41 -0
  60. package/src/collections/state-rules.ts +73 -0
  61. package/src/collections/test-time.ts +288 -0
  62. package/src/collections/tests.ts +181 -0
  63. package/src/collections/wikis.ts +66 -0
  64. package/src/comfy/comfy-panel.ts +292 -0
  65. package/src/contracts/apply-contracts.ts +17 -12
  66. package/src/contracts/contract-types.ts +16 -4
  67. package/src/contracts/contracts-engine.ts +78 -42
  68. package/src/contracts/manifests/discord.contract.ts +6 -6
  69. package/src/contracts/manifests/identity.contract.ts +3 -3
  70. package/src/contracts/manifests/itch.contract.ts +12 -15
  71. package/src/contracts/manifests/social.contract.ts +5 -5
  72. package/src/contracts/manifests/steam.contract.ts +16 -16
  73. package/src/contracts/run-contracts.ts +4 -3
  74. package/src/core/attach.ts +96 -0
  75. package/src/core/canonical-scripts.ts +25 -0
  76. package/src/core/check-ctx.ts +204 -14
  77. package/src/core/check-file-names.ts +112 -0
  78. package/src/core/check-pass.ts +84 -0
  79. package/src/core/check-runner.ts +796 -0
  80. package/src/core/config-keys.ts +256 -0
  81. package/src/core/config-rewrite.ts +77 -0
  82. package/src/core/discover-checks.ts +166 -90
  83. package/src/core/discover-legacy-checks.ts +26 -0
  84. package/src/core/doc-block.ts +13 -0
  85. package/src/core/floor-resolve.ts +264 -0
  86. package/src/core/floor.ts +20 -137
  87. package/src/core/git-bin.ts +24 -0
  88. package/src/core/git-root.ts +108 -0
  89. package/src/core/instance.ts +195 -0
  90. package/src/core/jobs.ts +118 -0
  91. package/src/core/layout.ts +52 -0
  92. package/src/core/legacy-shapes.ts +207 -0
  93. package/src/core/lib-root.ts +5 -0
  94. package/src/core/locked-write.ts +110 -39
  95. package/src/core/machine-commit.ts +5 -4
  96. package/src/core/migrate-layout.ts +557 -0
  97. package/src/core/motherhome.ts +2 -8
  98. package/src/core/nesting.ts +4 -12
  99. package/src/core/one-way-in-policy.ts +73 -17
  100. package/src/core/outcome-words.ts +45 -0
  101. package/src/core/package-manager.ts +86 -0
  102. package/src/core/parallel.ts +21 -0
  103. package/src/core/process-tree.ts +44 -0
  104. package/src/core/raw-access.ts +93 -0
  105. package/src/core/read-set.ts +152 -0
  106. package/src/core/run-check.ts +155 -213
  107. package/src/core/run-ci.ts +153 -189
  108. package/src/core/run-fix.ts +96 -0
  109. package/src/core/run-fixers.ts +53 -30
  110. package/src/core/self-update.ts +9 -3
  111. package/src/core/slots.ts +83 -0
  112. package/src/core/spawn-timeout.ts +34 -3
  113. package/src/core/when-block.ts +401 -0
  114. package/src/core/when.ts +135 -0
  115. package/src/core/write-status.ts +5 -2
  116. package/src/hooks/leak-guard.ts +10 -8
  117. package/src/hooks/one-way-in-guard.ts +13 -5
  118. package/src/hooks/root-identity.ts +2 -1
  119. package/src/hooks/run-hooks.ts +35 -23
  120. package/src/init-repo.ts +39 -23
  121. package/src/ledger/check-stats.ts +155 -0
  122. package/src/ledger/check-table.ts +179 -0
  123. package/src/ledger/db.ts +255 -0
  124. package/src/ledger/gate-checks.ts +433 -0
  125. package/src/ledger/git-key.ts +116 -77
  126. package/src/ledger/git-scan.ts +96 -35
  127. package/src/ledger/ledger.ts +116 -42
  128. package/src/ledger/live-runs.ts +111 -0
  129. package/src/ledger/live-timeline.ts +97 -0
  130. package/src/ledger/repo-view.ts +73 -0
  131. package/src/ledger/run-log.ts +448 -0
  132. package/src/ledger/run-timeline.ts +173 -0
  133. package/src/ledger/store.ts +23 -12
  134. package/src/ledger/suite-log.ts +161 -0
  135. package/src/ledger/toolchain-identity.ts +7 -1
  136. package/src/ledger/world-cache.ts +111 -0
  137. package/src/mv/mv-md.ts +16 -6
  138. package/src/problems/extract.ts +182 -0
  139. package/src/problems/problem-board.ts +37 -0
  140. package/src/problems/problem-line.ts +77 -0
  141. package/src/prose/prose-view.ts +90 -0
  142. package/src/prose/run-prose.ts +60 -19
  143. package/src/prose/ste.ts +140 -24
  144. package/src/reflections/reflections-queue.ts +81 -0
  145. package/src/release/git-facts.ts +15 -9
  146. package/src/release/run-bump.ts +6 -5
  147. package/src/release/run-release.ts +25 -11
  148. package/src/release/run-releases.ts +2 -2
  149. package/src/release/unit-gate.ts +7 -54
  150. package/src/release/unit-resolve.ts +282 -0
  151. package/src/release/units.ts +33 -217
  152. package/src/repo-config.ts +155 -85
  153. package/src/rules/rule.ts +36 -96
  154. package/src/rules/untrack-generated.ts +5 -4
  155. package/src/scaffold/files/vscode-tasks.json +10 -31
  156. package/src/scaffold/jsonc-spans.ts +113 -0
  157. package/src/scaffold/project-scaffold.ts +16 -43
  158. package/src/scan.ts +98 -0
  159. package/src/serve/gate-trigger.ts +148 -0
  160. package/src/serve/health-schedule.ts +102 -0
  161. package/src/serve/hub-app/chart-view.ts +179 -0
  162. package/src/serve/hub-app/check-view.tsx +345 -0
  163. package/src/serve/hub-app/check-words.ts +164 -0
  164. package/src/serve/hub-app/checks-svc.ts +280 -0
  165. package/src/serve/hub-app/clock-svc.ts +33 -0
  166. package/src/serve/hub-app/col-card.tsx +23 -39
  167. package/src/serve/hub-app/cols-svc.ts +44 -20
  168. package/src/serve/hub-app/comfy-svc.ts +171 -0
  169. package/src/serve/hub-app/comfy-view.tsx +81 -0
  170. package/src/serve/hub-app/dur.ts +41 -0
  171. package/src/serve/hub-app/hash-route.ts +121 -0
  172. package/src/serve/hub-app/header-bar.tsx +117 -0
  173. package/src/serve/hub-app/health-svc.ts +23 -28
  174. package/src/serve/hub-app/help.tsx +29 -0
  175. package/src/serve/hub-app/hub-app.tsx +122 -119
  176. package/src/serve/hub-app/hub-st.ts +115 -42
  177. package/src/serve/hub-app/hub-types.ts +117 -2
  178. package/src/serve/hub-app/hub.css +534 -192
  179. package/src/serve/hub-app/index.html +2 -0
  180. package/src/serve/hub-app/live-svc.ts +174 -0
  181. package/src/serve/hub-app/md.ts +15 -0
  182. package/src/serve/hub-app/media-svc.ts +4 -1
  183. package/src/serve/hub-app/media-view.tsx +15 -0
  184. package/src/serve/hub-app/next-run.ts +68 -0
  185. package/src/serve/hub-app/project-gutter.css +65 -0
  186. package/src/serve/hub-app/project-gutter.tsx +128 -0
  187. package/src/serve/hub-app/projects-svc.ts +226 -0
  188. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  189. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  190. package/src/serve/hub-app/prose-hits.css +21 -0
  191. package/src/serve/hub-app/run-strip.ts +47 -0
  192. package/src/serve/hub-app/runs-tabs.ts +24 -0
  193. package/src/serve/hub-app/test-view.tsx +107 -0
  194. package/src/serve/hub-app/tests-svc.ts +62 -0
  195. package/src/serve/hub-app/tests-view.tsx +238 -0
  196. package/src/serve/hub-app/time-ago.tsx +34 -0
  197. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  198. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  199. package/src/serve/hub-app/timeline-svc.ts +62 -0
  200. package/src/serve/hub-app/timeline-view.tsx +245 -0
  201. package/src/serve/hub-app/tree-build.ts +410 -0
  202. package/src/serve/hub-app/tree-row.tsx +42 -0
  203. package/src/serve/hub-app/tree-svc.ts +174 -0
  204. package/src/serve/hub-app/wikis-view.tsx +53 -0
  205. package/src/serve/other-repos.ts +79 -0
  206. package/src/serve/project-gutter-route.ts +350 -0
  207. package/src/serve/project-gutter.ts +449 -0
  208. package/src/serve/run-serve.ts +245 -16
  209. package/src/serve/same-origin.ts +14 -0
  210. package/src/serve/serve-hub.ts +213 -18
  211. package/src/shipkit.ts +19 -12
  212. package/src/smoke/run-smoke.ts +3 -2
  213. package/src/start-repo.ts +75 -36
  214. package/src/suite/judge-suites.ts +209 -15
  215. package/src/suite/kept-timings.ts +58 -0
  216. package/src/suite/runner-shapes.ts +88 -0
  217. package/src/suite/suite-cli.ts +147 -24
  218. package/src/suite/suite-cpu-preload.ts +92 -0
  219. package/src/suite/test-units.ts +243 -0
  220. package/src/ts/config.ts +4 -69
  221. package/src/ts/jsonc.ts +69 -0
  222. package/src/ts/lex.ts +143 -0
  223. package/src/ts/rename.ts +73 -10
  224. package/src/ts/source-graph.ts +28 -8
  225. package/src/ts/tsconfig-projects.ts +6 -97
  226. package/src/ts/tsconfig-resolve.ts +108 -0
  227. package/src/tui/build-steps-panel.ts +61 -0
  228. package/src/tui/checks-panel.ts +24 -16
  229. package/src/tui/run-tui.ts +2 -2
  230. package/src/tui/tui-panel.ts +5 -4
  231. package/src/wiki/graph-colors.ts +68 -0
  232. package/src/wiki/graph-mount.ts +7 -9
  233. package/src/wiki/graph-page.ts +42 -25
  234. package/src/wiki/graph-positions.ts +105 -0
  235. package/src/wiki/run-wiki.ts +1 -1
  236. package/src/wiki/serve-graph.ts +33 -3
  237. package/src/cli/cmd_Rules.ts +0 -51
  238. package/src/cli/cmd_Units.ts +0 -82
  239. package/src/collections/graphs.ts +0 -32
  240. package/src/contracts/contracts-once.ts +0 -43
  241. package/src/core/check-suite.ts +0 -302
  242. package/src/core/repair-contract-dirs.ts +0 -136
  243. package/src/core/task-runner.ts +0 -339
  244. package/src/ledger/gate-units.ts +0 -138
  245. package/src/ledger/unit-table.ts +0 -105
  246. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  247. package/src/rules/SK003-claude-md-shape.ts +0 -70
  248. package/src/rules/SK004-gitignore-policy.ts +0 -238
  249. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  250. package/src/rules/rule-registry.ts +0 -64
  251. package/src/rules/run-fix.ts +0 -72
  252. package/src/tui/tasks-panel.ts +0 -60
@@ -1,21 +1,14 @@
1
- // what a single unit must prove before its version is written or published.
2
- // The ROOT unit is the repo, so `shipkit ci` already gates its tests and its
3
- // typecheck; this adds the per-unit proof only a SUB-unit lacks, because a
4
- // lib compiles and tests standalone under its own tsconfig and the root
5
- // tsconfig deliberately excludes it.
6
- import { existsSync } from 'fs'
7
- import { join } from 'path'
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
+ }
@@ -1,227 +1,35 @@
1
- // a repo is N releasable units. The ROOT unit is mandatory, never declared,
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 { existsSync, readFileSync, statSync } from 'fs'
11
- import type { TestsConfig } from '../suite/judge-suites.ts'
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
- export type ReleaseUnitDecl = {
16
- /** repo-relative directory holding the unit's package.json — the only required key */
17
- dir: string
18
- /** default: basename(dir) */
19
- id?: string
20
- /** tag template, `{name}` and `{version}` substituted (default '{name}@{version}') */
21
- tag?: string
22
- /** repo-relative changelog (default '<dir>/CHANGELOG.md') */
23
- changelog?: string
24
- /** publish this sub-unit to npm. REQUIRED unless its package.json says
25
- * `private: true`: the npm target is declared, never inferred, and the
26
- * ambiguous package REFUSES rather than defaulting to the quiet answer.
27
- * `true` on a private package.json is a resolution FAILURE */
28
- npm?: boolean
29
- /** cut a GitHub release for this unit's tag (default false for sub-units) */
30
- githubRelease?: boolean
31
- /** repo-relative tsconfig for the unit's own typecheck (default '<dir>/tsconfig.json' when it exists) */
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
+