@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4

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 (188) hide show
  1. package/README.md +2 -1
  2. package/api/build/build.type.d.mts +2 -2
  3. package/api/build/build.type.mjs +2 -2
  4. package/api/component/component.type.d.mts +6 -6
  5. package/api/component/component.type.mjs +19 -19
  6. package/api/discover/discover.type.d.mts +4 -4
  7. package/api/discover/discover.type.mjs +10 -10
  8. package/api/docs/_adapter.d.mts +37 -24
  9. package/api/docs/_adapter.mjs +169 -83
  10. package/api/docs/compiled-topics.test.mjs +78 -0
  11. package/api/docs/detail/detail.mjs +14 -63
  12. package/api/docs/detail/section/section.d.mts +1 -1
  13. package/api/docs/detail/section/section.mjs +44 -20
  14. package/api/docs/detail/section/section.test.mjs +41 -0
  15. package/api/docs/docs.d.mts +7 -2
  16. package/api/docs/docs.doc.mjs +27 -10
  17. package/api/docs/docs.mjs +16 -9
  18. package/api/docs/docs.test.mjs +6 -0
  19. package/api/docs/docs.type.d.mts +40 -3
  20. package/api/docs/docs.type.mjs +36 -8
  21. package/api/docs/index/index.d.mts +18 -0
  22. package/api/docs/index/index.mjs +32 -0
  23. package/api/docs/index/index.test.mjs +62 -0
  24. package/api/docs/integrationDocs.test.mjs +106 -0
  25. package/api/doctor/doctor.d.mts +48 -0
  26. package/api/doctor/doctor.mjs +232 -0
  27. package/api/doctor/doctor.test.mjs +196 -0
  28. package/api/hook/hook.type.d.mts +3 -3
  29. package/api/hook/hook.type.mjs +11 -11
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.mjs +5 -3
  32. package/api/integration/add-contribution.test.mjs +4 -4
  33. package/api/integration/integration-authoring.type.d.mts +1 -1
  34. package/api/integration/pack-check.mjs +49 -7
  35. package/api/integration/pack-check.test.mjs +249 -0
  36. package/api/search/search.d.mts +1 -1
  37. package/api/search/search.mjs +5 -5
  38. package/api/search/search.type.d.mts +2 -2
  39. package/api/search/search.type.mjs +1 -1
  40. package/api/swizzle/swizzle.type.d.mts +2 -2
  41. package/api/swizzle/swizzle.type.mjs +2 -2
  42. package/api/template/template.d.mts +1 -1
  43. package/api/template/template.type.d.mts +6 -6
  44. package/api/template/template.type.mjs +12 -12
  45. package/api/theme/build/build.mjs +20 -6
  46. package/api/theme/build/build.test.mjs +127 -0
  47. package/api/theme/palette/generate/generate.mjs +1 -1
  48. package/api/theme/palette/generate/generator.d.mts +10 -13
  49. package/api/theme/palette/generate/generator.mjs +7 -3
  50. package/api/theme/theme.type.d.mts +170 -11
  51. package/api/theme/theme.type.mjs +94 -27
  52. package/api/upgrade/_adapter.mjs +71 -5
  53. package/api/upgrade/project-context.test.mjs +272 -0
  54. package/api/upgrade/upgrade.doc.mjs +4 -3
  55. package/api/upgrade/upgrade.type.d.mts +5 -5
  56. package/api/upgrade/upgrade.type.mjs +11 -11
  57. package/assets/codemods/integration-discovery.mjs +40 -2
  58. package/assets/codemods/integration-discovery.test.mjs +58 -0
  59. package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
  60. package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
  61. package/assets/docs/README.md +9 -0
  62. package/assets/docs/authoring.doc.mjs +14 -0
  63. package/assets/docs/cli-integrations.doc.mjs +86 -15
  64. package/assets/docs/styling-libraries.doc.mjs +1 -1
  65. package/assets/docs/working-with-ai.doc.mjs +1 -1
  66. package/authoring/_shared/contract.ts +22 -0
  67. package/authoring/codemod/codemod.doc.mjs +6 -1
  68. package/authoring/codemod/parse.d.mts +8 -8
  69. package/authoring/codemod/parse.mjs +8 -6
  70. package/authoring/config/parse.d.mts +13 -13
  71. package/authoring/config/parse.mjs +8 -8
  72. package/authoring/config/type.ts +3 -3
  73. package/authoring/debug/parse.d.mts +5 -5
  74. package/authoring/debug/parse.mjs +3 -3
  75. package/authoring/doctypes/_schema.d.mts +788 -23
  76. package/authoring/doctypes/_schema.mjs +492 -39
  77. package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
  78. package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
  79. package/authoring/doctypes/base/type.ts +40 -0
  80. package/authoring/doctypes/command/command.doc.mjs +3 -2
  81. package/authoring/doctypes/command/parse.d.mts +2 -2
  82. package/authoring/doctypes/command/parse.mjs +1 -1
  83. package/authoring/doctypes/command/type.ts +3 -2
  84. package/authoring/doctypes/component/component.doc.mjs +6 -3
  85. package/authoring/doctypes/component/parse.d.mts +2 -2
  86. package/authoring/doctypes/component/parse.mjs +1 -1
  87. package/authoring/doctypes/component/type.ts +4 -3
  88. package/authoring/doctypes/enum/parse.d.mts +2 -2
  89. package/authoring/doctypes/enum/parse.mjs +1 -1
  90. package/authoring/doctypes/enum/type.ts +3 -1
  91. package/authoring/doctypes/function/function.doc.mjs +4 -0
  92. package/authoring/doctypes/function/parse.d.mts +2 -2
  93. package/authoring/doctypes/function/parse.mjs +1 -1
  94. package/authoring/doctypes/function/type.ts +6 -2
  95. package/authoring/doctypes/hook/hook.doc.mjs +4 -0
  96. package/authoring/doctypes/hook/parse.d.mts +2 -2
  97. package/authoring/doctypes/hook/parse.mjs +1 -1
  98. package/authoring/doctypes/hook/type.ts +3 -2
  99. package/authoring/doctypes/legacy.d.mts +8 -6
  100. package/authoring/doctypes/legacy.mjs +5 -4
  101. package/authoring/doctypes/load-contract.test.mjs +207 -0
  102. package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
  103. package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
  104. package/authoring/doctypes/namespace/parse.d.mts +12 -0
  105. package/authoring/doctypes/namespace/parse.mjs +25 -0
  106. package/authoring/doctypes/namespace/parse.test.mjs +165 -0
  107. package/authoring/doctypes/namespace/type.ts +71 -0
  108. package/authoring/doctypes/parse.d.mts +20 -18
  109. package/authoring/doctypes/parse.mjs +16 -10
  110. package/authoring/doctypes/parse.test.mjs +77 -3
  111. package/authoring/doctypes/reference/parse.d.mts +2 -2
  112. package/authoring/doctypes/reference/parse.mjs +8 -5
  113. package/authoring/doctypes/reference/reference.doc.mjs +17 -4
  114. package/authoring/doctypes/reference/type.ts +51 -5
  115. package/authoring/doctypes/schema/parse.d.mts +2 -2
  116. package/authoring/doctypes/schema/parse.mjs +1 -1
  117. package/authoring/doctypes/schema/type.ts +3 -2
  118. package/authoring/doctypes/template/parse.d.mts +92 -1
  119. package/authoring/doctypes/template/parse.mjs +36 -2
  120. package/authoring/doctypes/template/parse.test.mjs +8 -2
  121. package/authoring/doctypes/template/template.doc.mjs +4 -0
  122. package/authoring/doctypes/template/type.ts +5 -2
  123. package/authoring/doctypes/types.ts +10 -9
  124. package/authoring/gap-report/parse.d.mts +10 -10
  125. package/authoring/gap-report/parse.mjs +6 -6
  126. package/authoring/gap-report/type.ts +1 -1
  127. package/authoring/identity/identity.doc.d.mts +9 -0
  128. package/authoring/identity/identity.doc.mjs +61 -0
  129. package/authoring/identity/type.ts +132 -0
  130. package/authoring/index.d.mts +1 -0
  131. package/authoring/index.d.ts +49 -17
  132. package/authoring/index.mjs +1 -0
  133. package/authoring/integration/integration.doc.mjs +13 -6
  134. package/authoring/integration/parse.d.mts +2 -2
  135. package/authoring/integration/parse.mjs +1 -1
  136. package/authoring/integration/parse.test.mjs +10 -1
  137. package/authoring/integration/schema.d.mts +6 -4
  138. package/authoring/integration/schema.mjs +9 -3
  139. package/authoring/integration/type.ts +23 -6
  140. package/authoring/shadcn/receipt.d.mts +6 -6
  141. package/clients/cli/commands/docs.doc.mjs +13 -3
  142. package/clients/cli/commands/docs.mjs +121 -21
  143. package/clients/cli/commands/docs.test.mjs +88 -0
  144. package/clients/cli/commands/integration-authoring.test.mjs +13 -9
  145. package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
  146. package/clients/cli/commands/upgrade.doc.mjs +2 -2
  147. package/clients/cli/formatters/index.mjs +162 -1
  148. package/clients/cli/formatters/index.test.mjs +91 -0
  149. package/clients/cli/lib/manifest.mjs +7 -2
  150. package/foundation/config/project.mjs +21 -6
  151. package/foundation/discovery/authoring-self-docs.d.mts +69 -0
  152. package/foundation/discovery/authoring-self-docs.mjs +214 -0
  153. package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
  154. package/foundation/discovery/component-discovery.d.mts +1 -1
  155. package/foundation/discovery/component-discovery.mjs +2 -1
  156. package/foundation/discovery/docs-discovery.d.mts +11 -4
  157. package/foundation/discovery/docs-discovery.mjs +208 -88
  158. package/foundation/discovery/docs-discovery.test.mjs +279 -13
  159. package/foundation/discovery/docs-output-budget.d.mts +28 -0
  160. package/foundation/discovery/docs-output-budget.mjs +50 -0
  161. package/foundation/discovery/docs-section-key.d.mts +98 -0
  162. package/foundation/discovery/docs-section-key.mjs +221 -0
  163. package/foundation/discovery/docs-section-key.test.mjs +224 -0
  164. package/foundation/discovery/template-adapter.mjs +2 -1
  165. package/foundation/discovery/theming-targets.test.mjs +4 -0
  166. package/foundation/doc-compiler/compile.d.mts +162 -0
  167. package/foundation/doc-compiler/compile.mjs +262 -0
  168. package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
  169. package/foundation/doc-compiler/ir.d.mts +9 -0
  170. package/foundation/doc-compiler/ir.mjs +287 -0
  171. package/foundation/doc-compiler/lenses.d.mts +33 -0
  172. package/foundation/doc-compiler/lenses.mjs +127 -0
  173. package/foundation/identity/provider-identity.d.mts +90 -0
  174. package/foundation/identity/provider-identity.mjs +320 -0
  175. package/foundation/identity/provider-identity.test.mjs +254 -0
  176. package/foundation/identity/providers.d.mts +7 -0
  177. package/foundation/identity/providers.mjs +16 -0
  178. package/foundation/integrations/autolink.mjs +12 -5
  179. package/foundation/integrations/integration-warnings.mjs +6 -0
  180. package/foundation/integrations/integrations.d.mts +46 -2
  181. package/foundation/integrations/integrations.mjs +167 -8
  182. package/foundation/integrations/integrations.test.mjs +384 -1
  183. package/foundation/integrations/provider-conflicts.test.mjs +125 -0
  184. package/foundation/integrations/validate-contributions.d.mts +2 -0
  185. package/foundation/integrations/validate-contributions.mjs +10 -0
  186. package/foundation/response/json-contract.test.mjs +46 -17
  187. package/foundation/response/response-types.doc.mjs +6 -1
  188. package/package.json +9 -11
