@astryxdesign/cli 0.7.2-canary.f16dc9f → 0.7.2-canary.f2b60a0

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 (243) hide show
  1. package/README.md +15 -14
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +41 -1
  4. package/api/component/_adapter.mjs +93 -7
  5. package/api/component/component.mjs +22 -5
  6. package/api/component/component.test.mjs +420 -167
  7. package/api/component/integration-export-name.test.mjs +644 -0
  8. package/api/component/list/list.mjs +42 -4
  9. package/api/docs/_adapter.d.mts +11 -0
  10. package/api/docs/_adapter.mjs +244 -1
  11. package/api/docs/integrationDocs.test.mjs +124 -0
  12. package/api/doctor/doctor.mjs +27 -3
  13. package/api/doctor/doctor.test.mjs +43 -0
  14. package/api/doctor/theme-checks.mjs +1 -1
  15. package/api/gap-report/gap-report.mjs +67 -4
  16. package/api/index.d.mts +2 -0
  17. package/api/index.mjs +2 -0
  18. package/api/init/run/run.mjs +2 -2
  19. package/api/integration/_adapter.d.mts +12 -0
  20. package/api/integration/_adapter.mjs +54 -0
  21. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  22. package/api/integration/add-contribution.mjs +8 -4
  23. package/api/integration/add-contribution.test.mjs +4 -2
  24. package/api/integration/authoring-checks.mjs +11 -2
  25. package/api/integration/authoring-checks.test.mjs +39 -0
  26. package/api/integration/authoring-checks.type.d.mts +9 -0
  27. package/api/integration/authoring-checks.type.mjs +14 -1
  28. package/api/integration/check/check.d.mts +96 -0
  29. package/api/integration/check/check.mjs +357 -0
  30. package/api/integration/check/check.test.mjs +489 -0
  31. package/api/integration/integration-check.type.d.mts +118 -0
  32. package/api/integration/integration-check.type.mjs +84 -0
  33. package/api/integration/integrationCheck.doc.d.mts +10 -0
  34. package/api/integration/integrationCheck.doc.mjs +73 -0
  35. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  36. package/api/integration/pack-check.mjs +61 -107
  37. package/api/integration/pack-check.test.mjs +359 -0
  38. package/api/integration/validate-integration.mjs +8 -0
  39. package/api/integration/validate-integration.test.mjs +11 -0
  40. package/api/integration/validate-integration.type.d.mts +8 -0
  41. package/api/integration/validate-integration.type.mjs +12 -1
  42. package/api/integration/validateIntegration.doc.mjs +17 -3
  43. package/api/search/search.d.mts +22 -6
  44. package/api/search/search.domain-priority.test.mjs +108 -18
  45. package/api/search/search.integration-check.test.mjs +24 -0
  46. package/api/search/search.mjs +141 -25
  47. package/api/search/search.test.mjs +36 -0
  48. package/api/swizzle/copy/copy.mjs +28 -14
  49. package/api/template/template.test.mjs +120 -3
  50. package/api/theme/build/build.family.test.mjs +64 -0
  51. package/api/theme/build/build.mjs +180 -24
  52. package/api/theme/themeBuild.doc.mjs +4 -4
  53. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  54. package/assets/codemods/transforms/next/index.mjs +11 -1
  55. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  56. package/assets/docs/README.md +10 -0
  57. package/assets/docs/author-a-theme.doc.mjs +1 -1
  58. package/assets/docs/getting-started.doc.mjs +3 -3
  59. package/assets/docs/styling-overview.doc.mjs +1 -1
  60. package/assets/docs/theme.doc.dense.mjs +1 -1
  61. package/assets/docs/theme.doc.mjs +1 -1
  62. package/assets/docs/theme.doc.zh.mjs +1 -1
  63. package/assets/docs/tree/add-a-component.doc.mjs +11 -1
  64. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  65. package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
  66. package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
  67. package/assets/docs/tree/block-template.doc.mjs +8 -20
  68. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  69. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  70. package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
  71. package/assets/docs/tree/checks.doc.mjs +24 -16
  72. package/assets/docs/tree/codemods.doc.mjs +4 -4
  73. package/assets/docs/tree/component-family.doc.mjs +8 -6
  74. package/assets/docs/tree/component-imports.doc.mjs +10 -2
  75. package/assets/docs/tree/components.doc.mjs +1 -1
  76. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
  77. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  78. package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
  79. package/assets/docs/tree/docs.doc.mjs +3 -3
  80. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  81. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  82. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  83. package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
  84. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  85. package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
  86. package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
  87. package/assets/docs/tree/integrations.doc.mjs +8 -0
  88. package/assets/docs/tree/links.doc.mjs +6 -5
  89. package/assets/docs/tree/migration-setup.doc.mjs +1 -0
  90. package/assets/docs/tree/migration-verification.doc.mjs +0 -1
  91. package/assets/docs/tree/migration.doc.mjs +0 -1
  92. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  93. package/assets/docs/tree/page-template.doc.mjs +4 -8
  94. package/assets/docs/tree/publishing.doc.mjs +1 -1
  95. package/assets/docs/tree/quick-start.doc.mjs +37 -21
  96. package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
  97. package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
  98. package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
  99. package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
  100. package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
  101. package/assets/docs/tree/single-component.doc.mjs +15 -6
  102. package/assets/docs/tree/start-a-template.doc.mjs +3 -3
  103. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  104. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  105. package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
  106. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  107. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  108. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  109. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  110. package/assets/docs/tree/templates.doc.mjs +1 -1
  111. package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
  112. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  113. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  114. package/assets/docs/tree/troubleshooting.doc.mjs +13 -5
  115. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  116. package/assets/docs/tree/upgrading.doc.mjs +3 -3
  117. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
  118. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  119. package/assets/docs/tree/versioning.doc.mjs +7 -6
  120. package/assets/docs/tree/write-the-template-file.doc.mjs +4 -3
  121. package/assets/docs/use-a-theme.doc.mjs +2 -2
  122. package/assets/docs/working-with-ai.doc.mjs +43 -4
  123. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  124. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  125. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  126. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  127. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  128. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  129. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  130. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  131. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  132. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  133. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  134. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  135. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  136. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  137. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  138. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  139. package/assets/templates/pages/detail-page/page.tsx +3 -3
  140. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  141. package/assets/theme.template.ts +1 -1
  142. package/authoring/codemod/codemod.doc.mjs +7 -3
  143. package/authoring/config/config.doc.mjs +9 -6
  144. package/authoring/config/type.ts +4 -2
  145. package/authoring/discover/discover.doc.mjs +1 -1
  146. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  147. package/authoring/doctypes/base/type.ts +6 -0
  148. package/authoring/doctypes/command/command.doc.mjs +14 -12
  149. package/authoring/doctypes/component/component.doc.mjs +31 -28
  150. package/authoring/doctypes/component/type.ts +13 -8
  151. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  152. package/authoring/doctypes/function/function.doc.mjs +9 -8
  153. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  154. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  155. package/authoring/doctypes/reference/reference.doc.mjs +11 -10
  156. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  157. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  158. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  159. package/authoring/doctypes/template/parse.d.mts +6 -2
  160. package/authoring/doctypes/template/parse.mjs +3 -1
  161. package/authoring/doctypes/template/template.doc.mjs +3 -3
  162. package/authoring/doctypes/template/type.ts +22 -0
  163. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  164. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  165. package/authoring/identity/identity.doc.mjs +3 -3
  166. package/authoring/index.d.ts +1 -0
  167. package/authoring/integration/integration.doc.mjs +12 -4
  168. package/authoring/integration/parse.test.mjs +10 -0
  169. package/authoring/integration/schema.d.mts +2 -0
  170. package/authoring/integration/schema.mjs +1 -0
  171. package/authoring/integration/type.ts +4 -0
  172. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  173. package/clients/cli/commands/build-theme.mjs +1 -1
  174. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  175. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  176. package/clients/cli/commands/cli-postinstall.test.mjs +49 -1
  177. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  178. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  179. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  180. package/clients/cli/commands/doctor.mjs +190 -0
  181. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  182. package/clients/cli/commands/integration-check.test.mjs +267 -0
  183. package/clients/cli/commands/integration.doc.mjs +6 -2
  184. package/clients/cli/commands/integration.mjs +107 -1
  185. package/clients/cli/commands/setup-nudge.test.mjs +79 -1
  186. package/clients/cli/commands/swizzle.mjs +5 -3
  187. package/clients/cli/commands/swizzle.routing.test.mjs +18 -1
  188. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  189. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  190. package/clients/cli/index.mjs +30 -5
  191. package/clients/cli/lib/component-format.mjs +5 -1
  192. package/foundation/agent-docs/agent-doc-state.d.mts +14 -0
  193. package/foundation/agent-docs/agent-doc-state.mjs +38 -0
  194. package/foundation/agent-docs/agent-docs.d.mts +2 -1
  195. package/foundation/agent-docs/agent-docs.mjs +8 -6
  196. package/foundation/config/project.mjs +82 -0
  197. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  198. package/foundation/discovery/component-discovery.d.mts +190 -10
  199. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  200. package/foundation/discovery/component-discovery.mjs +864 -56
  201. package/foundation/discovery/component-discovery.test.mjs +96 -0
  202. package/foundation/discovery/component-replacement.mjs +18 -0
  203. package/foundation/discovery/component-replacement.test.mjs +40 -0
  204. package/foundation/discovery/doc-types.d.mts +157 -0
  205. package/foundation/discovery/doc-types.mjs +1224 -0
  206. package/foundation/discovery/doc-types.test.mjs +652 -0
  207. package/foundation/discovery/docs-discovery.d.mts +81 -0
  208. package/foundation/discovery/docs-discovery.mjs +57 -0
  209. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  210. package/foundation/discovery/integration-doc-files.mjs +231 -0
  211. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  212. package/foundation/discovery/module-exports.d.mts +27 -0
  213. package/foundation/discovery/module-exports.mjs +356 -31
  214. package/foundation/discovery/module-exports.test.mjs +172 -0
  215. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  216. package/foundation/discovery/package-tsconfig.mjs +228 -0
  217. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  218. package/foundation/discovery/project-typescript.d.mts +23 -0
  219. package/foundation/discovery/project-typescript.mjs +61 -0
  220. package/foundation/discovery/prop-drift.d.mts +278 -0
  221. package/foundation/discovery/prop-drift.mjs +2263 -0
  222. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  223. package/foundation/discovery/template-adapter.mjs +74 -5
  224. package/foundation/discovery/type-match.d.mts +53 -0
  225. package/foundation/discovery/type-match.mjs +660 -0
  226. package/foundation/discovery/type-match.test.mjs +169 -0
  227. package/foundation/discovery/type-text.d.mts +68 -0
  228. package/foundation/discovery/type-text.mjs +658 -0
  229. package/foundation/discovery/type-text.test.mjs +196 -0
  230. package/foundation/discovery/unresolved-references.d.mts +62 -0
  231. package/foundation/discovery/unresolved-references.mjs +790 -0
  232. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  233. package/foundation/doc-compiler/doc-loads.test.mjs +11 -1
  234. package/foundation/doc-compiler/inputs.mjs +23 -0
  235. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  236. package/foundation/doc-compiler/links.d.mts +9 -5
  237. package/foundation/doc-compiler/links.mjs +4 -2
  238. package/foundation/integrations/contribution-inventory.mjs +6 -1
  239. package/foundation/response/response-types.doc.mjs +7 -2
  240. package/foundation/xle/registry.mjs +4 -0
  241. package/foundation/xle/registry.test.mjs +28 -0
  242. package/package.json +10 -10
  243. package/scripts/postinstall.mjs +3 -3
