@astryxdesign/cli 0.6.3-canary.db4e378 → 0.6.3-canary.db7b338

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 (276) hide show
  1. package/README.md +22 -1
  2. package/api/blog/blog.doc.mjs +1 -0
  3. package/api/build/build.doc.mjs +1 -0
  4. package/api/component/component.doc.mjs +1 -0
  5. package/api/discover/discover.doc.mjs +1 -0
  6. package/api/docs/_adapter.d.mts +3 -8
  7. package/api/docs/_adapter.mjs +13 -105
  8. package/api/docs/detail/section/section.mjs +9 -15
  9. package/api/docs/detail/section/section.test.mjs +15 -6
  10. package/api/docs/docs.doc.mjs +1 -0
  11. package/api/docs/list/list.mjs +3 -7
  12. package/api/doctor/doctor.d.mts +25 -3
  13. package/api/doctor/doctor.doc.mjs +1 -0
  14. package/api/doctor/doctor.mjs +186 -14
  15. package/api/doctor/doctor.test.mjs +332 -7
  16. package/api/gap-report/gap-report.doc.mjs +1 -0
  17. package/api/hook/_adapter.mjs +19 -5
  18. package/api/hook/hook.doc.mjs +1 -0
  19. package/api/hook/list/list.d.mts +1 -1
  20. package/api/hook/list/list.mjs +69 -17
  21. package/api/init/init.doc.mjs +1 -0
  22. package/api/integration/add-contribution.mjs +7 -7
  23. package/api/integration/add-contribution.test.mjs +37 -3
  24. package/api/integration/add-theme.mjs +34 -64
  25. package/api/integration/add-theme.test.mjs +105 -21
  26. package/api/integration/authoring-checks.test.mjs +8 -4
  27. package/api/integration/integration-block-exports.test.mjs +10 -6
  28. package/api/integration/integrationAdd.doc.mjs +1 -0
  29. package/api/integration/integrationAddAgentDoc.doc.mjs +1 -0
  30. package/api/integration/integrationAddCodemod.doc.mjs +1 -0
  31. package/api/integration/integrationAddComponent.doc.mjs +1 -0
  32. package/api/integration/integrationAddDoc.doc.mjs +1 -0
  33. package/api/integration/integrationAddTemplate.doc.mjs +1 -0
  34. package/api/integration/integrationAddTheme.doc.mjs +6 -5
  35. package/api/integration/integrationComponentConflicts.doc.mjs +1 -0
  36. package/api/integration/integrationDocConflicts.doc.mjs +1 -0
  37. package/api/integration/integrationPackCheck.doc.mjs +1 -0
  38. package/api/integration/integrationTemplateConflicts.doc.mjs +1 -0
  39. package/api/integration/pack-check.test.mjs +17 -47
  40. package/api/integration/summarizeIssues.doc.mjs +1 -0
  41. package/api/integration/validate-integration-fixes.test.mjs +1389 -0
  42. package/api/integration/validate-integration.mjs +50 -102
  43. package/api/integration/validate-integration.test.mjs +85 -23
  44. package/api/integration/validate-unread-theme-folders.test.mjs +110 -0
  45. package/api/integration/validateIntegration.doc.mjs +1 -0
  46. package/api/json/assertResponse.doc.mjs +1 -0
  47. package/api/json/isError.doc.mjs +1 -0
  48. package/api/json/parseResponse.doc.mjs +1 -0
  49. package/api/layout/layoutCheck.doc.mjs +1 -0
  50. package/api/layout/layoutExpand.doc.mjs +1 -0
  51. package/api/layout/layoutGrammar.doc.mjs +1 -0
  52. package/api/search/search.doc.mjs +1 -0
  53. package/api/search/search.mjs +16 -6
  54. package/api/swizzle/swizzle.doc.mjs +1 -0
  55. package/api/template/template-suffix.test.mjs +41 -21
  56. package/api/template/template.doc.mjs +1 -0
  57. package/api/theme/_adapter.d.mts +2 -3
  58. package/api/theme/_adapter.mjs +4 -5
  59. package/api/theme/add/add.binary.test.mjs +10 -17
  60. package/api/theme/add/add.test.mjs +14 -1
  61. package/api/theme/generateTonalPalette.doc.mjs +1 -0
  62. package/api/theme/integration-themes.test.mjs +39 -28
  63. package/api/theme/list/list.test.mjs +19 -20
  64. package/api/theme/listThemes.doc.mjs +6 -5
  65. package/api/theme/themeAdd.doc.mjs +4 -3
  66. package/api/theme/themeBuild.doc.mjs +1 -0
  67. package/api/theme/themeList.doc.mjs +6 -3
  68. package/api/theme/themeListAvailable.doc.mjs +5 -3
  69. package/api/theme/themePaletteGenerate.doc.mjs +1 -0
  70. package/api/theme/themeTargets.doc.mjs +1 -0
  71. package/api/theme/themeTemplate.doc.mjs +1 -0
  72. package/api/upgrade/_adapter.d.mts +32 -5
  73. package/api/upgrade/_adapter.mjs +97 -69
  74. package/api/upgrade/provider-agreement.test.mjs +152 -0
  75. package/api/upgrade/run/run.mjs +356 -59
  76. package/api/upgrade/upgrade.doc.mjs +6 -1
  77. package/api/upgrade/upgrade.type.d.mts +34 -0
  78. package/api/upgrade/upgrade.type.mjs +15 -0
  79. package/assets/codemods/__tests__/runner.test.mjs +330 -8
  80. package/assets/codemods/integration-discovery.mjs +8 -2
  81. package/assets/codemods/integration-discovery.test.mjs +15 -0
  82. package/assets/codemods/integration-runner.mjs +56 -4
  83. package/assets/codemods/integration-runner.protection.test.mjs +153 -0
  84. package/assets/codemods/run-codemod.mjs +177 -34
  85. package/assets/codemods/runner.mjs +350 -102
  86. package/assets/codemods/transforms/next/__tests__/migrate-native-picker-to-presentation.test.mjs +63 -0
  87. package/assets/codemods/transforms/next/__tests__/migrate-theme-catalog-to-descriptors.test.mjs +220 -0
  88. package/assets/codemods/transforms/next/index.mjs +19 -1
  89. package/assets/codemods/transforms/next/migrate-native-picker-to-presentation.mjs +148 -0
  90. package/assets/codemods/transforms/next/migrate-theme-catalog-to-descriptors.mjs +141 -0
  91. package/assets/docs/cli-integrations.doc.mjs +30 -30
  92. package/assets/docs/cli.doc.mjs +15 -0
  93. package/assets/docs/theme.doc.mjs +1 -1
  94. package/assets/templates/blocks/components/DateInput/DateInputDateRange.tsx +1 -1
  95. package/assets/templates/blocks/components/Item/ItemDocumentTabs.doc.mjs +14 -0
  96. package/assets/templates/blocks/components/Item/ItemDocumentTabs.tsx +100 -0
  97. package/assets/templates/blocks/components/TimeInput/TimeInputConstrained.tsx +1 -0
  98. package/assets/templates/themes/butter/butterTheme.doc.mjs +11 -0
  99. package/assets/templates/themes/chocolate/chocolateTheme.doc.mjs +11 -0
  100. package/assets/templates/themes/gothic/gothicTheme.doc.mjs +11 -0
  101. package/assets/templates/themes/matcha/matchaTheme.doc.mjs +11 -0
  102. package/assets/templates/themes/neutral/neutralTheme.doc.mjs +11 -0
  103. package/assets/templates/themes/stone/stoneTheme.doc.mjs +11 -0
  104. package/assets/templates/themes/y2k/y2kTheme.doc.mjs +11 -0
  105. package/authoring/codemod/codemod.doc.mjs +1 -1
  106. package/authoring/codemod/type.ts +12 -0
  107. package/authoring/config/config.doc.mjs +1 -1
  108. package/authoring/debug/debug.doc.d.mts +11 -0
  109. package/authoring/debug/debug.doc.mjs +182 -0
  110. package/authoring/doctypes/_schema.d.mts +71 -141
  111. package/authoring/doctypes/_schema.mjs +29 -2
  112. package/authoring/doctypes/base/graph-fields.doc.mjs +1 -1
  113. package/authoring/doctypes/command/command.doc.mjs +1 -1
  114. package/authoring/doctypes/command/type.ts +1 -1
  115. package/authoring/doctypes/component/type.ts +2 -2
  116. package/authoring/doctypes/doctypes-new.test.mjs +48 -6
  117. package/authoring/doctypes/enum/enum.doc.mjs +1 -1
  118. package/authoring/doctypes/enum/type.ts +1 -1
  119. package/authoring/doctypes/function/function.doc.mjs +1 -1
  120. package/authoring/doctypes/function/type.ts +1 -1
  121. package/authoring/doctypes/hook/type.ts +2 -2
  122. package/authoring/doctypes/load-contract.test.mjs +3 -2
  123. package/authoring/doctypes/namespace/namespace.doc.mjs +2 -2
  124. package/authoring/doctypes/namespace/parse.test.mjs +23 -25
  125. package/authoring/doctypes/namespace/type.ts +5 -2
  126. package/authoring/doctypes/parse.d.mts +4 -2
  127. package/authoring/doctypes/parse.mjs +10 -5
  128. package/authoring/doctypes/reference/reference.doc.mjs +6 -4
  129. package/authoring/doctypes/reference/type.ts +9 -10
  130. package/authoring/doctypes/schema/schema.doc.mjs +1 -1
  131. package/authoring/doctypes/schema/type.ts +1 -2
  132. package/authoring/doctypes/template/template.doc.mjs +3 -3
  133. package/authoring/doctypes/theme/parse.d.mts +35 -0
  134. package/authoring/doctypes/theme/parse.mjs +76 -0
  135. package/authoring/doctypes/theme/theme.doc.d.mts +9 -0
  136. package/authoring/doctypes/theme/theme.doc.mjs +79 -0
  137. package/authoring/doctypes/theme/type.ts +42 -0
  138. package/authoring/doctypes/types.ts +2 -1
  139. package/authoring/gap-report/gap-report.doc.d.mts +12 -0
  140. package/authoring/gap-report/gap-report.doc.mjs +183 -0
  141. package/authoring/index.d.mts +1 -0
  142. package/authoring/index.d.ts +7 -4
  143. package/authoring/index.mjs +2 -1
  144. package/authoring/integration/integration.doc.mjs +2 -2
  145. package/authoring/integration/type.ts +3 -9
  146. package/clients/cli/__tests__/cliManifest.test.ts +27 -29
  147. package/clients/cli/commands/blog.doc.mjs +1 -1
  148. package/clients/cli/commands/build-theme.adaptations.test.mjs +100 -1
  149. package/clients/cli/commands/build-theme.mjs +10 -44
  150. package/clients/cli/commands/build.doc.mjs +1 -1
  151. package/clients/cli/commands/component.doc.mjs +1 -1
  152. package/clients/cli/commands/discover.doc.mjs +1 -1
  153. package/clients/cli/commands/docs.doc.mjs +1 -1
  154. package/clients/cli/commands/docs.mjs +15 -58
  155. package/clients/cli/commands/docs.test.mjs +11 -14
  156. package/clients/cli/commands/doctor-integration-components.doc.mjs +1 -1
  157. package/clients/cli/commands/doctor-integration-docs.doc.mjs +1 -1
  158. package/clients/cli/commands/doctor-integration-templates.doc.mjs +1 -1
  159. package/clients/cli/commands/doctor-integration-validate.doc.mjs +1 -1
  160. package/clients/cli/commands/doctor-integration.doc.mjs +1 -1
  161. package/clients/cli/commands/doctor-integration.test.mjs +15 -7
  162. package/clients/cli/commands/doctor.doc.mjs +1 -1
  163. package/clients/cli/commands/gap-report.doc.mjs +1 -1
  164. package/clients/cli/commands/hook.doc.mjs +1 -1
  165. package/clients/cli/commands/init.doc.mjs +1 -1
  166. package/clients/cli/commands/integration-add.controls.test.mjs +1 -1
  167. package/clients/cli/commands/integration-add.doc.mjs +1 -1
  168. package/clients/cli/commands/integration-pack.doc.mjs +1 -1
  169. package/clients/cli/commands/integration-real-world.test.mjs +3 -9
  170. package/clients/cli/commands/integration.doc.mjs +1 -1
  171. package/clients/cli/commands/layout-check.doc.mjs +1 -1
  172. package/clients/cli/commands/layout-expand.doc.mjs +1 -1
  173. package/clients/cli/commands/layout-grammar.doc.mjs +1 -1
  174. package/clients/cli/commands/layout.doc.mjs +1 -1
  175. package/clients/cli/commands/manifest.doc.mjs +1 -1
  176. package/clients/cli/commands/search.doc.mjs +1 -1
  177. package/clients/cli/commands/swizzle.doc.mjs +1 -1
  178. package/clients/cli/commands/template.doc.mjs +1 -1
  179. package/clients/cli/commands/text-json-parity.test.mjs +718 -0
  180. package/clients/cli/commands/theme-add.doc.mjs +2 -2
  181. package/clients/cli/commands/theme-build.doc.mjs +1 -1
  182. package/clients/cli/commands/theme-list.doc.mjs +2 -2
  183. package/clients/cli/commands/theme-palette-generate.doc.mjs +3 -3
  184. package/clients/cli/commands/theme-palette.doc.mjs +1 -1
  185. package/clients/cli/commands/theme-targets.behavior.test.mjs +4 -3
  186. package/clients/cli/commands/theme-targets.doc.mjs +1 -1
  187. package/clients/cli/commands/theme-template.doc.mjs +1 -1
  188. package/clients/cli/commands/theme.doc.mjs +1 -1
  189. package/clients/cli/commands/upgrade.doc.mjs +3 -2
  190. package/clients/cli/commands/upgrade.file-protection.test.mjs +228 -0
  191. package/clients/cli/commands/upgrade.mjs +29 -7
  192. package/clients/cli/formatters/index.mjs +2 -0
  193. package/clients/cli/formatters/index.test.mjs +6 -0
  194. package/clients/cli/lib/hook-format.mjs +14 -5
  195. package/foundation/config/project-themes.test.mjs +11 -19
  196. package/foundation/config/project.d.mts +8 -0
  197. package/foundation/config/project.mjs +50 -21
  198. package/foundation/config/project.test.mjs +3 -16
  199. package/foundation/discovery/authoring-self-docs.d.mts +6 -0
  200. package/foundation/discovery/authoring-self-docs.mjs +17 -7
  201. package/foundation/discovery/authoring-self-docs.test.mjs +3 -2
  202. package/foundation/discovery/authoring-surface.d.mts +74 -0
  203. package/foundation/discovery/authoring-surface.mjs +525 -0
  204. package/foundation/discovery/authoring-surface.test.mjs +392 -0
  205. package/foundation/discovery/cli-self-docs.d.mts +113 -0
  206. package/foundation/discovery/cli-self-docs.mjs +514 -0
  207. package/foundation/discovery/cli-self-docs.test.mjs +437 -0
  208. package/foundation/discovery/component-loader.d.mts +35 -38
  209. package/foundation/discovery/component-loader.mjs +53 -222
  210. package/foundation/discovery/docs-discovery.d.mts +3 -2
  211. package/foundation/discovery/docs-discovery.mjs +14 -17
  212. package/foundation/discovery/docs-discovery.test.mjs +15 -15
  213. package/foundation/discovery/docs-section-key.d.mts +19 -8
  214. package/foundation/discovery/docs-section-key.mjs +119 -32
  215. package/foundation/discovery/docs-section-key.test.mjs +41 -19
  216. package/foundation/discovery/template-adapter.d.mts +7 -0
  217. package/foundation/discovery/template-adapter.mjs +27 -40
  218. package/foundation/discovery/template-adapter.test.mjs +15 -0
  219. package/foundation/discovery/theme-discovery.d.mts +67 -7
  220. package/foundation/discovery/theme-discovery.mjs +916 -186
  221. package/foundation/discovery/theme-discovery.test.mjs +613 -219
  222. package/foundation/doc-compiler/bundle.d.mts +47 -0
  223. package/foundation/doc-compiler/bundle.mjs +218 -0
  224. package/foundation/doc-compiler/bundle.test.mjs +255 -0
  225. package/foundation/doc-compiler/compile.d.mts +200 -0
  226. package/foundation/doc-compiler/compile.mjs +259 -9
  227. package/foundation/doc-compiler/diagnostics.d.mts +126 -0
  228. package/foundation/doc-compiler/diagnostics.mjs +277 -0
  229. package/foundation/doc-compiler/doc-compiler.test.mjs +28 -1
  230. package/foundation/doc-compiler/doc-loads.test.mjs +1642 -0
  231. package/foundation/doc-compiler/import.d.mts +24 -0
  232. package/foundation/doc-compiler/import.mjs +59 -0
  233. package/foundation/doc-compiler/inputs.d.mts +102 -0
  234. package/foundation/doc-compiler/inputs.mjs +286 -0
  235. package/foundation/doc-compiler/inputs.test.mjs +299 -0
  236. package/foundation/doc-compiler/ir.d.mts +13 -0
  237. package/foundation/doc-compiler/ir.mjs +189 -12
  238. package/foundation/doc-compiler/lower-doc.test.mjs +492 -0
  239. package/foundation/doc-compiler/overlays.d.mts +37 -0
  240. package/foundation/doc-compiler/overlays.mjs +206 -0
  241. package/foundation/doc-compiler/parse-readable.d.mts +9 -0
  242. package/foundation/doc-compiler/parse-readable.mjs +29 -0
  243. package/foundation/doc-compiler/read.d.mts +126 -0
  244. package/foundation/doc-compiler/read.mjs +320 -0
  245. package/foundation/doc-compiler/read.test.mjs +313 -0
  246. package/foundation/doc-compiler/source.d.mts +33 -0
  247. package/foundation/doc-compiler/source.mjs +128 -0
  248. package/foundation/fs/file-protection.d.mts +33 -0
  249. package/foundation/fs/file-protection.mjs +825 -0
  250. package/foundation/fs/file-protection.test.mjs +250 -0
  251. package/foundation/integrations/autolink.d.mts +58 -1
  252. package/foundation/integrations/autolink.mjs +143 -57
  253. package/foundation/integrations/contribution-fixes.d.mts +145 -0
  254. package/foundation/integrations/contribution-fixes.mjs +1284 -0
  255. package/foundation/integrations/contribution-inventory.d.mts +1 -1
  256. package/foundation/integrations/contribution-inventory.mjs +17 -20
  257. package/foundation/integrations/contribution-inventory.test.mjs +67 -27
  258. package/foundation/integrations/integrations.d.mts +3 -0
  259. package/foundation/integrations/integrations.mjs +11 -97
  260. package/foundation/integrations/provider-ledger.test.mjs +275 -0
  261. package/foundation/integrations/provider-resolution.d.mts +152 -0
  262. package/foundation/integrations/provider-resolution.mjs +576 -0
  263. package/foundation/integrations/provider-resolution.test.mjs +369 -0
  264. package/foundation/integrations/theme-descriptor.d.mts +8 -0
  265. package/foundation/integrations/theme-descriptor.mjs +44 -0
  266. package/foundation/integrations/validate-contributions.mjs +104 -10
  267. package/foundation/response/error-codes.d.mts +3 -1
  268. package/foundation/response/error-codes.d.ts +2 -0
  269. package/foundation/response/error-codes.doc.mjs +12 -2
  270. package/foundation/response/error-codes.mjs +7 -1
  271. package/foundation/response/error-codes.test.mjs +55 -11
  272. package/foundation/response/response-types.doc.mjs +1 -1
  273. package/foundation/response/response.doc.mjs +1 -1
  274. package/foundation/text/string-utils.mjs +22 -10
  275. package/package.json +10 -9
  276. package/assets/templates/themes/manifest.json +0 -95