@@ -27,6 +27,15 @@ import {MIN_NODE_VERSION, isNodeVersionSupported} from '../../foundation/env/nod
27
27
  import {CLI_ROOT, findCoreDir, findInstalledPackage} from '../../foundation/fs/paths.mjs';
28
28
  import {explainPackageManager, getCliInvocation} from '../../foundation/env/package-manager.mjs';
29
29
  import {findConfigPath, Project} from '../../foundation/config/project.mjs';
30
+ import {DocsCatalog} from '../../foundation/discovery/docs-discovery.mjs';
31
+ import {buildDocsIndexData} from '../../foundation/discovery/docs-section-key.mjs';
32
+ import {
33
+ DOC_OUTPUT_BUDGET_BYTES,
34
+ docsIndexBytes,
35
+ oversizedDocSections,
36
+ } from '../../foundation/discovery/docs-output-budget.mjs';
37
+ import {compileTopic, overlayLanguages} from '../docs/_adapter.mjs';
38
+ import {detailView} from '../../foundation/doc-compiler/lenses.mjs';
30
39
  import {semverCompare, isValidSemver, satisfiesRange} from '../../foundation/env/semver.mjs';
31
40
 
32
41
  /**
@@ -52,6 +61,11 @@ import {semverCompare, isValidSemver, satisfiesRange} from '../../foundation/env
52
61
  * @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]|null} [integrations]
53
62
  * Every integration the project loaded, or null when the project could not be
54
63
  * read at all.
64
+ * @property {DocsCatalog|null} [docsCatalog] - The topics a docs read sees.
65
+ * @property {Array<{package?: string, code: string, message: string}>} [docsCatalogIssues]
66
+ * `invalid_doc` issues from the project's contributed docs.
67
+ * @property {string|null} [docsCatalogError] - Why the project's docs catalog
68
+ * could not be built, when it could not.
55
69
  * @property {Error|null} [configError] - Error thrown while resolving the config
56
70
  * path (e.g. multiple config files present), surfaced by checkConfig as a FAIL.
57
71
  */