@@ -16,6 +16,7 @@ import {
16
16
  discoverComponents,
17
17
  discoverExternalComponentsGrouped,
18
18
  discoverValidIntegrationComponents,
19
+ familyMember,
19
20
  findComponentReadme,
20
21
  findExternalComponentDoc,
21
22
  resolveImportPath,
@@ -66,6 +67,33 @@ async function legacyImport(ext, name, coreDir, docOpts) {
66
67
  */
67
68
  const coreEntry = doc => ({name: doc.name, package: CORE_PACKAGE, ...doc});
68
69
 
70
+ /**
71
+ * The full-level entry for a component documented as a member of its family's
72
+ * doc (VStack in Stack's doc): a component doc built from the member's own
73
+ * entry, under its own name, display name and import, shaped like every other
74
+ * full entry. Null when the doc is the component's own.
75
+ * @param {any} docs
76
+ * @param {string} name
77
+ * @param {string} coreDir
78
+ * @returns {any}
79
+ */
80
+ function memberEntry(docs, name, coreDir) {
81
+ const member = familyMember(docs, name);
82
+ if (!member) return null;
83
+ return {
84
+ name,
85
+ package: CORE_PACKAGE,
86
+ displayName: member.displayName ?? name,
87
+ ...(docs.group ? {group: docs.group} : {}),
88
+ ...(docs.category ? {category: docs.category} : {}),
89
+ usage: member.usage ?? {description: member.description ?? ''},
90
+ props: member.props ?? [],
91
+ ...(member.examples ? {examples: member.examples} : {}),
92
+ ...(docs.theming ? {theming: docs.theming} : {}),
93
+ import: resolveImportPath(coreDir, name),
94
+ };
95
+ }
96
+
69
97
  /**
70
98
  * The entry for a Core slot an integration component replaces
71
99
  * (spec:AST-035 FR11), at the list's detail level: the replacement's own name,
@@ -177,7 +205,11 @@ export async function componentList(
177
205
  entries.push({
178
206
  name: comp,
179
207
  package: CORE_PACKAGE,
180
- description: docs.usage?.description || docs.description || '',
208
+ description:
209
+ familyMember(docs, comp)?.description ||
210
+ docs.usage?.description ||
211
+ docs.description ||
212
+ '',
181
213
  import: resolveImportPath(coreDir, comp),
182
214
  });
183
215
  } catch {
@@ -215,7 +247,8 @@ export async function componentList(
215
247
  const readme = findComponentReadme(coreDir, comp);
216
248
  if (readme && readme.endsWith('.doc.mjs')) {
217
249
  try {
218
- entries.push(coreEntry(await loadComponentDoc(readme, {zh, lang, dense})));
250
+ const docs = await loadComponentDoc(readme, {zh, lang, dense});
251
+ entries.push(memberEntry(docs, comp, coreDir) ?? coreEntry(docs));
219
252
  } catch {
220
253
  entries.push({name: `XDS${comp}`, package: CORE_PACKAGE, description: ''});
221
254
  }
@@ -269,7 +302,11 @@ export async function componentList(
269
302
  result[cat].push({
270
303
  name: comp,
271
304
  package: CORE_PACKAGE,
272
- description: docs.usage?.description || docs.description || '',
305
+ description:
306
+ familyMember(docs, comp)?.description ||
307
+ docs.usage?.description ||
308
+ docs.description ||
309
+ '',
273
310
  import: resolveImportPath(coreDir, comp),
274
311
  });
275
312
  } catch {
@@ -310,7 +347,8 @@ export async function componentList(
310
347
  const readme = findComponentReadme(coreDir, comp);
311
348
  if (readme && readme.endsWith('.doc.mjs')) {
312
349
  try {
313
- result[cat].push(coreEntry(await loadComponentDoc(readme, {zh, lang, dense})));
350
+ const docs = await loadComponentDoc(readme, {zh, lang, dense});
351
+ result[cat].push(memberEntry(docs, comp, coreDir) ?? coreEntry(docs));
314
352
  } catch {
315
353
  result[cat].push({name: `XDS${comp}`, package: CORE_PACKAGE, description: ''});
316
354
  }
@@ -281,6 +281,17 @@ export function resolveTopicDocs(topic: unknown, options?: {
281
281
  lang: string | null;
282
282
  entry: import("../../foundation/discovery/docs-discovery.mjs").DocsTopicEntry;
283
283
  }>;
284
+ /**
285
+ * Each doc one integration package ships, checked against the published
286
+ * authoring type for its kind with the project's own TypeScript (spec:AST-039
287
+ * FR13). Without TypeScript the report says nothing was checked.
288
+ * @param {any} integration a loaded integration
289
+ * @param {string} cwd
290
+ * @returns {Promise<import('../../foundation/discovery/doc-types.mjs').DocTypesReport & {fixtures?: number}>}
291
+ */
292
+ export function integrationDocTypes(integration: any, cwd: string): Promise<import("../../foundation/discovery/doc-types.mjs").DocTypesReport & {
293
+ fixtures?: number;
294
+ }>;
284
295
  /**
285
296
  * A doc a link found: what a read shows of the link, and the typed doc behind
286
297
  * it when a reference block can include it.
@@ -17,8 +17,11 @@
17
17
  * foundation/discovery/docs-discovery, which the catalog comes from.
18
18
  */
19
19
 
20
+ import * as path from 'node:path';
20
21
  import {Project} from '../../foundation/config/project.mjs';
21
22
  import {DocsCatalog} from '../../foundation/discovery/docs-discovery.mjs';
23
+ import {checkDocTypes} from '../../foundation/discovery/doc-types.mjs';
24
+ import {integrationDocFiles} from '../../foundation/discovery/integration-doc-files.mjs';
22
25
  import {
23
26
  linkReferenceTopic,
24
27
  lowerReferenceTopic,
@@ -53,6 +56,17 @@ import {
53
56
  selfDocSection,
54
57
  } from '../../foundation/discovery/authoring-self-docs.mjs';
55
58
  import {CLI_PROVIDER_ID} from '../../foundation/identity/providers.mjs';
59
+ import {
60
+ CORE_PACKAGE,
61
+ discoverOwnedComponents,
62
+ discoverValidIntegrationComponents,
63
+ } from '../../foundation/discovery/component-discovery.mjs';
64
+ import {loadComponentDoc} from '../../foundation/discovery/component-loader.mjs';
65
+ import {
66
+ discoverCoreTemplates,
67
+ discoverIntegrationTemplatesForOne,
68
+ } from '../../foundation/discovery/template-adapter.mjs';
69
+ import {findCoreDir} from '../../foundation/fs/paths.mjs';
56
70
  import {AstryxError} from '../error.mjs';
57
71
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
58
72
 
@@ -495,12 +509,205 @@ async function docFinder(catalog, fromProvider) {
495
509
  };
496
510
  }
497
511
  }
512
+ if (CONTRIBUTED_KINDS.has(parsed.kind)) {
513
+ const link = await contributedDocLink(
514
+ catalog,
515
+ provider,
516
+ parsed.kind,
517
+ parsed.name,
518
+ );
519
+ if (link)
520
+ return {link: {target, ...link}, kind: parsed.kind, typed: null};
521
+ }
522
+ const searchType =
523
+ parsed.kind === 'component'
524
+ ? 'component'
525
+ : parsed.kind === 'page' || parsed.kind === 'block'
526
+ ? 'template'
527
+ : 'doc';
498
528
  return {
499
- problem: `"${target}" names no doc. Find it with \`astryx search ${parsed.name} --type doc\`, then name it as \`[<provider>:]<kind>:<name>\`.`,
529
+ problem: `"${target}" names no doc. Find it with \`astryx search ${parsed.name} --type ${searchType}\`, then name it as \`[<provider>:]<kind>:<name>\`.`,
500
530
  };
501
531
  };
