@intentius/chant 0.124.31 → 0.124.32

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 (321) hide show
  1. package/bin/chant +7 -2
  2. package/dist/cli/commands/check-lexicon-plugin.d.ts +7 -4
  3. package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
  4. package/dist/cli/commands/check-lexicon-plugin.js +10 -7
  5. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  6. package/dist/cli/commands/check-lexicon.js +18 -12
  7. package/dist/cli/commands/import.d.ts +10 -0
  8. package/dist/cli/commands/import.d.ts.map +1 -1
  9. package/dist/cli/commands/import.js +37 -0
  10. package/dist/cli/commands/init-lexicon/templates/project.d.ts.map +1 -1
  11. package/dist/cli/commands/init-lexicon/templates/project.js +4 -3
  12. package/dist/cli/commands/onboard.d.ts.map +1 -1
  13. package/dist/cli/commands/onboard.js +8 -4
  14. package/dist/cli/commands/update.d.ts.map +1 -1
  15. package/dist/cli/commands/update.js +2 -1
  16. package/dist/cli/dependencies.d.ts +18 -0
  17. package/dist/cli/dependencies.d.ts.map +1 -0
  18. package/dist/cli/dependencies.js +120 -0
  19. package/dist/cli/handlers/build.d.ts.map +1 -1
  20. package/dist/cli/handlers/build.js +23 -4
  21. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  22. package/dist/cli/handlers/fan-out.js +12 -4
  23. package/dist/cli/handlers/graph.d.ts.map +1 -1
  24. package/dist/cli/handlers/graph.js +16 -6
  25. package/dist/cli/handlers/lint.d.ts.map +1 -1
  26. package/dist/cli/handlers/lint.js +6 -0
  27. package/dist/cli/handlers/operator.d.ts +2 -2
  28. package/dist/cli/handlers/operator.js +3 -3
  29. package/dist/cli/handlers/pr.d.ts +14 -0
  30. package/dist/cli/handlers/pr.d.ts.map +1 -1
  31. package/dist/cli/handlers/pr.js +488 -32
  32. package/dist/cli/handlers/promote.d.ts.map +1 -1
  33. package/dist/cli/handlers/promote.js +3 -1
  34. package/dist/cli/handlers/run.d.ts +13 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +38 -15
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +44 -16
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +15 -0
  41. package/dist/cli/registry.d.ts +13 -4
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/registry.js +6 -2
  44. package/dist/codegen/generate-post-synth-barrel.js +2 -2
  45. package/dist/codegen/lexicon-regen.d.ts +4 -0
  46. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  47. package/dist/codegen/lexicon-regen.js +1 -1
  48. package/dist/codegen/pinned-upgrade.js +2 -2
  49. package/dist/components/capability.d.ts +44 -3
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/capability.js +21 -0
  52. package/dist/components/changed-files.d.ts +7 -1
  53. package/dist/components/changed-files.d.ts.map +1 -1
  54. package/dist/components/changed-files.js +9 -1
  55. package/dist/components/driver.d.ts +38 -0
  56. package/dist/components/driver.d.ts.map +1 -1
  57. package/dist/components/driver.js +47 -6
  58. package/dist/components/fan-out-record.d.ts +6 -1
  59. package/dist/components/fan-out-record.d.ts.map +1 -1
  60. package/dist/components/fan-out-record.js +24 -0
  61. package/dist/components/fan-out-run.d.ts +20 -0
  62. package/dist/components/fan-out-run.d.ts.map +1 -1
  63. package/dist/components/fan-out-run.js +21 -1
  64. package/dist/components/pr-pipeline.d.ts +65 -2
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +107 -15
  67. package/dist/components/pr-run.d.ts +127 -29
  68. package/dist/components/pr-run.d.ts.map +1 -1
  69. package/dist/components/pr-run.js +290 -70
  70. package/dist/components/wave-plan.d.ts +5 -2
  71. package/dist/components/wave-plan.d.ts.map +1 -1
  72. package/dist/components/wave-plan.js +12 -1
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/fold-import.js +16 -6
  75. package/dist/discovery/sandbox/bundle.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/bundle.js +6 -0
  77. package/dist/discovery/specifier-resolve.d.ts.map +1 -1
  78. package/dist/discovery/specifier-resolve.js +8 -2
  79. package/dist/fold/fold.d.ts.map +1 -1
  80. package/dist/fold/fold.js +68 -1
  81. package/dist/fold/subset.d.ts +37 -0
  82. package/dist/fold/subset.d.ts.map +1 -1
  83. package/dist/fold/subset.js +88 -0
  84. package/dist/lexicon-module.d.ts +10 -0
  85. package/dist/lexicon-module.d.ts.map +1 -1
  86. package/dist/lexicon-module.js +53 -8
  87. package/dist/lexicon-plugin-helpers.d.ts.map +1 -1
  88. package/dist/lexicon-plugin-helpers.js +6 -4
  89. package/dist/lexicon.d.ts +2 -0
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/affected.d.ts.map +1 -1
  92. package/dist/lifecycle/affected.js +7 -1
  93. package/dist/lifecycle/release-ledger.d.ts +21 -0
  94. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  95. package/dist/lifecycle/release-ledger.js +24 -0
  96. package/dist/lifecycle/work-dir.d.ts +45 -0
  97. package/dist/lifecycle/work-dir.d.ts.map +1 -0
  98. package/dist/lifecycle/work-dir.js +78 -0
  99. package/dist/lint/discover.d.ts +17 -9
  100. package/dist/lint/discover.d.ts.map +1 -1
  101. package/dist/lint/discover.js +62 -54
  102. package/dist/module-source.d.ts +57 -0
  103. package/dist/module-source.d.ts.map +1 -0
  104. package/dist/module-source.js +104 -0
  105. package/dist/op/activities/factory.js +1 -1
  106. package/dist/op/discover.d.ts.map +1 -1
  107. package/dist/op/discover.js +13 -3
  108. package/dist/op/gate.d.ts.map +1 -1
  109. package/dist/op/gate.js +3 -0
  110. package/dist/op/op-waves-run.d.ts.map +1 -1
  111. package/dist/op/op-waves-run.js +20 -5
  112. package/dist/op/operator.d.ts +2 -2
  113. package/dist/op/operator.js +2 -2
  114. package/dist/op/steward.d.ts +6 -4
  115. package/dist/op/steward.d.ts.map +1 -1
  116. package/dist/op/steward.js +5 -3
  117. package/dist/op/work-lease-run.d.ts +20 -7
  118. package/dist/op/work-lease-run.d.ts.map +1 -1
  119. package/dist/op/work-lease-run.js +56 -12
  120. package/dist/pr-forge.d.ts +10 -0
  121. package/dist/pr-forge.d.ts.map +1 -1
  122. package/dist/pr-forge.js +39 -0
  123. package/dist/pr-loop.d.ts +141 -4
  124. package/dist/pr-loop.d.ts.map +1 -1
  125. package/dist/pr-loop.js +122 -6
  126. package/dist/pr-report-url.d.ts +2 -2
  127. package/dist/pr-report-url.d.ts.map +1 -1
  128. package/dist/pr-report-url.js +5 -5
  129. package/dist/ts-loader.d.ts +38 -4
  130. package/dist/ts-loader.d.ts.map +1 -1
  131. package/dist/ts-loader.js +190 -16
  132. package/dist/wave-runtime.mjs +208 -208
  133. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts +34 -0
  134. package/dist/workspace/chant-migrations/fountain-retired-kinds.d.ts.map +1 -0
  135. package/dist/workspace/chant-migrations/fountain-retired-kinds.js +176 -0
  136. package/dist/workspace/chant-migrations.d.ts.map +1 -1
  137. package/dist/workspace/chant-migrations.js +2 -0
  138. package/dist/workspace/checks/boxes.d.ts +6 -6
  139. package/dist/workspace/checks/boxes.js +9 -9
  140. package/dist/workspace/ci-green-forge.d.ts +6 -4
  141. package/dist/workspace/ci-green-forge.d.ts.map +1 -1
  142. package/dist/workspace/ci-green.d.ts +28 -12
  143. package/dist/workspace/ci-green.d.ts.map +1 -1
  144. package/dist/workspace/ci-green.js +49 -21
  145. package/dist/workspace/ci-list.schema.json +12 -6
  146. package/dist/workspace/ci-report-url.d.ts +18 -0
  147. package/dist/workspace/ci-report-url.d.ts.map +1 -0
  148. package/dist/workspace/ci-report-url.js +33 -0
  149. package/dist/workspace/ci-run-index.d.ts +12 -0
  150. package/dist/workspace/ci-run-index.d.ts.map +1 -1
  151. package/dist/workspace/ci-run-index.js +6 -4
  152. package/dist/workspace/ci-run-index.schema.json +5 -3
  153. package/dist/workspace/ci-run.d.ts +75 -2
  154. package/dist/workspace/ci-run.d.ts.map +1 -1
  155. package/dist/workspace/ci-run.js +195 -53
  156. package/dist/workspace/ci-runner-key.d.ts +16 -3
  157. package/dist/workspace/ci-runner-key.d.ts.map +1 -1
  158. package/dist/workspace/ci-runner-key.js +30 -10
  159. package/dist/workspace/ci-serve.d.ts +54 -4
  160. package/dist/workspace/ci-serve.d.ts.map +1 -1
  161. package/dist/workspace/ci-serve.js +175 -26
  162. package/dist/workspace/declaration.d.ts +8 -0
  163. package/dist/workspace/declaration.d.ts.map +1 -1
  164. package/dist/workspace/declaration.js +11 -2
  165. package/dist/workspace/declaration.schema.json +7 -2
  166. package/dist/workspace/export.d.ts +1 -1
  167. package/dist/workspace/export.d.ts.map +1 -1
  168. package/dist/workspace/export.js +7 -3
  169. package/dist/workspace/ls.d.ts +6 -1
  170. package/dist/workspace/ls.d.ts.map +1 -1
  171. package/dist/workspace/ls.js +1 -0
  172. package/dist/workspace/ls.schema.json +7 -1
  173. package/dist/workspace/pin-integrity.d.ts.map +1 -1
  174. package/dist/workspace/pin-integrity.js +3 -2
  175. package/dist/workspace/pr-report.schema.json +74 -7
  176. package/dist/workspace/reason-codes.d.ts +1 -1
  177. package/dist/workspace/reason-codes.d.ts.map +1 -1
  178. package/dist/workspace/reason-codes.js +2 -2
  179. package/dist/workspace/run-event.schema.json +3 -1
  180. package/dist/workspace/status.schema.json +3 -1
  181. package/dist/workspace/write-scope.d.ts +9 -0
  182. package/dist/workspace/write-scope.d.ts.map +1 -1
  183. package/dist/workspace/write-scope.js +11 -1
  184. package/package.json +1 -1
  185. package/src/__fixtures__/pr-loop/pr-apply-wave-waiting.note.golden.md +48 -0
  186. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +3 -3
  187. package/src/cli/commands/audit.test.ts +2 -2
  188. package/src/cli/commands/check-lexicon-plugin.test.ts +9 -0
  189. package/src/cli/commands/check-lexicon-plugin.ts +11 -8
  190. package/src/cli/commands/check-lexicon.test.ts +1 -1
  191. package/src/cli/commands/check-lexicon.ts +16 -11
  192. package/src/cli/commands/import.ts +40 -0
  193. package/src/cli/commands/init-lexicon/templates/project.ts +4 -3
  194. package/src/cli/commands/init-lexicon.test.ts +8 -5
  195. package/src/cli/commands/onboard.test.ts +45 -0
  196. package/src/cli/commands/onboard.ts +8 -4
  197. package/src/cli/commands/update.test.ts +6 -0
  198. package/src/cli/commands/update.ts +4 -1
  199. package/src/cli/compiled-cli.e2e.test.ts +76 -11
  200. package/src/cli/dependencies.e2e.test.ts +101 -0
  201. package/src/cli/dependencies.test.ts +93 -0
  202. package/src/cli/dependencies.ts +122 -0
  203. package/src/cli/handlers/build.test.ts +69 -0
  204. package/src/cli/handlers/build.ts +20 -4
  205. package/src/cli/handlers/fan-out.test.ts +73 -0
  206. package/src/cli/handlers/fan-out.ts +11 -4
  207. package/src/cli/handlers/graph.test.ts +76 -0
  208. package/src/cli/handlers/graph.ts +19 -12
  209. package/src/cli/handlers/lint.test.ts +42 -0
  210. package/src/cli/handlers/lint.ts +6 -0
  211. package/src/cli/handlers/operator.ts +3 -3
  212. package/src/cli/handlers/pr-waves.test.ts +449 -0
  213. package/src/cli/handlers/pr.test.ts +134 -0
  214. package/src/cli/handlers/pr.ts +523 -31
  215. package/src/cli/handlers/promote.test.ts +26 -1
  216. package/src/cli/handlers/promote.ts +3 -2
  217. package/src/cli/handlers/run-components-exit.test.ts +99 -0
  218. package/src/cli/handlers/run-signer.test.ts +79 -0
  219. package/src/cli/handlers/run.ts +34 -16
  220. package/src/cli/main.test.ts +18 -0
  221. package/src/cli/main.ts +43 -16
  222. package/src/cli/parse-args.ts +12 -0
  223. package/src/cli/registry.ts +19 -6
  224. package/src/codegen/generate-post-synth-barrel.ts +2 -2
  225. package/src/codegen/lexicon-regen.ts +1 -1
  226. package/src/codegen/pinned-upgrade.ts +2 -2
  227. package/src/codegen/post-synth-barrel-guards.test.ts +6 -11
  228. package/src/components/capability.ts +51 -3
  229. package/src/components/changed-files.test.ts +16 -1
  230. package/src/components/changed-files.ts +9 -1
  231. package/src/components/driver.test.ts +36 -1
  232. package/src/components/driver.ts +76 -4
  233. package/src/components/fan-out-record.ts +29 -1
  234. package/src/components/fan-out-run.test.ts +35 -0
  235. package/src/components/fan-out-run.ts +40 -1
  236. package/src/components/pr-pipeline.test.ts +100 -0
  237. package/src/components/pr-pipeline.ts +132 -17
  238. package/src/components/pr-run.test.ts +323 -18
  239. package/src/components/pr-run.ts +354 -75
  240. package/src/components/wave-plan.ts +16 -4
  241. package/src/discovery/fold-host-method.test.ts +87 -0
  242. package/src/discovery/fold-import.ts +15 -5
  243. package/src/discovery/fold-package-composite.test.ts +27 -0
  244. package/src/discovery/fold-specifiers.test.ts +8 -0
  245. package/src/discovery/sandbox/bundle.ts +6 -0
  246. package/src/discovery/specifier-resolve.ts +8 -2
  247. package/src/fold/fold.test.ts +72 -0
  248. package/src/fold/fold.ts +71 -0
  249. package/src/fold/subset.test.ts +31 -0
  250. package/src/fold/subset.ts +99 -0
  251. package/src/lexicon-dist.e2e.test.ts +254 -0
  252. package/src/lexicon-module.test.ts +47 -0
  253. package/src/lexicon-module.ts +49 -7
  254. package/src/lexicon-plugin-helpers.ts +6 -4
  255. package/src/lexicon.ts +2 -0
  256. package/src/lifecycle/affected.test.ts +7 -0
  257. package/src/lifecycle/affected.ts +6 -1
  258. package/src/lifecycle/release-ledger.ts +34 -0
  259. package/src/lifecycle/work-dir.test.ts +77 -0
  260. package/src/lifecycle/work-dir.ts +82 -0
  261. package/src/lint/discover.test.ts +46 -2
  262. package/src/lint/discover.ts +55 -49
  263. package/src/module-source.test.ts +85 -0
  264. package/src/module-source.ts +112 -0
  265. package/src/op/activities/factory.ts +1 -1
  266. package/src/op/discover-root.test.ts +31 -2
  267. package/src/op/discover.ts +14 -4
  268. package/src/op/gate.test.ts +30 -0
  269. package/src/op/gate.ts +3 -0
  270. package/src/op/op-waves-run.ts +20 -5
  271. package/src/op/op-waves.test.ts +49 -0
  272. package/src/op/operator.ts +2 -2
  273. package/src/op/steward.ts +6 -4
  274. package/src/op/work-lease-run.test.ts +88 -2
  275. package/src/op/work-lease-run.ts +52 -12
  276. package/src/pr-forge-bundle.test.ts +2 -2
  277. package/src/pr-forge.test.ts +42 -0
  278. package/src/pr-forge.ts +40 -0
  279. package/src/pr-loop-bundle.test.ts +2 -1
  280. package/src/pr-loop.test.ts +117 -1
  281. package/src/pr-loop.ts +252 -10
  282. package/src/pr-report-url.ts +5 -5
  283. package/src/terraform/__fixtures__/advise.test.ts +21 -2
  284. package/src/ts-loader.test.ts +89 -1
  285. package/src/ts-loader.ts +180 -13
  286. package/src/workspace/changes.test.ts +1 -1
  287. package/src/workspace/chant-migrations/fountain-retired-kinds.test.ts +33 -0
  288. package/src/workspace/chant-migrations/fountain-retired-kinds.ts +190 -0
  289. package/src/workspace/chant-migrations.e2e.test.ts +5 -0
  290. package/src/workspace/chant-migrations.ts +2 -0
  291. package/src/workspace/checks/boxes.ts +9 -9
  292. package/src/workspace/ci-green-forge.ts +6 -4
  293. package/src/workspace/ci-green-runs.test.ts +53 -11
  294. package/src/workspace/ci-green.test.ts +1 -1
  295. package/src/workspace/ci-green.ts +50 -22
  296. package/src/workspace/ci-list.schema.json +12 -6
  297. package/src/workspace/ci-report-url.test.ts +220 -0
  298. package/src/workspace/ci-report-url.ts +33 -0
  299. package/src/workspace/ci-run-index.schema.json +5 -3
  300. package/src/workspace/ci-run-index.test.ts +42 -3
  301. package/src/workspace/ci-run-index.ts +18 -4
  302. package/src/workspace/ci-run.test.ts +185 -7
  303. package/src/workspace/ci-run.ts +224 -54
  304. package/src/workspace/ci-runner-key.test.ts +55 -17
  305. package/src/workspace/ci-runner-key.ts +36 -11
  306. package/src/workspace/ci-serve.test.ts +150 -6
  307. package/src/workspace/ci-serve.ts +169 -25
  308. package/src/workspace/declaration.schema.json +7 -2
  309. package/src/workspace/declaration.test.ts +23 -0
  310. package/src/workspace/declaration.ts +19 -3
  311. package/src/workspace/export.ts +6 -3
  312. package/src/workspace/ls.schema.json +7 -1
  313. package/src/workspace/ls.ts +7 -2
  314. package/src/workspace/pin-integrity.test.ts +5 -0
  315. package/src/workspace/pin-integrity.ts +3 -2
  316. package/src/workspace/pr-report.schema.json +74 -7
  317. package/src/workspace/reason-codes.ts +2 -2
  318. package/src/workspace/run-event.schema.json +3 -1
  319. package/src/workspace/status.schema.json +3 -1
  320. package/src/workspace/write-scope.test.ts +29 -1
  321. package/src/workspace/write-scope.ts +12 -1