@@ -627,6 +641,201 @@ export function checkPackageManager(ctx) {
627
641
  };
628
642
  }
629
643
 
644
+ /**
645
+ * Check 6b — every contributing integration owns its provider identity.
646
+ *
647
+ * Artifact and document IDs are provider-scoped, so a package that claims a
648
+ * provider ID an earlier-loaded package already holds is loaded inert: its
649
+ * components, templates, themes, docs, and codemods are withdrawn while the
650
+ * earlier package keeps contributing. That can be a deliberate transition
651
+ * (a renamed package installed beside its predecessor), so it warns rather
652
+ * than fails, but it is never allowed to happen quietly.
653
+ *
654
+ * @param {DoctorContext} ctx
655
+ * @returns {DoctorCheck}
656
+ */
657
+ export function checkProviderIdentity(ctx) {
658
+ const id = 'provider-identity';
659
+ const label = 'Integration provider identity';
660
+
661
+ if (ctx.integrations == null) {
662
+ return {
663
+ id,
664
+ label,
665
+ status: 'info',
666
+ message: 'Skipped — the project configuration could not be read.',
667
+ };
668
+ }
669
+
670
+ const conflicts = ctx.integrations.filter(
671
+ integration => integration.__providerConflict,
672
+ );
673
+ if (conflicts.length > 0) {
674
+ return {
675
+ id,
676
+ label,
677
+ status: 'warn',
678
+ message: conflicts
679
+ .map(integration => integration.__providerConflict?.message)
680
+ .join(' '),
681
+ fix:
682
+ 'Give each integration its own `providerId` in astryx.integration.*. ' +
683
+ "A renamed package may keep its predecessor's ID only when the " +
684
+ 'predecessor is no longer installed.',
685
+ };
686
+ }
687
+
688
+ const count = ctx.integrations.filter(
689
+ integration =>
690
+ integration.providerId != null && integration.__loadError == null,
691
+ ).length;
692
+ if (count === 0) {
693
+ return {
694
+ id,
695
+ label,
696
+ status: 'info',
697
+ message: 'None — no loaded integration has a provider identity.',
698
+ };
699
+ }
700
+ return {
701
+ id,
702
+ label,
703
+ status: 'pass',
704
+ message:
705
+ count === 1
706
+ ? '1 loaded integration has its own provider ID.'
707
+ : `${count} loaded integrations each have their own provider ID.`,
708
+ };
709
+ }
710
+
711
+ /** @param {number} bytes */
712
+ const kilobytes = bytes => `${Math.ceil(bytes / 1024)} KB`;
713
+
714
+ /**
715
+ * @param {string[]} problems
716
+ * @returns {string}
717
+ */
718
+ function joinProblems(problems) {
719
+ return problems.length === 1
720
+ ? problems[0]
721
+ : `${problems.length} problems: ${problems.join('; ')}`;
722
+ }
723
+
724
+ /**
725
+ * Every authoring self-doc is reachable from `astryx docs authoring`, loads,
726
+ * and fits in one read. The audit is imported here, inside the try, so a
727
+ * malformed self-doc is reported rather than taking Doctor down.
728
+ * @param {DoctorContext} [_ctx]
729
+ * @returns {Promise<DoctorCheck>}
730
+ */
731
+ export async function checkAuthoringDocs(_ctx) {
732
+ const id = 'authoring-docs';
733
+ const label = 'Authoring docs';
734
+ try {
735
+ const {auditAuthoringSelfDocs} = await import(
736
+ '../../foundation/discovery/authoring-self-docs.mjs'
737
+ );
738
+ const audit = await auditAuthoringSelfDocs();
739
+ const problems = [
740
+ ...audit.unreachable.map(
741
+ source => `${source} is not in \`astryx docs authoring\``,
742
+ ),
743
+ ...audit.failed.map(({source, error}) => `${source} failed to load: ${error}`),
744
+ ...audit.oversized.map(
745
+ ({key, bytes}) =>
746
+ `authoring section "${key}" is ${kilobytes(bytes)}, over the ${kilobytes(DOC_OUTPUT_BUDGET_BYTES)} one read may return`,
747
+ ),
748
+ ];
749
+ if (problems.length > 0) {
750
+ return {
751
+ id,
752
+ label,
753
+ status: 'fail',
754
+ message: joinProblems(problems),
755
+ fix: 'List every authoring self-doc in AUTHORING_SELF_DOCS, fix the one that fails to load, and split a section that is too large.',
756
+ };
757
+ }
758
+ return {
759
+ id,
760
+ label,
761
+ status: 'pass',
762
+ message: `All ${audit.sections} authoring schemas are readable in \`astryx docs authoring\`.`,
763
+ };
764
+ } catch (err) {
765
+ return {
766
+ id,
767
+ label,
768
+ status: 'fail',
769
+ message: `The authoring docs could not be audited: ${err instanceof Error ? err.message : String(err)}`,
770
+ fix: 'Reinstall @astryxdesign/cli.',
771
+ };
772
+ }
773
+ }
774
+
775
+ /**
776
+ * Every topic reads progressively, in every language it ships: it loads, its
777
+ * section index and each of its sections fit in one read, and no contributed
778
+ * doc is invalid.
779
+ * @param {DoctorContext | Partial<DoctorContext>} ctx
780
+ * @returns {Promise<DoctorCheck>}
781
+ */
782
+ export async function checkDocsProgressiveDisclosure(ctx) {
783
+ const id = 'docs-progressive-disclosure';
784
+ const label = 'Documentation navigation and size';
785
+ const budget = kilobytes(DOC_OUTPUT_BUDGET_BYTES);
786
+ /** @type {string[]} */
787
+ const problems = [];
788
+ if (ctx.docsCatalogError) {
789
+ problems.push(`The docs catalog could not be built: ${ctx.docsCatalogError}`);
790
+ }
791
+ for (const issue of ctx.docsCatalogIssues ?? []) {
792
+ problems.push(`${issue.package ?? 'a contributed doc'}: ${issue.message}`);
793
+ }
794
+ let topics = 0;
795
+ const catalog = ctx.docsCatalog;
796
+ if (catalog) {
797
+ for (const entry of catalog.entries()) {
798
+ for (const lang of [null, ...overlayLanguages(entry)]) {
799
+ const where = lang ? `${entry.name} [${lang}]` : entry.name;
800
+ try {
801
+ const doc = detailView(await compileTopic(catalog, entry, lang));
802
+ if (lang == null) topics += 1;
803
+ const indexBytes = docsIndexBytes(buildDocsIndexData(doc));
804
+ if (indexBytes > DOC_OUTPUT_BUDGET_BYTES) {
805
+ problems.push(
806
+ `${where}: its section index is ${kilobytes(indexBytes)}, over the ${budget} one read may return`,
807
+ );
808
+ }
809
+ for (const over of oversizedDocSections(doc.sections)) {
810
+ problems.push(
811
+ `${where} ${over.key}: ${kilobytes(over.bytes)}, over the ${budget} one read may return`,
812
+ );
813
+ }
814
+ } catch (err) {
815
+ problems.push(
816
+ `${where}: ${err instanceof Error ? err.message : String(err)}`,
817
+ );
818
+ }
819
+ }
820
+ }
821
+ }
822
+ if (problems.length > 0) {
823
+ return {
824
+ id,
825
+ label,
826
+ status: 'fail',
827
+ message: joinProblems(problems),
828
+ fix: 'Fix the doc each problem names; split a section that is too large into smaller ones, each with its own key.',
829
+ };
830
+ }
831
+ return {
832
+ id,
833
+ label,
834
+ status: 'pass',
835
+ message: `${topics} topics: every section index and section fits in one ${budget} read.`,
836
+ };
837
+ }
838
+
630
839
  /**
631
840
  * Ordered list of synchronous check functions. Append here to add a check.
632
841
  * (checkConfig is async and is awaited separately by {@link runChecks}.)
@@ -638,6 +847,7 @@ export const SYNC_CHECKS = [
638
847
  checkVersionAlignment,
639
848
  checkThemes,
640
849
  checkImplicitIntegrations,
850
+ checkProviderIdentity,
641
851
  checkAgentDocs,
642
852
  checkPeerDeps,
643
853
  checkPackageManager,
@@ -669,11 +879,28 @@ export async function runChecks(options = {}) {
669
879
  let configTheme = null;
670
880
  /** @type {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]|null} */
