@endora-commerce/cli 0.100.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 (356) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -0
  3. package/dist/bin/endora.d.ts +28 -0
  4. package/dist/bin/endora.d.ts.map +1 -0
  5. package/dist/bin/endora.js +926 -0
  6. package/dist/bin/endora.js.map +1 -0
  7. package/dist/check/estate.d.ts +189 -0
  8. package/dist/check/estate.d.ts.map +1 -0
  9. package/dist/check/estate.js +1037 -0
  10. package/dist/check/estate.js.map +1 -0
  11. package/dist/check/hosts.d.ts +25 -0
  12. package/dist/check/hosts.d.ts.map +1 -0
  13. package/dist/check/hosts.js +1347 -0
  14. package/dist/check/hosts.js.map +1 -0
  15. package/dist/check/index.d.ts +60 -0
  16. package/dist/check/index.d.ts.map +1 -0
  17. package/dist/check/index.js +111 -0
  18. package/dist/check/index.js.map +1 -0
  19. package/dist/check/layout.d.ts +136 -0
  20. package/dist/check/layout.d.ts.map +1 -0
  21. package/dist/check/layout.js +262 -0
  22. package/dist/check/layout.js.map +1 -0
  23. package/dist/check/ledger.d.ts +98 -0
  24. package/dist/check/ledger.d.ts.map +1 -0
  25. package/dist/check/ledger.js +173 -0
  26. package/dist/check/ledger.js.map +1 -0
  27. package/dist/check/peer-owners.d.ts +116 -0
  28. package/dist/check/peer-owners.d.ts.map +1 -0
  29. package/dist/check/peer-owners.js +225 -0
  30. package/dist/check/peer-owners.js.map +1 -0
  31. package/dist/check/report.d.ts +33 -0
  32. package/dist/check/report.d.ts.map +1 -0
  33. package/dist/check/report.js +107 -0
  34. package/dist/check/report.js.map +1 -0
  35. package/dist/check/run.d.ts +147 -0
  36. package/dist/check/run.d.ts.map +1 -0
  37. package/dist/check/run.js +111 -0
  38. package/dist/check/run.js.map +1 -0
  39. package/dist/checks.d.ts +17 -0
  40. package/dist/checks.d.ts.map +1 -0
  41. package/dist/checks.js +17 -0
  42. package/dist/checks.js.map +1 -0
  43. package/dist/dev/index.d.ts +83 -0
  44. package/dist/dev/index.d.ts.map +1 -0
  45. package/dist/dev/index.js +298 -0
  46. package/dist/dev/index.js.map +1 -0
  47. package/dist/generate/divergence.d.ts +38 -0
  48. package/dist/generate/divergence.d.ts.map +1 -0
  49. package/dist/generate/divergence.js +237 -0
  50. package/dist/generate/divergence.js.map +1 -0
  51. package/dist/generate/index.d.ts +90 -0
  52. package/dist/generate/index.d.ts.map +1 -0
  53. package/dist/generate/index.js +369 -0
  54. package/dist/generate/index.js.map +1 -0
  55. package/dist/index.d.ts +23 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +35 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/inputs/declaration.d.ts +18 -0
  60. package/dist/inputs/declaration.d.ts.map +1 -0
  61. package/dist/inputs/declaration.js +64 -0
  62. package/dist/inputs/declaration.js.map +1 -0
  63. package/dist/inputs/env-file.d.ts +73 -0
  64. package/dist/inputs/env-file.d.ts.map +1 -0
  65. package/dist/inputs/env-file.js +134 -0
  66. package/dist/inputs/env-file.js.map +1 -0
  67. package/dist/inputs/prompt.d.ts +21 -0
  68. package/dist/inputs/prompt.d.ts.map +1 -0
  69. package/dist/inputs/prompt.js +59 -0
  70. package/dist/inputs/prompt.js.map +1 -0
  71. package/dist/inputs/resolve.d.ts +163 -0
  72. package/dist/inputs/resolve.d.ts.map +1 -0
  73. package/dist/inputs/resolve.js +290 -0
  74. package/dist/inputs/resolve.js.map +1 -0
  75. package/dist/install/host.d.ts +27 -0
  76. package/dist/install/host.d.ts.map +1 -0
  77. package/dist/install/host.js +90 -0
  78. package/dist/install/host.js.map +1 -0
  79. package/dist/install/index.d.ts +173 -0
  80. package/dist/install/index.d.ts.map +1 -0
  81. package/dist/install/index.js +793 -0
  82. package/dist/install/index.js.map +1 -0
  83. package/dist/install/wizard.d.ts +144 -0
  84. package/dist/install/wizard.d.ts.map +1 -0
  85. package/dist/install/wizard.js +362 -0
  86. package/dist/install/wizard.js.map +1 -0
  87. package/dist/lib/admin-artefacts.d.ts +70 -0
  88. package/dist/lib/admin-artefacts.d.ts.map +1 -0
  89. package/dist/lib/admin-artefacts.js +354 -0
  90. package/dist/lib/admin-artefacts.js.map +1 -0
  91. package/dist/lib/admin-surfaces.d.ts +298 -0
  92. package/dist/lib/admin-surfaces.d.ts.map +1 -0
  93. package/dist/lib/admin-surfaces.js +669 -0
  94. package/dist/lib/admin-surfaces.js.map +1 -0
  95. package/dist/lib/delegated-composer.d.ts +85 -0
  96. package/dist/lib/delegated-composer.d.ts.map +1 -0
  97. package/dist/lib/delegated-composer.js +241 -0
  98. package/dist/lib/delegated-composer.js.map +1 -0
  99. package/dist/lib/divergence-artefacts.d.ts +305 -0
  100. package/dist/lib/divergence-artefacts.d.ts.map +1 -0
  101. package/dist/lib/divergence-artefacts.js +828 -0
  102. package/dist/lib/divergence-artefacts.js.map +1 -0
  103. package/dist/lib/divergence.d.ts +337 -0
  104. package/dist/lib/divergence.d.ts.map +1 -0
  105. package/dist/lib/divergence.js +1005 -0
  106. package/dist/lib/divergence.js.map +1 -0
  107. package/dist/lib/docs-artefacts.d.ts +395 -0
  108. package/dist/lib/docs-artefacts.d.ts.map +1 -0
  109. package/dist/lib/docs-artefacts.js +781 -0
  110. package/dist/lib/docs-artefacts.js.map +1 -0
  111. package/dist/lib/emitted-exports.d.ts +21 -0
  112. package/dist/lib/emitted-exports.d.ts.map +1 -0
  113. package/dist/lib/emitted-exports.js +96 -0
  114. package/dist/lib/emitted-exports.js.map +1 -0
  115. package/dist/lib/emitted-freshness.d.ts +112 -0
  116. package/dist/lib/emitted-freshness.d.ts.map +1 -0
  117. package/dist/lib/emitted-freshness.js +288 -0
  118. package/dist/lib/emitted-freshness.js.map +1 -0
  119. package/dist/lib/entity-index-artefact.d.ts +95 -0
  120. package/dist/lib/entity-index-artefact.d.ts.map +1 -0
  121. package/dist/lib/entity-index-artefact.js +210 -0
  122. package/dist/lib/entity-index-artefact.js.map +1 -0
  123. package/dist/lib/instance-build-inputs.d.ts +108 -0
  124. package/dist/lib/instance-build-inputs.d.ts.map +1 -0
  125. package/dist/lib/instance-build-inputs.js +165 -0
  126. package/dist/lib/instance-build-inputs.js.map +1 -0
  127. package/dist/lib/module-docs.d.ts +473 -0
  128. package/dist/lib/module-docs.d.ts.map +1 -0
  129. package/dist/lib/module-docs.js +711 -0
  130. package/dist/lib/module-docs.js.map +1 -0
  131. package/dist/lib/module-package-subpaths.d.ts +56 -0
  132. package/dist/lib/module-package-subpaths.d.ts.map +1 -0
  133. package/dist/lib/module-package-subpaths.js +223 -0
  134. package/dist/lib/module-package-subpaths.js.map +1 -0
  135. package/dist/lib/module-packages.d.ts +200 -0
  136. package/dist/lib/module-packages.d.ts.map +1 -0
  137. package/dist/lib/module-packages.js +580 -0
  138. package/dist/lib/module-packages.js.map +1 -0
  139. package/dist/lib/module-population.d.ts +129 -0
  140. package/dist/lib/module-population.d.ts.map +1 -0
  141. package/dist/lib/module-population.js +172 -0
  142. package/dist/lib/module-population.js.map +1 -0
  143. package/dist/lib/module-roots.d.ts +337 -0
  144. package/dist/lib/module-roots.d.ts.map +1 -0
  145. package/dist/lib/module-roots.js +586 -0
  146. package/dist/lib/module-roots.js.map +1 -0
  147. package/dist/lib/nested-checkouts.d.ts +33 -0
  148. package/dist/lib/nested-checkouts.d.ts.map +1 -0
  149. package/dist/lib/nested-checkouts.js +160 -0
  150. package/dist/lib/nested-checkouts.js.map +1 -0
  151. package/dist/lib/platform-root.d.ts +43 -0
  152. package/dist/lib/platform-root.d.ts.map +1 -0
  153. package/dist/lib/platform-root.js +134 -0
  154. package/dist/lib/platform-root.js.map +1 -0
  155. package/dist/lib/platform-surface.d.ts +235 -0
  156. package/dist/lib/platform-surface.d.ts.map +1 -0
  157. package/dist/lib/platform-surface.js +393 -0
  158. package/dist/lib/platform-surface.js.map +1 -0
  159. package/dist/lib/port-registrations.d.ts +223 -0
  160. package/dist/lib/port-registrations.d.ts.map +1 -0
  161. package/dist/lib/port-registrations.js +532 -0
  162. package/dist/lib/port-registrations.js.map +1 -0
  163. package/dist/lib/read-size.d.ts +154 -0
  164. package/dist/lib/read-size.d.ts.map +1 -0
  165. package/dist/lib/read-size.js +182 -0
  166. package/dist/lib/read-size.js.map +1 -0
  167. package/dist/lib/registration-owners.d.ts +79 -0
  168. package/dist/lib/registration-owners.d.ts.map +1 -0
  169. package/dist/lib/registration-owners.js +77 -0
  170. package/dist/lib/registration-owners.js.map +1 -0
  171. package/dist/lib/release-index.d.ts +53 -0
  172. package/dist/lib/release-index.d.ts.map +1 -0
  173. package/dist/lib/release-index.js +162 -0
  174. package/dist/lib/release-index.js.map +1 -0
  175. package/dist/lib/repeating-timers.d.ts +79 -0
  176. package/dist/lib/repeating-timers.d.ts.map +1 -0
  177. package/dist/lib/repeating-timers.js +189 -0
  178. package/dist/lib/repeating-timers.js.map +1 -0
  179. package/dist/lib/source-text.d.ts +34 -0
  180. package/dist/lib/source-text.d.ts.map +1 -0
  181. package/dist/lib/source-text.js +80 -0
  182. package/dist/lib/source-text.js.map +1 -0
  183. package/dist/lib/specifiers.d.ts +20 -0
  184. package/dist/lib/specifiers.d.ts.map +1 -0
  185. package/dist/lib/specifiers.js +130 -0
  186. package/dist/lib/specifiers.js.map +1 -0
  187. package/dist/lib/sql-tables.d.ts +166 -0
  188. package/dist/lib/sql-tables.d.ts.map +1 -0
  189. package/dist/lib/sql-tables.js +464 -0
  190. package/dist/lib/sql-tables.js.map +1 -0
  191. package/dist/lib/switchable-modules.d.ts +54 -0
  192. package/dist/lib/switchable-modules.d.ts.map +1 -0
  193. package/dist/lib/switchable-modules.js +104 -0
  194. package/dist/lib/switchable-modules.js.map +1 -0
  195. package/dist/lib/tailwind-sources.d.ts +136 -0
  196. package/dist/lib/tailwind-sources.d.ts.map +1 -0
  197. package/dist/lib/tailwind-sources.js +307 -0
  198. package/dist/lib/tailwind-sources.js.map +1 -0
  199. package/dist/lib/ui-layer.d.ts +54 -0
  200. package/dist/lib/ui-layer.d.ts.map +1 -0
  201. package/dist/lib/ui-layer.js +57 -0
  202. package/dist/lib/ui-layer.js.map +1 -0
  203. package/dist/lib/workspace-packages.d.ts +186 -0
  204. package/dist/lib/workspace-packages.d.ts.map +1 -0
  205. package/dist/lib/workspace-packages.js +351 -0
  206. package/dist/lib/workspace-packages.js.map +1 -0
  207. package/dist/new-instance/deploy.d.ts +211 -0
  208. package/dist/new-instance/deploy.d.ts.map +1 -0
  209. package/dist/new-instance/deploy.js +1381 -0
  210. package/dist/new-instance/deploy.js.map +1 -0
  211. package/dist/new-instance/docs-toolchain.d.ts +66 -0
  212. package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
  213. package/dist/new-instance/docs-toolchain.js +69 -0
  214. package/dist/new-instance/docs-toolchain.js.map +1 -0
  215. package/dist/new-instance/host.d.ts +124 -0
  216. package/dist/new-instance/host.d.ts.map +1 -0
  217. package/dist/new-instance/host.js +276 -0
  218. package/dist/new-instance/host.js.map +1 -0
  219. package/dist/new-instance/index.d.ts +118 -0
  220. package/dist/new-instance/index.d.ts.map +1 -0
  221. package/dist/new-instance/index.js +567 -0
  222. package/dist/new-instance/index.js.map +1 -0
  223. package/dist/new-instance/modules.d.ts +188 -0
  224. package/dist/new-instance/modules.d.ts.map +1 -0
  225. package/dist/new-instance/modules.js +392 -0
  226. package/dist/new-instance/modules.js.map +1 -0
  227. package/dist/new-instance/template.d.ts +505 -0
  228. package/dist/new-instance/template.d.ts.map +1 -0
  229. package/dist/new-instance/template.js +1886 -0
  230. package/dist/new-instance/template.js.map +1 -0
  231. package/dist/new-module/emit.d.ts +67 -0
  232. package/dist/new-module/emit.d.ts.map +1 -0
  233. package/dist/new-module/emit.js +1393 -0
  234. package/dist/new-module/emit.js.map +1 -0
  235. package/dist/new-module/host.d.ts +67 -0
  236. package/dist/new-module/host.d.ts.map +1 -0
  237. package/dist/new-module/host.js +224 -0
  238. package/dist/new-module/host.js.map +1 -0
  239. package/dist/new-module/index.d.ts +32 -0
  240. package/dist/new-module/index.d.ts.map +1 -0
  241. package/dist/new-module/index.js +193 -0
  242. package/dist/new-module/index.js.map +1 -0
  243. package/dist/new-module/spec.d.ts +188 -0
  244. package/dist/new-module/spec.d.ts.map +1 -0
  245. package/dist/new-module/spec.js +404 -0
  246. package/dist/new-module/spec.js.map +1 -0
  247. package/dist/new-module/text.d.ts +15 -0
  248. package/dist/new-module/text.d.ts.map +1 -0
  249. package/dist/new-module/text.js +22 -0
  250. package/dist/new-module/text.js.map +1 -0
  251. package/dist/new-storefront/dockerfile.d.ts +20 -0
  252. package/dist/new-storefront/dockerfile.d.ts.map +1 -0
  253. package/dist/new-storefront/dockerfile.js +131 -0
  254. package/dist/new-storefront/dockerfile.js.map +1 -0
  255. package/dist/new-storefront/gitignore.d.ts +23 -0
  256. package/dist/new-storefront/gitignore.d.ts.map +1 -0
  257. package/dist/new-storefront/gitignore.js +39 -0
  258. package/dist/new-storefront/gitignore.js.map +1 -0
  259. package/dist/new-storefront/index.d.ts +93 -0
  260. package/dist/new-storefront/index.d.ts.map +1 -0
  261. package/dist/new-storefront/index.js +329 -0
  262. package/dist/new-storefront/index.js.map +1 -0
  263. package/dist/new-storefront/npmrc.d.ts +98 -0
  264. package/dist/new-storefront/npmrc.d.ts.map +1 -0
  265. package/dist/new-storefront/npmrc.js +217 -0
  266. package/dist/new-storefront/npmrc.js.map +1 -0
  267. package/dist/new-storefront/reference.d.ts +189 -0
  268. package/dist/new-storefront/reference.d.ts.map +1 -0
  269. package/dist/new-storefront/reference.js +430 -0
  270. package/dist/new-storefront/reference.js.map +1 -0
  271. package/dist/new-storefront/rewrite.d.ts +171 -0
  272. package/dist/new-storefront/rewrite.d.ts.map +1 -0
  273. package/dist/new-storefront/rewrite.js +701 -0
  274. package/dist/new-storefront/rewrite.js.map +1 -0
  275. package/dist/release-index.json +284 -0
  276. package/dist/rules/action-route-permissions.d.ts +141 -0
  277. package/dist/rules/action-route-permissions.d.ts.map +1 -0
  278. package/dist/rules/action-route-permissions.js +556 -0
  279. package/dist/rules/action-route-permissions.js.map +1 -0
  280. package/dist/rules/bundle-pairing.d.ts +74 -0
  281. package/dist/rules/bundle-pairing.d.ts.map +1 -0
  282. package/dist/rules/bundle-pairing.js +281 -0
  283. package/dist/rules/bundle-pairing.js.map +1 -0
  284. package/dist/rules/channel-resolution.d.ts +17 -0
  285. package/dist/rules/channel-resolution.d.ts.map +1 -0
  286. package/dist/rules/channel-resolution.js +382 -0
  287. package/dist/rules/channel-resolution.js.map +1 -0
  288. package/dist/rules/command-coverage.d.ts +210 -0
  289. package/dist/rules/command-coverage.d.ts.map +1 -0
  290. package/dist/rules/command-coverage.js +714 -0
  291. package/dist/rules/command-coverage.js.map +1 -0
  292. package/dist/rules/container-imports.d.ts +60 -0
  293. package/dist/rules/container-imports.d.ts.map +1 -0
  294. package/dist/rules/container-imports.js +158 -0
  295. package/dist/rules/container-imports.js.map +1 -0
  296. package/dist/rules/default-language-prose.d.ts +212 -0
  297. package/dist/rules/default-language-prose.d.ts.map +1 -0
  298. package/dist/rules/default-language-prose.js +710 -0
  299. package/dist/rules/default-language-prose.js.map +1 -0
  300. package/dist/rules/diacritic-folds.d.ts +238 -0
  301. package/dist/rules/diacritic-folds.d.ts.map +1 -0
  302. package/dist/rules/diacritic-folds.js +681 -0
  303. package/dist/rules/diacritic-folds.js.map +1 -0
  304. package/dist/rules/entity-tenant-classification.d.ts +171 -0
  305. package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
  306. package/dist/rules/entity-tenant-classification.js +323 -0
  307. package/dist/rules/entity-tenant-classification.js.map +1 -0
  308. package/dist/rules/entry-presence.d.ts +142 -0
  309. package/dist/rules/entry-presence.d.ts.map +1 -0
  310. package/dist/rules/entry-presence.js +339 -0
  311. package/dist/rules/entry-presence.js.map +1 -0
  312. package/dist/rules/entry-scope.d.ts +91 -0
  313. package/dist/rules/entry-scope.d.ts.map +1 -0
  314. package/dist/rules/entry-scope.js +404 -0
  315. package/dist/rules/entry-scope.js.map +1 -0
  316. package/dist/rules/env-inputs.d.ts +222 -0
  317. package/dist/rules/env-inputs.d.ts.map +1 -0
  318. package/dist/rules/env-inputs.js +951 -0
  319. package/dist/rules/env-inputs.js.map +1 -0
  320. package/dist/rules/kernel-boundary.d.ts +37 -0
  321. package/dist/rules/kernel-boundary.d.ts.map +1 -0
  322. package/dist/rules/kernel-boundary.js +195 -0
  323. package/dist/rules/kernel-boundary.js.map +1 -0
  324. package/dist/rules/nul-bytes.d.ts +233 -0
  325. package/dist/rules/nul-bytes.d.ts.map +1 -0
  326. package/dist/rules/nul-bytes.js +332 -0
  327. package/dist/rules/nul-bytes.js.map +1 -0
  328. package/dist/rules/platform-surface.d.ts +479 -0
  329. package/dist/rules/platform-surface.d.ts.map +1 -0
  330. package/dist/rules/platform-surface.js +749 -0
  331. package/dist/rules/platform-surface.js.map +1 -0
  332. package/dist/rules/port-catches.d.ts +225 -0
  333. package/dist/rules/port-catches.d.ts.map +1 -0
  334. package/dist/rules/port-catches.js +1374 -0
  335. package/dist/rules/port-catches.js.map +1 -0
  336. package/dist/rules/port-shape.d.ts +213 -0
  337. package/dist/rules/port-shape.d.ts.map +1 -0
  338. package/dist/rules/port-shape.js +670 -0
  339. package/dist/rules/port-shape.js.map +1 -0
  340. package/dist/rules/queue-names.d.ts +108 -0
  341. package/dist/rules/queue-names.d.ts.map +1 -0
  342. package/dist/rules/queue-names.js +395 -0
  343. package/dist/rules/queue-names.js.map +1 -0
  344. package/dist/rules/singleton-identity.d.ts +205 -0
  345. package/dist/rules/singleton-identity.d.ts.map +1 -0
  346. package/dist/rules/singleton-identity.js +830 -0
  347. package/dist/rules/singleton-identity.js.map +1 -0
  348. package/dist/rules/subscribe-seam.d.ts +121 -0
  349. package/dist/rules/subscribe-seam.d.ts.map +1 -0
  350. package/dist/rules/subscribe-seam.js +594 -0
  351. package/dist/rules/subscribe-seam.js.map +1 -0
  352. package/dist/rules/transaction-context.d.ts +40 -0
  353. package/dist/rules/transaction-context.d.ts.map +1 -0
  354. package/dist/rules/transaction-context.js +294 -0
  355. package/dist/rules/transaction-context.js.map +1 -0
  356. package/package.json +59 -0
