@astryxdesign/cli 0.7.2-canary.e2909cf → 0.7.2-canary.e4bdd70

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 (194) 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/integration-export-name.test.mjs +644 -0
  7. package/api/docs/_adapter.d.mts +11 -0
  8. package/api/docs/_adapter.mjs +244 -1
  9. package/api/docs/integrationDocs.test.mjs +124 -0
  10. package/api/doctor/theme-checks.mjs +1 -1
  11. package/api/gap-report/gap-report.mjs +67 -4
  12. package/api/index.d.mts +2 -0
  13. package/api/index.mjs +2 -0
  14. package/api/init/run/run.mjs +2 -2
  15. package/api/integration/_adapter.d.mts +12 -0
  16. package/api/integration/_adapter.mjs +54 -0
  17. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  18. package/api/integration/add-contribution.mjs +8 -4
  19. package/api/integration/add-contribution.test.mjs +4 -2
  20. package/api/integration/authoring-checks.mjs +11 -2
  21. package/api/integration/authoring-checks.test.mjs +39 -0
  22. package/api/integration/authoring-checks.type.d.mts +9 -0
  23. package/api/integration/authoring-checks.type.mjs +14 -1
  24. package/api/integration/check/check.d.mts +96 -0
  25. package/api/integration/check/check.mjs +357 -0
  26. package/api/integration/check/check.test.mjs +489 -0
  27. package/api/integration/integration-check.type.d.mts +118 -0
  28. package/api/integration/integration-check.type.mjs +84 -0
  29. package/api/integration/integrationCheck.doc.d.mts +10 -0
  30. package/api/integration/integrationCheck.doc.mjs +73 -0
  31. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  32. package/api/integration/pack-check.mjs +51 -97
  33. package/api/integration/pack-check.test.mjs +359 -0
  34. package/api/integration/validate-integration.mjs +8 -0
  35. package/api/integration/validate-integration.test.mjs +11 -0
  36. package/api/integration/validate-integration.type.d.mts +8 -0
  37. package/api/integration/validate-integration.type.mjs +12 -1
  38. package/api/integration/validateIntegration.doc.mjs +17 -3
  39. package/api/search/search.d.mts +22 -6
  40. package/api/search/search.domain-priority.test.mjs +93 -18
  41. package/api/search/search.integration-check.test.mjs +24 -0
  42. package/api/search/search.mjs +133 -22
  43. package/api/swizzle/copy/copy.mjs +28 -14
  44. package/api/theme/build/build.family.test.mjs +54 -6
  45. package/api/theme/build/build.mjs +114 -17
  46. package/api/theme/themeBuild.doc.mjs +4 -4
  47. package/assets/docs/README.md +10 -0
  48. package/assets/docs/author-a-theme.doc.mjs +1 -1
  49. package/assets/docs/getting-started.doc.mjs +3 -3
  50. package/assets/docs/styling-overview.doc.mjs +1 -1
  51. package/assets/docs/theme.doc.dense.mjs +1 -1
  52. package/assets/docs/theme.doc.mjs +1 -1
  53. package/assets/docs/theme.doc.zh.mjs +1 -1
  54. package/assets/docs/tree/add-a-component.doc.mjs +11 -1
  55. package/assets/docs/tree/add-a-theme.doc.mjs +1 -1
  56. package/assets/docs/tree/add-a-topic.doc.mjs +10 -7
  57. package/assets/docs/tree/agent-guidance.doc.mjs +2 -2
  58. package/assets/docs/tree/block-template.doc.mjs +8 -20
  59. package/assets/docs/tree/build-the-template.doc.mjs +1 -1
  60. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  61. package/assets/docs/tree/check-your-docs.doc.mjs +51 -5
  62. package/assets/docs/tree/checks.doc.mjs +24 -16
  63. package/assets/docs/tree/codemods.doc.mjs +4 -4
  64. package/assets/docs/tree/component-family.doc.mjs +8 -6
  65. package/assets/docs/tree/component-imports.doc.mjs +10 -2
  66. package/assets/docs/tree/components.doc.mjs +1 -1
  67. package/assets/docs/tree/debug-and-gap-reports.doc.mjs +7 -18
  68. package/assets/docs/tree/define-the-theme.doc.mjs +6 -5
  69. package/assets/docs/tree/describe-the-component.doc.mjs +2 -2
  70. package/assets/docs/tree/docs.doc.mjs +3 -3
  71. package/assets/docs/tree/document-the-template.doc.mjs +1 -1
  72. package/assets/docs/tree/document-the-theme.doc.mjs +1 -1
  73. package/assets/docs/tree/export-template-assets.doc.mjs +1 -1
  74. package/assets/docs/tree/extend-or-replace.doc.mjs +6 -6
  75. package/assets/docs/tree/fonts-and-assets.doc.mjs +2 -2
  76. package/assets/docs/tree/generate-a-palette.doc.mjs +5 -3
  77. package/assets/docs/tree/grade-template-with-agent.doc.mjs +1 -1
  78. package/assets/docs/tree/integrations.doc.mjs +8 -0
  79. package/assets/docs/tree/links.doc.mjs +6 -5
  80. package/assets/docs/tree/package-and-test.doc.mjs +1 -1
  81. package/assets/docs/tree/page-template.doc.mjs +4 -8
  82. package/assets/docs/tree/publishing.doc.mjs +1 -1
  83. package/assets/docs/tree/quick-start.doc.mjs +37 -21
  84. package/assets/docs/tree/replace-a-core-component.doc.mjs +4 -6
  85. package/assets/docs/tree/replace-a-core-template.doc.mjs +6 -8
  86. package/assets/docs/tree/sections-and-placement.doc.mjs +4 -6
  87. package/assets/docs/tree/see-it-in-an-app.doc.mjs +4 -2
  88. package/assets/docs/tree/short-and-findable.doc.mjs +6 -5
  89. package/assets/docs/tree/single-component.doc.mjs +15 -6
  90. package/assets/docs/tree/start-a-template.doc.mjs +3 -3
  91. package/assets/docs/tree/subcomponent.doc.mjs +6 -6
  92. package/assets/docs/tree/template-assets.doc.mjs +2 -2
  93. package/assets/docs/tree/template-doc-overview.doc.mjs +4 -15
  94. package/assets/docs/tree/template-fonts.doc.mjs +1 -1
  95. package/assets/docs/tree/template-icons.doc.mjs +1 -1
  96. package/assets/docs/tree/template-images-media.doc.mjs +1 -1
  97. package/assets/docs/tree/template-styles.doc.mjs +2 -2
  98. package/assets/docs/tree/templates.doc.mjs +1 -1
  99. package/assets/docs/tree/test-in-an-app.doc.mjs +3 -3
  100. package/assets/docs/tree/test-template-in-app.doc.mjs +1 -1
  101. package/assets/docs/tree/tokens-and-setup.doc.mjs +19 -15
  102. package/assets/docs/tree/troubleshooting.doc.mjs +13 -5
  103. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  104. package/assets/docs/tree/upgrading.doc.mjs +3 -3
  105. package/assets/docs/tree/use-a-theme-in-an-app.doc.mjs +3 -1
  106. package/assets/docs/tree/verify-packed-template.doc.mjs +1 -1
  107. package/assets/docs/tree/versioning.doc.mjs +7 -6
  108. package/assets/docs/tree/write-the-template-file.doc.mjs +4 -3
  109. package/assets/docs/use-a-theme.doc.mjs +2 -2
  110. package/assets/theme.template.ts +1 -1
  111. package/authoring/codemod/codemod.doc.mjs +7 -3
  112. package/authoring/config/config.doc.mjs +9 -6
  113. package/authoring/config/type.ts +4 -2
  114. package/authoring/discover/discover.doc.mjs +1 -1
  115. package/authoring/doctypes/base/graph-fields.doc.mjs +6 -4
  116. package/authoring/doctypes/command/command.doc.mjs +14 -12
  117. package/authoring/doctypes/component/component.doc.mjs +29 -26
  118. package/authoring/doctypes/component/type.ts +13 -8
  119. package/authoring/doctypes/enum/enum.doc.mjs +7 -4
  120. package/authoring/doctypes/function/function.doc.mjs +9 -8
  121. package/authoring/doctypes/hook/hook.doc.mjs +5 -4
  122. package/authoring/doctypes/namespace/namespace.doc.mjs +7 -6
  123. package/authoring/doctypes/reference/reference.doc.mjs +11 -10
  124. package/authoring/doctypes/schema/schema.doc.mjs +9 -9
  125. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  126. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  127. package/authoring/doctypes/template/parse.d.mts +6 -2
  128. package/authoring/doctypes/template/parse.mjs +3 -1
  129. package/authoring/doctypes/template/template.doc.mjs +3 -3
  130. package/authoring/doctypes/template/type.ts +22 -0
  131. package/authoring/doctypes/theme/theme.doc.mjs +2 -2
  132. package/authoring/gap-report/gap-report.doc.mjs +2 -2
  133. package/authoring/identity/identity.doc.mjs +3 -3
  134. package/authoring/index.d.ts +1 -0
  135. package/authoring/integration/integration.doc.mjs +12 -4
  136. package/authoring/integration/parse.test.mjs +10 -0
  137. package/authoring/integration/schema.d.mts +2 -0
  138. package/authoring/integration/schema.mjs +1 -0
  139. package/authoring/integration/type.ts +4 -0
  140. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  141. package/clients/cli/commands/build-theme.mjs +1 -1
  142. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  143. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  144. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  145. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  146. package/clients/cli/commands/doctor.mjs +190 -0
  147. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  148. package/clients/cli/commands/integration-check.test.mjs +267 -0
  149. package/clients/cli/commands/integration.doc.mjs +6 -2
  150. package/clients/cli/commands/integration.mjs +107 -1
  151. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  152. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  153. package/clients/cli/index.mjs +1 -0
  154. package/foundation/config/project.mjs +82 -0
  155. package/foundation/discovery/authoring-self-docs.mjs +2 -1
  156. package/foundation/discovery/component-discovery.d.mts +178 -10
  157. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  158. package/foundation/discovery/component-discovery.mjs +659 -62
  159. package/foundation/discovery/component-replacement.mjs +18 -0
  160. package/foundation/discovery/component-replacement.test.mjs +40 -0
  161. package/foundation/discovery/doc-types.d.mts +157 -0
  162. package/foundation/discovery/doc-types.mjs +1224 -0
  163. package/foundation/discovery/doc-types.test.mjs +652 -0
  164. package/foundation/discovery/docs-discovery.d.mts +81 -0
  165. package/foundation/discovery/docs-discovery.mjs +57 -0
  166. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  167. package/foundation/discovery/integration-doc-files.mjs +231 -0
  168. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  169. package/foundation/discovery/module-exports.d.mts +27 -0
  170. package/foundation/discovery/module-exports.mjs +356 -31
  171. package/foundation/discovery/module-exports.test.mjs +172 -0
  172. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  173. package/foundation/discovery/package-tsconfig.mjs +228 -0
  174. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  175. package/foundation/discovery/project-typescript.d.mts +23 -0
  176. package/foundation/discovery/project-typescript.mjs +61 -0
  177. package/foundation/discovery/prop-drift.d.mts +278 -0
  178. package/foundation/discovery/prop-drift.mjs +2263 -0
  179. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  180. package/foundation/discovery/type-match.d.mts +53 -0
  181. package/foundation/discovery/type-match.mjs +660 -0
  182. package/foundation/discovery/type-match.test.mjs +169 -0
  183. package/foundation/discovery/type-text.d.mts +68 -0
  184. package/foundation/discovery/type-text.mjs +658 -0
  185. package/foundation/discovery/type-text.test.mjs +196 -0
  186. package/foundation/discovery/unresolved-references.d.mts +62 -0
  187. package/foundation/discovery/unresolved-references.mjs +790 -0
  188. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  189. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  190. package/foundation/doc-compiler/links.d.mts +9 -5
  191. package/foundation/doc-compiler/links.mjs +4 -2
  192. package/foundation/integrations/contribution-inventory.mjs +6 -1
  193. package/foundation/response/response-types.doc.mjs +7 -2
  194. package/package.json +9 -9