671
881
  let integrations = null;
882
+ // A docs read falls back to the built-in topics when the project cannot be
883
+ // read, so the docs checks do too; the config check reports the config.
884
+ /** @type {DocsCatalog|null} */
885
+ let docsCatalog = DocsCatalog.fromBuiltins();
886
+ /** @type {Array<{package?: string, code: string, message: string}>} */
887
+ let docsCatalogIssues = [];
888
+ /** @type {string|null} */
889
+ let docsCatalogError = null;
672
890
  try {
673
891
  const project = await Project.load(cwd);
674
892
  configTheme =
675
893
  /** @type {{theme?: string}} */ (project.config ?? {}).theme ?? null;
676
894
  integrations = project.loadedIntegrations;
895
+ try {
896
+ docsCatalog = await project.docs();
897
+ docsCatalogIssues = (await project.issues()).filter(
898
+ issue => issue.code === 'invalid_doc',
899
+ );
900
+ } catch (err) {
901
+ docsCatalog = null;
902
+ docsCatalogError = err instanceof Error ? err.message : String(err);
903
+ }
677
904
  } catch {
678
905
  // Best-effort: a missing/invalid config leaves configTheme null.
679
906
  }
@@ -686,6 +913,9 @@ export async function runChecks(options = {}) {
686
913
  configPath,
687
914
  configTheme,
688
915
  integrations,
916
+ docsCatalog,
917
+ docsCatalogIssues,
918
+ docsCatalogError,
689
919
  configError,
690
920
  };