502
532
  }
503
533
 
534
+ /** The doc kinds no docs tree holds: components and templates. */
535
+ const CONTRIBUTED_KINDS = new Set(['component', 'page', 'block']);
536
+
537
+ /**
538
+ * The link to a component, page, or block doc (spec:AST-047 FR9). No docs tree
539
+ * holds these, so a link finds them among the component and template records
540
+ * the catalog carries, the ones `astryx component` and `astryx template` open,
541
+ * and reads as that command. A component is named by its export name, a
542
+ * template by its id; anything but Core's names its package.
543
+ * @param {DocsCatalog} catalog
544
+ * @param {string} provider normalized provider id
545
+ * @param {string} kind `component`, `page`, or `block`
546
+ * @param {string} name
547
+ * @returns {Promise<Omit<import('../../foundation/doc-compiler/links.mjs').DocLink, 'target'> | null>}
548
+ */
549
+ async function contributedDocLink(catalog, provider, kind, name) {
550
+ const contributions = await catalog.contributions();
551
+ if (!contributions) return null;
552
+ const sameProvider = (/** @type {string} */ providerId) => {
553
+ try {
554
+ return normalizeProviderId(providerId) === provider;
555
+ } catch {
556
+ return false;
557
+ }
558
+ };
559
+ if (kind === 'component') {
560
+ const record = contributions.components.find(
561
+ component =>
562
+ component.name === name && sameProvider(component.providerId),
563
+ );
564
+ if (!record) return null;
565
+ /** @type {any} */
566
+ let doc = null;
567
+ try {
568
+ doc = record.docPath ? await loadComponentDoc(record.docPath) : null;
569
+ } catch {
570
+ // The component checks report a doc that does not load; the link
571
+ // still opens the component.
572
+ }
573
+ const entry =
574
+ doc?.name === name
575
+ ? doc
576
+ : doc?.components?.find(
577
+ (/** @type {any} */ each) => each.name === name,
578
+ );
579
+ // Bare, Core's name opens Core's component unless another package ships
580
+ // the same name or replaces it (spec:AST-035 FR11); then the link selects
581
+ // Core, as it always selects any other package.
582
+ const selectPackage =
583
+ record.package !== CORE_PACKAGE ||
584
+ contributions.components.some(
585
+ other =>
586
+ other.package !== CORE_PACKAGE &&
587
+ (other.name === name || other.replaces === name),
588
+ );
589
+ return {
590
+ id: createDocId(provider, 'component', name),
591
+ route: null,
592
+ title: entry?.displayName ?? name,
593
+ summary: entry?.description ?? doc?.description ?? '',
594
+ command: `astryx component ${name}${selectPackage ? ` --package ${record.package}` : ''}`,
595
+ };
596
+ }
597
+ const template = contributions.templates.find(
598
+ each =>
599
+ each.type === kind && each.id === name && sameProvider(each.providerId),
600
+ );
601
+ if (!template) return null;
602
+ // The same for a Core template: another package's template of its kind
603
+ // with its id, or one that replaces it, takes the bare id.
604
+ const selectPackage =
605
+ template.package !== CORE_PACKAGE ||
606
+ contributions.templates.some(
607
+ other =>
608
+ other.package !== CORE_PACKAGE &&
609
+ other.type === template.type &&
610
+ (other.id === name || other.replaces === name),
611
+ );
612
+ return {
613
+ id: createDocId(provider, template.type, name),
614
+ route: null,
615
+ title: template.displayName ?? template.name,
616
+ summary: template.description ?? '',
617
+ command: `astryx template ${name} --type ${template.type}${selectPackage ? ` --package ${template.package}` : ''}`,
618
+ };
619
+ }
620
+
621
+ /**
622
+ * The component and template docs one integration package's links can name:
623
+ * Core's and its own, as `doctor integration docs` checks it before any app
624
+ * configures it.
625
+ * @param {any} integration a loaded integration: its name, provider id, roots,
626
+ * and package directory
627
+ * @returns {Promise<import('../../foundation/discovery/docs-discovery.mjs').LinkableContributions>}
628
+ */
629
+ async function packageContributions(integration) {
630
+ const own = integration.providerId ?? integration.name;
631
+ /** @type {import('../../foundation/discovery/docs-discovery.mjs').LinkableComponent[]} */
632
+ const components = [];
633
+ const coreDir = findCoreDir(integration.__packageDir ?? process.cwd());
634
+ if (coreDir) {
635
+ try {
636
+ for (const record of discoverOwnedComponents(coreDir, [])) {
637
+ components.push({
638
+ providerId: CORE_PACKAGE,
639
+ package: CORE_PACKAGE,
640
+ name: record.name,
641
+ docPath: record.docPath,
642
+ });
643
+ }
644
+ } catch {
645
+ // The components check reports a Core it cannot read.
646
+ }
647
+ }
648
+ if (integration.components) {
649
+ try {
650
+ const {components: records} =
651
+ await discoverValidIntegrationComponents(integration);
652
+ for (const record of records) {
653
+ components.push({
654
+ providerId: own,
655
+ package: integration.name,
656
+ name: record.name,
657
+ docPath: record.docPath,
658
+ // Declared: this package alone cannot tell whether an app applies
659
+ // it, so a link to the Core name it names selects Core.
660
+ ...(typeof record.replaces === 'string'
661
+ ? {replaces: record.replaces}
662
+ : {}),
663
+ });
664
+ }
665
+ } catch {
666
+ // `validate` reports components that do not load.
667
+ }
668
+ }
669
+ /** @type {import('../../foundation/discovery/docs-discovery.mjs').LinkableTemplate[]} */
670
+ const templates = [];
671
+ /** @param {any} template @param {string} providerId @param {string} pkg */
672
+ const addTemplate = (template, providerId, pkg) => {
673
+ if (template.type !== 'page' && template.type !== 'block') return;
674
+ templates.push({
675
+ providerId,
676
+ package: pkg,
677
+ type: template.type,
678
+ id: template.dirName,
679
+ name: template.name,
680
+ ...(template.displayName == null
681
+ ? {}
682
+ : {displayName: template.displayName}),
683
+ ...(template.description == null
684
+ ? {}
685
+ : {description: template.description}),
686
+ ...(typeof template.replaces === 'string'
687
+ ? {replaces: template.replaces}
688
+ : {}),
689
+ });
690
+ };
691
+ try {
692
+ for (const template of await discoverCoreTemplates()) {
693
+ addTemplate(template, CORE_PACKAGE, CORE_PACKAGE);
694
+ }
695
+ } catch {
696
+ // Core templates that do not load leave nothing to link to.
697
+ }
698
+ if (integration.templates) {
699
+ try {
700
+ const {templates: records} =
701
+ await discoverIntegrationTemplatesForOne(integration);
702
+ for (const template of records)
703
+ addTemplate(template, own, integration.name);
704
+ } catch {
705
+ // `validate` reports templates that do not load.
706
+ }
707
+ }
708
+ return {components, templates};
709
+ }
710
+
504
711
  /**
505
712
  * How a doc's links find their targets (spec:AST-047 FR9), as
506
713
  * {@link docFinder} finds them: each resolves to the link a read shows.
@@ -701,6 +908,7 @@ export async function docsLinkProblems(
701
908
  */
