rvlib-shipkit 0.38.0 → 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 +371 -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 +216 -18
  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
package/src/ts/config.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  // skip entries are repo-relative dir prefixes; the defaults below always apply.
13
13
  import { existsSync, readdirSync, readFileSync, statSync } from 'fs'
14
14
  import { join } from 'path'
15
+ import { gitRoot } from '../core/git-root.ts'
15
16
 
16
17
  export type RvtPackage = {
17
18
  name: string // package.json "name" — the import root for files inside
@@ -35,78 +36,12 @@ export const ARTIFACT_SKIP = ['dist', 'build', 'cjs', 'out']
35
36
  export const DEFAULT_SKIP = [...ALWAYS_SKIP, ...ARTIFACT_SKIP]
36
37
 
37
38
  export function findRepoRoot(cwd: string = process.cwd()): string {
38
- const p = Bun.spawnSync(['git', 'rev-parse', '--show-toplevel'], { cwd, stdout: 'pipe', stderr: 'ignore' })
39
- const out = p.exitCode === 0 ? p.stdout.toString().trim() : ''
40
- return out !== '' ? out : cwd
39
+ return gitRoot(cwd)?.top ?? cwd
41
40
  }
42
41
 
43
- // tsconfig files are JSONC: strip comments and trailing commas.
44
- //
45
- // A quote-aware scanner rather than regexes, because tsconfig is the one
46
- // dialect where the regex version is guaranteed to be wrong: `"src/**/*"` is
47
- // a `/*` with its own `*/`, so a block-comment regex eats an arbitrary span of
48
- // a perfectly valid file. Measured across ~/dev, the regex version THREW on 8
49
- // real tsconfigs (this repo's included) and silently rewrote `"src/**/*.ts"`
50
- // to `"src*.ts"` in 8 more. This parser gates typecheck AND publish, so it
51
- // does not get to be loose.
52
- export function parseJsonc(text: string): unknown {
53
- let out = ''
54
- let i = 0
55
- while (i < text.length) {
56
- const c = text[i]!
57
- if (c === '"') {
58
- out += c
59
- i++
60
- while (i < text.length) {
61
- const s = text[i]!
62
- out += s
63
- i++
64
- if (s === '\\') { out += text[i] ?? ''; i++; continue }
65
- if (s === '"') break
66
- }
67
- continue
68
- }
69
- if (c === '/' && text[i + 1] === '/') { while (i < text.length && text[i] !== '\n') i++; continue }
70
- if (c === '/' && text[i + 1] === '*') {
71
- i += 2
72
- while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++
73
- i += 2
74
- continue
75
- }
76
- out += c
77
- i++
78
- }
79
- return JSON.parse(dropTrailingCommas(out))
80
- }
42
+ import { parseJsonc } from './jsonc.ts'
81
43
 
82
- /** a comma before the next `}` or `]`, outside any string, is dropped */
83
- function dropTrailingCommas(text: string): string {
84
- let out = ''
85
- let i = 0
86
- let pendingComma = -1 // index in `out` of a comma awaiting its next token
87
- while (i < text.length) {
88
- const c = text[i]!
89
- if (c === '"') {
90
- if (pendingComma >= 0) pendingComma = -1
91
- out += c
92
- i++
93
- while (i < text.length) {
94
- const s = text[i]!
95
- out += s
96
- i++
97
- if (s === '\\') { out += text[i] ?? ''; i++; continue }
98
- if (s === '"') break
99
- }
100
- continue
101
- }
102
- if (c === ',') { pendingComma = out.length; out += c; i++; continue }
103
- if ((c === '}' || c === ']') && pendingComma >= 0) out = out.slice(0, pendingComma) + out.slice(pendingComma + 1)
104
- if (!/\s/.test(c)) pendingComma = -1
105
- out += c
106
- i++
107
- }
108
- return out
109
- }
44
+ export { parseJsonc }
110
45
 
111
46
  function rootsFromTsconfig(root: string): string[] {
112
47
  for (const name of ['tsconfig.base.json', 'tsconfig.json']) {
@@ -0,0 +1,69 @@
1
+ // tsconfig files are JSONC: strip comments and trailing commas. Its own module
2
+ // with no disk access, so a caller that parses a tsconfig it was handed never
3
+ // imports the loader in ts/config.ts (core/raw-access.ts).
4
+ //
5
+ // A quote-aware scanner rather than regexes, because tsconfig is the one
6
+ // dialect where the regex version is guaranteed to be wrong: `"src/**/*"` is
7
+ // a `/*` with its own `*/`, so a block-comment regex eats an arbitrary span of
8
+ // a perfectly valid file. Measured across ~/dev, the regex version THREW on 8
9
+ // real tsconfigs (this repo's included) and silently rewrote `"src/**/*.ts"`
10
+ // to `"src*.ts"` in 8 more. This parser gates typecheck AND publish, so it
11
+ // does not get to be loose.
12
+ export function parseJsonc(text: string): unknown {
13
+ let out = ''
14
+ let i = 0
15
+ while (i < text.length) {
16
+ const c = text[i]!
17
+ if (c === '"') {
18
+ out += c
19
+ i++
20
+ while (i < text.length) {
21
+ const s = text[i]!
22
+ out += s
23
+ i++
24
+ if (s === '\\') { out += text[i] ?? ''; i++; continue }
25
+ if (s === '"') break
26
+ }
27
+ continue
28
+ }
29
+ if (c === '/' && text[i + 1] === '/') { while (i < text.length && text[i] !== '\n') i++; continue }
30
+ if (c === '/' && text[i + 1] === '*') {
31
+ i += 2
32
+ while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) i++
33
+ i += 2
34
+ continue
35
+ }
36
+ out += c
37
+ i++
38
+ }
39
+ return JSON.parse(dropTrailingCommas(out))
40
+ }
41
+
42
+ /** a comma before the next `}` or `]`, outside any string, is dropped */
43
+ function dropTrailingCommas(text: string): string {
44
+ let out = ''
45
+ let i = 0
46
+ let pendingComma = -1 // index in `out` of a comma awaiting its next token
47
+ while (i < text.length) {
48
+ const c = text[i]!
49
+ if (c === '"') {
50
+ if (pendingComma >= 0) pendingComma = -1
51
+ out += c
52
+ i++
53
+ while (i < text.length) {
54
+ const s = text[i]!
55
+ out += s
56
+ i++
57
+ if (s === '\\') { out += text[i] ?? ''; i++; continue }
58
+ if (s === '"') break
59
+ }
60
+ continue
61
+ }
62
+ if (c === ',') { pendingComma = out.length; out += c; i++; continue }
63
+ if ((c === '}' || c === ']') && pendingComma >= 0) out = out.slice(0, pendingComma) + out.slice(pendingComma + 1)
64
+ if (!/\s/.test(c)) pendingComma = -1
65
+ out += c
66
+ i++
67
+ }
68
+ return out
69
+ }
package/src/ts/lex.ts ADDED
@@ -0,0 +1,143 @@
1
+ // a small TypeScript lexer for guards that read source as TEXT: comments
2
+ // dropped, every string literal and template chunk pulled out as data, and the
3
+ // code left with a `\0<index>\0` placeholder where each one stood. It is not a
4
+ // parser. It knows the four things that make a naive regex lie: comments,
5
+ // template `${…}` nesting, regex literals, and an apostrophe inside JSX text.
6
+ //
7
+ // The constraint a reader breaks first: a `/` is a regex only where an
8
+ // expression can start, which is read off the last significant code character.
9
+
10
+ export type Lexed = {
11
+ /** the source without comments, each literal replaced by `\0<i>\0` */
12
+ code: string
13
+ /** the literal texts, escapes resolved, a template's chunks one entry each */
14
+ strings: string[]
15
+ /** per entry of `strings`: true for a template chunk, false for a quoted literal */
16
+ template: boolean[]
17
+ }
18
+
19
+ const SIMPLE_ESCAPES: Record<string, string> = { n: '\n', t: '\t', r: '\r', b: '\b', f: '\f', v: '\v', '0': '\0' }
20
+
21
+ /** the escape at src[j] (the backslash): the text it stands for, and its length */
22
+ function unescapeAt(src: string, j: number): [string, number] {
23
+ const c = src[j + 1] ?? ''
24
+ if (c in SIMPLE_ESCAPES) return [SIMPLE_ESCAPES[c]!, 2]
25
+ if (c === 'x' && /^[\da-f]{2}$/i.test(src.slice(j + 2, j + 4))) return [String.fromCharCode(parseInt(src.slice(j + 2, j + 4), 16)), 4]
26
+ if (c === 'u' && src[j + 2] === '{') {
27
+ const end = src.indexOf('}', j + 3)
28
+ const cp = parseInt(src.slice(j + 3, end), 16)
29
+ if (end > 0 && Number.isFinite(cp)) return [String.fromCodePoint(cp), end - j + 1]
30
+ }
31
+ if (c === 'u' && /^[\da-f]{4}$/i.test(src.slice(j + 2, j + 6))) return [String.fromCharCode(parseInt(src.slice(j + 2, j + 6), 16)), 6]
32
+ if (c === '\n') return ['', 2]
33
+ return [c, 2]
34
+ }
35
+
36
+ const REGEX_BEFORE = new Set(['', '(', ',', '=', ':', '[', '!', '&', '|', '?', '{', '}', ';', '+', '-', '*', '%', '<', '>', '~', '^'])
37
+ const REGEX_AFTER_WORD = /(?:^|[^\w$.])(?:return|typeof|case|do|else|in|of|void|yield|await|delete|throw|new)$/
38
+
39
+ export function lexSource(src: string, o: { jsx?: boolean } = {}): Lexed {
40
+ const strings: string[] = []
41
+ const template: boolean[] = []
42
+ let code = ''
43
+ let i = 0
44
+ const put = (text: string, isTemplate: boolean): void => { strings.push(text); template.push(isTemplate); code += `\0${strings.length - 1}\0` }
45
+ const lastSignificant = (): string => { const t = code.trimEnd(); return t.at(-1) ?? '' }
46
+
47
+ /** code until the `}` closing a `${`, at brace depth 0 */
48
+ const codeUntilClose = (): void => {
49
+ let depth = 0
50
+ while (i < src.length) {
51
+ const ch = src[i]!
52
+ if (ch === '}' && depth === 0) { i++; return }
53
+ if (ch === '{') depth++
54
+ if (ch === '}') depth--
55
+ if (!step()) { code += ch; i++ }
56
+ }
57
+ }
58
+
59
+ const readTemplate = (): void => {
60
+ i++ // the opening backtick
61
+ let text = ''
62
+ while (i < src.length && src[i] !== '`') {
63
+ if (src[i] === '\\') { const [t, n] = unescapeAt(src, i); text += t; i += n; continue }
64
+ if (src[i] === '$' && src[i + 1] === '{') {
65
+ put(text, true)
66
+ text = ''
67
+ i += 2
68
+ code += '${'
69
+ codeUntilClose()
70
+ code += '}'
71
+ continue
72
+ }
73
+ text += src[i]
74
+ i++
75
+ }
76
+ i++ // the closing backtick
77
+ put(text, true)
78
+ }
79
+
80
+ /** one lexical element that is not plain code; false when src[i] is plain */
81
+ const step = (): boolean => {
82
+ const ch = src[i]!
83
+ const next = src[i + 1]
84
+ if (ch === '/' && next === '/') {
85
+ while (i < src.length && src[i] !== '\n') i++
86
+ return true
87
+ }
88
+ if (ch === '/' && next === '*') {
89
+ const end = src.indexOf('*/', i + 2)
90
+ i = end < 0 ? src.length : end + 2
91
+ return true
92
+ }
93
+ if (ch === '`') { code += '`'; readTemplate(); return true }
94
+ if (ch === "'" || ch === '"') {
95
+ // an apostrophe right after a letter is JSX text (`don't`), never a
96
+ // string: in code a literal never follows an identifier character
97
+ if (o.jsx === true && /[\p{L}\d]/u.test(src[i - 1] ?? '')) return false
98
+ let j = i + 1
99
+ let text = ''
100
+ while (j < src.length && src[j] !== ch && src[j] !== '\n') {
101
+ if (src[j] === '\\') { const [t, n] = unescapeAt(src, j); text += t; j += n; continue }
102
+ text += src[j]
103
+ j++
104
+ }
105
+ put(text, false)
106
+ i = j + 1
107
+ return true
108
+ }
109
+ if (ch === '/') {
110
+ const last = lastSignificant()
111
+ // `</tag>` and `<Tag />` are markup, never a regex
112
+ if (o.jsx === true && (code.endsWith('<') || next === '>')) return false
113
+ if (!REGEX_BEFORE.has(last) && !REGEX_AFTER_WORD.test(code.trimEnd())) return false
114
+ let j = i + 1
115
+ let inClass = false
116
+ while (j < src.length && src[j] !== '\n') {
117
+ if (src[j] === '\\') { j += 2; continue }
118
+ if (src[j] === '[') inClass = true
119
+ else if (src[j] === ']') inClass = false
120
+ else if (src[j] === '/' && !inClass) break
121
+ j++
122
+ }
123
+ j++
124
+ while (j < src.length && /[a-z]/.test(src[j]!)) j++
125
+ code += '/re/'
126
+ i = j
127
+ return true
128
+ }
129
+ return false
130
+ }
131
+
132
+ while (i < src.length) {
133
+ if (!step()) { code += src[i]; i++ }
134
+ }
135
+ return { code, strings, template }
136
+ }
137
+
138
+ /** the JSX text runs of lexed code: what sits between a tag or an `{…}`
139
+ * expression and the next one. A run that reads as code (an assignment, a
140
+ * statement end, a logical operator) is code between two tags, and is left out */
141
+ export function jsxTexts(code: string): string[] {
142
+ return [...code.matchAll(/[>}]([^<>{}\0]*)[<{]/g)].map((m) => m[1]!).filter((t) => /\p{L}/u.test(t) && !/[=;]|&&|\|\||\breturn\b/.test(t))
143
+ }
package/src/ts/rename.ts CHANGED
@@ -1,9 +1,10 @@
1
- // Rename / move a file or folder AND update every exact occurrence of its
2
- // repo-relative path across all tracked + untracked source files.
3
- //
4
- // Assumes non-relative import paths with explicit extensions, e.g.
5
- // import { X } from 'src/foo/bar.ts'
6
- // so the exact path string is a reliable search key.
1
+ // Rename / move a file or folder AND update every reference to it across all
2
+ // tracked + untracked source files: every exact occurrence of its repo-relative
3
+ // path (or package-name path), and every RELATIVE import in a code file that
4
+ // resolves to what moves. A relative specifier only means something from the
5
+ // file that writes it, so each one is resolved from its importer and written
6
+ // again from where both ends land: an importer of the moved file, and the moved
7
+ // file's own imports of what stays behind.
7
8
  //
8
9
  // Modes:
9
10
  // file -> file path renames the file
@@ -13,6 +14,10 @@
13
14
  import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'fs'
14
15
  import { basename, dirname, extname, join, relative, resolve } from 'path'
15
16
  import { canonicalSpecifier, isSkipped, loadConfig, toPosix, type RvtConfig } from 'rvlib-shipkit/src/ts/config.ts'
17
+ import { extractSpecs } from 'rvlib-shipkit/src/ts/source-graph.ts'
18
+ import { gitBin } from '../core/git-bin.ts'
19
+
20
+ const CODE_RE = /\.[cm]?[jt]sx?$/
16
21
 
17
22
  const BINARY_EXTS = new Set([
18
23
  '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.tiff',
@@ -93,9 +98,37 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
93
98
 
94
99
  const pkgDirs = cfg.packages.map((p) => p.dir)
95
100
  // scan all tracked + untracked (non-ignored) files
96
- const ls = Bun.spawnSync(['git', 'ls-files', '-co', '--exclude-standard', '-z'], { cwd: repoRoot })
101
+ const ls = Bun.spawnSync([gitBin(), 'ls-files', '-co', '--exclude-standard', '-z'], { cwd: repoRoot })
97
102
  const allFiles = ls.stdout.toString().split('\u0000').filter(Boolean)
98
103
 
104
+ const movedTo = (rel: string): string | null =>
105
+ rel === srcRel ? dstRel : srcIsFolder && rel.startsWith(`${srcRel}/`) ? `${dstRel}${rel.slice(srcRel.length)}` : null
106
+ const isFile = (rel: string): boolean => {
107
+ try { return statSync(join(repoRoot, rel)).isFile() } catch { return false }
108
+ }
109
+ const rewriteFile = (file: string, content: string): { text: string; count: number } => {
110
+ const rel = CODE_RE.test(file) ? relativeRewrites(file, content, movedTo, isFile) : new Map<string, string>()
111
+ if (rel.size === 0) return scanPairs(content, pairs)
112
+ // the relative specifiers this move retargets are set aside while the
113
+ // path scan runs, so the two rewrites can never compound on one string
114
+ let masked = content
115
+ let relCount = 0
116
+ const holes: string[] = []
117
+ for (const [from, to] of rel) {
118
+ for (const q of ["'", '"']) {
119
+ const parts = masked.split(`${q}${from}${q}`)
120
+ if (parts.length === 1) continue
121
+ relCount += parts.length - 1
122
+ masked = parts.join(`\u0000mv${holes.length}\u0000`)
123
+ holes.push(`${q}${to}${q}`)
124
+ }
125
+ }
126
+ const scanned = scanPairs(masked, pairs)
127
+ let text = scanned.text
128
+ holes.forEach((h, i) => { text = text.split(`\u0000mv${i}\u0000`).join(h) })
129
+ return { text, count: scanned.count + relCount }
130
+ }
131
+
99
132
  const hits: { file: string; count: number }[] = []
100
133
  let totalRefs = 0
101
134
  for (const file of allFiles) {
@@ -109,7 +142,7 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
109
142
  }
110
143
  const nul = content.indexOf('\u0000')
111
144
  if (nul !== -1 && nul < 4096) continue // cheap binary sniff
112
- const count = scanPairs(content, pairs).count
145
+ const count = rewriteFile(file, content).count
113
146
  if (count > 0) {
114
147
  hits.push({ file, count })
115
148
  totalRefs += count
@@ -140,12 +173,12 @@ export function runRename(srcArg: string, dstArg: string, opts: { apply?: boolea
140
173
  for (const h of hits) {
141
174
  const full = join(repoRoot, h.file)
142
175
  const before = readFileSync(full, 'utf8')
143
- const after = scanPairs(before, pairs).text
176
+ const after = rewriteFile(h.file, before).text
144
177
  if (after !== before) writeFileSync(full, after)
145
178
  }
146
179
  const dstDir = dirname(dstAbs)
147
180
  if (!existsSync(dstDir)) mkdirSync(dstDir, { recursive: true })
148
- const mv = Bun.spawnSync(['git', 'mv', srcAbs, dstAbs], { cwd: repoRoot, stdout: 'pipe', stderr: 'pipe' })
181
+ const mv = Bun.spawnSync([gitBin(), 'mv', srcAbs, dstAbs], { cwd: repoRoot, stdout: 'pipe', stderr: 'pipe' })
149
182
  if (mv.exitCode !== 0) renameSync(srcAbs, dstAbs) // untracked file: plain fs move
150
183
  }
151
184
 
@@ -179,6 +212,36 @@ export function renameCli(argv: string[]): number {
179
212
  return 0
180
213
  }
181
214
 
215
+ /** the relative specifiers of one code file that this move retargets, each
216
+ * mapped to the specifier that names the same file after the move, written in
217
+ * the same style (explicit extension, `.js` for a `.ts` file, no extension, or
218
+ * a folder's index) */
219
+ function relativeRewrites(file: string, content: string, movedTo: (rel: string) => string | null, isFile: (rel: string) => boolean): Map<string, string> {
220
+ const out = new Map<string, string>()
221
+ const fromAfter = movedTo(file) ?? file
222
+ for (const spec of new Set(extractSpecs(content))) {
223
+ if (!spec.startsWith('./') && !spec.startsWith('../')) continue
224
+ const base = toPosix(join(dirname(file), spec))
225
+ const forms: [string, (p: string) => string][] = [
226
+ [base, (p) => p],
227
+ [base.replace(/\.js$/, '.ts'), (p) => p.replace(/\.ts$/, '.js')],
228
+ [`${base}.ts`, (p) => p.replace(/\.ts$/, '')],
229
+ [`${base}.tsx`, (p) => p.replace(/\.tsx$/, '')],
230
+ [`${base}/index.ts`, (p) => p.replace(/\/index\.ts$/, '')],
231
+ [`${base}/index.tsx`, (p) => p.replace(/\/index\.tsx$/, '')],
232
+ ]
233
+ const hit = forms.find(([candidate]) => isFile(candidate))
234
+ if (hit == null) continue
235
+ const [target, restyle] = hit
236
+ const targetAfter = movedTo(target) ?? target
237
+ if (targetAfter === target && fromAfter === file) continue
238
+ let next = toPosix(relative(dirname(fromAfter), restyle(targetAfter)))
239
+ if (!next.startsWith('../')) next = `./${next}`
240
+ if (next !== spec) out.set(spec, next)
241
+ }
242
+ return out
243
+ }
244
+
182
245
  // simultaneous multi-needle replace: earliest match wins (longest needle on a
183
246
  // tie) and the scan continues PAST the replacement — sequential split/join
184
247
  // would let one pair's output feed the next (the package form is a substring
@@ -32,7 +32,23 @@ export function crawlSources(cfg: RvtConfig, dir: string, out: string[]): void {
32
32
  // strip comments (line comments only when preceded by whitespace, so
33
33
  // 'http://...' survives) and type-only import/export statements, then extract
34
34
  // the remaining runtime specifiers
35
- export function extractSpecs(content: string): string[] {
35
+ export function extractSpecs(content: string, o: { dynamic?: boolean } = {}): string[] {
36
+ const memo = o.dynamic === false ? staticSpecs : allSpecs
37
+ const hit = memo.get(content)
38
+ if (hit != null) return [...hit]
39
+ const specs = scanSpecs(content, o)
40
+ if (memo.size >= SPECS_MEMO_MAX) memo.clear()
41
+ memo.set(content, specs)
42
+ return [...specs]
43
+ }
44
+
45
+ /** extractSpecs is a pure function of the text, and every gate asks it of the
46
+ * same check closures again, so the answer is remembered per text */
47
+ const allSpecs = new Map<string, string[]>()
48
+ const staticSpecs = new Map<string, string[]>()
49
+ const SPECS_MEMO_MAX = 4000
50
+
51
+ function scanSpecs(content: string, o: { dynamic?: boolean }): string[] {
36
52
  // template-literal content is text, a codegen emitting import lines inside
37
53
  // a backtick block is not an import of this module. single-line templates
38
54
  // strip in place, multiline ones blank via the shared mask (null mask =
@@ -40,9 +56,11 @@ export function extractSpecs(content: string): string[] {
40
56
  const rawLines = content.split('\n')
41
57
  const mask = templateLineMask(rawLines)
42
58
  const demasked = mask == null ? content : rawLines.map((l, i) => (mask[i] ? '' : l)).join('\n')
59
+ // ONE left to right pass over both comment shapes: whichever opens first
60
+ // owns the text, so a `/*` inside a line comment ("src/*") never starts a
61
+ // block comment that swallows the imports below it
43
62
  const cleaned = demasked
44
- .replace(/\/\*[\s\S]*?\*\//g, '')
45
- .replace(/(^|\s)\/\/[^\n]*/g, '$1')
63
+ .replace(/\/\*[\s\S]*?\*\/|(^|\s)\/\/[^\n]*/g, (m, lead: string | undefined) => (m.startsWith('/*') ? '' : lead ?? ''))
46
64
  .replace(/`(?:\\.|[^`\\\n])*`/g, '``')
47
65
  .replace(/\b(?:import|export)\s+type\s[^'"]*?from\s*(['"])[^'"\n]*\1/g, '')
48
66
  const specs: string[] = []
@@ -57,7 +75,9 @@ export function extractSpecs(content: string): string[] {
57
75
  }
58
76
  for (const re of [
59
77
  /\bimport\s*['"]([^'"\n]+)['"]/g, // side-effect import
60
- /\b(?:import|require)\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g, // dynamic import / require
78
+ // dynamic import / require. `dynamic: false` leaves them out: what a
79
+ // module's LOAD runs, not what a function it exports may run later
80
+ ...(o.dynamic === false ? [] : [/\b(?:import|require)\s*\(\s*['"]([^'"\n]+)['"]\s*\)/g]),
61
81
  ]) {
62
82
  for (const m of cleaned.matchAll(re)) {
63
83
  if (m[1] != null) specs.push(m[1])
@@ -120,13 +140,13 @@ export function buildSourceGraph(cfg: RvtConfig = loadConfig()): SourceGraph {
120
140
  * the transitive import closure of `entries`, entries included, WITHOUT
121
141
  * crawling the repo: files are read only as the walk reaches them.
122
142
  *
123
- * This is what puts a unit's own code in its cache key. A check that imports a
143
+ * This is what puts a check's own code in its cache key. A check that imports a
124
144
  * shared helper carries that helper's identity, so fixing the helper moves the
125
145
  * check's key. An entry that does not exist is dropped: the caller already
126
- * knows a missing declared path makes a unit uncacheable, and a throw here
146
+ * knows a missing declared path makes a check uncacheable, and a throw here
127
147
  * would turn a stale declaration into a dead gate.
128
148
  */
129
- export function importClosure(cfg: RvtConfig, entries: string[]): string[] {
149
+ export function importClosure(cfg: RvtConfig, entries: string[], o: { dynamic?: boolean } = {}): string[] {
130
150
  const probe = new Map<string, boolean>()
131
151
  const has = (p: string): boolean => {
132
152
  let r = probe.get(p)
@@ -147,7 +167,7 @@ export function importClosure(cfg: RvtConfig, entries: string[]): string[] {
147
167
  } catch {
148
168
  continue
149
169
  }
150
- for (const spec of extractSpecs(content)) {
170
+ for (const spec of extractSpecs(content, o)) {
151
171
  const target = resolveSpec(cfg, file, spec, has)
152
172
  if (target == null || seen.has(target)) continue
153
173
  seen.add(target)
@@ -1,101 +1,10 @@
1
- // which tsconfig files a typecheck must actually visit.
2
- //
3
- // `tsc --noEmit -p <solution>` on a solution-style config (`files: []` plus
4
- // `references`) compiles ZERO files and exits 0. The gate then prints
5
- // `ok: typecheck` while a type error sits in src, which is the single worst
6
- // output this repo can produce: an unmeasured green nobody can tell from a
7
- // measured one. tsc will not warn about it, so the caller has to resolve the
8
- // leaves itself and refuse when there are none.
9
- import { existsSync, readFileSync, statSync } from 'fs'
10
- import { dirname, isAbsolute, join, resolve } from 'path'
11
- import { parseJsonc } from 'rvlib-shipkit/src/ts/config.ts'
1
+ // which tsconfig files a typecheck must actually visit, read from the disk.
2
+ // ts/tsconfig-resolve.ts owns the resolution and why it exists.
3
+ import { fsUnitReader } from '../release/units.ts'
4
+ import { resolveTypecheckProjects as resolveWith, type TypecheckProjects } from './tsconfig-resolve.ts'
12
5
 
13
- export type TypecheckProjects = {
14
- /** absolute paths, in declaration order, of the configs that compile files */
15
- projects: string[]
16
- /** nothing would be compiled: the check would pass without checking */
17
- vacuous: boolean
18
- /** references pointing at a path that does not exist, verbatim as declared */
19
- missing: string[]
20
- }
21
-
22
- type RawConfig = {
23
- files?: unknown
24
- include?: unknown
25
- references?: { path?: unknown }[]
26
- }
6
+ export type { TypecheckProjects } from './tsconfig-resolve.ts'
27
7
 
28
- /** a reference may name a directory, a config file, or a dir with no config */
29
- function configPathFor(base: string, declared: string): string | null {
30
- const p = isAbsolute(declared) ? declared : resolve(base, declared)
31
- if (!existsSync(p)) return null
32
- try { if (statSync(p).isDirectory()) return existsSync(join(p, 'tsconfig.json')) ? join(p, 'tsconfig.json') : null }
33
- catch { return null }
34
- return p
35
- }
36
-
37
- /**
38
- * whether tsc would compile anything for THIS config, ignoring its references.
39
- *
40
- * tsc's rule, and every branch here is a shape found in the field:
41
- * - `files` non-empty, or `include` non-empty: compiles those
42
- * - neither key present: the default `include` is `**\/*`, so it compiles the
43
- * whole directory tree, which is why a bare `{ references: [...] }` is NOT
44
- * a solution config
45
- * - `files: []` with no `include`: the solution-style marker, compiles nothing
46
- * - `include: []`: compiles nothing whatever `files` says, unless `files` lists
47
- * something
48
- */
49
- function compilesOwnFiles(cfg: RawConfig): boolean {
50
- const files = Array.isArray(cfg.files) ? cfg.files : null
51
- const include = Array.isArray(cfg.include) ? cfg.include : null
52
- if (files != null && files.length > 0) return true
53
- if (include != null) return include.length > 0
54
- return files == null
55
- }
56
-
57
- function read(path: string): RawConfig {
58
- let text: string
59
- try { text = readFileSync(path, 'utf8') }
60
- catch (e) { throw new Error(`tsconfig unreadable at ${path}: ${String(e).slice(0, 120)}`) }
61
- try { return (parseJsonc(text) ?? {}) as RawConfig }
62
- catch (e) { throw new Error(`tsconfig unparseable at ${path}: ${String(e).slice(0, 120)}`) }
63
- }
64
-
65
- /**
66
- * Walk `references` to the leaves. A leaf that declares `files: []` with no
67
- * `include` compiles nothing and is dropped, so a solution whose every leaf is
68
- * empty comes back `vacuous` rather than as a command that passes for free.
69
- */
70
8
  export function resolveTypecheckProjects(tsconfigPath: string): TypecheckProjects {
71
- const start = resolve(tsconfigPath)
72
- const projects: string[] = []
73
- const missing: string[] = []
74
- // a reference loop is legal to WRITE and would otherwise recurse forever
75
- const seen = new Set<string>()
76
-
77
- const visit = (path: string): void => {
78
- if (seen.has(path)) return
79
- seen.add(path)
80
- const cfg = read(path)
81
- // references are checked FIRST so the list reads dependencies before
82
- // dependents, the order `tsc -b` would build in
83
- for (const r of Array.isArray(cfg.references) ? cfg.references : []) {
84
- const declared = typeof r?.path === 'string' ? r.path : null
85
- if (declared == null) continue
86
- const next = configPathFor(dirname(path), declared)
87
- if (next == null) missing.push(declared)
88
- else visit(next)
89
- }
90
- // `references` does NOT suppress a config's own inputs: only an empty
91
- // `files` with no `include` does, and that is what makes a config
92
- // solution-style. Skipping a referencing config outright is how the
93
- // first version of this file reintroduced the false green it exists to
94
- // kill, on `{ include: ['src'], references: [...] }`, which is the
95
- // ordinary monorepo package shape
96
- if (compilesOwnFiles(cfg)) projects.push(path)
97
- }
98
-
99
- visit(start)
100
- return { projects, vacuous: projects.length === 0, missing }
9
+ return resolveWith(tsconfigPath, fsUnitReader('/'), '/')
101
10
  }