691
921
 
@@ -698,6 +928,8 @@ export async function runChecks(options = {}) {
698
928
  checks.push(await checkConfig(ctx));
699
929
  }
700
930
  }
931
+ checks.push(await checkAuthoringDocs(ctx));
932
+ checks.push(await checkDocsProgressiveDisclosure(ctx));
701
933
 
702
934
  const summary = {pass: 0, warn: 0, fail: 0, info: 0};
703
935
  for (const c of checks) summary[c.status] += 1;
@@ -11,9 +11,13 @@ import * as fs from 'node:fs';
11
11
  import * as os from 'node:os';
12
12
  import * as path from 'node:path';
13
13
  import {fileURLToPath} from 'node:url';
14
+ import {DocsCatalog} from '../../foundation/discovery/docs-discovery.mjs';
14
15
  import {
15
16
  doctor,
17
+ checkAuthoringDocs,
18
+ checkDocsProgressiveDisclosure,
16
19
  checkImplicitIntegrations,
20
+ checkProviderIdentity,
17
21
  checkVersionAlignment,
18
22
  checkPackageManager,
19
23
  } from './doctor.mjs';
@@ -214,6 +218,77 @@ describe('checkPackageManager', () => {
214
218
  });
215
219
  });
216
220
 
221
+ describe('checkProviderIdentity', () => {
222
+ /** @param {object} [fields] */
223
+ const loaded = (fields = {}) => ({
224
+ name: '@acme/widgets',
225
+ providerId: '@acme/widgets',
226
+ version: '1.0.0',
227
+ __spec: '@acme/widgets',
228
+ __packageDir: '/abs/node_modules/@acme/widgets',
229
+ __manifestFile: '/abs/node_modules/@acme/widgets/astryx.integration.mjs',
230
+ ...fields,
231
+ });
232
+
233
+ it('skips when the project could not be read', () => {
234
+ expect(checkProviderIdentity({integrations: null}).status).toBe('info');
235
+ });
236
+
237
+ it('reports none when nothing is loaded', () => {
238
+ const c = checkProviderIdentity({integrations: []});
239
+ expect(c.status).toBe('info');
240
+ expect(c.message).toContain('None');
241
+ });
242
+
243
+ it('passes when each loaded integration has its own provider ID', () => {
244
+ const c = checkProviderIdentity({
245
+ integrations: [
246
+ loaded(),
247
+ loaded({
248
+ name: '@acme/charts',
249
+ providerId: '@acme/charts',
250
+ __spec: '@acme/charts',
251
+ }),
252
+ ],
253
+ });
254
+ expect(c.status).toBe('pass');
255
+ expect(c.message).toContain('2 loaded integrations');
256
+ });
257
+
258
+ it('warns and names both packages when a later claimant is set aside', () => {
259
+ const message =
260
+ '@acme/renamed@2.0.0 and @acme/widgets@1.0.0 both claim provider ID ' +
261
+ '"@acme/widgets". @acme/widgets@1.0.0 loads first and is used; ' +
262
+ '@acme/renamed@2.0.0 contributes nothing until one package changes ' +
263
+ 'its providerId.';
264
+ const c = checkProviderIdentity({
265
+ integrations: [
266
+ loaded(),
267
+ loaded({
268
+ name: '@acme/renamed',
269
+ version: '2.0.0',
270
+ __spec: '@acme/renamed',
271
+ __providerConflict: {
272
+ providerId: '@acme/widgets',
273
+ claimedBy: '@acme/widgets',
274
+ message,
275
+ },
276
+ }),
277
+ ],
278
+ });
279
+ expect(c.status).toBe('warn');
280
+ expect(c.message).toBe(message);
281
+ expect(c.fix).toContain('providerId');
282
+ });
283
+
284
+ it('is part of the report doctor returns', async () => {
285
+ const r = await doctor({cwd});
286
+ expect(r.data.checks.map(check => check.id)).toContain(
287
+ 'provider-identity',
288
+ );
289
+ }, SLOW);
290
+ });
291
+
217
292
  describe('checkImplicitIntegrations', () => {
218
293
  /** @param {object} [fields] */
219
294
  const autolinked = (fields = {}) => ({
@@ -297,3 +372,124 @@ describe('checkImplicitIntegrations', () => {
297
372
  expect(r.data.checks.map(c => c.id)).toContain('implicit-integrations');
298
373
  }, SLOW);
299
374
  });
375
+
376
+ describe('checkDocsProgressiveDisclosure', () => {
377
+ it('passes when every topic index and section fits one read', async () => {
378
+ const c = await checkDocsProgressiveDisclosure({
379
+ docsCatalog: DocsCatalog.fromBuiltins(),
380
+ docsCatalogIssues: [],
381
+ });
382
+ expect(c).toMatchObject({id: 'docs-progressive-disclosure', status: 'pass'});
383
+ expect(c.message).toMatch(/^\d+ topics: /);
384
+ }, SLOW);
385
+
386
+ it('fails on an invalid doc an integration contributed', async () => {
387
+ const c = await checkDocsProgressiveDisclosure({
388
+ docsCatalogIssues: [
389
+ {
390
+ package: '@acme/widgets',
391
+ code: 'invalid_doc',
392
+ severity: 'error',
393
+ message: 'bad.doc.mjs exports no doc',
394
+ },
395
+ ],
396
+ });
397
+ expect(c.status).toBe('fail');
398
+ expect(c.message).toBe('@acme/widgets: bad.doc.mjs exports no doc');
399
+ });
400
+
401
+ it('fails when the docs catalog cannot be built', async () => {
402
+ const c = await checkDocsProgressiveDisclosure({docsCatalogError: 'boom'});
403
+ expect(c.status).toBe('fail');
404
+ expect(c.message).toContain('boom');
405
+ });
406
+
407
+ it('names a section over the budget and a topic that fails to load', async () => {
408
+ const dir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-doctor-docs-'));
409
+ tmpDirs.push(dir);
410
+ const huge = {
411
+ name: 'huge',
412
+ title: 'Huge',
413
+ description: 'Too big for one read.',
414
+ sections: [
415
+ {title: 'Small', content: [{type: 'prose', text: 'Fits.'}]},
416
+ {title: 'Everything', content: [{type: 'prose', text: 'x'.repeat(40 * 1024)}]},
417
+ ],
418
+ };
419
+ fs.writeFileSync(
420
+ path.join(dir, 'huge.doc.mjs'),
421
+ `export const docs = ${JSON.stringify(huge)};\n`,
422
+ );
423
+ fs.writeFileSync(path.join(dir, 'broken.doc.mjs'), 'export const docs = {;\n');
424
+ const c = await checkDocsProgressiveDisclosure({
425
+ docsCatalog: DocsCatalog.fromBuiltins({
426
+ huge: path.join(dir, 'huge.doc.mjs'),
427
+ broken: path.join(dir, 'broken.doc.mjs'),
428
+ }),
429
+ docsCatalogIssues: [],
430
+ });
431
+ expect(c.status).toBe('fail');
432
+ expect(c.message).toMatch(/^2 problems: /);
433
+ expect(c.message).toContain('huge everything: 41 KB, over the 32 KB one read may return');
434
+ expect(c.message).toContain('broken: ');
435
+ expect(c.message).not.toContain('huge small');
436
+ });
437
+ });
438
+
439
+ describe('checkAuthoringDocs', () => {
440
+ it('passes when every authoring self-doc is reachable and fits one read', async () => {
441
+ const c = await checkAuthoringDocs();
442
+ expect(c).toMatchObject({id: 'authoring-docs', status: 'pass'});
443
+ expect(c.message).toContain('astryx docs authoring');
444
+ }, SLOW);
445
+ });
446
+
447
+ describe('doctor docs checks', () => {
448
+ it('runs both docs checks and they pass on the repo', async () => {
449
+ const r = await doctor({cwd});
450
+ const byId = Object.fromEntries(r.data.checks.map(c => [c.id, c.status]));
451
+ expect(byId['authoring-docs']).toBe('pass');
452
+ expect(byId['docs-progressive-disclosure']).toBe('pass');
453
+ }, SLOW);
454
+ });
455
+
456
+ describe('checkDocsProgressiveDisclosure languages', () => {
457
+ it('checks every overlay a topic ships, not only English', async () => {
458
+ const dir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-doctor-lang-'));
459
+ tmpDirs.push(dir);
460
+ const deploying = {
461
+ name: 'deploying',
462
+ title: 'Deploying',
463
+ description: 'Ship it.',
464
+ sections: [{title: 'Overview', content: [{type: 'prose', text: 'Push the button.'}]}],
465
+ };
466
+ fs.writeFileSync(
467
+ path.join(dir, 'deploying.doc.mjs'),
468
+ `export const docs = ${JSON.stringify(deploying)};\n`,
469
+ );
470
+ fs.writeFileSync(
471
+ path.join(dir, 'deploying.doc.zh.mjs'),
472
+ "throw new Error('zh overlay broken');\n",
473
+ );
474
+ fs.writeFileSync(
475
+ path.join(dir, 'deploying.doc.dense.mjs'),
476
+ `export const docsDense = ${JSON.stringify({
477
+ sections: [
478
+ {
479
+ section: 'Overview',
480
+ title: 'Overview',
481
+ content: [{type: 'prose', text: 'x'.repeat(40 * 1024)}],
482
+ },
483
+ ],
484
+ })};\n`,
485
+ );
486
+ const c = await checkDocsProgressiveDisclosure({
487
+ docsCatalog: DocsCatalog.fromBuiltins({deploying: path.join(dir, 'deploying.doc.mjs')}),
488
+ docsCatalogIssues: [],
489
+ });
490
+ expect(c.status).toBe('fail');
491
+ expect(c.message).toContain('deploying [zh]: zh overlay broken');
492
+ expect(c.message).toContain('deploying [dense] overview: 41 KB');
493
+ expect(c.message).not.toMatch(/deploying overview:/);
494
+ });
495
+ });
@@ -4,7 +4,7 @@
4
4
  export type HookDoc = import("@astryxdesign/cli/authoring").HookDoc;
5
5
  export type HookParamDoc = import("@astryxdesign/cli/authoring").HookParamDoc;
6
6
  /**
7
- * xds --json hook [--list] [--category X] [--detail names|compact|full]
7
+ * astryx --json hook [--list] [--category X] [--detail names|compact|full]
8
8
  *
9
9
  * The list view emits ONE `hook.list` type across all three detail levels; the
10
10
  * depth is carried in `data.detail` and `data.components` holds the grouped map
@@ -39,14 +39,14 @@ export type HookBriefEntry = {
39
39
  import: string;
40
40
  };
41
41
  /**
42
- * xds --json hook <name>
42
+ * astryx --json hook <name>
43
43
  */
44
44
  export type HookDetailResponse = {
45
45
  type: "hook.detail";
46
46
  data: HookDoc;
47
47
  };
48
48
  /**
49
- * xds --json hook <name> --params
49
+ * astryx --json hook <name> --params
50
50
  */
51
51
  export type HookDetailParamsResponse = {
52
52
  type: "hook.detail.params";
@@ -11,14 +11,14 @@
11
11
  *
12
12
  * Invocation -> type discriminator
13
13
  * ------------------------------------------------------------------
14
- * xds --json hook -> hook.list (data.detail='names')
15
- * xds --json hook --list -> hook.list (data.detail='names')
16
- * xds --json hook --category State -> hook.list (filtered)
17
- * xds --json hook --list --detail compact -> hook.list (data.detail='compact')
18
- * xds --json hook --list --detail full -> hook.list (data.detail='full')
19
- * xds --json hook useMediaQuery -> hook.detail
20
- * xds --json hook useMediaQuery --params -> hook.detail.params
21
- * (not found) -> CLIError
14
+ * astryx --json hook -> hook.list (data.detail='names')
15
+ * astryx --json hook --list -> hook.list (data.detail='names')
16
+ * astryx --json hook --category State -> hook.list (filtered)
17
+ * astryx --json hook --list --detail compact -> hook.list (data.detail='compact')
18
+ * astryx --json hook --list --detail full -> hook.list (data.detail='full')
19
+ * astryx --json hook useMediaQuery -> hook.detail
20
+ * astryx --json hook useMediaQuery --params -> hook.detail.params
21
+ * (not found) -> CLIError
22
22
  */
23
23
 
24
24
  // Re-export the authored-doc types from core so the hook leaves reference these
@@ -28,7 +28,7 @@
28
28
  /** @typedef {import('@astryxdesign/cli/authoring').HookParamDoc} HookParamDoc */
29
29
 
30
30
  /**
31
- * xds --json hook [--list] [--category X] [--detail names|compact|full]
31
+ * astryx --json hook [--list] [--category X] [--detail names|compact|full]
32
32
  *
33
33
  * The list view emits ONE `hook.list` type across all three detail levels; the
34
34
  * depth is carried in `data.detail` and `data.components` holds the grouped map
@@ -56,14 +56,14 @@
56
56
  */
57
57
 
58
58
  /**
59
- * xds --json hook <name>
59
+ * astryx --json hook <name>
60
60
  * @typedef {object} HookDetailResponse
61
61
  * @property {'hook.detail'} type
62
62
  * @property {HookDoc} data
63
63
  */
64
64
 
65
65
  /**
66
- * xds --json hook <name> --params
66
+ * astryx --json hook <name> --params
67
67
  * @typedef {object} HookDetailParamsResponse
68
68
  * @property {'hook.detail.params'} type
69
69
  * @property {HookParamDoc[]} data
@@ -13,7 +13,7 @@
13
13
  export function list({ cwd, category, detail, zh, lang }?: {
14
14
  cwd?: string | undefined;
15
15
  category?: string | undefined;
16
- detail?: "names" | "compact" | "full" | "brief" | undefined;
16
+ detail?: "compact" | "full" | "names" | "brief" | undefined;
17
17
  zh?: boolean | undefined;
18
18
  lang?: string | null | undefined;
19
19
  }): Promise<import("../hook.type.mjs").HookListResponse>;
@@ -250,7 +250,8 @@ async function addComponent(name, options) {
250
250
  }
251
251
 
252
252
  const sourcePath = projectPath(path.relative(packageDir, sourceFile));
253
- const importSpecifier = `${owner}/${sourcePath}`;
253
+ const extensionlessPath = sourcePath.replace(/\.tsx?$/u, '');
254
+ const importSpecifier = `${owner}/${extensionlessPath}`;
254
255
  const docContents = `export default {\n type: 'component',\n name: '${name}',\n import: ${JSON.stringify(importSpecifier)},\n description: '${name} component.',\n props: [],\n};\n`;
255
256
  const sourceContents = `export function ${name}() {\n return <div>${name}</div>;\n}\n`;
256
257
 
@@ -263,7 +264,7 @@ async function addComponent(name, options) {
263
264
  packageFile,
264
265
  rootPath,
265
266
  path.basename(manifestFile),
266
- [{subpath: sourcePath, target: sourcePath}],
267
+ [{subpath: extensionlessPath, target: sourcePath}],
267
268
  );
268
269
  if (pkgUpdate != null) {
269
270
  plans.push({
@@ -576,6 +577,7 @@ async function addTemplate(name, options) {
576
577
 
577
578
  const pascalName = kebabToPascal(name);
578
579
  const sourcePath = projectPath(path.relative(packageDir, sourceFile));
580
+ const extensionlessPath = sourcePath.replace(/\.tsx?$/u, '');
579
581
  const specContents = `export default {\n type: '${templateType}',\n name: '${name}',\n description: '${kebabToTitle(name)} template.',\n};\n`;
580
582
  const sourceContents = `export default function ${pascalName}() {\n return <div>${kebabToTitle(name)}</div>;\n}\n`;
581
583
 
@@ -588,7 +590,7 @@ async function addTemplate(name, options) {
588
590
  packageFile,
589
591
  rootPath,
590
592
  path.basename(manifestFile),
591
- [{subpath: sourcePath, target: sourcePath}],
593
+ [{subpath: extensionlessPath, target: sourcePath}],
592
594
  );
593
595
  if (pkgUpdate != null) {
594
596
  plans.push({
@@ -118,14 +118,14 @@ describe('integrationAdd component', () => {
118
118
  );
119
119
  expect(pkg.exports).toEqual({
120
120
  '.': './index.mjs',
121
- './components/MyWidget.tsx': './components/MyWidget.tsx',
121
+ './components/MyWidget': './components/MyWidget.tsx',
122
122
  });
123
123
  expect(
124
124
  fs.readFileSync(
125
125
  path.join(tmpDir, 'components/MyWidget.doc.mjs'),
126
126
  'utf-8',
127
127
  ),
128
- ).toContain('import: "@acme/integration/components/MyWidget.tsx"');
128
+ ).toContain('import: "@acme/integration/components/MyWidget"');
129
129
  });
130
130
 
131
131
  it('does not create exports when the package has no exports map', async () => {
@@ -143,7 +143,7 @@ describe('integrationAdd component', () => {
143
143
  setup({
144
144
  exports: {
145
145
  '.': './index.mjs',
146
- './components/MyWidget.tsx': './different.tsx',
146
+ './components/MyWidget': './different.tsx',
147
147
  },
148
148
  });
149
149
 
@@ -421,7 +421,7 @@ describe('integrationAdd template', () => {
421
421
  );
422
422
  expect(pkg.exports).toEqual({
423
423
  '.': './index.mjs',
424
- './templates/my-widget.tsx': './templates/my-widget.tsx',
424
+ './templates/my-widget': './templates/my-widget.tsx',
425
425
  });
426
426
  });
427
427
 
@@ -26,7 +26,7 @@ export type IntegrationAddThemeOptions = IntegrationAddBaseOptions;
26
26
  export type IntegrationAddOptions = {
27
27
  cwd?: string | undefined;
28
28
  dryRun?: boolean | undefined;
29
- templateType?: "block" | "page" | undefined;
29
+ templateType?: "page" | "block" | undefined;
30
30
  replaces?: string | undefined;
31
31
  extends?: string | undefined;
32
32
  to?: string | undefined;