702
909
  export async function packageDocsProblems(integration, discovered) {
703
910
  const catalog = DocsCatalog.fromBuiltins();
911
+ catalog.setContributions(() => packageContributions(integration));
704
912
  for (const record of discovered.records) catalog.add(record);
705
913
  catalog.addTreeInputs({
706
914
  namespaces: discovered.namespaces.map(input => ({...input, rank: 1})),
@@ -794,6 +1002,7 @@ async function namespaceReferenceTarget(catalog, route, lang) {
794
1002
  */
795
1003
  export async function packageReferenceProblems(integration, discovered) {
796
1004
  const catalog = DocsCatalog.fromBuiltins();
1005
+ catalog.setContributions(() => packageContributions(integration));
797
1006
  for (const record of discovered.records) catalog.add(record);
798
1007
  catalog.addTreeInputs({
799
1008
  namespaces: discovered.namespaces.map(input => ({...input, rank: 1})),
@@ -1125,3 +1334,37 @@ export async function resolveTopicDocs(topic, options = {}) {
1125
1334
  const node = await lowerTopic(catalog, entry, effectiveLang);
1126
1335
  return {catalog, node, lang: effectiveLang, entry};
1127
1336
  }
1337
+
1338
+ /**
1339
+ * Each doc one integration package ships, checked against the published
1340
+ * authoring type for its kind with the project's own TypeScript (spec:AST-039
1341
+ * FR13). Without TypeScript the report says nothing was checked.
1342
+ * @param {any} integration a loaded integration
1343
+ * @param {string} cwd
1344
+ * @returns {Promise<import('../../foundation/discovery/doc-types.mjs').DocTypesReport & {fixtures?: number}>}
1345
+ */
1346
+ export async function integrationDocTypes(integration, cwd) {
1347
+ const packageDir = /** @type {{__packageDir?: string}} */ (integration)
1348
+ .__packageDir;
1349
+ const {files, unread, fixtures} = await integrationDocFiles(integration);
1350
+ const checked = await checkDocTypes(files, {
1351
+ packageDir: packageDir ?? cwd,
1352
+ resolveFrom: [cwd, packageDir],
1353
+ });
1354
+ // Test fixtures no reader serves don't ship as docs: only counted.
1355
+ const report =
1356
+ checked.status === 'checked' && fixtures > 0
1357
+ ? {...checked, fixtures}
1358
+ : checked;
1359
+ if (report.status !== 'checked' || unread.length === 0) return report;
1360
+ // A root its reader cannot read serves no doc; say so rather than skip it.
1361
+ const rel = (/** @type {string} */ file) =>
1362
+ path.relative(packageDir ?? cwd, file).split(path.sep).join('/') || '.';
1363
+ return {
1364
+ ...report,
1365
+ unchecked: [
1366
+ ...report.unchecked,
1367
+ ...unread.map(entry => ({doc: rel(entry.path), reason: entry.reason})),
1368
+ ].sort((a, b) => a.doc.localeCompare(b.doc)),
1369
+ };
1370
+ }
@@ -22,6 +22,7 @@ import {docs} from './docs.mjs';
22
22
  import {search} from '../search/search.mjs';
23
23
  import {loadDocsCatalog} from './_adapter.mjs';
24
24
  import {AstryxError} from '../error.mjs';
25
+ import {discoverCoreTemplates} from '../../foundation/discovery/template-adapter.mjs';
25
26
 
26
27
  const SLOW = 30_000;
27
28
 
@@ -110,6 +111,129 @@ describe('integration-contributed topics', () => {
110
111
  expect(section.data.title).toBe('Overview');
111
112
  }, SLOW);
112
113
 
114
+ it('reads a link to a component or a template as the command that opens it', async () => {
115
+ scaffold({
116
+ 'using-widgets.doc.mjs': topic({
117
+ name: 'using-widgets',
118
+ title: 'Using widgets',
119
+ sections: [
120
+ {
121
+ title: 'Overview',
122
+ content: [
123
+ {
124
+ type: 'prose',
125
+ text: 'Start with {@link component:AcmeButton} or {@link block:acme-card}, built on {@link @astryxdesign/core:component:Button}.',
126
+ },
127
+ ],
128
+ },
129
+ ],
130
+ }),
131
+ });
132
+ const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
133
+ fs.writeFileSync(
134
+ path.join(pkgDir, 'astryx.integration.mjs'),
135
+ "export default {components: './components', templates: './templates', docs: './docs'};\n",
136
+ );
137
+ fs.mkdirSync(path.join(pkgDir, 'components'));
138
+ fs.writeFileSync(
139
+ path.join(pkgDir, 'components', 'AcmeButton.doc.mjs'),
140
+ "export default {type: 'component', name: 'AcmeButton', displayName: 'Acme Button', description: 'A button.', props: []};\n",
141
+ );
142
+ fs.writeFileSync(
143
+ path.join(pkgDir, 'components', 'AcmeButton.tsx'),
144
+ 'export function AcmeButton() { return null; }\n',
145
+ );
146
+ fs.mkdirSync(path.join(pkgDir, 'templates'));
147
+ fs.writeFileSync(
148
+ path.join(pkgDir, 'templates', 'acme-card.doc.mjs'),
149
+ "export default {type: 'block', name: 'Acme card', description: 'A card.'};\n",
150
+ );
151
+ fs.writeFileSync(
152
+ path.join(pkgDir, 'templates', 'acme-card.tsx'),
153
+ 'export default function Fixture() { return null; }\n',
154
+ );
155
+
156
+ const detail = await docs('using-widgets', undefined, {cwd: tmpDir, full: true});
157
+
158
+ const prose = JSON.stringify(detail.data.sections);
159
+ expect(prose).toContain('`astryx component AcmeButton --package @acme/widgets`');
160
+ expect(prose).toContain(
161
+ '`astryx template acme-card --type block --package @acme/widgets`',
162
+ );
163
+ expect(prose).toContain('`astryx component Button`');
164
+ expect(prose).not.toContain('{@link');
165
+ }, SLOW);
166
+
167
+ it('selects Core in a link only where the bare name would open something else', async () => {
168
+ const core = await discoverCoreTemplates();
169
+ const page = core.find(template => template.type === 'page')?.dirName;
170
+ const block = core.find(template => template.type === 'block')?.dirName;
171
+ scaffold({
172
+ 'using-widgets.doc.mjs': topic({
173
+ name: 'using-widgets',
174
+ title: 'Using widgets',
175
+ sections: [
176
+ {
177
+ title: 'Overview',
178
+ content: [
179
+ {
180
+ type: 'prose',
181
+ text:
182
+ 'Core: {@link @astryxdesign/core:component:Button}, {@link @astryxdesign/core:component:Badge}, ' +
183
+ `{@link @astryxdesign/core:page:${page}}, {@link @astryxdesign/core:block:${block}}.`,
184
+ },
185
+ ],
186
+ },
187
+ ],
188
+ }),
189
+ });
190
+ const pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
191
+ // The CLI range that turns component replacement on.
192
+ fs.writeFileSync(
193
+ path.join(pkgDir, 'package.json'),
194
+ JSON.stringify({
195
+ name: '@acme/widgets',
196
+ version: '1.0.0',
197
+ peerDependencies: {'@astryxdesign/cli': '>=0.6.7'},
198
+ peerDependenciesMeta: {'@astryxdesign/cli': {optional: true}},
199
+ }),
200
+ );
201
+ fs.writeFileSync(
202
+ path.join(pkgDir, 'astryx.integration.mjs'),
203
+ "export default {components: './components', templates: './templates', docs: './docs'};\n",
204
+ );
205
+ fs.mkdirSync(path.join(pkgDir, 'components'));
206
+ // AcmeButton replaces Core's Button, so bare `Button` opens AcmeButton.
207
+ fs.writeFileSync(
208
+ path.join(pkgDir, 'components', 'AcmeButton.doc.mjs'),
209
+ "export default {type: 'component', name: 'AcmeButton', displayName: 'Acme Button', description: 'A button.', replaces: 'Button', props: []};\n",
210
+ );
211
+ fs.writeFileSync(
212
+ path.join(pkgDir, 'components', 'AcmeButton.tsx'),
213
+ 'export function AcmeButton() { return null; }\n',
214
+ );
215
+ // A page template with a Core page's id makes the bare id ambiguous.
216
+ fs.mkdirSync(path.join(pkgDir, 'templates'));
217
+ fs.writeFileSync(
218
+ path.join(pkgDir, 'templates', `${page}.doc.mjs`),
219
+ "export default {type: 'page', name: 'Acme page', displayName: 'Acme page', description: 'A page.'};\n",
220
+ );
221
+ fs.writeFileSync(
222
+ path.join(pkgDir, 'templates', `${page}.tsx`),
223
+ 'export default function Fixture() { return null; }\n',
224
+ );
225
+
226
+ const detail = await docs('using-widgets', undefined, {cwd: tmpDir, full: true});
227
+
228
+ const prose = JSON.stringify(detail.data.sections);
229
+ expect(prose).toContain('`astryx component Button --package @astryxdesign/core`');
230
+ expect(prose).toContain('`astryx component Badge`');
231
+ expect(prose).toContain(
232
+ `\`astryx template ${page} --type page --package @astryxdesign/core\``,
233
+ );
234
+ expect(prose).toContain(`\`astryx template ${block} --type block\``);
235
+ }, SLOW);
236
+
113
237
  it('is invisible to a project that does not configure the integration', async () => {
114
238
  scaffold({'deploying.doc.mjs': topic()}, {config: 'export default {};\n'});
115
239
  const listed = await docs(undefined, undefined, {cwd: tmpDir});
@@ -351,6 +351,26 @@ export async function checkConfig(ctx) {
351
351
  }
352
352
  }
353
353
 
354
+ /**
355
+ * Why no integration is implicitly linked, from what actually loaded. A
356
+ * configured integration is named in astryx.config; the package's own manifest
357
+ * loads as itself (`__local`). Only the first may be described as named in
358
+ * astryx.config: a package with no config that loads its own manifest has no
359
+ * config file to name it.
360
+ *
361
+ * @param {Array<{name?: string, __local?: boolean}>} integrations loaded, none
362
+ * autolinked and none that failed to load
363
+ * @returns {string}
364
+ */
365
+ function describeNoImplicit(integrations) {
366
+ const own = integrations.filter(integration => integration.__local);
367
+ const configured = integrations.length - own.length;
368
+ const ownNames = own.map(integration => integration.name ?? 'this package').join(', ');
369
+ if (own.length === 0) return 'None — every loaded integration is named in astryx.config.';
370
+ if (configured === 0) return `None — the only loaded integration is this package's own manifest (${ownNames}).`;
371
+ return `None — every loaded integration is named in astryx.config or is this package's own manifest (${ownNames}).`;
372
+ }
373
+
354
374
  /**
355
375
  * Check 6 — integrations that are loaded without an astryx.config entry.
356
376
  *
@@ -395,14 +415,18 @@ export function checkImplicitIntegrations(ctx) {
395
415
  const unreadable = describeUnreadableManifests(ctx.autolinkFailures ?? []);
396
416
 
397
417
  if (implicit.length === 0) {
418
+ // A record that carries `__loadError` (the package's own manifest, or a
419
+ // configured one, that failed to load) loaded nothing, so it is not named
420
+ // as loaded beside the line reporting that it could not be loaded.
421
+ const loaded = ctx.integrations.filter(integration => !integration.__loadError);
398
422
  return {
399
423
  id,
400
424
  label,
401
425
  status: 'info',
402
426
  message:
403
- (ctx.integrations.length > 0
404
- ? 'None — every loaded integration is named in astryx.config.'
405
- : unreadable
427
+ (loaded.length > 0
428
+ ? describeNoImplicit(loaded)
429
+ : ctx.integrations.length > 0 || unreadable
406
430
  ? 'None loaded.'
407
431
  : 'None — no installed dependency ships an astryx.integration.* manifest.') +
408
432
  unreadable,
@@ -353,6 +353,49 @@ describe('checkImplicitIntegrations', () => {
353
353
  expect(c.message).toContain('named in astryx.config');
354
354
  });
355
355
 
356
+ // A package with no astryx.config that loads its own manifest: there is no
357
+ // config file, so the message must not say anything is named in one.
358
+ it("names the package's own manifest instead of a config that does not exist", () => {
359
+ const c = checkImplicitIntegrations({
360
+ integrations: [autolinked({__autolinked: false, __local: true, name: '@acme/kit'})],
361
+ });
362
+ expect(c.status).toBe('info');
363
+ expect(c.message).toBe(
364
+ "None — the only loaded integration is this package's own manifest (@acme/kit).",
365
+ );
366
+ expect(c.message).not.toContain('astryx.config');
367
+ });
368
+
369
+ // An own manifest that failed to load keeps a `__local` record carrying
370
+ // `__loadError`; it loaded nothing, so it is never named as loaded.
371
+ it("does not name the package's own manifest as loaded when it failed to load", () => {
372
+ const failed = autolinked({
373
+ __autolinked: false,
374
+ __local: true,
375
+ name: '@acme/kit',
376
+ __loadError: 'astryx.integration.mjs threw: boom',
377
+ });
378
+ const alone = checkImplicitIntegrations({integrations: [failed]});
379
+ expect(alone.status).toBe('info');
380
+ expect(alone.message).toBe('None loaded.');
381
+ const beside = checkImplicitIntegrations({
382
+ integrations: [autolinked({__autolinked: false}), failed],
383
+ });
384
+ expect(beside.message).toBe('None — every loaded integration is named in astryx.config.');
385
+ });
386
+
387
+ it('names both when configured integrations load beside the own manifest', () => {
388
+ const c = checkImplicitIntegrations({
389
+ integrations: [
390
+ autolinked({__autolinked: false}),
391
+ autolinked({__autolinked: false, __local: true, name: '@acme/kit'}),
392
+ ],
393
+ });
394
+ expect(c.message).toBe(
395
+ "None — every loaded integration is named in astryx.config or is this package's own manifest (@acme/kit).",
396
+ );
397
+ });
398
+
356
399
  it('names the package, the field, and what it contributes', () => {
357
400
  // Roots count only when they exist, so this one gives them real folders.
358
401
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-implicit-'));
@@ -736,7 +736,7 @@ export async function checkAppThemes(cwd) {
736
736
  label: 'App imports the theme module',
737
737
  status: 'warn',
738
738
  message: `Could not prove that project source imports ${state.module.path}${importEvidence.parseFailures ? `; ${importEvidence.parseFailures} source file(s) could not be parsed` : ''}.`,
739
- fix: `Import {themes, defaultThemeSlug} from the generated module and {Theme} from '@astryxdesign/core', then render <Theme theme={themes[defaultThemeSlug]}>.`,
739
+ fix: `Import {themes, defaultThemeSlug} from the generated module and {Theme} from '@astryxdesign/core/theme', then render <Theme theme={themes[defaultThemeSlug]}>.`,
740
740
  });
741
741
  }
742
742