@@ -0,0 +1,781 @@
1
+ /**
2
+ * The documentation artefacts a tree is built from — this repository's site and
3
+ * a client instance's alike (`contracts/instance-repository.md` R3.2, R3.5;
4
+ * `contracts/instance-tree.md` §2.6; feature 100 / roadmap F12).
5
+ *
6
+ * ## Why these renderers live in the package
7
+ *
8
+ * They were `backend/scripts/generate-composer.ts`', which no client can reach.
9
+ * `instance-tree.md` §2.6 lists the documentation registry among the three
10
+ * artefacts an instance generates, and until this move there was no
11
+ * implementation on the other side of that sentence: a scaffolded instance could
12
+ * install thirty module packages, each shipping its own `docs/` layer, and had
13
+ * no way to render a navigation over them. That is `admin-artefacts.ts`' story
14
+ * one artefact family over, and R3.5's answer is the same one — *"one generator,
15
+ * one derivation … never a second implementation"*, with the **population** as
16
+ * the parameter.
17
+ *
18
+ * ## The population enters as {@link DocsModule}, never as a walk
19
+ *
20
+ * `composer:generate` builds it from the manifest index walk (workspace members
21
+ * plus the modules the host itself owns); `endora generate` builds it from the
22
+ * packages an instance installed. Neither walk is in here, because the two
23
+ * populations are genuinely different questions and only the *rendering* is one
24
+ * program. What a module owes this file is four facts — its id, where its pages
25
+ * are, whether it has decided it has none, and which file its manifest is — and
26
+ * `DiscoveredManifest` is structurally one of these, so the workspace host hands
27
+ * its own record over unchanged.
28
+ *
29
+ * ## What it cannot see, stated here rather than discovered later
30
+ *
31
+ * Everything `lib/module-docs.ts`' header already records — front matter is a
32
+ * leading `---` block, a doc id is a file path, and nothing here says whether a
33
+ * page is good, current or complete. This file adds one of its own: the
34
+ * reference page is rendered from the manifest it is handed and from nothing
35
+ * else, so a module whose manifest is a compiled artefact is described by that
36
+ * artefact, which is the previous build if nobody rebuilt it (D-164).
37
+ */
38
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
39
+ import { dirname, join, relative } from 'node:path';
40
+ import { pathToFileURL } from 'node:url';
41
+ import { attributeDocs, collectDocPages, collectModuleDocPages, comparePages, copyTargetOf, categoryPositionOf, docsDeclarationIn, duplicateDocIds, isDirectory, labelOf, slugForModule, DOCS_SIDEBAR_ARTEFACT, MODULE_MAP_ARTEFACT, MODULE_REFERENCE_CATEGORY, MODULES_CATEGORY, PAGE_EXTENSIONS, } from './module-docs.js';
42
+ import { ModulePackageError, publishedManifestEntryOf, } from './module-packages.js';
43
+ /** The label the module map's first column uses when a page carries none. */
44
+ /**
45
+ * The "do not edit" headers this repository's own generator writes.
46
+ *
47
+ * Parameters rather than literals, on `admin-artefacts.ts`' precedent: the two
48
+ * hosts tell a reader different things to run — `composer:generate` here,
49
+ * `endora generate` in an instance — and an artefact that named the wrong one
50
+ * would send a client to a script their tree does not have. The default is this
51
+ * repository's, so the committed artefacts are byte-identical across the move
52
+ * and a host that means the other one says so.
53
+ */
54
+ export const COMPOSER_DOCS_HEADER = `// AUTO-GENERATED by scripts/generate-composer.ts — DO NOT EDIT.\n` +
55
+ `// Run \`pnpm --filter backend run composer:generate\` (or rebuild the backend)\n` +
56
+ `// to refresh. Editing this file by hand is undone by the next build, and\n` +
57
+ `// \`pnpm --filter backend run overlay:check\` fails on the drift.\n`;
58
+ /**
59
+ * The same, for a markdown page, whose comment syntax is HTML's.
60
+ *
61
+ * **It is emitted *below* the front matter, and that placement is load-bearing
62
+ * rather than aesthetic** (feature 133, FR-012). Front matter is front matter
63
+ * only at byte 0: a `---` fence that opens under a four-line banner is never
64
+ * parsed, so `title`, `sidebar_label` and `description` are inert — and the
65
+ * banner is then also the page's first content node, which is not a `# `
66
+ * heading, so the `contentTitle` route to a title is closed too. Every
67
+ * generated page shipped titled with its own doc id (`catalog | B2B Platform`)
68
+ * for as long as this was emitted first. Below the fence the comment is still a
69
+ * plain "do not edit" banner to a reader of the source and is invisible in the
70
+ * rendered page, which is all it was ever for.
71
+ */
72
+ export const COMPOSER_DOCS_PAGE_HEADER = `<!-- AUTO-GENERATED by scripts/generate-composer.ts — DO NOT EDIT.\n` +
73
+ ` Run \`pnpm --filter backend run composer:generate\` to refresh. Editing this\n` +
74
+ ` file by hand is undone by the next run, and\n` +
75
+ ` \`pnpm --filter backend run overlay:check\` fails on the drift. -->`;
76
+ const MAP_FALLBACK_LABEL = (moduleId) => moduleId;
77
+ /**
78
+ * Every registered module, with the documentation the site holds for it.
79
+ *
80
+ * The population is the **index's**, so a module with no page is an entry with
81
+ * no documentation rather than an absence — the module map is a census of the
82
+ * platform, not a census of what somebody happened to write.
83
+ */
84
+ export function collectDocsRegistry(registered, attribution, packages,
85
+ /** Modules that declare `docs: false` — they owe no page, generated or written. */
86
+ declinedDocs = new Set()) {
87
+ const byModule = new Map(attribution.documented.map((entry) => [entry.moduleId, entry]));
88
+ const packageName = new Map(packages.map((pkg) => [pkg.moduleId, pkg.name]));
89
+ return [...registered]
90
+ .sort((a, b) => a.localeCompare(b))
91
+ .map((moduleId) => ({
92
+ moduleId,
93
+ docs: byModule.get(moduleId) ?? null,
94
+ shipsFrom: packageName.get(moduleId) ?? 'core',
95
+ referenceDocId: declinedDocs.has(moduleId)
96
+ ? null
97
+ : `${MODULE_REFERENCE_CATEGORY}/${slugForModule(moduleId)}`,
98
+ }));
99
+ }
100
+ /** A JS string literal for the emitted CommonJS fragment. */
101
+ function jsString(value) {
102
+ return `'${value.split('\\').join('\\\\').split("'").join("\\'")}'`;
103
+ }
104
+ /** Emit `label` + `key` so Docusaurus derives stable `sidebar.main.*` translation ids. */
105
+ function jsSidebarItemLabel(key, message) {
106
+ return `label: ${jsString(message)}, key: ${jsString(key)}`;
107
+ }
108
+ /**
109
+ * The sidebar's Modules category, as the array Docusaurus already accepts.
110
+ *
111
+ * `.js` rather than `.ts` because `sidebars.js` is `.js`, `sidebarPath` is
112
+ * `require`d by Docusaurus, and `docs/tsconfig.json` extends
113
+ * `@docusaurus/tsconfig`, which sets no `allowJs` — so a `.ts` fragment would be
114
+ * read by the site and by no type-checker, which is worse than either.
115
+ *
116
+ * Ordering is **flat and alphabetical by label**, with a page's own
117
+ * `sidebar_position` overriding (research D-5). Today's hand-written list
118
+ * clusters the payment and delivery vendors out of alphabetical order, and that
119
+ * clustering is not derivable — `tpay` does not declare `payments` in its
120
+ * manifest dependencies at all — so the flat list is taken and the loss is
121
+ * stated rather than papered over with a front-matter field this repository
122
+ * would have invented. `spec.md` Q1 is the owner's question about it.
123
+ */
124
+ export function emitDocsSidebar(entries, header = COMPOSER_DOCS_HEADER) {
125
+ const items = entries
126
+ // A module with neither prose nor a reference page contributes nothing: it
127
+ // declared `docs: false`, and an entry for it would be a navigation line
128
+ // naming a page that is deliberately not there.
129
+ .filter((entry) => entry.docs !== null || entry.referenceDocId !== null)
130
+ .map((entry) => ({
131
+ label: entry.docs === null
132
+ ? MAP_FALLBACK_LABEL(entry.moduleId)
133
+ : labelOf(entry.docs.entry, MAP_FALLBACK_LABEL(entry.moduleId)),
134
+ position: entry.docs === null ? null : categoryPositionOf(entry.docs.entry),
135
+ entry,
136
+ }))
137
+ .sort(comparePages)
138
+ .map(({ label, entry }) => {
139
+ const { docs, referenceDocId } = entry;
140
+ // A module nobody has written about yet still has a reference page, and
141
+ // this is where a reader reaches it. Without the entry the page would be
142
+ // findable only by guessing a URL — `spec.md` § 0.2's defect, arriving
143
+ // through the artefact meant to answer it.
144
+ if (docs === null) {
145
+ return ` { type: 'doc', id: ${jsString(referenceDocId ?? '')}, ${jsSidebarItemLabel(entry.moduleId, label)} },`;
146
+ }
147
+ const items = [
148
+ ...docs.children.map((child) => child.docId),
149
+ ...(referenceDocId === null ? [] : [referenceDocId]),
150
+ ];
151
+ if (items.length === 0) {
152
+ return ` { type: 'doc', id: ${jsString(docs.entry.docId)}, ${jsSidebarItemLabel(entry.moduleId, label)} },`;
153
+ }
154
+ // The reference page goes **last**, after whatever sub-pages a module
155
+ // wrote: a generated table is what a reader falls back to, not what they
156
+ // are shown first.
157
+ const children = items.map((docId) => ` ${jsString(docId)},`).join('\n');
158
+ return (` {\n` +
159
+ ` type: 'category',\n` +
160
+ ` ${jsSidebarItemLabel(entry.moduleId, label)},\n` +
161
+ ` link: { type: 'doc', id: ${jsString(docs.entry.docId)} },\n` +
162
+ ` items: [\n${children}\n ],\n` +
163
+ ` },`);
164
+ })
165
+ .join('\n');
166
+ // The generated map is navigation for a generated page, so it belongs in the
167
+ // generated fragment: putting it in `sidebars.js` would make the category's
168
+ // item list a hand-edited file again, one entry short of the thing this
169
+ // artefact exists to remove.
170
+ const map = ` { type: 'doc', id: ${jsString(`${MODULES_CATEGORY}/${MODULE_MAP_ARTEFACT.replace(/\.mdx?$/, '')}`)}, ${jsSidebarItemLabel('module-map', 'Module map')} },`;
171
+ return `${header}//
172
+ // The Modules category of the documentation sidebar (feature 100 / roadmap F12,
173
+ // \`contracts/docs-registry.md\` §1). \`sidebars.js\` requires it:
174
+ //
175
+ // items: require('./sidebars.modules.generated.js'),
176
+ //
177
+ // Every entry is derived — the module set from the generated manifest index, the
178
+ // page from the site's own tree, the label from the page's own Docusaurus front
179
+ // matter (\`sidebar_label\`, else \`title\`), the order from \`sidebar_position\` and
180
+ // then alphabetically by label. No field this repository invented appears here or
181
+ // in any page, so a third-party module author writes ordinary Docusaurus
182
+ // markdown and learns nothing from us.
183
+ //
184
+ // It exists because the hand-written list this replaces was edited by 12 of the
185
+ // 12 most recently added modules and forgotten by seven of them: \`ksef\`,
186
+ // \`newsletter\`, \`pwa\`, \`returns\`, \`shipments\`, \`transactional_emails\` and
187
+ // \`google_analytics\` each had a written page a reader could only reach by
188
+ // guessing a URL, and nothing in the repository could see it.
189
+
190
+ /** @type {import('@docusaurus/plugin-content-docs').SidebarItemConfig[]} */
191
+ const modules = [
192
+ ${map}
193
+ ${items}
194
+ ];
195
+
196
+ module.exports = modules;
197
+ `;
198
+ }
199
+ /** Escape a cell so a capability sentence carrying a pipe cannot break the table. */
200
+ function markdownCell(value) {
201
+ return value.split('|').join('\\|').split('\n').join(' ').trim();
202
+ }
203
+ /**
204
+ * A front-matter value YAML will read back as the string it was given.
205
+ *
206
+ * Plain scalars are left plain — a quoted `title` would rewrite 152 tracked
207
+ * pages for nothing — and anything YAML would choke on or reinterpret is
208
+ * double-quoted. The case that shipped: `description` is a sentence reading
209
+ * *"…manifest declares: permissions, …"*, and `: ` inside a plain scalar is an
210
+ * incomplete mapping pair, so `docusaurus build` died in `gray-matter` on the
211
+ * first generated page it parsed. It parsed none of them before feature 133
212
+ * moved the front matter to byte 0, which is why nothing caught it earlier.
213
+ *
214
+ * The `#` rule is the mirror image of the colon's and was written backwards
215
+ * once: a comment starts at a `#` **preceded** by whitespace or at the start of
216
+ * the value, whatever follows it, so *"Sync catalog #1 with PIM"* truncates
217
+ * silently while `C#` and `a#b` are perfectly good plain scalars. Testing for a
218
+ * `#` followed by whitespace detected neither case. Exported for
219
+ * `test/docs-front-matter-scalar.test.ts`, which is the only reason this helper
220
+ * is not file-local.
221
+ */
222
+ export function yamlScalar(value) {
223
+ const plain = !/(^|\s)#|:(\s|$)|^[\s>|&*!%@`'"[{-]|[:\s]$/.test(value);
224
+ return plain ? value : `"${value.split('\\').join('\\\\').split('"').join('\\"')}"`;
225
+ }
226
+ /**
227
+ * The module map — one row per **registered** module, never per page.
228
+ *
229
+ * A module the index registers and no page documents gets a row saying so,
230
+ * rather than being silently absent: the map is a census of the platform, and an
231
+ * absence is the defect this feature exists to end. 23 registered modules had no
232
+ * row when this landed.
233
+ *
234
+ * The old table's third column, `Owns HTTP surface?`, is **dropped**. It was
235
+ * hand-written, wrong in several rows, and is not cheaply derivable — a module's
236
+ * routes are registered through `ctx.routes` at composition and declared in no
237
+ * manifest. A column that cannot be derived is a column that goes stale, which
238
+ * is the defect this artefact replaces.
239
+ */
240
+ export function emitModuleMap(entries, header = COMPOSER_DOCS_PAGE_HEADER) {
241
+ const rows = entries
242
+ .map((entry) => {
243
+ if (entry.docs === null) {
244
+ return (`| \`${entry.moduleId}\` | _no page yet_ | ${markdownCell(entry.shipsFrom)} |`);
245
+ }
246
+ const label = labelOf(entry.docs.entry, MAP_FALLBACK_LABEL(entry.moduleId));
247
+ const href = `./${entry.docs.entry.relativePath}`;
248
+ const capability = entry.docs.entry.frontMatter.description ?? '_no description yet_';
249
+ return `| [${markdownCell(label)}](${href}) | ${markdownCell(capability)} | ${markdownCell(entry.shipsFrom)} |`;
250
+ })
251
+ .join('\n');
252
+ return `---
253
+ title: Module map
254
+ sidebar_label: Module map
255
+ description: Every module this platform composes, with the capability it owns and the package that ships it.
256
+ ---
257
+
258
+ ${header}
259
+
260
+ # Module map
261
+
262
+ One row per module the platform registers — derived from the generated manifest
263
+ index, the pages on disk and each page's own \`description\` front matter. A
264
+ module with no page is listed with none rather than left out: this is a census
265
+ of the platform, not of what happens to be written.
266
+
267
+ | Module | Capability | Ships from |
268
+ | --- | --- | --- |
269
+ ${rows}
270
+ `;
271
+ }
272
+ /** Raised when a page in the reference category belongs to no module. */
273
+ export class StrayReferencePageError extends ModulePackageError {
274
+ }
275
+ /** A manifest field, read defensively — the generator must not trust a shape. */
276
+ function arrayOf(value) {
277
+ return Array.isArray(value) ? value : [];
278
+ }
279
+ function stringOr(value, fallback) {
280
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
281
+ }
282
+ function stringOrNull(value) {
283
+ return typeof value === 'string' && value.length > 0 ? value : null;
284
+ }
285
+ /**
286
+ * One module's reference record, from its manifest and the package that ships it.
287
+ *
288
+ * The manifest is **imported**, not parsed out of its source text, for the
289
+ * reason `renderComposer` already imports one: a manifest is TypeScript with
290
+ * arrays, spreads and helper calls in it, and a text scan of that is a second
291
+ * reader of a declaration whose first reader is the platform. What is imported
292
+ * is the module's own `manifest.ts` — never the generated index, which is this
293
+ * generator's own previous answer.
294
+ */
295
+ export function referenceOf(moduleId, loaded, shipsFrom, prosePage, publishedLicense = null) {
296
+ const manifest = loaded.manifest ?? {};
297
+ const activationRaw = manifest.activation;
298
+ const activation = activationRaw === undefined
299
+ ? null
300
+ : activationRaw.nonDeactivatable === true
301
+ ? { kind: 'locked', reason: stringOr(activationRaw.reason, 'not stated') }
302
+ : {
303
+ kind: 'control',
304
+ settingCode: stringOr(activationRaw.settingCode, '(unnamed)'),
305
+ default: activationRaw.default === true,
306
+ };
307
+ const settings = manifest.settings;
308
+ const i18n = manifest.i18n;
309
+ return {
310
+ moduleId,
311
+ slug: slugForModule(moduleId),
312
+ name: stringOr(manifest.name, moduleId),
313
+ version: stringOr(manifest.version, '0.0.0'),
314
+ shipsFrom,
315
+ license: publishedLicense,
316
+ activation,
317
+ dependencies: [...arrayOf(manifest.dependencies).map(String)].sort(),
318
+ acknowledgedDependencies: arrayOf(manifest.acknowledgedDependencies)
319
+ .map((entry) => ({
320
+ moduleId: stringOr(entry.moduleId, '?'),
321
+ port: stringOr(entry.port, '?'),
322
+ reason: stringOr(entry.reason, ''),
323
+ }))
324
+ .sort((a, b) => `${a.moduleId}${a.port}`.localeCompare(`${b.moduleId}${b.port}`)),
325
+ nonBindingDependencies: arrayOf(manifest.nonBindingDependencies)
326
+ .map((entry) => ({
327
+ moduleId: stringOr(entry.moduleId, '?'),
328
+ name: stringOr(entry.name, '?'),
329
+ kind: stringOr(entry.kind, '?'),
330
+ whenAbsent: stringOrNull(entry.whenAbsent),
331
+ }))
332
+ .sort((a, b) => `${a.moduleId}${a.name}`.localeCompare(`${b.moduleId}${b.name}`)),
333
+ permissions: arrayOf(manifest.permissions)
334
+ .map((entry) => ({
335
+ code: stringOr(entry.code, '?'),
336
+ label: stringOr(entry.label, ''),
337
+ requires: Array.isArray(entry.requires) ? entry.requires.map(String) : [],
338
+ }))
339
+ .sort((a, b) => a.code.localeCompare(b.code)),
340
+ actions: arrayOf(manifest.actions)
341
+ .map((entry) => ({
342
+ id: stringOr(entry.id, '?'),
343
+ targetRoute: stringOr(entry.targetRoute, '?'),
344
+ requiredPermission: stringOrNull(entry.requiredPermission),
345
+ }))
346
+ .sort((a, b) => a.id.localeCompare(b.id)),
347
+ settings: arrayOf(settings?.settings)
348
+ .map((entry) => ({
349
+ code: stringOr(entry.code, '?'),
350
+ name: stringOr(entry.name, ''),
351
+ valueType: stringOr(entry.valueType, '?'),
352
+ }))
353
+ .sort((a, b) => a.code.localeCompare(b.code)),
354
+ bundlesDir: i18n === undefined ? null : stringOr(i18n.bundlesDir, 'i18n'),
355
+ cliCommands: (loaded.cliCommands ?? [])
356
+ .map((entry) => ({
357
+ name: stringOr(entry.name, '?'),
358
+ summary: stringOr(entry.summary, ''),
359
+ }))
360
+ .sort((a, b) => a.name.localeCompare(b.name)),
361
+ prosePage,
362
+ };
363
+ }
364
+ /**
365
+ * The licence the package shipping a module is published under, read from the
366
+ * nearest `package.json` above its manifest — or `null` when that file is not
367
+ * the shipping package's (a host-owned module, `shipsFrom === 'core'`, whose
368
+ * nearest `package.json` is the host's) or declares none.
369
+ *
370
+ * The nearest file is the package's own in both trees this renders over: in
371
+ * this repository the manifest is `packages/modules/<id>/src/manifest.ts`, and
372
+ * in an instance it is the installed package's `dist/manifest.js`, whose
373
+ * `package.json` is exactly what the registry published. The name is checked
374
+ * rather than assumed, because a licence read off the wrong file is a public
375
+ * statement about the wrong package.
376
+ */
377
+ export function publishedLicenseOf(manifestPath, shipsFrom) {
378
+ let dir = dirname(manifestPath);
379
+ for (;;) {
380
+ const candidate = join(dir, 'package.json');
381
+ if (existsSync(candidate)) {
382
+ const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
383
+ if (pkg.name !== shipsFrom)
384
+ return null;
385
+ return stringOrNull(pkg.license);
386
+ }
387
+ const parent = dirname(dir);
388
+ if (parent === dir)
389
+ return null;
390
+ dir = parent;
391
+ }
392
+ }
393
+ /**
394
+ * The licence row's value. An SPDX expression is shown as declared; a
395
+ * `SEE LICENSE IN <file>` is shown as declared and says where the terms are —
396
+ * the file ships in the package. Flat on purpose: this is a statement of fact
397
+ * about a package, and `specs/conventions/commercial-data.md` §3 N4 holds it to
398
+ * *inform, never press*.
399
+ */
400
+ function licenceCell(license) {
401
+ const own = license === null ? null : /^SEE LICENSE IN (.+)$/.exec(license);
402
+ if (own === null)
403
+ return code(license);
404
+ return `${code(license)} — the package's own terms, shipped in its ${code(own[1].trim())}`;
405
+ }
406
+ /** A markdown table, or the sentence that says there is nothing in it. */
407
+ function table(headings, rows) {
408
+ if (rows.length === 0)
409
+ return '_None._\n';
410
+ return (`| ${headings.join(' | ')} |\n` +
411
+ `| ${headings.map(() => '---').join(' | ')} |\n` +
412
+ rows.map((row) => `| ${row.map(markdownCell).join(' | ')} |`).join('\n') +
413
+ '\n');
414
+ }
415
+ /** A value the reader is meant to copy, or an em dash where there is none. */
416
+ function code(value) {
417
+ return value === null || value === '' ? '—' : `\`${value}\``;
418
+ }
419
+ /**
420
+ * One module's reference page.
421
+ *
422
+ * Pure over the record, so a test drives every section on input this repository
423
+ * does not contain. Every list is sorted by the collector rather than by the
424
+ * walk that produced it, because `overlay:check` renders twice and compares:
425
+ * an ordering that came off a filesystem read would make the artefact
426
+ * non-deterministic in a way that only shows up on somebody else's machine.
427
+ */
428
+ export function emitModuleReference(reference, header = COMPOSER_DOCS_PAGE_HEADER) {
429
+ const activation = reference.activation === null
430
+ ? 'This module declares no activation control, so an operator cannot switch it off ' +
431
+ 'from the Admin UI. Its presence is the platform-availability axis alone — the ' +
432
+ 'lifecycle registry, changed by deployment tooling.\n'
433
+ : reference.activation.kind === 'locked'
434
+ ? `**This module cannot be switched off.** ${reference.activation.reason}\n`
435
+ : `An operator switches this module on and off on **/platform/modules**. The choice ` +
436
+ `is the setting ${code(reference.activation.settingCode)}, and it defaults to ` +
437
+ `**${reference.activation.default ? 'on' : 'off'}**.\n`;
438
+ const dependencies = table(['Module', 'Binding', 'What it means'], [
439
+ ...reference.dependencies.map((id) => [
440
+ `\`${id}\``,
441
+ 'yes',
442
+ 'installs and migrates after it, and an operator cannot switch it off underneath ' +
443
+ 'this module',
444
+ ]),
445
+ ...reference.acknowledgedDependencies.map((entry) => [
446
+ `\`${entry.moduleId}\``,
447
+ 'gating only',
448
+ `resolves \`${entry.port}\`; withheld from \`dependencies\` because the install ` +
449
+ 'order cannot carry the edge',
450
+ ]),
451
+ ...reference.nonBindingDependencies.map((entry) => [
452
+ `\`${entry.moduleId}\``,
453
+ 'no',
454
+ `${entry.kind} \`${entry.name}\`` +
455
+ (entry.whenAbsent === null ? '' : ` — ${entry.whenAbsent}`),
456
+ ]),
457
+ ]);
458
+ const summary = `Everything the \`${reference.moduleId}\` module's manifest declares: permissions, ` +
459
+ 'palette actions, settings, activation and dependencies.';
460
+ return (`---\n` +
461
+ `title: ${yamlScalar(`${reference.moduleId} — module reference`)}\n` +
462
+ `sidebar_label: Reference\n` +
463
+ `description: ${yamlScalar(summary)}\n` +
464
+ `---\n\n` +
465
+ `${header}\n\n` +
466
+ `# \`${reference.moduleId}\` — module reference\n\n` +
467
+ `Rendered from the module's own manifest, and from nothing written by hand. ` +
468
+ (reference.prosePage === null
469
+ ? `Nobody has written a page about what this module *does* yet; the ` +
470
+ `[module map](../${MODULES_CATEGORY}/${MODULE_MAP_ARTEFACT}) lists every module ` +
471
+ `the platform composes.\n\n`
472
+ : `What the module *does* is [its own page](${reference.prosePage}).\n\n`) +
473
+ `| | |\n| --- | --- |\n` +
474
+ `| Module id | ${code(reference.moduleId)} |\n` +
475
+ `| Name | ${markdownCell(reference.name)} |\n` +
476
+ `| Version | ${code(reference.version)} |\n` +
477
+ `| Ships from | ${code(reference.shipsFrom)} |\n` +
478
+ `| Licence | ${licenceCell(reference.license)} |\n\n` +
479
+ `## Activation\n\n${activation}\n` +
480
+ `## Dependencies\n\n${dependencies}\n` +
481
+ `## Permissions\n\n` +
482
+ table(['Code', 'Label', 'Also needs'], reference.permissions.map((entry) => [
483
+ `\`${entry.code}\``,
484
+ entry.label,
485
+ entry.requires.length === 0 ? '—' : entry.requires.map((c) => `\`${c}\``).join(', '),
486
+ ])) +
487
+ `\n## Command palette\n\n` +
488
+ table(['Action', 'Opens', 'Permission'], reference.actions.map((entry) => [
489
+ `\`${entry.id}\``,
490
+ `\`${entry.targetRoute}\``,
491
+ code(entry.requiredPermission),
492
+ ])) +
493
+ `\n## Settings\n\n` +
494
+ table(['Code', 'Name', 'Type'], reference.settings.map((entry) => [`\`${entry.code}\``, entry.name, `\`${entry.valueType}\``])) +
495
+ `\n## Translations\n\n` +
496
+ (reference.bundlesDir === null
497
+ ? 'This module declares no translation bundles.\n'
498
+ : `Bundles at ${code(reference.bundlesDir)} inside the module, one file per shipped ` +
499
+ 'language.\n') +
500
+ `\n## Operator commands\n\n` +
501
+ table(['Command', 'What it does'], reference.cliCommands.map((entry) => [
502
+ `\`pnpm --filter backend run cli -- ${reference.moduleId} ${entry.name}\``,
503
+ entry.summary,
504
+ ])));
505
+ }
506
+ /**
507
+ * A module's documentation layer, from its manifest's own source text.
508
+ *
509
+ * A **computed** `dir` throws rather than reading as "declares nothing" (issue
510
+ * #113): a directory this walk cannot place is a module whose pages would
511
+ * simply not be collected, with no error anywhere.
512
+ */
513
+ export function docsRootOf(id, source, moduleRoot) {
514
+ const declaration = docsDeclarationIn(source);
515
+ if (declaration === undefined || declaration === false)
516
+ return null;
517
+ if (declaration === 'unreadable') {
518
+ throw new ModulePackageError(`[composer] ${id}'s manifest declares \`docs\` with no literal \`dir\`. The directory is ` +
519
+ 'the anchor the platform joins to the module\'s own root, so a value this generator ' +
520
+ 'cannot read is a documentation layer nothing collects and nothing reports.');
521
+ }
522
+ return join(moduleRoot, declaration.dir);
523
+ }
524
+ /**
525
+ * Every page the platform can see, the site's own tree and the modules' alike.
526
+ *
527
+ * A **mixed** tree is the supported state and not a transitional accident: a
528
+ * page in the site tree and a page in a module package are attributed by the
529
+ * same derivation, which is what lets Phase 2 land one batch at a time
530
+ * (`plan.md` § Phasing). `_lifecycle` is the standing resident of the site half
531
+ * — its manifest resolves inside the platform package's **build output**, and
532
+ * documentation is not a compiled asset, so it has no package root to ship
533
+ * from.
534
+ *
535
+ * A declared directory that is **not on disk** is a refusal naming the module
536
+ * (FR-017), and two sources claiming one doc id is a refusal too: the copy
537
+ * would write one over the other and whichever ran second would win, making the
538
+ * site's content depend on a directory read order.
539
+ */
540
+ function collectAllDocPages(layout, manifests) {
541
+ const sources = [];
542
+ const missing = [];
543
+ for (const manifest of manifests) {
544
+ if (manifest.docsRoot === null)
545
+ continue;
546
+ if (!isDirectory(manifest.docsRoot)) {
547
+ missing.push(`${manifest.id} -> ${manifest.docsRoot}`);
548
+ continue;
549
+ }
550
+ sources.push({ moduleId: manifest.id, root: manifest.docsRoot });
551
+ }
552
+ if (missing.length > 0) {
553
+ throw new ModulePackageError(`[composer] ${missing.length} module(s) declare a documentation directory that is not ` +
554
+ `on disk:\n${missing.map((entry) => ` - ${entry}`).join('\n')}\n` +
555
+ 'A declared directory that is absent is a refusal and never "this module ships no ' +
556
+ 'documentation" — declare `docs: false` if that is the decision, or ship the directory.');
557
+ }
558
+ const modulePages = collectModuleDocPages(sources);
559
+ const copies = new Set(modulePages.map((page) => copyTargetOf(page, layout.modulesRoot)));
560
+ const pages = [...collectDocPages(layout.modulesRoot, copies), ...modulePages];
561
+ const duplicates = duplicateDocIds(pages);
562
+ if (duplicates.length > 0) {
563
+ throw new ModulePackageError(`[composer] ${duplicates.length} documentation page(s) are claimed twice:\n` +
564
+ duplicates
565
+ .map((entry) => ` - ${entry.docId}\n ${entry.paths.join('\n ')}`)
566
+ .join('\n'));
567
+ }
568
+ return pages.sort((a, b) => a.docId.localeCompare(b.docId));
569
+ }
570
+ /**
571
+ * Where each module's generated reference page lands, by module id.
572
+ *
573
+ * Synchronous: *which* pages exist is a question about the manifests' `docs`
574
+ * declarations, which the caller's walk has already read, and only their
575
+ * **content** needs the import.
576
+ *
577
+ * Two modules folding onto one slug is a refusal rather than a page written
578
+ * twice — `slugForModule` strips a leading underscore, so a hypothetical
579
+ * `i18n` beside `_i18n` would have one of the two silently overwrite the other,
580
+ * and which one would depend on the order the manifest walk happened to
581
+ * produce.
582
+ */
583
+ export function referencePagePaths(layout, manifests) {
584
+ const paths = new Map();
585
+ const bySlug = new Map();
586
+ for (const manifest of manifests) {
587
+ if (manifest.declaresNoDocs)
588
+ continue;
589
+ const slug = slugForModule(manifest.id);
590
+ const clash = bySlug.get(slug);
591
+ if (clash !== undefined) {
592
+ throw new ModulePackageError(`[composer] '${clash}' and '${manifest.id}' both document at the reference slug ` +
593
+ `'${slug}'. One page would be written over the other and which one survived would ` +
594
+ 'depend on the order the manifest walk produced.');
595
+ }
596
+ bySlug.set(slug, manifest.id);
597
+ paths.set(manifest.id, join(layout.contentRoot, MODULE_REFERENCE_CATEGORY, `${slug}.md`));
598
+ }
599
+ return paths;
600
+ }
601
+ /** Pages on disk in the reference category that this run does not write. */
602
+ export function strayReferencePages(layout, expected) {
603
+ const directory = join(layout.contentRoot, MODULE_REFERENCE_CATEGORY);
604
+ if (!isDirectory(directory))
605
+ return [];
606
+ return readdirSync(directory)
607
+ .filter((name) => PAGE_EXTENSIONS.some((extension) => name.endsWith(extension)))
608
+ .map((name) => join(directory, name))
609
+ .filter((path) => !expected.has(path))
610
+ .sort();
611
+ }
612
+ /**
613
+ * The documentation population of a **client's instance** — the module packages
614
+ * it installed, and nothing else.
615
+ *
616
+ * The workspace host's population comes off the generated manifest index, which
617
+ * an instance does not have and does not want: what an instance composes is what
618
+ * `node_modules` holds (D-119/D-155), and the packages have already been scanned
619
+ * by the caller. Each module's manifest is located by its own `exports` map's
620
+ * `.` subpath, so the docs declaration is read out of the file
621
+ * `import '<name>'` loads — never out of a `manifest.ts`, which a published
622
+ * package does not ship.
623
+ */
624
+ export function installedDocsModules(packages) {
625
+ return packages
626
+ .map((pkg) => {
627
+ const { manifestPath } = publishedManifestEntryOf(pkg);
628
+ const source = readFileSync(manifestPath, 'utf8');
629
+ return {
630
+ id: pkg.moduleId,
631
+ // The **package** directory, not the manifest file's: a module
632
+ // package's `docs/` sits at the package root beside `i18n/`, outside
633
+ // `src/`, and travels in the `files` list.
634
+ docsRoot: docsRootOf(pkg.moduleId, source, pkg.dir),
635
+ declaresNoDocs: docsDeclarationIn(source) === false,
636
+ manifestPath,
637
+ };
638
+ })
639
+ .sort((a, b) => a.id.localeCompare(b.id));
640
+ }
641
+ /**
642
+ * The one read every documentation artefact is rendered from.
643
+ *
644
+ * The site is a **parameter** and so is the module set: a host that walked for
645
+ * itself in here would be a second derivation of a population its caller has
646
+ * already decided, which is the state feature 100 ended for three hand-written
647
+ * lists and the state R3.5 forbids one artefact family over.
648
+ */
649
+ export function docsRegistryOf(layout, modules, packages) {
650
+ const ids = modules.map((module) => module.id);
651
+ const pages = collectAllDocPages(layout, modules);
652
+ const attribution = attributeDocs(pages, ids);
653
+ const declinedDocs = new Set(modules.filter((module) => module.declaresNoDocs).map((module) => module.id));
654
+ return {
655
+ layout,
656
+ modules,
657
+ pages,
658
+ attribution,
659
+ entries: collectDocsRegistry(ids, attribution, packages, declinedDocs),
660
+ entrySources: new Map(pages
661
+ .filter((page) => page.origin.kind === 'module')
662
+ .map((page) => [copyTargetOf(page, layout.modulesRoot), page.path])),
663
+ };
664
+ }
665
+ /**
666
+ * Pure render — the target path + expected content of the sidebar fragment.
667
+ *
668
+ * The path is derived from the workspace member holding the Docusaurus
669
+ * configuration, never written down (D-100), exactly as the admin registry's is
670
+ * derived from the member declaring the `"@/*"` alias.
671
+ */
672
+ export function renderDocsSidebarFrom(registry, header = COMPOSER_DOCS_HEADER) {
673
+ return {
674
+ outputPath: join(registry.layout.member.dir, DOCS_SIDEBAR_ARTEFACT),
675
+ content: emitDocsSidebar(registry.entries, header),
676
+ entryRoot: registry.layout.contentRoot,
677
+ entrySources: registry.entrySources,
678
+ };
679
+ }
680
+ /** Pure render — the target path + expected content of the module map. */
681
+ export function renderModuleMapFrom(registry, header = COMPOSER_DOCS_PAGE_HEADER) {
682
+ return {
683
+ outputPath: join(registry.layout.modulesRoot, MODULE_MAP_ARTEFACT),
684
+ content: emitModuleMap(registry.entries, header),
685
+ entryRoot: registry.layout.contentRoot,
686
+ entrySources: registry.entrySources,
687
+ };
688
+ }
689
+ export async function renderModuleReferencesFrom(registry, options = {}) {
690
+ const header = options.header ?? COMPOSER_DOCS_PAGE_HEADER;
691
+ const { layout, modules, entries, entrySources } = registry;
692
+ const paths = referencePagePaths(layout, modules);
693
+ const byModule = new Map(entries.map((entry) => [entry.moduleId, entry]));
694
+ const rendered = [];
695
+ for (const module of modules) {
696
+ const outputPath = paths.get(module.id);
697
+ if (outputPath === undefined)
698
+ continue;
699
+ const entry = byModule.get(module.id);
700
+ const loaded = (await import(pathToFileURL(module.manifestPath).href));
701
+ const shipsFrom = entry?.shipsFrom ?? 'core';
702
+ const reference = referenceOf(module.id, loaded, shipsFrom, entry?.docs == null ? null : `../${MODULES_CATEGORY}/${entry.docs.entry.relativePath}`, publishedLicenseOf(module.manifestPath, shipsFrom));
703
+ rendered.push({
704
+ label: `module-reference (${module.id})`,
705
+ outputPath,
706
+ content: emitModuleReference(reference, header),
707
+ entryRoot: layout.contentRoot,
708
+ entrySources,
709
+ });
710
+ }
711
+ const expected = new Set(rendered.map((artefact) => artefact.outputPath));
712
+ const stray = strayReferencePages(layout, expected);
713
+ if (stray.length > 0 && (options.stray ?? 'refuse') === 'refuse') {
714
+ throw new StrayReferencePageError(`[composer] ${stray.length} page(s) under ${MODULE_REFERENCE_CATEGORY}/ belong to no ` +
715
+ `registered module:\n${stray.map((path) => ` - ${path}`).join('\n')}\n` +
716
+ 'Every page in that category is generated from a manifest, so one nothing renders is ' +
717
+ 'a module that has gone: `git rm` it. It is refused rather than swept because a ' +
718
+ 'committed file this generator deleted is a change no reviewer asked for.');
719
+ }
720
+ if (options.stray === 'sweep')
721
+ for (const page of stray)
722
+ rmSync(page);
723
+ return { pages: rendered, swept: stray };
724
+ }
725
+ /**
726
+ * The record of what the last collection wrote, so the next one can undo it.
727
+ *
728
+ * The copies are **not committed** (`.gitignore`), which is what keeps 10,311
729
+ * lines of prose from existing twice in this repository — one editable copy and
730
+ * one that looks editable and is not. The consequence is that nothing else
731
+ * knows which files under the modules category are copies, and a page a module
732
+ * deletes would otherwise be served for ever. The stamp answers exactly that
733
+ * and nothing else: a run removes the files the previous run wrote and no
734
+ * longer writes, and a fresh checkout with no stamp removes nothing, which is
735
+ * correct because it has copied nothing.
736
+ */
737
+ const COPY_STAMP = '.module-docs-copies.json';
738
+ /**
739
+ * Copy every module-owned page into the site's tree, preserving its address.
740
+ *
741
+ * **Copy, never symlink** (research D-8): Docusaurus resolves `docs.path` and
742
+ * its `include` globs against the site directory, and a symlinked subtree makes
743
+ * the file watcher, webpack's module graph and the markdown link resolver
744
+ * disagree about where a page is — issue #255's finding, one tool reading one
745
+ * tree while another reads a second.
746
+ *
747
+ * The target is the page's own relative path inside the category, so the copy
748
+ * preserves the doc id, the permalink and every relative link written against
749
+ * it (`module-documentation-layer.md` R5.2). That is what makes the move
750
+ * invisible to a reader and to an inbound link alike.
751
+ */
752
+ export function collectDocsIntoSiteFrom(registry) {
753
+ const { layout, pages } = registry;
754
+ const stampPath = join(layout.member.dir, COPY_STAMP);
755
+ const previous = existsSync(stampPath)
756
+ ? JSON.parse(readFileSync(stampPath, 'utf8'))
757
+ : [];
758
+ const copied = [];
759
+ for (const page of pages) {
760
+ if (page.origin.kind !== 'module')
761
+ continue;
762
+ const target = copyTargetOf(page, layout.modulesRoot);
763
+ mkdirSync(dirname(target), { recursive: true });
764
+ copyFileSync(page.path, target);
765
+ copied.push(relative(layout.member.dir, target));
766
+ }
767
+ const current = new Set(copied);
768
+ const removed = [];
769
+ for (const stale of previous) {
770
+ if (current.has(stale))
771
+ continue;
772
+ const target = join(layout.member.dir, stale);
773
+ if (!existsSync(target))
774
+ continue;
775
+ rmSync(target);
776
+ removed.push(stale);
777
+ }
778
+ writeFileSync(stampPath, `${JSON.stringify([...copied].sort(), null, 2)}\n`, 'utf8');
779
+ return { modulesRoot: layout.modulesRoot, copied: copied.sort(), removed: removed.sort() };
780
+ }
781
+ //# sourceMappingURL=docs-artefacts.js.map