@@ -34,7 +34,10 @@ const SOURCE_TITLE = Symbol('astryx.docs.sourceTitle');
34
34
  * @returns {T}
35
35
  */
36
36
  export function withSourceTitle(section, title) {
37
- Object.defineProperty(section, SOURCE_TITLE, {value: title, configurable: true});
37
+ Object.defineProperty(section, SOURCE_TITLE, {
38
+ value: title,
39
+ configurable: true,
40
+ });
38
41
  return section;
39
42
  }
40
43
 
@@ -77,9 +80,63 @@ export function sectionKey(section) {
77
80
  }
78
81
 
79
82
  /**
80
- * Problems with the keys of a topic's sections: an authored id that is not a
81
- * stable key, a title no key derives from, and two sections sharing a key.
82
- * Keys are never suffixed to make them unique, because readers link to them.
83
+ * The first section the 0.6.x title-substring lookup would return for a query.
84
+ * @param {any[]} sections
85
+ * @param {string} query
86
+ * @returns {number}
87
+ */
88
+ function legacyTitleMatchIndex(sections, query) {
89
+ const lower = query.toLowerCase();
90
+ return sections.findIndex(section => {
91
+ const title = sourceTitle(section);
92
+ return typeof title === 'string' && title.toLowerCase().includes(lower);
93
+ });
94
+ }
95
+
96
+ /**
97
+ * Fatal key problems: only an explicitly authored `id` can make a previously
98
+ * readable topic invalid. Derived-key collisions and titles with no Latin
99
+ * letters or digits are handled by {@link withSectionKeys} for 0.6.x
100
+ * compatibility.
101
+ * @param {any[]} sections
102
+ * @returns {string[]}
103
+ */
104
+ export function sectionKeyErrors(sections) {
105
+ /** @type {string[]} */
106
+ const problems = [];
107
+ /** @type {Map<string, number>} */
108
+ const seen = new Map();
109
+ sections.forEach((section, s) => {
110
+ if (section?.id == null) return;
111
+ const at = `sections[${s}]`;
112
+ if (typeof section.id !== 'string' || !SECTION_KEY_RE.test(section.id)) {
113
+ problems.push(
114
+ `${at}.id: ${JSON.stringify(section.id)} is not a stable key. Use lowercase letters and digits joined by single hyphens.`,
115
+ );
116
+ return;
117
+ }
118
+ const first = seen.get(section.id);
119
+ if (first != null) {
120
+ problems.push(
121
+ `${at}: the key "${section.id}" is already used by sections[${first}]. Give one of them a distinct id.`,
122
+ );
123
+ return;
124
+ }
125
+ const legacyOwner = legacyTitleMatchIndex(sections, section.id);
126
+ if (legacyOwner !== -1 && legacyOwner !== s) {
127
+ problems.push(
128
+ `${at}.id: the key "${section.id}" is already a legacy title query for sections[${legacyOwner}]. Choose an id that resolves to this section.`,
129
+ );
130
+ return;
131
+ }
132
+ seen.set(section.id, s);
133
+ });
134
+ return problems;
135
+ }
136
+
137
+ /**
138
+ * Compatibility notices for section keys. These inputs loaded in 0.6.x, so
139
+ * readers keep accepting them and assign deterministic fallback keys.
83
140
  * @param {any[]} sections
84
141
  * @returns {string[]}
85
142
  */