@@ -274,13 +274,16 @@ describe("scaffold content validation", () => {
274
274
  });
275
275
 
276
276
  // #2090, the scaffolded package.json failed tier 1 as generated: a bare
277
- // string exports["."], a build script that never deletes emitted .js, and
278
- // a prepack that never bundled, so dist/manifest.json never existed.
279
- test("package.json exports[\".\"].default is ./src/index.ts, build deletes emitted JS, prepack bundles (#2090)", () => {
277
+ // string exports["."] and a prepack that never bundled, so
278
+ // dist/manifest.json never existed. #3981 — the package ships compiled JS.
279
+ test("package.json exports[\".\"] is development src, default dist JS, build keeps JS, prepack bundles (#2090, #3981)", () => {
280
280
  const pkg = JSON.parse(readFileSync(join(targetDir, "package.json"), "utf-8"));
281
- expect(pkg.exports["."].default).toBe("./src/index.ts");
282
- expect(pkg.scripts.build).toContain("-delete");
281
+ expect(pkg.exports["."].development).toBe("./src/index.ts");
282
+ expect(pkg.exports["."].default).toBe("./dist/index.js");
283
+ expect(pkg.exports["./*"].default).toBe("./dist/*.js");
284
+ expect(pkg.scripts.build).not.toContain("-delete");
283
285
  expect(pkg.scripts.prepack).toContain("bundle");
286
+ expect(pkg.scripts.prepack).toContain("npm run build");
284
287
  });
285
288
 
286
289
  test("tsconfig.build.json uses bundler resolution + the development condition (#749)", () => {
@@ -305,6 +305,51 @@ describe("onboardCommand", () => {
305
305
  });
306
306
  });
307
307
 
308
+ describe("chant.yml validate step for a lexicon it already has (#2877)", () => {
309
+ let root: string;
310
+
311
+ beforeEach(() => {
312
+ root = makeTempDir();
313
+ writeFixtureRepo(root);
314
+ });
315
+
316
+ afterEach(() => {
317
+ rmSync(root, { recursive: true, force: true });
318
+ });
319
+
320
+ const withGithubStep = (run: string) =>
321
+ ciContent.replace(
322
+ " - name: Generate and validate K8s lexicon",
323
+ ` - name: Generate and validate GitHub lexicon\n ${run}\n\n - name: Generate and validate K8s lexicon`,
324
+ );
325
+
326
+ const githubSteps = (ci: string) => ci.match(/Generate and validate github lexicon/gi)?.length;
327
+
328
+ test("a named step's bare run: line counts as the lexicon's prepack line", () => {
329
+ const before = withGithubStep("run: npm run --prefix lexicons/github prepack");
330
+ expect(before).not.toBe(ciContent);
331
+ writeFileSync(join(root, ".github/workflows/chant.yml"), before);
332
+
333
+ const result = onboardCommand({ name: "github", root });
334
+ expect(result.patched).not.toContain("chant.yml (prepack + validate)");
335
+ expect(result.skipped).toContain("chant.yml: github already in chant.yml");
336
+ expect(readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8")).toBe(before);
337
+ });
338
+
339
+ test("the step-name check ignores case (Github vs GitHub)", () => {
340
+ // A step whose run line is not a plain prepack call, so only the name can
341
+ // tell onboard the validate step already exists.
342
+ writeFileSync(
343
+ join(root, ".github/workflows/chant.yml"),
344
+ withGithubStep("run: npm run --prefix lexicons/github prepack -- --strict"),
345
+ );
346
+
347
+ onboardCommand({ name: "github", root });
348
+ const ci = readFileSync(join(root, ".github/workflows/chant.yml"), "utf-8");
349
+ expect(githubSteps(ci)).toBe(1);
350
+ });
351
+ });
352
+
308
353
  describe("root tsconfig.json paths (#1614)", () => {
309
354
  let root: string;
310
355
 
@@ -32,13 +32,14 @@ function findRepoRoot(): string {
32
32
  }
33
33
 
34
34
  /**
35
- * A real prepack invocation line: a Dockerfile `RUN`, a workflow `- run:`,
36
- * or a bare line inside a `run: |` block. Comments, `echo` messages and
35
+ * A real prepack invocation line: a Dockerfile `RUN`, a workflow `- run:` or
36
+ * `run:` step key (a named step puts `run:` on its own line, #2877), or a bare
37
+ * line inside a `run: |` block. Comments, `echo` messages and
37
38
  * `$lex` / `<lex>` placeholders do not match. Matching those placeholder
38
39
  * substrings was how onboard appended duplicates to Dockerfile.smoke on
39
40
  * every run (#1678).
40
41
  */
41
- const PREPACK_LINE = /^\s*(?:RUN\s+|-\s*run:\s*)?npm run --prefix lexicons\/[a-z0-9-]+ prepack\s*$/i;
42
+ const PREPACK_LINE = /^\s*(?:RUN\s+|(?:-\s*)?run:\s*)?npm run --prefix lexicons\/[a-z0-9-]+ prepack\s*$/i;
42
43
 
43
44
  function isPrepackLine(line: string): boolean {
44
45
  return PREPACK_LINE.test(line);
@@ -256,7 +257,10 @@ function patchCiWorkflow(root: string, name: string): PatchResult {
256
257
  const displayName = name.charAt(0).toUpperCase() + name.slice(1);
257
258
  const validateStepName = `Generate and validate ${displayName} lexicon`;
258
259
 
259
- if (!lines.some((l) => l.includes(validateStepName))) {
260
+ // Case-insensitive: the name is built from the directory ("Github"), and an
261
+ // existing step may spell the product its own way ("GitHub", #2877).
262
+ const stepNameLower = validateStepName.toLowerCase();
263
+ if (!lines.some((l) => l.toLowerCase().includes(stepNameLower))) {
260
264
  let lastValidateRunIdx = -1;
261
265
  for (let i = 0; i < lines.length; i++) {
262
266
  if (lines[i].includes("Generate and validate")) {
@@ -25,6 +25,12 @@ describe("updateCommand", () => {
25
25
  const result = await updateCommand({ path: testDir });
26
26
  expect(result.success).toBe(true);
27
27
  expect(result.warnings.some((w) => w.includes("not found"))).toBe(true);
28
+ // chant #3988 — the warning names the install command for the package.
29
+ expect(
30
+ result.warnings.some((w) =>
31
+ w.includes('npm install --save-dev @intentius/chant-lexicon-nonexistent'),
32
+ ),
33
+ ).toBe(true);
28
34
  });
29
35
  });
30
36
 
@@ -154,7 +154,10 @@ export async function updateCommand(options: UpdateOptions): Promise<UpdateResul
154
154
  const pkgPath = resolvePackagePath(pkgName, projectDir);
155
155
 
156
156
  if (!pkgPath) {
157
- warnings.push(`${pkgName} not found in node_modules. Run "npm install" first.`);
157
+ // chant #3988 — update copies from node_modules and never installs.
158
+ warnings.push(
159
+ `${pkgName} not found in node_modules. chant update does not install lexicons: run "npm install --save-dev ${pkgName}" first.`,
160
+ );
158
161
  continue;
159
162
  }
160
163
 
@@ -7,14 +7,19 @@
7
7
  * Each run is traced through a load hook that NODE_OPTIONS hands to the CLI's
8
8
  * node, so the test can say what loaded: none of chant's TypeScript source,
9
9
  * ever, and tsx only for a command that imports TypeScript (here the
10
- * lexicon and the project source). tsx's ESM hooks run on a thread of their
11
- * own, about 200 MB of the process (#3276), so a workspace read must not
12
- * register it either.
10
+ * project source). tsx's ESM hooks run on a thread of their own, about 200 MB
11
+ * of the process (#3276), so a workspace read must not register it either.
12
+ *
13
+ * The lexicon ships compiled JS (#3981): its exports' `default` is
14
+ * `dist/*.js`. No command loads a module of the lexicon's TypeScript, and
15
+ * `serve mcp` on a project whose config is JSON loads no tsx at all, with the
16
+ * same lint rules and post-synth checks as the lexicon loaded from source.
13
17
  *
14
18
  * End to end because it packs, installs from the registry and spawns the CLI.
15
19
  */
16
20
 
17
21
  import { execFileSync, spawnSync } from "node:child_process";
22
+ import { loadPlugin } from "./plugins";
18
23
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
19
24
  import { tmpdir } from "node:os";
20
25
  import { join, resolve } from "node:path";
@@ -33,14 +38,20 @@ let waves: string;
33
38
  let workspace: string;
34
39
  let chant: string;
35
40
  let tracer: string;
41
+ let mcpProject: string;
36
42
 
37
43
  const TRACER = `
38
44
  import { registerHooks } from "node:module";
39
45
  import { appendFileSync } from "node:fs";
40
46
  const out = process.env.CHANT_TRACE_OUT;
41
- const seen = { chantSrc: 0, chantDist: 0, tsx: 0, typescript: 0, dist: [] };
47
+ const seen = { chantSrc: 0, chantDist: 0, tsx: 0, typescript: 0, dist: [], lexiconTs: 0, lexiconJs: 0 };
42
48
  registerHooks({
43
49
  load(url, context, next) {
50
+ if (url.includes("/@intentius/chant-lexicon-")) {
51
+ const path = url.replace(/[?#].*$/, "");
52
+ if (/\\.[cm]?tsx?$/.test(path) && !/\\.d\\.[cm]?ts$/.test(path)) seen.lexiconTs++;
53
+ else if (path.includes("/dist/") && path.endsWith(".js")) seen.lexiconJs++;
54
+ }
44
55
  if (url.includes("/@intentius/chant/src/")) seen.chantSrc++;
45
56
  else if (url.includes("/@intentius/chant/dist/")) { seen.chantDist++; seen.dist.push(url.slice(url.indexOf("/@intentius/chant/dist/") + 23)); }
46
57
  else if (url.includes("/node_modules/tsx/")) seen.tsx++;
@@ -61,15 +72,15 @@ interface Run {
61
72
  status: number | null;
62
73
  stdout: string;
63
74
  stderr: string;
64
- /** Counts of what loaded, plus each compiled module's path under `dist/`. */
65
- loaded: { chantSrc: number; chantDist: number; tsx: number; typescript: number; dist: string[] };
75
+ /** Counts of what loaded, plus each compiled module's path under `dist/`. `lexiconTs` and `lexiconJs` count the lexicon's TypeScript and compiled modules. */
76
+ loaded: { chantSrc: number; chantDist: number; tsx: number; typescript: number; dist: string[]; lexiconTs: number; lexiconJs: number };
66
77
  }
67
78
 
68
- function run(cwd: string, args: string[], extraEnv: NodeJS.ProcessEnv = {}, command = chant): Run {
79
+ function run(cwd: string, args: string[], extraEnv: NodeJS.ProcessEnv = {}, command = chant, input?: string): Run {
69
80
  const traceOut = join(scratch, `trace-${Math.random().toString(36).slice(2)}.jsonl`);
70
81
  const env: NodeJS.ProcessEnv = { ...process.env, ...extraEnv, NODE_OPTIONS: `--import ${tracer}`, CHANT_TRACE_OUT: traceOut };
71
82
  delete env.CHANT_ENV;
72
- const result = spawnSync(command, args, { cwd, env, encoding: "utf-8" });
83
+ const result = spawnSync(command, args, { cwd, env, encoding: "utf-8", input, timeout: 120_000 });
73
84
  const lines = readFileSync(traceOut, "utf-8").trim().split("\n");
74
85
  return { status: result.status, stdout: result.stdout, stderr: result.stderr, loaded: JSON.parse(lines[0]!) as Run["loaded"] };
75
86
  }
@@ -80,7 +91,9 @@ beforeAll(() => {
80
91
  writeFileSync(tracer, TRACER);
81
92
  const core = pack(coreRoot);
82
93
  // The lexicon's prepack regenerates it from upstream specs; the checkout's
83
- // generated source and dist are what this test needs, so pack them as they are.
94
+ // generated source and bundle are what this test needs, so pack them as they
95
+ // are, after the prepack's last step, the build that writes dist/*.js.
96
+ execFileSync("npm", ["run", "build"], { cwd: renderRoot, stdio: ["ignore", "pipe", "pipe"] });
84
97
  const render = pack(renderRoot, true);
85
98
 
86
99
  project = join(scratch, "project");
@@ -95,6 +108,13 @@ beforeAll(() => {
95
108
  );
96
109
  chant = join(project, "node_modules", ".bin", "chant");
97
110
 
111
+ // The same lexicon, from a config that is JSON, with no source: nothing for tsx to load.
112
+ mcpProject = join(scratch, "mcp-project");
113
+ mkdirSync(mcpProject);
114
+ writeFileSync(join(mcpProject, "package.json"), JSON.stringify({ name: "compiled-cli-mcp-e2e", private: true, type: "module" }));
115
+ writeFileSync(join(mcpProject, "chant.config.json"), JSON.stringify({ lexicons: ["render"] }));
116
+ symlinkSync(join(project, "node_modules"), join(mcpProject, "node_modules"), "dir");
117
+
98
118
  // The same project with no package.json at all, so nothing above the config
99
119
  // says "type": "module" and tsx compiles chant.config.ts as CommonJS (#3897).
100
120
  cjsProject = join(scratch, "cjs-project");
@@ -217,19 +237,64 @@ describe("the installed chant CLI (#3878)", () => {
217
237
  expect(heavy).toEqual([]);
218
238
  });
219
239
 
220
- test("build loads the TypeScript lexicon and project through tsx, against the one compiled core", () => {
240
+ test("build loads the project through tsx and the lexicon as compiled JS, against the one compiled core", () => {
221
241
  const r = run(project, ["build", "src", "-o", "out/render.json"]);
222
242
  expect(r.status, r.stderr).toBe(0);
223
243
  const built = JSON.parse(readFileSync(join(project, "out", "render.json"), "utf-8")) as Record<string, { body?: { name?: string } }>;
224
244
  expect(built.db?.body?.name).toBe("e2e-db");
225
245
  expect(r.loaded.tsx).toBeGreaterThan(0);
226
246
  expect(r.loaded.chantSrc).toBe(0);
247
+ expect(r.loaded.lexiconJs).toBeGreaterThan(0);
248
+ expect(r.loaded.lexiconTs).toBe(0);
227
249
  });
228
250
 
229
- test("lint passes on the project", () => {
251
+ test("lint passes on the project and loads none of the lexicon's TypeScript", () => {
230
252
  const r = run(project, ["lint", "src"]);
231
253
  expect(r.status, r.stdout + r.stderr).toBe(0);
232
254
  expect(r.loaded.chantSrc).toBe(0);
255
+ expect(r.loaded.lexiconTs).toBe(0);
256
+ });
257
+
258
+ test("serve mcp loads the packed lexicon's plugin with no tsx (#3981)", () => {
259
+ const input =
260
+ [
261
+ { jsonrpc: "2.0", id: 1, method: "initialize", params: { protocolVersion: "2024-11-05", clientInfo: { name: "e2e" } } },
262
+ { jsonrpc: "2.0", method: "notifications/initialized" },
263
+ { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} },
264
+ { jsonrpc: "2.0", id: 3, method: "resources/list", params: {} },
265
+ ]
266
+ .map((m) => JSON.stringify(m))
267
+ .join("\n") + "\n";
268
+ const r = run(mcpProject, ["serve", "mcp"], {}, chant, input);
269
+ expect(r.status, r.stderr).toBe(0);
270
+ const responses = r.stdout.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line) as { id?: number; result?: Record<string, unknown> });
271
+ const result = (id: number) => responses.find((m) => m.id === id)?.result;
272
+ expect((result(1)?.serverInfo as { name?: string } | undefined)?.name).toBe("chant");
273
+ const tools = ((result(2)?.tools ?? []) as Array<{ name: string }>).map((t) => t.name);
274
+ for (const name of ["build", "lint", "explain"]) expect(tools).toContain(name);
275
+ expect(r.loaded.lexiconJs).toBeGreaterThan(0);
276
+ expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0, lexiconTs: 0 });
277
+ });
278
+
279
+ test("the packed lexicon's plugin has the lint rules and post-synth checks of its source (#3981)", async () => {
280
+ // The plugin as the installed CLI loads it: compiled core, plain node, the lexicon's dist/*.js.
281
+ const script =
282
+ `const core = ${JSON.stringify(pathToFileURL(join(project, "node_modules", "@intentius", "chant", "dist")).href)};\n` +
283
+ `const { installCompiledCli } = await import(core + "/ts-loader.js");\n` +
284
+ `installCompiledCli();\n` +
285
+ `const { loadPlugin } = await import(core + "/cli/plugins.js");\n` +
286
+ `const plugin = await loadPlugin("render");\n` +
287
+ `process.stdout.write(JSON.stringify({ lintRules: plugin.lintRules().map((r) => r.id).sort(), postSynthChecks: (plugin.postSynthChecks?.() ?? []).map((c) => c.id).sort() }));\n`;
288
+ const r = run(mcpProject, ["--input-type=module", "-e", script], {}, process.execPath);
289
+ expect(r.status, r.stderr).toBe(0);
290
+ expect(r.loaded).toMatchObject({ chantSrc: 0, tsx: 0, lexiconTs: 0 });
291
+ const packed = JSON.parse(r.stdout) as { lintRules: string[]; postSynthChecks: string[] };
292
+
293
+ // The same plugin from the repository's source, in this (vitest) process.
294
+ const source = await loadPlugin("render");
295
+ expect(packed.lintRules).toEqual((source.lintRules?.() ?? []).map((rule) => rule.id).sort());
296
+ expect(packed.postSynthChecks).toEqual((source.postSynthChecks?.() ?? []).map((check) => check.id).sort());
297
+ expect(packed.lintRules.length).toBeGreaterThan(0);
233
298
  });
234
299
 
235
300
  test("build --sandbox bundles chant's source for the sandboxed child", () => {
@@ -0,0 +1,101 @@
1
+ /**
2
+ * chant #4116, end to end through the real CLI: a project whose dependencies
3
+ * are not installed is told so, and a project without node_modules that builds
4
+ * today still builds.
5
+ *
6
+ * Both fixtures are temporary git repositories with no package.json install,
7
+ * run with this checkout's chant from source the way
8
+ * ../workspace/member-commands.e2e.test.ts runs it:
9
+ *
10
+ * - `bare` is a clone of a project like examples/fan-out-estate: its source
11
+ * imports `@intentius/chant-lexicon-k8s` and nothing installs it;
12
+ * - `linked` is member-commands' setup: a node_modules holding only the
13
+ * lexicon, linked to this checkout, and no `@intentius/chant`. chant loads
14
+ * the lexicon, and the lexicon resolves `@intentius/chant` from its own
15
+ * location, so it builds. The first attempt at #4116 (#4118) refused it.
16
+ */
17
+ import { execFileSync, spawnSync } from "node:child_process";
18
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
19
+ import { tmpdir } from "node:os";
20
+ import { dirname, join } from "node:path";
21
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
22
+
23
+ const REPO = realpathSync(join(import.meta.dirname, "..", "..", "..", ".."));
24
+ const LOADER = join(REPO, "node_modules", "tsx", "dist", "loader.mjs");
25
+ const MAIN = join(REPO, "packages", "core", "src", "cli", "main.ts");
26
+ const TIMEOUT = 240_000;
27
+
28
+ let root: string;
29
+
30
+ function chant(args: string[], cwd: string): { status: number | null; stdout: string; stderr: string } {
31
+ const r = spawnSync(process.execPath, ["--import", `file://${LOADER}`, MAIN, ...args], {
32
+ cwd,
33
+ encoding: "utf-8",
34
+ timeout: TIMEOUT,
35
+ env: { ...process.env, TSX_DISABLE_CACHE: "1", NO_COLOR: "1" },
36
+ maxBuffer: 64 * 1024 * 1024,
37
+ });
38
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
39
+ }
40
+
41
+ function project(name: string): string {
42
+ const dir = join(root, name);
43
+ const write = (path: string, text: string) => {
44
+ mkdirSync(dirname(join(dir, path)), { recursive: true });
45
+ writeFileSync(join(dir, path), text);
46
+ };
47
+ write("chant.config.ts", 'export default { lexicons: ["k8s"] };\n');
48
+ write(
49
+ "src/infra.ts",
50
+ 'import { Namespace } from "@intentius/chant-lexicon-k8s";\nexport const appNs = new Namespace({ metadata: { name: "app" } });\n',
51
+ );
52
+ write(".gitignore", "node_modules\nout\n");
53
+ const git = (...args: string[]) =>
54
+ execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", ...args], { cwd: dir, stdio: "ignore" });
55
+ git("init", "-q");
56
+ git("add", "-A");
57
+ git("commit", "-qm", "fixture");
58
+ return dir;
59
+ }
60
+
61
+ let bare: string;
62
+ let linked: string;
63
+
64
+ beforeAll(() => {
65
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-4116-e2e-")));
66
+ bare = project("bare");
67
+ linked = project("linked");
68
+ mkdirSync(join(linked, "node_modules", "@intentius"), { recursive: true });
69
+ symlinkSync(join(REPO, "lexicons", "k8s"), join(linked, "node_modules", "@intentius", "chant-lexicon-k8s"), "dir");
70
+ }, TIMEOUT);
71
+
72
+ afterAll(() => {
73
+ if (root) rmSync(root, { recursive: true, force: true });
74
+ });
75
+
76
+ describe("dependencies not installed (#4116)", () => {
77
+ test("graph says the dependencies are not installed, not that the source has lint errors", () => {
78
+ const r = chant(["graph", "--format", "ir"], bare);
79
+ expect(r.status, r.stderr).toBe(1);
80
+ expect(r.stderr).toContain(`dependencies not installed: @intentius/chant-lexicon-k8s does not resolve from ${bare}`);
81
+ expect(r.stderr).toContain("run npm install (or the project's package manager)");
82
+ expect(r.stderr).not.toMatch(/lint errors/i);
83
+ expect(r.stdout).toBe("");
84
+ }, TIMEOUT);
85
+
86
+ test("build and lint lead with the dependencies message and exit 1", () => {
87
+ for (const verb of ["build", "lint"]) {
88
+ const r = chant([verb], bare);
89
+ expect(r.status, `${verb}: ${r.stderr}`).toBe(1);
90
+ expect(r.stderr, verb).toContain("dependencies not installed: @intentius/chant-lexicon-k8s does not resolve from");
91
+ }
92
+ }, TIMEOUT);
93
+
94
+ test("a project without @intentius/chant in node_modules that builds today still builds", () => {
95
+ for (const args of [["build"], ["lint"], ["graph", "--format", "ir"]]) {
96
+ const r = chant(args, linked);
97
+ expect(r.status, `${args.join(" ")}: ${r.stderr}`).toBe(0);
98
+ expect(r.stderr, args.join(" ")).not.toContain("dependencies not installed");
99
+ }
100
+ }, TIMEOUT);
101
+ });
@@ -0,0 +1,93 @@
1
+ /**
2
+ * chant #4116: which failure messages are imports of packages the project has
3
+ * not installed. The command paths that print the message have their own tests
4
+ * (handlers/graph.test.ts, handlers/lint.test.ts, dependencies.e2e.test.ts).
5
+ */
6
+ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
7
+ import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { reportUninstalledDependencies, uninstalledDependencyIn } from "./dependencies";
11
+
12
+ let root: string;
13
+ let project: string;
14
+
15
+ const notFound = (pkg: string, file = "src/a.ts") => `Cannot find package '${pkg}' imported from ${join(project, file)}`;
16
+
17
+ beforeEach(() => {
18
+ root = realpathSync(mkdtempSync(join(tmpdir(), "chant-4116-")));
19
+ project = join(root, "proj");
20
+ mkdirSync(join(project, "src"), { recursive: true });
21
+ });
22
+
23
+ afterEach(() => {
24
+ vi.restoreAllMocks();
25
+ rmSync(root, { recursive: true, force: true });
26
+ });
27
+
28
+ describe("uninstalledDependencyIn", () => {
29
+ test("an @intentius package no node_modules holds, as lint and build report it", () => {
30
+ expect(uninstalledDependencyIn(notFound("@intentius/chant-lexicon-aws"), project)).toEqual({
31
+ package: "@intentius/chant-lexicon-aws",
32
+ dir: project,
33
+ });
34
+ expect(uninstalledDependencyIn(`Component discovery error: ${notFound("@intentius/chant", "a.component.ts")}`, project)).toEqual({
35
+ package: "@intentius/chant",
36
+ dir: project,
37
+ });
38
+ });
39
+
40
+ test("a subpath import names its package", () => {
41
+ expect(uninstalledDependencyIn("Cannot find module '@intentius/chant/params' imported from /x/a.ts", project)?.package).toBe("@intentius/chant");
42
+ });
43
+
44
+ test("a package found in a parent node_modules is installed", () => {
45
+ mkdirSync(join(root, "node_modules", "@intentius", "chant-lexicon-aws"), { recursive: true });
46
+ expect(uninstalledDependencyIn(notFound("@intentius/chant-lexicon-aws"), project)).toBeUndefined();
47
+ });
48
+
49
+ test("a package found from the importing file's directory is installed", () => {
50
+ const lexicon = join(root, "elsewhere", "lexicon");
51
+ mkdirSync(join(root, "elsewhere", "node_modules", "@intentius", "chant"), { recursive: true });
52
+ mkdirSync(lexicon, { recursive: true });
53
+ expect(uninstalledDependencyIn(`Cannot find package '@intentius/chant' imported from ${join(lexicon, "index.ts")}`, project)).toBeUndefined();
54
+ });
55
+
56
+ test("another package counts only when the project's package.json declares it", () => {
57
+ expect(uninstalledDependencyIn(notFound("lodahs"), project)).toBeUndefined();
58
+ writeFileSync(join(project, "package.json"), JSON.stringify({ dependencies: { yaml: "*" } }));
59
+ expect(uninstalledDependencyIn(notFound("yaml"), project)).toEqual({ package: "yaml", dir: project });
60
+ expect(uninstalledDependencyIn(notFound("lodahs"), project)).toBeUndefined();
61
+ });
62
+
63
+ test("relative imports and other messages are not dependencies", () => {
64
+ expect(uninstalledDependencyIn("Cannot find module './missing' imported from /x/a.ts", project)).toBeUndefined();
65
+ expect(uninstalledDependencyIn("Exported declarable 'x' is never referenced", project)).toBeUndefined();
66
+ });
67
+ });
68
+
69
+ describe("reportUninstalledDependencies", () => {
70
+ let stderr: string[];
71
+ beforeEach(() => {
72
+ stderr = [];
73
+ vi.spyOn(console, "error").mockImplementation((s: string) => void stderr.push(s));
74
+ });
75
+
76
+ test("names the first uninstalled package with the install hint; all when every message is one", () => {
77
+ expect(reportUninstalledDependencies([notFound("@intentius/chant-lexicon-aws"), notFound("@intentius/chant")], project)).toBe("all");
78
+ expect(stderr).toHaveLength(1);
79
+ expect(stderr[0]).toContain(`dependencies not installed: @intentius/chant-lexicon-aws does not resolve from ${project}`);
80
+ expect(stderr[0]).toContain("run npm install (or the project's package manager)");
81
+ });
82
+
83
+ test("some when other errors are beside them", () => {
84
+ expect(reportUninstalledDependencies([notFound("@intentius/chant"), "a real problem"], project)).toBe("some");
85
+ expect(stderr).toHaveLength(1);
86
+ });
87
+
88
+ test("prints nothing when no message is one", () => {
89
+ expect(reportUninstalledDependencies(["a real problem"], project)).toBeUndefined();
90
+ expect(reportUninstalledDependencies([], project)).toBeUndefined();
91
+ expect(stderr).toEqual([]);
92
+ });
93
+ });
@@ -0,0 +1,122 @@
1
+ /**
2
+ * chant #4116: a failure caused by a project whose dependencies are not installed.
3
+ *
4
+ * In a clone with no node_modules, every project file that imports
5
+ * `@intentius/chant` or a lexicon package fails to load. Lint reports each one
6
+ * (LEX002, COMP000), the graph's lint gate then says the source has lint
7
+ * errors, and build prints node's own "Cannot find package" per file. The
8
+ * source is fine; the install is missing.
9
+ *
10
+ * This runs only after something has already failed, on that failure's
11
+ * messages. A project chant builds today never reaches it, whatever its
12
+ * node_modules looks like: chant resolves a lexicon from its own install first,
13
+ * and a project that imports only what it has installed loads fine. That is why
14
+ * the check is not made up front (#4118, reverted in #4125).
15
+ *
16
+ * A message counts when it is node's "Cannot find package|module '<spec>'",
17
+ * `<spec>` names a package that is `@intentius/*` or declared in the project's
18
+ * package.json, and no node_modules from the project directory up holds it. A
19
+ * typo in an import of an undeclared package keeps its message as it is.
20
+ */
21
+ import { existsSync, readFileSync } from "node:fs";
22
+ import { dirname, isAbsolute, join, resolve } from "node:path";
23
+ import { fileURLToPath } from "node:url";
24
+ import { packageNameOf } from "../lexicon-module";
25
+ import { formatError } from "./format";
26
+
27
+ /** A package that a failure could not import and that is not installed for the project. */
28
+ export interface UninstalledDependency {
29
+ package: string;
30
+ /** The project directory it does not resolve from. */
31
+ dir: string;
32
+ }
33
+
34
+ const NOT_FOUND = /Cannot find (?:package|module) '([^']+)'(?: imported from (\S+))?/g;
35
+
36
+ /** True when a node_modules at `dir` or above holds `pkg`. */
37
+ function installedFrom(pkg: string, dir: string): boolean {
38
+ for (let d = resolve(dir); ; d = dirname(d)) {
39
+ if (existsSync(join(d, "node_modules", ...pkg.split("/")))) return true;
40
+ if (dirname(d) === d) return false;
41
+ }
42
+ }
43
+
44
+ /** The packages the nearest package.json at `dir` or above declares, in any dependency field. */
45
+ function declaredPackages(dir: string): Set<string> {
46
+ for (let d = resolve(dir); ; d = dirname(d)) {
47
+ const file = join(d, "package.json");
48
+ if (existsSync(file)) {
49
+ try {
50
+ const json = JSON.parse(readFileSync(file, "utf-8")) as Record<string, unknown>;
51
+ const names = new Set<string>();
52
+ for (const field of ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"]) {
53
+ const deps = json[field];
54
+ if (deps && typeof deps === "object") for (const name of Object.keys(deps)) names.add(name);
55
+ }
56
+ return names;
57
+ } catch {
58
+ return new Set();
59
+ }
60
+ }
61
+ if (dirname(d) === d) return new Set();
62
+ }
63
+ }
64
+
65
+ function importerDir(importer: string | undefined): string | undefined {
66
+ if (!importer) return undefined;
67
+ try {
68
+ const path = importer.startsWith("file:") ? fileURLToPath(importer) : importer;
69
+ return isAbsolute(path) ? dirname(path) : undefined;
70
+ } catch {
71
+ return undefined;
72
+ }
73
+ }
74
+
75
+ /** The uninstalled package `message` reports, or `undefined` when it reports none. */
76
+ export function uninstalledDependencyIn(message: string, projectDir: string): UninstalledDependency | undefined {
77
+ const dir = resolve(projectDir);
78
+ let declared: Set<string> | undefined;
79
+ for (const match of message.matchAll(NOT_FOUND)) {
80
+ const pkg = packageNameOf(match[1]);
81
+ if (pkg === undefined) continue;
82
+ if (!pkg.startsWith("@intentius/")) {
83
+ declared ??= declaredPackages(dir);
84
+ if (!declared.has(pkg)) continue;
85
+ }
86
+ if (installedFrom(pkg, dir)) continue;
87
+ const from = importerDir(match[2]);
88
+ if (from !== undefined && installedFrom(pkg, from)) continue;
89
+ return { package: pkg, dir };
90
+ }
91
+ return undefined;
92
+ }
93
+
94
+ /** The dependencies message, formatted as an error with the install hint. */
95
+ export function formatDependenciesNotInstalled(dep: UninstalledDependency): string {
96
+ return formatError({
97
+ message: `dependencies not installed: ${dep.package} does not resolve from ${dep.dir}`,
98
+ hint: "run npm install (or the project's package manager)",
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Print the dependencies message, naming the first uninstalled package, when
104
+ * any of a failure's `messages` reports one. Prints nothing and returns
105
+ * `undefined` when none does. Returns `"all"` when every message reports one,
106
+ * so a caller can drop its own wording when that wording blames the source.
107
+ */
108
+ export function reportUninstalledDependencies(
109
+ messages: readonly string[],
110
+ projectDir: string,
111
+ ): "all" | "some" | undefined {
112
+ let first: UninstalledDependency | undefined;
113
+ let all = messages.length > 0;
114
+ for (const message of messages) {
115
+ const dep = uninstalledDependencyIn(message, projectDir);
116
+ if (dep) first ??= dep;
117
+ else all = false;
118
+ }
119
+ if (!first) return undefined;
120
+ console.error(formatDependenciesNotInstalled(first));
121
+ return all ? "all" : "some";
122
+ }