@@ -754,6 +754,285 @@ describe('integrationPackCheck', () => {
754
754
  );
755
755
  });
756
756
 
757
+ it('checks the export a component doc names when its file is named otherwise', async () => {
758
+ fs.writeFileSync(
759
+ path.join(tmpDir, 'package.json'),
760
+ `${JSON.stringify({
761
+ name: '@acme/widgets',
762
+ version: '1.0.0',
763
+ files: ['astryx.integration.mjs', 'components'],
764
+ exports: {'./components/Slides': './components/Slides.tsx'},
765
+ })}\n`,
766
+ );
767
+ fs.writeFileSync(
768
+ path.join(tmpDir, 'astryx.integration.mjs'),
769
+ "export default {components: './components'};\n",
770
+ );
771
+ fs.mkdirSync(path.join(tmpDir, 'components'));
772
+ // Slides.tsx exports AcmeCarousel and nothing named Slides.
773
+ fs.writeFileSync(
774
+ path.join(tmpDir, 'components', 'Slides.tsx'),
775
+ 'export function AcmeCarousel() { return null; }\n',
776
+ );
777
+ fs.writeFileSync(
778
+ path.join(tmpDir, 'components', 'Slides.doc.mjs'),
779
+ "export default {type: 'component', name: 'AcmeCarousel', import: '@acme/widgets/components/Slides', description: 'x', props: []};\n",
780
+ );
781
+
782
+ const result = await integrationPackCheck({cwd: tmpDir});
783
+ expect(result.data.packable).toBe(true);
784
+ expect(result.data.contributions.packed.components).toEqual([
785
+ 'AcmeCarousel',
786
+ ]);
787
+ });
788
+
789
+ it('checks each family member against the module its family doc imports', async () => {
790
+ fs.writeFileSync(
791
+ path.join(tmpDir, 'package.json'),
792
+ `${JSON.stringify({
793
+ name: '@acme/widgets',
794
+ version: '1.0.0',
795
+ files: ['astryx.integration.mjs', 'components'],
796
+ exports: {'./tabs': './components/tabs/index.ts'},
797
+ })}\n`,
798
+ );
799
+ fs.writeFileSync(
800
+ path.join(tmpDir, 'astryx.integration.mjs'),
801
+ "export default {components: './components'};\n",
802
+ );
803
+ const dir = path.join(tmpDir, 'components', 'tabs');
804
+ fs.mkdirSync(dir, {recursive: true});
805
+ fs.writeFileSync(
806
+ path.join(dir, 'AcmeTabs.tsx'),
807
+ 'export function AcmeTabs() { return null; }\nexport function AcmeTabsLegacy() { return null; }\n',
808
+ );
809
+ fs.writeFileSync(
810
+ path.join(dir, 'tab.tsx'),
811
+ 'export function AcmeTab() { return null; }\n',
812
+ );
813
+ // The module apps import exports AcmeTabs and AcmeTab, not AcmeTabsLegacy.
814
+ fs.writeFileSync(
815
+ path.join(dir, 'index.ts'),
816
+ "export {AcmeTabs} from './AcmeTabs';\nexport {AcmeTab} from './tab';\n",
817
+ );
818
+ fs.writeFileSync(
819
+ path.join(dir, 'AcmeTabs.doc.mjs'),
820
+ "export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabs', description: 'x', props: []}, {name: 'AcmeTab', description: 'x', props: []}, {name: 'AcmeTabsLegacy', description: 'x', props: []}]};\n",
821
+ );
822
+
823
+ const result = await integrationPackCheck({cwd: tmpDir});
824
+
825
+ expect(result.data.issues).toEqual([]);
826
+ expect(result.data.packable).toBe(true);
827
+ expect(result.data.contributions.packed.components).toEqual([
828
+ 'AcmeTab',
829
+ 'AcmeTabs',
830
+ ]);
831
+ });
832
+
833
+ it.each([
834
+ 'a clean checkout',
835
+ 'a stale build',
836
+ 'a stale build that exports more',
837
+ ])(
838
+ 'reads members a family imports from output its pack lifecycle builds, from %s',
839
+ async start => {
840
+ fs.writeFileSync(
841
+ path.join(tmpDir, 'package.json'),
842
+ `${JSON.stringify({
843
+ name: '@acme/widgets',
844
+ version: '1.0.0',
845
+ files: ['astryx.integration.mjs', 'components', 'dist'],
846
+ exports: {'./tabs': './dist/tabs/index.mjs'},
847
+ scripts: {prepack: 'node build.mjs'},
848
+ })}\n`,
849
+ );
850
+ fs.writeFileSync(
851
+ path.join(tmpDir, 'astryx.integration.mjs'),
852
+ "export default {components: './components'};\n",
853
+ );
854
+ // The build writes the module the family imports; a clean checkout has none.
855
+ fs.writeFileSync(
856
+ path.join(tmpDir, 'build.mjs'),
857
+ "import fs from 'node:fs';\nfs.mkdirSync('dist/tabs', {recursive: true});\nfs.writeFileSync('dist/tabs/index.mjs', \"export {AcmeTabs, AcmeTab} from '../../components/tabs/AcmeTabs.tsx';\\n\");\n",
858
+ );
859
+ const dir = path.join(tmpDir, 'components', 'tabs');
860
+ fs.mkdirSync(dir, {recursive: true});
861
+ fs.writeFileSync(
862
+ path.join(dir, 'AcmeTabs.tsx'),
863
+ 'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\nexport function AcmeTabLegacy() { return null; }\n',
864
+ );
865
+ fs.writeFileSync(
866
+ path.join(dir, 'AcmeTabs.doc.mjs'),
867
+ "export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabs', description: 'x', props: []}, {name: 'AcmeTab', description: 'x', props: []}, {name: 'AcmeTabLegacy', description: 'x', props: []}]};\n",
868
+ );
869
+
870
+ if (start !== 'a clean checkout') {
871
+ // An earlier build: before AcmeTab was exported, or while AcmeTabLegacy
872
+ // still was.
873
+ const names =
874
+ start === 'a stale build'
875
+ ? 'AcmeTabs'
876
+ : 'AcmeTabs, AcmeTab, AcmeTabLegacy';
877
+ fs.mkdirSync(path.join(tmpDir, 'dist', 'tabs'), {recursive: true});
878
+ fs.writeFileSync(
879
+ path.join(tmpDir, 'dist', 'tabs', 'index.mjs'),
880
+ `export {${names}} from '../../components/tabs/AcmeTabs.tsx';\n`,
881
+ );
882
+ }
883
+
884
+ const result = await integrationPackCheck({cwd: tmpDir});
885
+
886
+ expect(result.data.issues).toEqual([]);
887
+ expect(result.data.packable).toBe(true);
888
+ expect(result.data.contributions.local.components).toEqual(
889
+ result.data.contributions.packed.components,
890
+ );
891
+ },
892
+ );
893
+
894
+ it.each(['a clean checkout', 'an incremental build'])(
895
+ 'reads members built output re-exports from compiled files, from %s',
896
+ async start => {
897
+ fs.writeFileSync(
898
+ path.join(tmpDir, 'package.json'),
899
+ `${JSON.stringify({
900
+ name: '@acme/widgets',
901
+ version: '1.0.0',
902
+ files: ['astryx.integration.mjs', 'components', 'dist'],
903
+ exports: {'./tabs': './dist/tabs/index.mjs'},
904
+ scripts: {prepack: 'node build.mjs'},
905
+ })}\n`,
906
+ );
907
+ fs.writeFileSync(
908
+ path.join(tmpDir, 'astryx.integration.mjs'),
909
+ "export default {components: './components'};\n",
910
+ );
911
+ // The build compiles each source to its own file and, like an
912
+ // incremental build, rewrites only the files whose output changed.
913
+ const output = {
914
+ 'index.mjs':
915
+ "export {AcmeTabs} from './tabs.mjs';\nexport * from './tab.mjs';\n",
916
+ 'tabs.mjs': 'export function AcmeTabs() { return null; }\n',
917
+ 'tab.mjs': 'export function AcmeTab() { return null; }\n',
918
+ };
919
+ fs.writeFileSync(
920
+ path.join(tmpDir, 'build.mjs'),
921
+ `import fs from 'node:fs';\nconst output = ${JSON.stringify(output)};\nfs.mkdirSync('dist/tabs', {recursive: true});\nfor (const [file, text] of Object.entries(output)) {\n const target = 'dist/tabs/' + file;\n if (!fs.existsSync(target) || fs.readFileSync(target, 'utf-8') !== text) fs.writeFileSync(target, text);\n}\n`,
922
+ );
923
+ const dir = path.join(tmpDir, 'components', 'tabs');
924
+ fs.mkdirSync(dir, {recursive: true});
925
+ fs.writeFileSync(
926
+ path.join(dir, 'AcmeTabs.tsx'),
927
+ 'export function AcmeTabs() { return null; }\n',
928
+ );
929
+ fs.writeFileSync(
930
+ path.join(dir, 'AcmeTab.tsx'),
931
+ 'export function AcmeTab() { return null; }\n',
932
+ );
933
+ fs.writeFileSync(
934
+ path.join(dir, 'AcmeTabs.doc.mjs'),
935
+ "export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabs', description: 'x', props: []}, {name: 'AcmeTab', description: 'x', props: []}]};\n",
936
+ );
937
+ if (start === 'an incremental build') {
938
+ // An earlier build from before AcmeTab existed: the file the import
939
+ // names is already current, so the build leaves it untouched.
940
+ fs.mkdirSync(path.join(tmpDir, 'dist', 'tabs'), {recursive: true});
941
+ for (const [file, text] of Object.entries({...output, 'tab.mjs': ''})) {
942
+ fs.writeFileSync(path.join(tmpDir, 'dist', 'tabs', file), text);
943
+ }
944
+ }
945
+
946
+ const result = await integrationPackCheck({cwd: tmpDir});
947
+
948
+ expect(result.data.issues).toEqual([]);
949
+ expect(result.data.packable).toBe(true);
950
+ expect(result.data.contributions.packed.components).toEqual([
951
+ 'AcmeTab',
952
+ 'AcmeTabs',
953
+ ]);
954
+ expect(result.data.contributions.local.components).toEqual(
955
+ result.data.contributions.packed.components,
956
+ );
957
+ },
958
+ );
959
+
960
+ it('passes a member another folder declares, which has no source', async () => {
961
+ fs.writeFileSync(
962
+ path.join(tmpDir, 'package.json'),
963
+ `${JSON.stringify({
964
+ name: '@acme/widgets',
965
+ version: '1.0.0',
966
+ files: ['astryx.integration.mjs', 'components'],
967
+ exports: {'./menu': './components/menu/index.ts'},
968
+ })}\n`,
969
+ );
970
+ fs.writeFileSync(
971
+ path.join(tmpDir, 'astryx.integration.mjs'),
972
+ "export default {components: './components'};\n",
973
+ );
974
+ const menu = path.join(tmpDir, 'components', 'menu');
975
+ const shared = path.join(tmpDir, 'components', 'shared');
976
+ fs.mkdirSync(menu, {recursive: true});
977
+ fs.mkdirSync(shared, {recursive: true});
978
+ fs.writeFileSync(
979
+ path.join(menu, 'AcmeMenu.doc.mjs'),
980
+ "export default {type: 'component', name: 'AcmeMenu', import: '@acme/widgets/menu', description: 'x', components: [{name: 'AcmeMenuItem', description: 'x', props: []}]};\n",
981
+ );
982
+ fs.writeFileSync(
983
+ path.join(menu, 'AcmeMenu.tsx'),
984
+ 'export function AcmeMenu() { return null; }\n',
985
+ );
986
+ fs.writeFileSync(
987
+ path.join(menu, 'index.ts'),
988
+ "export {AcmeMenu} from './AcmeMenu';\nexport {AcmeMenuItem} from '../shared/AcmeMenuItem';\n",
989
+ );
990
+ fs.writeFileSync(
991
+ path.join(shared, 'AcmeMenuItem.tsx'),
992
+ 'export function AcmeMenuItem() { return null; }\n',
993
+ );
994
+
995
+ const result = await integrationPackCheck({cwd: tmpDir});
996
+
997
+ expect(result.data.issues).toEqual([]);
998
+ expect(result.data.packable).toBe(true);
999
+ expect(result.data.contributions.packed.components).toEqual([
1000
+ 'AcmeMenu',
1001
+ 'AcmeMenuItem',
1002
+ ]);
1003
+ });
1004
+
1005
+ it('reports a family doc the loader rejects once, not once per member', async () => {
1006
+ fs.writeFileSync(
1007
+ path.join(tmpDir, 'package.json'),
1008
+ `${JSON.stringify({
1009
+ name: '@acme/widgets',
1010
+ version: '1.0.0',
1011
+ files: ['astryx.integration.mjs', 'components'],
1012
+ })}\n`,
1013
+ );
1014
+ fs.writeFileSync(
1015
+ path.join(tmpDir, 'astryx.integration.mjs'),
1016
+ "export default {components: './components'};\n",
1017
+ );
1018
+ fs.mkdirSync(path.join(tmpDir, 'components'));
1019
+ fs.writeFileSync(
1020
+ path.join(tmpDir, 'components', 'AcmeTabs.tsx'),
1021
+ 'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\nexport function AcmeTabPanel() { return null; }\n',
1022
+ );
1023
+ // A displayName that is not a string makes the loader reject the doc.
1024
+ fs.writeFileSync(
1025
+ path.join(tmpDir, 'components', 'AcmeTabs.doc.mjs'),
1026
+ "export default {type: 'component', name: 'AcmeTabs', displayName: 1, description: 'x', components: [{name: 'AcmeTab', description: 'x', props: []}, {name: 'AcmeTabPanel', description: 'x', props: []}]};\n",
1027
+ );
1028
+
1029
+ const result = await integrationPackCheck({cwd: tmpDir});
1030
+ const codes = result.data.issues.map(issue => issue.code);
1031
+
1032
+ expect(codes).toContain('component_doc_unloadable');
1033
+ expect(codes.length).toBe(new Set(codes).size);
1034
+ });
1035
+
757
1036
  it('passes a generated component through its exact packed export', async () => {
758
1037
  fs.writeFileSync(
759
1038
  path.join(tmpDir, 'package.json'),
@@ -811,6 +1090,86 @@ describe('integrationPackCheck', () => {
811
1090
  );
812
1091
  });
813
1092
 
1093
+ it('does not count a type-only re-export as the component', async () => {
1094
+ fs.writeFileSync(
1095
+ path.join(tmpDir, 'package.json'),
1096
+ `${JSON.stringify({
1097
+ name: '@acme/widgets',
1098
+ version: '1.0.0',
1099
+ files: ['astryx.integration.mjs', 'src'],
1100
+ exports: {'.': './src/index.ts'},
1101
+ })}\n`,
1102
+ );
1103
+ fs.writeFileSync(
1104
+ path.join(tmpDir, 'astryx.integration.mjs'),
1105
+ "export default {components: './src'};\n",
1106
+ );
1107
+ fs.mkdirSync(path.join(tmpDir, 'src'));
1108
+ fs.writeFileSync(
1109
+ path.join(tmpDir, 'src', 'index.ts'),
1110
+ "export type {AcmeWidget} from './AcmeWidget';\n",
1111
+ );
1112
+ fs.writeFileSync(
1113
+ path.join(tmpDir, 'src', 'AcmeWidget.tsx'),
1114
+ 'export function AcmeWidget() { return null; }\n',
1115
+ );
1116
+ fs.writeFileSync(
1117
+ path.join(tmpDir, 'src', 'AcmeWidget.doc.mjs'),
1118
+ "export default {type: 'component', name: 'AcmeWidget', import: '@acme/widgets', description: 'Widget.', props: []};\n",
1119
+ );
1120
+
1121
+ const result = await integrationPackCheck({cwd: tmpDir});
1122
+
1123
+ expect(result.data.issues).toContainEqual(
1124
+ expect.objectContaining({code: 'component_export_missing'}),
1125
+ );
1126
+ });
1127
+
1128
+ it('follows a re-export of a folder to its index file', async () => {
1129
+ // `./primitives` names a folder: TypeScript and bundlers load its index file.
1130
+ fs.writeFileSync(
1131
+ path.join(tmpDir, 'package.json'),
1132
+ `${JSON.stringify({
1133
+ name: '@acme/widgets',
1134
+ version: '1.0.0',
1135
+ files: ['astryx.integration.mjs', 'src'],
1136
+ exports: {'.': './src/index.ts'},
1137
+ })}\n`,
1138
+ );
1139
+ fs.writeFileSync(
1140
+ path.join(tmpDir, 'astryx.integration.mjs'),
1141
+ "export default {components: './src'};\n",
1142
+ );
1143
+ const primitives = path.join(tmpDir, 'src', 'primitives');
1144
+ fs.mkdirSync(primitives, {recursive: true});
1145
+ fs.writeFileSync(
1146
+ path.join(tmpDir, 'src', 'index.ts'),
1147
+ "export * from './primitives';\n",
1148
+ );
1149
+ fs.writeFileSync(
1150
+ path.join(primitives, 'index.ts'),
1151
+ "export * from './AcmeWidget';\n",
1152
+ );
1153
+ fs.writeFileSync(
1154
+ path.join(primitives, 'AcmeWidget.tsx'),
1155
+ 'export function AcmeWidget() { return null; }\n',
1156
+ );
1157
+ fs.writeFileSync(
1158
+ path.join(primitives, 'AcmeWidget.doc.mjs'),
1159
+ "export default {type: 'component', name: 'AcmeWidget', import: '@acme/widgets', description: 'Widget.', props: []};\n",
1160
+ );
1161
+
1162
+ const result = await integrationPackCheck({cwd: tmpDir});
1163
+
1164
+ expect(result.data.issues).not.toContainEqual(
1165
+ expect.objectContaining({code: 'component_export_missing'}),
1166
+ );
1167
+ expect(
1168
+ result.data.packable,
1169
+ JSON.stringify(result.data.issues, null, 2),
1170
+ ).toBe(true);
1171
+ });
1172
+
814
1173
  it('fails when packed template source is hidden by package exports', async () => {
815
1174
  writePackage({
816
1175
  manifest: "export default {templates: './templates'};\n",
@@ -34,6 +34,7 @@
34
34
  import * as fs from 'node:fs';
35
35
  import * as path from 'node:path';
36
36
  import {assertWithin} from '../../foundation/fs/path-safety.mjs';
37
+ import {integrationDocTypes} from '../docs/_adapter.mjs';
37
38
  import {
38
39
  THEME_SLUG_RE,
39
40
  unreadThemeFolders,
@@ -461,6 +462,12 @@ export async function validateIntegration(pkg, options = {}) {
461
462
  const result = pkg
462
463
  ? await validateInstalledIntegration(pkg, cwd)
463
464
  : await validateLocalIntegration(cwd);
465
+ // Each doc the package ships, against the published authoring type for its
466
+ // kind, read with the project's own compiler. Findings are warnings only.
467
+ const integration = result.found ? result.integration : undefined;
468
+ const docTypes = integration
469
+ ? await integrationDocTypes(integration, cwd)
470
+ : undefined;
464
471
  return {
465
472
  type: 'integration.validate',
466
473
  data: {
@@ -472,6 +479,7 @@ export async function validateIntegration(pkg, options = {}) {
472
479
  issues: result.reason === 'no_package_json'
473
480
  ? [{code: 'no_package', severity: 'error', message: 'No package.json found. Run this command inside a package directory.'}]
474
481
  : result.issues,
482
+ ...(docTypes ? {docTypes} : {}),
475
483
  },
476
484
  };
477
485
  }
@@ -176,6 +176,17 @@ describe('validate-integration API', () => {
176
176
  expect(agentDocsIssues[0].severity).toBe('error');
177
177
  });
178
178
 
179
+ it('reports nothing for a docsiteUrl', async () => {
180
+ const pkgDir = path.join(tmpDir, 'pkg');
181
+ writePackage(pkgDir, {
182
+ manifest: `export default {docsiteUrl: 'https://widgets.example.com'};\n`,
183
+ });
184
+
185
+ const result = await validateLocalIntegration(pkgDir);
186
+
187
+ expect(result.issues).toEqual([]);
188
+ });
189
+
179
190
  it('flags a broken codemod as invalid_codemod error', async () => {
180
191
  const pkgDir = path.join(tmpDir, 'pkg');
181
192
  writePackage(pkgDir, {
@@ -39,5 +39,13 @@ export type ValidateIntegrationResponse = {
39
39
  name: string | null;
40
40
  version: string | null;
41
41
  issues: import("../../foundation/integrations/issue").AstryxIntegrationIssue[];
42
+ docTypes?: DocTypesReport;
42
43
  };
43
44
  };
45
+ /**
46
+ * `fixtures`: how many test fixture docs (under `__tests__` in the
47
+ * components or docs root, which those readers skip) were not checked
48
+ */
49
+ export type DocTypesReport = import("../../foundation/discovery/doc-types.mjs").DocTypesReport & {
50
+ fixtures?: number;
51
+ };
@@ -38,7 +38,18 @@
38
38
  * "healthy" — the two are otherwise byte-identical.
39
39
  * @typedef {object} ValidateIntegrationResponse
40
40
  * @property {'integration.validate'} type
41
- * @property {{validated: boolean, name: string | null, version: string | null, issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[]}} data
41
+ * @property {{validated: boolean, name: string | null, version: string | null, issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[], docTypes?: DocTypesReport}} data
42
+ */
43
+
44
+ /**
45
+ * Each doc the package ships, checked against the published authoring type
46
+ * for its kind. Present when the manifest loads. `status: 'skipped'`
47
+ * means nothing was checked (no TypeScript in the project) and `reason` says
48
+ * why. Every finding is a warning.
49
+ *
50
+ * @typedef {import('../../foundation/discovery/doc-types.mjs').DocTypesReport & {fixtures?: number}} DocTypesReport
51
+ * `fixtures`: how many test fixture docs (under `__tests__` in the
52
+ * components or docs root, which those readers skip) were not checked
42
53
  */
43
54
 
44
55
  export {};
@@ -23,11 +23,25 @@ export const doc = {
23
23
  'declared contribution root exists, and runs the codemod/template/component/' +
24
24
  'docs/theme validators, returning every finding as an AstryxIntegrationIssue. A missing ' +
25
25
  'local manifest is guidance (name null, no issues), not an error, so callers ' +
26
- 'can stay exit-0 in a non-integration directory.',
26
+ 'can stay exit-0 in a non-integration directory. It also checks every doc the ' +
27
+ 'package ships under its roots (component, template, docs topic, theme, and translation, served or not) against ' +
28
+ "the published authoring type for its kind, as `tsc` would, with the project's " +
29
+ 'own `typescript`: an unknown field, a missing required field, or a value outside ' +
30
+ "a field's type is a warning in `docTypes` naming the field and a fix. A test " +
31
+ 'fixture under `__tests__` in the components or docs root is counted, not checked. ' +
32
+ 'Without TypeScript, `docTypes` is `skipped` with a reason.',
27
33
  importPath: '@astryxdesign/cli/api',
28
34
  signature:
29
35
  'validateIntegration(pkg?: string, options?: ValidateIntegrationOptions): Promise<ValidateIntegrationResponse>',
30
- keywords: ['integration', 'validate', 'manifest', 'lint', 'check'],
36
+ keywords: [
37
+ 'integration',
38
+ 'validate',
39
+ 'manifest',
40
+ 'lint',
41
+ 'check',
42
+ 'types',
43
+ 'typecheck',
44
+ ],
31
45
  params: [
32
46
  {
33
47
  name: 'pkg',
@@ -46,7 +60,7 @@ export const doc = {
46
60
  {
47
61
  type: 'integration.validate',
48
62
  description:
49
- 'The result envelope: `data.validated`, false when nothing was validated — with no `package.json` at all, `data.issues` carries one `no_package` error-severity issue so the command exits 1; with a `package.json` but no integration manifest, `data.issues` is empty, which means "not looked at", not "healthy"; `data.name` and `data.version` of the validated package (both null when `data.validated` is false; `data.name` is `(local package)` when the local package.json has no readable name), plus `data.issues`, an AstryxIntegrationIssue[] of {code, severity: `warning` | `error`, message}.',
63
+ 'The result envelope: `data.validated`, false when nothing was validated — with no `package.json` at all, `data.issues` carries one `no_package` error-severity issue so the command exits 1; with a `package.json` but no integration manifest, `data.issues` is empty, which means "not looked at", not "healthy"; `data.name` and `data.version` of the validated package (both null when `data.validated` is false; `data.name` is `(local package)` when the local package.json has no readable name), plus `data.issues`, an AstryxIntegrationIssue[] of {code, severity: `warning` | `error`, message}. When the manifest loads, `data.docTypes` holds the doc type check: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `docs` (doc values checked), `findings` ({doc, line, export, type, field, problem: `unknown_field` | `missing_field` | `wrong_type`, severity: `warning`, message, fix}), `unchecked` ({doc, reason}: a doc that could not be fully checked, or a root its reader cannot read), and `fixtures` when the components or docs root holds test fixture docs under `__tests__`: how many, none of them checked.',
50
64
  },
51
65
  ],
52
66
  examples: [
@@ -40,19 +40,20 @@ export function tokenizeQuery(term: string): string[];
40
40
  * typography/font-setup, `side panel` is "Side panels");
41
41
  * - 1: a word of the query is the topic's name (`illustration` in a longer
42
42
  * question is the illustrations guide);
43
+ * - 1: a query of two or more words is one of the topic's own declared
44
+ * keywords, whatever the plural (`code review` on a guide that declares it);
43
45
  * - 1: the query holds one of the doc's titles whole (`resizable side panels`
44
46
  * names "Side panels"; `light dark mode button` names "Light/Dark Mode");
45
47
  * - 1: the doc matches every word of the query and one of them is a word of
46
48
  * its title (`switch to a dark theme` and "Use a theme").
47
49
  * A section answers to its topic's name.
48
50
  *
49
- * A doc that matched only by keyword, by a heading holding the query, or by
51
+ * A doc that matched only by a heading holding the query, by one word, or by
50
52
  * words spread through its text has no priority. Docs are split into many
51
- * small topics, and each declares its own keywords and headings, so a common
52
- * phrase such as `font size` hits a guide's keyword or heading exactly
53
- * (170-190) while the component the reader is after matches one word by
54
- * keyword (`Text`, 98). Ranked on text alone, every split adds another doc
55
- * above the component. Broad reference pages match every word of many queries
53
+ * small topics, each with its own headings, so a common phrase such as
54
+ * `font size` hits a heading exactly (170) while the component the reader is
55
+ * after matches one word by keyword (`Text`, 98). Ranked on text alone, every
56
+ * split adds another doc above the component. Broad reference pages match every word of many queries
56
57
  * in their text the same way. The score stays the text-match strength the
57
58
  * result reports, so callers that gate on it (`build`) see the same numbers.
58
59
  *
@@ -213,6 +214,12 @@ export type Candidate = {
213
214
  */
214
215
  aliases?: string[] | undefined;
215
216
  keywords?: string[] | undefined;
217
+ /**
218
+ * - A doc's own declared keywords: what its
219
+ * author lists in `keywords`, without the code terms and table cells its
220
+ * other keywords add.
221
+ */
222
+ _declared?: string[] | undefined;
216
223
  weakKeywords?: string[] | undefined;
217
224
  description?: string | undefined;
218
225
  prose?: string[] | undefined;
@@ -252,4 +259,13 @@ export type Candidate = {
252
259
  _kind?: "page" | "block" | undefined;
253
260
  _resultName?: string | undefined;
254
261
  _commandName?: string | undefined;
262
+ /**
263
+ * - How an integration component holds its
264
+ * name, for the owner rule.
265
+ */
266
+ _hold?: "file" | "doc" | undefined;
267
+ /**
268
+ * - An integration component's doc.
269
+ */
270
+ _docPath?: string | undefined;
255
271
  };