@@ -90,7 +147,6 @@ export function sectionKeyProblems(sections) {
90
147
  const seen = new Map();
91
148
  sections.forEach((section, s) => {
92
149
  const at = `sections[${s}]`;
93
- // A missing title is reported where titles are checked; no key to judge.
94
150
  if (
95
151
  section?.id == null &&
96
152
  (typeof section?.title !== 'string' || section.title === '')
@@ -109,14 +165,14 @@ export function sectionKeyProblems(sections) {
109
165
  const key = sectionKey(section);
110
166
  if (key === '') {
111
167
  problems.push(
112
- `${at}: no key derives from the title ${JSON.stringify(section?.title)}. Give the section an id.`,
168
+ `${at}: no key derives from the title ${JSON.stringify(section?.title)}. A deterministic section-N compatibility key is used; add an explicit id.`,
113
169
  );
114
170
  return;
115
171
  }
116
172
  const first = seen.get(key);
117
173
  if (first != null) {
118
174
  problems.push(
119
- `${at}: the key "${key}" is already used by sections[${first}]. Give one of them a distinct id or title.`,
175
+ `${at}: the key "${key}" is already used by sections[${first}]. A suffixed compatibility key is used for title-derived collisions; give one of them a distinct id or title.`,
120
176
  );
121
177
  return;
122
178
  }
@@ -133,48 +189,79 @@ export function sectionKeyProblems(sections) {
133
189
  * @returns {T}
134
190
  */
135
191
  export function withSectionKeys(doc) {
192
+ const reserved = new Set(
193
+ doc.sections
194
+ .map(section => section?.id)
195
+ .filter(id => typeof id === 'string' && SECTION_KEY_RE.test(id)),
196
+ );
197
+ const used = new Set();
136
198
  return {
137
199
  ...doc,
138
- sections: doc.sections.map(section =>
139
- section.id != null
140
- ? section
141
- : withSourceTitle(
142
- {...section, id: sectionKey(section)},
143
- sourceTitle(section),
144
- ),
145
- ),
200
+ sections: doc.sections.map((section, index) => {
201
+ if (section.id != null) {
202
+ used.add(section.id);
203
+ return section;
204
+ }
205
+ const base =
206
+ sectionTitleKey(sourceTitle(section)) || `section-${index + 1}`;
207
+ let key = base;
208
+ let suffix = 2;
209
+ while (
210
+ reserved.has(key) ||
211
+ used.has(key) ||
212
+ (legacyTitleMatchIndex(doc.sections, key) !== -1 &&
213
+ legacyTitleMatchIndex(doc.sections, key) !== index)
214
+ ) {
215
+ key = `${base}-${suffix}`;
216
+ suffix += 1;
217
+ }
218
+ used.add(key);
219
+ return withSourceTitle({...section, id: key}, sourceTitle(section));
220
+ }),
146
221
  };
147
222
  }
148
223
 
149
224
  /**
150
- * Find the section a reader asked for: by key, then by exact title (or the
151
- * key the query derives), then by a title that contains the query. More than
152
- * one match is refused rather than guessed.
225
+ * Find the section a reader asked for. The 0.6.x first-match title-substring
226
+ * lookup remains first in the authored-title view. In a localized view, an
227
+ * exact stable key resolves before translated title substrings because those
228
+ * translations cannot participate in language-stable key allocation. Generated
229
+ * keys avoid authored-title conflicts, and authored IDs that would shadow an
230
+ * authored legacy query are rejected.
153
231
  * @param {any[]} sections
154
232
  * @param {string} query
155
- * @returns {{section: any | null, candidates: any[]}} `candidates` lists the
156
- * matches when the query is ambiguous, and is empty when nothing matches
233
+ * @returns {{section: any | null, candidates: any[]}} `candidates` lists every
234
+ * legacy title match when the query is ambiguous
157
235
  */
158
236
  export function findDocSection(sections, query) {
159
237
  const wanted = query.trim();
160
238
  const byKey = sections.find(section => sectionKey(section) === wanted);
161
- if (byKey) return {section: byKey, candidates: []};
239
+ const hasLocalizedTitles = sections.some(
240
+ section => sourceTitle(section) !== section.title,
241
+ );
242
+ if (byKey && hasLocalizedTitles) {
243
+ return {section: byKey, candidates: []};
244
+ }
162
245
 
163
246
  const lower = wanted.toLowerCase();
164
- const derived = sectionTitleKey(wanted);
165
- const exact = sections.filter(
166
- section =>
167
- section.title.toLowerCase() === lower ||
168
- (derived !== '' && sectionTitleKey(sourceTitle(section)) === derived),
247
+ const legacy = sections.filter(section =>
248
+ section.title.toLowerCase().includes(lower),
169
249
  );
170
- if (exact.length === 1) return {section: exact[0], candidates: []};
171
- if (exact.length > 1) return {section: null, candidates: exact};
250
+ if (legacy.length > 0) {
251
+ return {section: legacy[0], candidates: legacy.length > 1 ? legacy : []};
252
+ }
172
253
 
173
- const partial = sections.filter(section =>
174
- section.title.toLowerCase().includes(lower),
254
+ const byFallbackKey = sections.find(
255
+ section => sectionKey(section) === wanted,
256
+ );
257
+ if (byFallbackKey) return {section: byFallbackKey, candidates: []};
258
+
259
+ const derived = sectionTitleKey(wanted);
260
+ const byDerivedKey = sections.find(
261
+ section =>
262
+ derived !== '' && sectionTitleKey(sourceTitle(section)) === derived,
175
263
  );
176
- if (partial.length === 1) return {section: partial[0], candidates: []};
177
- return {section: null, candidates: partial};
264
+ return {section: byDerivedKey ?? null, candidates: []};
178
265
  }
179
266
 
180
267
  /**
@@ -7,6 +7,7 @@ import {
7
7
  buildDocsIndexData,
8
8
  findDocSection,
9
9
  sectionKey,
10
+ sectionKeyErrors,
10
11
  sectionKeyProblems,
11
12
  sectionSummary,
12
13
  sectionTitleKey,
@@ -68,12 +69,17 @@ describe('sectionKeyProblems', () => {
68
69
  section('Quick-start'),
69
70
  ]);
70
71
  expect(problems).toHaveLength(1);
71
- expect(problems[0]).toContain('"quick-start" is already used by sections[0]');
72
+ expect(problems[0]).toContain(
73
+ '"quick-start" is already used by sections[0]',
74
+ );
72
75
  });
73
76
 
74
77
  it('rejects an authored id that collides with a derived key', () => {
75
78
  expect(
76
- sectionKeyProblems([section('Install'), section('Setup', {id: 'install'})]),
79
+ sectionKeyProblems([
80
+ section('Install'),
81
+ section('Setup', {id: 'install'}),
82
+ ]),
77
83
  ).toHaveLength(1);
78
84
  });
79
85
 
@@ -85,13 +91,13 @@ describe('sectionKeyProblems', () => {
85
91
  }
86
92
  });
87
93
 
88
- it('rejects a title no key derives from', () => {
94
+ it('reports a title no key derives from', () => {
89
95
  expect(sectionKeyProblems([section('亮/暗模式')])[0]).toContain(
90
- 'Give the section an id',
91
- );
92
- expect(sectionKeyProblems([section('亮/暗模式', {id: 'light-dark'})])).toEqual(
93
- [],
96
+ 'compatibility key is used',
94
97
  );
98
+ expect(
99
+ sectionKeyProblems([section('亮/暗模式', {id: 'light-dark'})]),
100
+ ).toEqual([]);
95
101
  });
96
102
 
97
103
  it('leaves a missing title to the title check', () => {
@@ -107,12 +113,21 @@ describe('withSectionKeys', () => {
107
113
  expect(doc.sections.map(s => s.id)).toEqual(['quick-start', 'use']);
108
114
  });
109
115
 
110
- it('keeps the authored title of a translated section', () => {
116
+ it('keeps authored titles and keys stable for translated sections', () => {
111
117
  const doc = withSectionKeys({
112
- sections: [withSourceTitle(section('快速开始'), 'Quick Start')],
118
+ sections: [
119
+ withSourceTitle(section('高级安装'), 'Install advanced'),
120
+ withSourceTitle(section('安装'), 'Install'),
121
+ ],
113
122
  });
114
- expect(doc.sections[0].id).toBe('quick-start');
115
- expect(sourceTitle(doc.sections[0])).toBe('Quick Start');
123
+ expect(doc.sections.map(s => s.id)).toEqual([
124
+ 'install-advanced',
125
+ 'install-2',
126
+ ]);
127
+ for (const target of doc.sections) {
128
+ expect(findDocSection(doc.sections, target.id).section).toBe(target);
129
+ }
130
+ expect(sourceTitle(doc.sections[0])).toBe('Install advanced');
116
131
  });
117
132
  });
118
133
 
@@ -127,7 +142,14 @@ describe('findDocSection', () => {
127
142
  ],
128
143
  }).sections;
129
144
 
130
- it('finds a section by key first', () => {
145
+ it('finds every emitted key without changing a legacy query', () => {
146
+ const colliding = withSectionKeys({
147
+ sections: [section('Install advanced'), section('Install')],
148
+ }).sections;
149
+ expect(findDocSection(colliding, 'install').section).toBe(colliding[0]);
150
+ for (const target of colliding) {
151
+ expect(findDocSection(colliding, target.id).section).toBe(target);
152
+ }
131
153
  expect(findDocSection(sections, 'tokens').section.title).toBe(
132
154
  'Theme Tokens',
133
155
  );
@@ -149,15 +171,15 @@ describe('findDocSection', () => {
149
171
  expect(findDocSection(sections, 'props').section.id).toBe('theme-props');
150
172
  });
151
173
 
152
- it('refuses an ambiguous exact title and lists the candidates', () => {
174
+ it('keeps the first ambiguous exact-title match and lists the candidates', () => {
153
175
  const {section: match, candidates} = findDocSection(sections, 'Overview');
154
- expect(match).toBeNull();
176
+ expect(match?.id).toBe('overview-a');
155
177
  expect(candidates.map(s => s.id)).toEqual(['overview-a', 'overview-b']);
156
178
  });
157
179
 
158
- it('refuses an ambiguous partial title', () => {
180
+ it('keeps the first ambiguous partial-title match', () => {
159
181
  const {section: match, candidates} = findDocSection(sections, 'theme');
160
- expect(match).toBeNull();
182
+ expect(match?.id).toBe('theme-props');
161
183
  expect(candidates.map(s => s.id)).toEqual(['theme-props', 'tokens']);
162
184
  });
163
185
 
@@ -185,9 +207,9 @@ describe('sectionSummary', () => {
185
207
  });
186
208
 
187
209
  it('is empty when the section has no text', () => {
188
- expect(sectionSummary({content: [{type: 'table', headers: [], rows: []}]})).toBe(
189
- '',
190
- );
210
+ expect(
211
+ sectionSummary({content: [{type: 'table', headers: [], rows: []}]}),
212
+ ).toBe('');
191
213
  });
192
214
 
193
215
  it('cuts a long summary at a word boundary', () => {
@@ -37,6 +37,13 @@ export function stripTemplateAssetRefs(source: string): string;
37
37
  * @returns {string[]}
38
38
  */
39
39
  export function listTemplates(): string[];
40
+ /**
41
+ * Resolve the template-spec file for a core page directory: the first existing
42
+ * metadata file in {@link ALL_TEMPLATE_SUFFIXES} precedence, or null.
43
+ * @param {string} dirPath
44
+ * @returns {string | null}
45
+ */
46
+ export function findPageDocFile(dirPath: string): string | null;
40
47
  /**
41
48
  * Discover only the templates built into @astryxdesign/core. Integration
42
49
  * authoring checks use this narrower surface so a broken project config or a
@@ -24,9 +24,7 @@
24
24
  import * as fs from 'node:fs';
25
25
  import * as path from 'node:path';
26
26
  import {createRequire} from 'node:module';
27
- import {createJiti} from 'jiti';
28
- import {loadModuleWithParser} from '../fs/module-loader.mjs';
29
- import {parseTemplate} from '../../authoring/doctypes/template/parse.mjs';
27
+ import {readDocView} from '../doc-compiler/read.mjs';
30
28
  import {CLI_ROOT, discoverExternalPackages} from '../fs/paths.mjs';
31
29
  import {CORE_PROVIDER_ID} from '../identity/providers.mjs';
32
30
  import {Project} from '../config/project.mjs';
@@ -84,31 +82,28 @@ export function pkgOf(t) {
84
82
  */
85
83
 
86
84
  /**
87
- * Canonical basename suffixes for template-spec files, in precedence order.
88
- * A template spec is a scaffoldable TEMPLATE (a plain object stamped with a
89
- * `type` of `'page'` or `'block'`), so `.template.*` is the descriptive family
90
- * name.
85
+ * Released compatibility suffixes, in precedence order. Stable 0.6.0
86
+ * documented `.template.*`, so discovery keeps reading those files while all
87
+ * new authoring uses `.doc.mjs`.
91
88
  */
92
89
  const TEMPLATE_SUFFIXES = ['.template.ts', '.template.mjs', '.template.js'];
93
90
 
94
91
  /**
95
- * Legacy basename suffixes for template-spec files, in precedence order.
96
- * `.doc.*` was inherited from the component-doc convention before templates
97
- * had their own name; it is still accepted during the transition window.
92
+ * Descriptor suffixes for templates, in precedence order. New authoring always
93
+ * emits `.doc.mjs`; the TypeScript and JavaScript variants remain readable.
98
94
  */
99
95
  const DOC_SUFFIXES = ['.doc.ts', '.doc.mjs', '.doc.js'];
100
96
 
101
97
  /**
102
- * The union of canonical + legacy template-spec suffixes, canonical first so
103
- * `.template.*` wins over `.doc.*` when both stems exist. All template
104
- * discovery matches this union so a `Foo.template.ts` file is treated exactly
105
- * like a legacy `Foo.doc.mjs`.
98
+ * Every template-spec suffix, in the released precedence a core page directory
99
+ * uses to pick one file. Integration discovery never picks: every match is a
100
+ * template, so two specs for one stem list twice and read as ambiguous.
106
101
  */
107
102
  const ALL_TEMPLATE_SUFFIXES = [...TEMPLATE_SUFFIXES, ...DOC_SUFFIXES];
108
103
 
109
104
  /**
110
105
  * The template-spec suffix present on `file`, or null if none matches.
111
- * Recognizes both the canonical `.template.*` and legacy `.doc.*` families.
106
+ * Recognizes both the canonical `.doc.*` and released `.template.*` families.
112
107
  * @param {string} file
113
108
  * @returns {string | null}
114
109
  */
@@ -122,16 +117,6 @@ function matchedTemplateSuffix(file) {
122
117
  */
123
118
  const TEMPLATE_SUFFIX_RE = /\.(template|doc)\.(ts|mjs|js)$/;
124
119
 
125
- /** @type {ReturnType<typeof createJiti> | undefined} */
126
- let jitiInstance;
127
- /** Lazily-created jiti for loading `.ts` template specs (JSX-capable). */
128
- function getJiti() {
129
- if (!jitiInstance) {
130
- jitiInstance = createJiti(import.meta.url, {jsx: true});
131
- }
132
- return jitiInstance;
133
- }
134
-
135
120
  /**
136
121
  * Load an integration template doc module and validate it against the template
137
122
  * envelope at the load boundary. Default export only — `.ts` via jiti,
@@ -144,7 +129,13 @@ function getJiti() {
144
129
  * @param {string} [label]
145
130
  */
146
131
  async function loadIntegrationDoc(file, label) {
147
- return loadModuleWithParser(file, parseTemplate, {label});
132
+ return readDocView(file, {
133
+ root: 'templates',
134
+ exports: ['default'],
135
+ label: label ?? file,
136
+ strict: true,
137
+ value: 'parsed',
138
+ });
148
139
  }
149
140
 
150
141
  const TEMPLATES_DIR = path.join(CLI_ROOT, 'assets', 'templates');
@@ -492,12 +483,11 @@ function unsafeFixtureReference(source, at, reason) {
492
483
  /**
493
484
  * Load a template-spec module and return its metadata object. Supports both
494
485
  * families of suffix:
495
- * - Legacy `.doc.*` core/external specs export `export const doc = {...}`.
496
- * - Canonical `.template.*` specs export the stamped object (`type: 'page' |
497
- * 'block'`) as the default export.
498
- * Prefers the default export, falling back to the named `doc` export, so a
499
- * `Foo.template.ts` (default export) is read identically to a legacy
500
- * `Foo.doc.mjs` (`doc` export). `.ts` is loaded via jiti; `.mjs`/`.js` via a
486
+ * - Canonical `.doc.*` specs may export the stamped object (`type: 'page' |
487
+ * 'block'`) as the default export or use the historical named `doc` export.
488
+ * - Released `.template.*` compatibility specs use the same object shape.
489
+ * Prefers the default export, falling back to the named `doc` export. `.ts` is
490
+ * loaded via jiti; `.mjs`/`.js` via a
501
491
  * native dynamic import. Returns null if the file does not exist.
502
492
  *
503
493
  * @param {string} docPath absolute path to the spec file
@@ -505,10 +495,7 @@ function unsafeFixtureReference(source, at, reason) {
505
495
  */
506
496
  async function loadDocModule(docPath) {
507
497
  if (!fs.existsSync(docPath)) return null;
508
- const docModule = docPath.endsWith('.ts')
509
- ? await getJiti().import(docPath)
510
- : await import(`file://${docPath}`);
511
- return docModule.default ?? docModule.doc;
498
+ return readDocView(docPath, {root: 'templates', loader: 'template'});
512
499
  }
513
500
 
514
501
  /**
@@ -553,11 +540,11 @@ function findDocFiles(dir, pattern) {
553
540
 
554
541
  /**
555
542
  * Resolve the template-spec file for a core page directory: the first existing
556
- * `template.<suffix>` in canonical-then-legacy precedence, or null.
543
+ * metadata file in {@link ALL_TEMPLATE_SUFFIXES} precedence, or null.
557
544
  * @param {string} dirPath
558
545
  * @returns {string | null}
559
546
  */
560
- function findPageDocFile(dirPath) {
547
+ export function findPageDocFile(dirPath) {
561
548
  for (const suffix of ALL_TEMPLATE_SUFFIXES) {
562
549
  const candidate = path.join(dirPath, `template${suffix}`);
563
550
  if (fs.existsSync(candidate)) return candidate;
@@ -733,7 +720,7 @@ export async function discoverAllWithErrors(cwd = process.cwd()) {
733
720
  /**
734
721
  * Recursively collect integration template-spec files under `root`.
735
722
  * Returns absolute paths to files ending in one of ALL_TEMPLATE_SUFFIXES
736
- * (canonical `.template.*` or legacy `.doc.*`).
723
+ * (canonical `.doc.*` or released `.template.*` compatibility files).
737
724
  *
738
725
  * @param {string} root
739
726
  * @returns {string[]}
@@ -763,7 +750,7 @@ function findIntegrationDocFiles(root) {
763
750
  * Discover templates contributed by configured integrations.
764
751
  *
765
752
  * For each integration with a resolved `templates` root, every
766
- * `<id>.template.{ts,mjs,js}` (or legacy `<id>.doc.{ts,mjs,js}`) file is a
753
+ * canonical `<id>.doc.{mjs,ts,js}` (or released `<id>.template.{ts,mjs,js}`) file is a
767
754
  * template whose id is its path relative to the templates root with the
768
755
  * matched suffix stripped (kebab-case, may be nested). The doc's `type`
769
756
  * (page|block) decides scaffolding — there is no `/pages` vs `/blocks`
@@ -18,6 +18,7 @@ import * as os from 'node:os';
18
18
  import * as path from 'node:path';
19
19
  import {
20
20
  extractComponents,
21
+ findPageDocFile,
21
22
  stripTemplateAssetRefs,
22
23
  } from './template-adapter.mjs';
23
24
 
@@ -164,3 +165,17 @@ describe('stripTemplateAssetRefs', () => {
164
165
  );
165
166
  });
166
167
  });
168
+
169
+ describe('findPageDocFile', () => {
170
+ it('keeps the released precedence when one page has two specs', () => {
171
+ const page = fs.mkdtempSync(path.join(dir, 'page-'));
172
+ fs.writeFileSync(path.join(page, 'template.doc.mjs'), 'export {};\n');
173
+ fs.writeFileSync(path.join(page, 'template.doc.ts'), 'export {};\n');
174
+ expect(findPageDocFile(page)).toBe(path.join(page, 'template.doc.ts'));
175
+
176
+ fs.writeFileSync(path.join(page, 'template.template.mjs'), 'export {};\n');
177
+ expect(findPageDocFile(page)).toBe(
178
+ path.join(page, 'template.template.mjs'),
179
+ );
180
+ });
181
+ });
@@ -2,15 +2,65 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  /**
5
- * Read and validate one theme catalog.
6
- * @param {string} themeRoot absolute catalog root
7
- * @param {string} owner package that owns the catalog
8
- * @param {{bundled?: boolean}} [options]
5
+ * How messages about a theme descriptor name it.
6
+ * @param {string} docPath
7
+ * @param {string} owner
8
+ */
9
+ export function themeDescriptorLabel(docPath: string, owner: string): string;
10
+ /**
11
+ * The static value a theme descriptor default-exports, read from its source
12
+ * without executing it. Throws when the file is not one static ThemeDoc
13
+ * object.
14
+ * @param {string} docPath
15
+ * @param {string} label
16
+ * @returns {Record<string, string | boolean>}
17
+ */
18
+ export function readThemeDescriptorValue(docPath: string, label: string): Record<string, string | boolean>;
19
+ /**
20
+ * Whether an entry at any depth of a themes root is outside every theme: a dot
21
+ * entry, or a name npm never publishes (npm-packlist's defaults).
22
+ * @param {string} name
23
+ */
24
+ export function isIgnoredThemeEntry(name: string): boolean;
25
+ /**
26
+ * Whether discovery reads a folder under a theme root as a theme. Dot-folders
27
+ * and folders with neither a descriptor nor a `<name>Theme` source are not.
28
+ * @param {string} folder absolute path
29
+ */
30
+ export function isThemeFolder(folder: string): boolean;
31
+ /**
32
+ * The files one theme folder ships: every regular file below it except
33
+ * {@link isIgnoredThemeEntry} entries. What pack-check requires and what
34
+ * `theme add` copies.
35
+ * @param {string} folder absolute path
36
+ * @returns {string[]} sorted POSIX paths relative to the folder
37
+ */
38
+ export function listThemeFiles(folder: string): string[];
39
+ /**
40
+ * Discover and validate one theme root.
41
+ * @param {string} themeRoot absolute root containing one directory per slug
42
+ * @param {string} owner package that owns the root
43
+ * @param {{bundled?: boolean}} [options] a bundled root is the CLI's own; its
44
+ * sources are checked by the CLI's tests rather than on every read
9
45
  * @returns {DiscoveredTheme[]}
10
46
  */
11
- export function discoverThemeCatalog(themeRoot: string, owner: string, { bundled }?: {
47
+ export function discoverThemeDirectory(themeRoot: string, owner: string, { bundled }?: {
12
48
  bundled?: boolean;
13
49
  }): DiscoveredTheme[];
50
+ /**
51
+ * Folders under a themes root that look like themes (see looksLikeTheme)
52
+ * but that discovery does not read as themes, and that no module elsewhere
53
+ * under the root imports, by a relative path or through the package's own
54
+ * name. Discovery skips them silently, as the released catalog skipped
55
+ * unlisted folders; doctor warns.
56
+ * @param {string} themeRoot
57
+ * @param {{packageDir?: string, packageName?: string}} [owner]
58
+ * @returns {string[]} absolute folder paths, sorted
59
+ */
60
+ export function unreadThemeFolders(themeRoot: string, { packageDir, packageName }?: {
61
+ packageDir?: string;
62
+ packageName?: string;
63
+ }): string[];
14
64
  /** @returns {DiscoveredTheme[]} */
15
65
  export function discoverBundledThemes(): DiscoveredTheme[];
16
66
  /**
@@ -20,8 +70,10 @@ export function discoverBundledThemes(): DiscoveredTheme[];
20
70
  export function discoverIntegrationThemes(integration: import("../integrations/integrations.mjs").LoadedIntegration): Promise<DiscoveredTheme[]>;
21
71
  export const BUNDLED_THEME_PACKAGE: "@astryxdesign/cli";
22
72
  export const THEMES_DIR: string;
23
- export const THEME_MANIFEST_BASENAME: "manifest.json";
24
- export const MANIFEST_PATH: string;
73
+ export const THEME_DOC_SUFFIX: ".doc.mjs";
74
+ export const THEME_MODULE_EXTENSIONS: string[];
75
+ /** A theme directory's name: lower-kebab, starting with a letter. */
76
+ export const THEME_SLUG_RE: RegExp;
25
77
  export type DiscoveredTheme = {
26
78
  slug: string;
27
79
  displayName: string;
@@ -29,6 +81,10 @@ export type DiscoveredTheme = {
29
81
  maintained: boolean;
30
82
  entry: string;
31
83
  exportName: string;
84
+ /**
85
+ * what `theme add` copies, relative to sourceDir,
86
+ * entry first
87
+ */
32
88
  files: string[];
33
89
  package: string;
34
90
  /**
@@ -36,4 +92,8 @@ export type DiscoveredTheme = {
36
92
  */
37
93
  sourceDir: string;
38
94
  bundled: boolean;
95
+ /**
96
+ * absolute descriptor path
97
+ */
98
+ docPath: string;
39
99
  };