@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
@@ -75,14 +75,75 @@ async function placedGuides(route, seen = new Set()) {
75
75
  }
76
76
 
77
77
  describe('search domain priority — the rule', () => {
78
- it('gives a doc no priority for a keyword or heading match', () => {
78
+ it('gives a doc no priority for a heading match or a one-word keyword', () => {
79
79
  const topic = {
80
80
  domain: 'doc',
81
81
  name: 'typography/font-setup',
82
- keywords: ['font size'],
82
+ keywords: ['size'],
83
83
  titles: ['Font Setup', 'Font Sizes'],
84
84
  };
85
85
  expect(priority('font size', topic)).toBe(0);
86
+ expect(priority('size', topic)).toBe(0);
87
+ });
88
+
89
+ it('gives a doc priority when the query is one of its declared keywords', () => {
90
+ const guide = {
91
+ domain: 'doc',
92
+ name: 'guides/quality',
93
+ titles: ['Is my code good?'],
94
+ keywords: ['code review', 'review my code'],
95
+ _declared: ['code review', 'review my code'],
96
+ };
97
+ expect(priority('code review', guide)).toBe(1);
98
+ expect(priority('Code Reviews', guide)).toBe(1);
99
+ expect(priority('review my code', guide)).toBe(1);
100
+ // A section's keywords are its headings and code terms.
101
+ const section = {
102
+ ...guide,
103
+ name: 'before-you-ship',
104
+ _topic: 'guides/quality',
105
+ _section: 'before-you-ship',
106
+ };
107
+ expect(priority('code review', section)).toBe(0);
108
+ });
109
+
110
+ it("gives a doc no priority for a code term or table cell it didn't declare", () => {
111
+ // A typed doc's keywords hold the first column of its tables and the code
112
+ // terms its text mentions; `missing_field` reads as two words.
113
+ const reference = {
114
+ domain: 'doc',
115
+ name: 'cli/api/functions/validate-integration',
116
+ titles: ['validateIntegration'],
117
+ keywords: ['validate-integration', 'missing_field', 'file required'],
118
+ _declared: [],
119
+ _topic: 'cli/api/functions/validate-integration',
120
+ };
121
+ expect(priority('missing field', reference)).toBe(0);
122
+ expect(priority('file required', reference)).toBe(0);
123
+ expect(
124
+ priority('missing field', {...reference, _declared: ['missing field']}),
125
+ ).toBe(1);
126
+ });
127
+
128
+ it('keeps a component the query names exactly above a doc that declares it', async () => {
129
+ const tokens = tokenizeQuery('empty state');
130
+ const component = {domain: 'component', name: 'EmptyState'};
131
+ const doc = {
132
+ domain: 'doc',
133
+ name: 'illustrations',
134
+ keywords: ['empty state'],
135
+ _declared: ['empty state'],
136
+ };
137
+ const c = scoreQuery('empty state', tokens, component);
138
+ const d = scoreQuery('empty state', tokens, doc);
139
+ expect(
140
+ domainPriority('empty state', tokens, /** @type {any} */ (component), c),
141
+ ).toBe(1);
142
+ expect(
143
+ domainPriority('empty state', tokens, /** @type {any} */ (doc), d),
144
+ ).toBe(1);
145
+ // Both have priority, so the stronger match wins: the exact name.
146
+ expect(c?.score).toBeGreaterThan(d?.score ?? 0);
86
147
  });
87
148
 
88
149
  it('gives a doc priority when the query names it', () => {
@@ -190,32 +251,61 @@ describe('search domain priority — the rule', () => {
190
251
 
191
252
  describe('search domain priority — real docs and components', () => {
192
253
  it(
193
- 'finds the Text component before the typography guides for `font size`',
254
+ 'finds a guide that declares the whole phrase above a component that matches one word',
194
255
  async () => {
195
- for (const q of ['font size', 'font weight']) {
196
- const results = (await search(q, {cwd})).data.results;
197
- expect(results[0], q).toMatchObject({
198
- domain: 'component',
199
- name: 'Text',
200
- });
201
- const guide = results.findIndex(
202
- r => r.domain === 'doc' && /^typography(?:\/|$)/.test(r.name),
203
- );
204
- // The guide still matches, below the component.
205
- expect(guide, q).toBeGreaterThan(0);
256
+ const results = (await search('font size', {cwd})).data.results;
257
+ expect(results[0]).toMatchObject({domain: 'doc'});
258
+ expect(results[0].name).toMatch(/^typography(?:\/|$)/);
259
+ // The component the phrase points to stays near the top.
260
+ expect(
261
+ results
262
+ .slice(0, 3)
263
+ .some(r => r.domain === 'component' && r.name === 'Text'),
264
+ ).toBe(true);
265
+ },
266
+ SLOW,
267
+ );
268
+
269
+ it(
270
+ 'keeps a component above a reference doc whose table or code terms hold the phrase',
271
+ async () => {
272
+ for (const q of ['missing field', 'file required']) {
273
+ const [top] = (await search(q, {cwd})).data.results;
274
+ expect(top, q).toMatchObject({domain: 'component', name: 'Field'});
206
275
  }
207
276
  },
208
277
  SLOW,
209
278
  );
210
279
 
211
280
  it(
212
- 'reports the text-match score unchanged',
281
+ 'opens the guide that answers a declared phrase, not one that only mentions it',
282
+ async () => {
283
+ const [layers] = (await search('cascade layers', {cwd})).data.results;
284
+ expect(layers).toMatchObject({
285
+ domain: 'doc',
286
+ name: 'migration/migration-setup',
287
+ });
288
+ const [keyboard] = (await search('keyboard navigation', {cwd})).data
289
+ .results;
290
+ expect(keyboard?.name).not.toBe('migration/migration-verification');
291
+ },
292
+ SLOW,
293
+ );
294
+
295
+ it(
296
+ 'keeps a component above docs that match only by a heading, with scores unchanged',
213
297
  async () => {
214
298
  const results = (await search('font size', {cwd})).data.results;
215
- const guide = results.find(
216
- r => r.domain === 'doc' && /^typography(?:\/|$)/.test(r.name),
299
+ const text = results.findIndex(
300
+ r => r.domain === 'component' && r.name === 'Text',
217
301
  );
218
- expect(guide?.score).toBeGreaterThan(results[0].score);
302
+ expect(text).toBeGreaterThanOrEqual(0);
303
+ // A doc below Text outscores it on text alone: priority moved it, not the score.
304
+ expect(
305
+ results
306
+ .slice(text + 1)
307
+ .some(r => r.domain === 'doc' && r.score > results[text].score),
308
+ ).toBe(true);
219
309
  },
220
310
  SLOW,
221
311
  );
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file A reader looking for the check a build runs finds
5
+ * `astryx integration check` first.
6
+ */
7
+
8
+ import {describe, expect, it} from 'vitest';
9
+ import {search} from './search.mjs';
10
+
11
+ describe('search finds integration check for CI', () => {
12
+ it.each([
13
+ 'ci',
14
+ 'continuous integration',
15
+ 'typecheck integration',
16
+ 'integration check',
17
+ ])('`%s` ranks it first', async query => {
18
+ const {results} = (await search(query)).data;
19
+ expect(results[0], JSON.stringify(results.slice(0, 3))).toMatchObject({
20
+ domain: 'doc',
21
+ name: 'cli/commands/integration-check',
22
+ });
23
+ });
24
+ });
@@ -57,9 +57,10 @@
57
57
  * Between domains, ranking adds one rule ahead of the score: domain priority
58
58
  * (see {@link domainPriority}). A component, hook, template, or theme whose
59
59
  * name or keyword a query word hits ranks ahead of every doc the reader did not
60
- * ask for by name or title, so `font size` finds `Text` before a typography
61
- * topic that declares the phrase. A doc the query names, or whose title the
62
- * query holds, keeps its place.
60
+ * ask for by name, title, or a declared phrase, so `font size` finds `Text`
61
+ * before a token page whose heading holds the phrase. A doc the query names,
62
+ * whose title the query holds, or that declares the whole query as a keyword
63
+ * keeps its place.
63
64
  *
64
65
  * Description and guidance are separate tiers on purpose. A component's own
65
66
  * one-line description saying "notification" is a claim about what it IS; the
@@ -81,11 +82,20 @@
81
82
 
82
83
  import {withListingCache} from '../../foundation/fs/listing-cache.mjs';
83
84
  import {readDocView} from '../../foundation/doc-compiler/read.mjs';
84
- import {findCoreDir} from '../../foundation/fs/paths.mjs';
85
+ import {
86
+ discoverExternalPackages,
87
+ findCoreDir,
88
+ } from '../../foundation/fs/paths.mjs';
85
89
  import {
86
90
  discoverComponents,
87
91
  discoverValidIntegrationComponents,
92
+ docStem,
93
+ familyMember,
88
94
  findComponentReadme,
95
+ forEachIntegrationDoc,
96
+ integrationMemberEntry,
97
+ integrationRecordHold,
98
+ isIntegrationMember,
89
99
  resolveImportPath,
90
100
  resolveIntegrationImportPath,
91
101
  CORE_PACKAGE,
@@ -97,6 +107,7 @@ import {
97
107
  import {
98
108
  loadComponentReplacements,
99
109
  loadIntegrationsSafely,
110
+ resolveLegacyExternalDoc,
100
111
  } from '../component/_adapter.mjs';
101
112
  import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
102
113
  import {discoverTemplates, extractComponents} from '../template/template.mjs';
@@ -128,6 +139,9 @@ import {setResultCoverage} from './coverage.mjs';
128
139
  * @property {string[]} [aliases] - Other names the candidate answers to, scored
129
140
  * with the same name signals: a theme's display name.
130
141
  * @property {string[]} [keywords]
142
+ * @property {string[]} [_declared] - A doc's own declared keywords: what its
143
+ * author lists in `keywords`, without the code terms and table cells its
144
+ * other keywords add.
131
145
  * @property {string[]} [weakKeywords]
132
146
  * @property {string} [description]
133
147
  * @property {string[]} [prose]
@@ -149,6 +163,9 @@ import {setResultCoverage} from './coverage.mjs';
149
163
  * @property {'page'|'block'} [_kind]
150
164
  * @property {string} [_resultName]
151
165
  * @property {string} [_commandName]
166
+ * @property {'file'|'doc'} [_hold] - How an integration component holds its
167
+ * name, for the owner rule.
168
+ * @property {string} [_docPath] - An integration component's doc.
152
169
  */
153
170
 
154
171
  /**
@@ -500,19 +517,20 @@ const STRONG_TOKEN_SCORE = 70;
500
517
  * typography/font-setup, `side panel` is "Side panels");
501
518
  * - 1: a word of the query is the topic's name (`illustration` in a longer
502
519
  * question is the illustrations guide);
520
+ * - 1: a query of two or more words is one of the topic's own declared
521
+ * keywords, whatever the plural (`code review` on a guide that declares it);
503
522
  * - 1: the query holds one of the doc's titles whole (`resizable side panels`
504
523
  * names "Side panels"; `light dark mode button` names "Light/Dark Mode");
505
524
  * - 1: the doc matches every word of the query and one of them is a word of
506
525
  * its title (`switch to a dark theme` and "Use a theme").
507
526
  * A section answers to its topic's name.
508
527
  *
509
- * A doc that matched only by keyword, by a heading holding the query, or by
528
+ * A doc that matched only by a heading holding the query, by one word, or by
510
529
  * words spread through its text has no priority. Docs are split into many
511
- * small topics, and each declares its own keywords and headings, so a common
512
- * phrase such as `font size` hits a guide's keyword or heading exactly
513
- * (170-190) while the component the reader is after matches one word by
514
- * keyword (`Text`, 98). Ranked on text alone, every split adds another doc
515
- * above the component. Broad reference pages match every word of many queries
530
+ * small topics, each with its own headings, so a common phrase such as
531
+ * `font size` hits a heading exactly (170) while the component the reader is
532
+ * after matches one word by keyword (`Text`, 98). Ranked on text alone, every
533
+ * split adds another doc above the component. Broad reference pages match every word of many queries
516
534
  * in their text the same way. The score stays the text-match strength the
517
535
  * result reports, so callers that gate on it (`build`) see the same numbers.
518
536
  *
@@ -538,6 +556,20 @@ export function domainPriority(term, tokens, candidate, hit) {
538
556
  if (named.some(n => samePhrase(term, n))) {
539
557
  return 2;
540
558
  }
559
+ // A query of two or more words that is one of the doc's own declared
560
+ // keywords, read as words whatever the plural, is the author naming the
561
+ // doc for that phrase: `code review` on a guide that declares it. Only
562
+ // what the author declared counts: the code terms and table cells a
563
+ // doc's other keywords add (`missing_field`) name what it mentions, not
564
+ // what it is for. One word is left out too, and a section declares
565
+ // nothing.
566
+ if (
567
+ phraseWords(term).length >= 2 &&
568
+ candidate._section == null &&
569
+ (candidate._declared ?? []).some(k => samePhrase(term, String(k)))
570
+ ) {
571
+ return 1;
572
+ }
541
573
  if (
542
574
  words.some(
543
575
  w =>
@@ -1079,7 +1111,11 @@ async function gatherCoreComponents(coreDir) {
1079
1111
  /** @type {string[]} */
1080
1112
  let guidance = [];
1081
1113
  if (readme && readme.endsWith('.doc.mjs')) {
1082
- const doc = await loadModuleDoc(readme);
1114
+ const loaded = await loadModuleDoc(readme);
1115
+ // A member documented in its family's doc (VStack in Stack's doc) is
1116
+ // indexed from its own entry: the family's keywords and guidance
1117
+ // describe the family, not the member.
1118
+ const doc = familyMember(loaded, comp) ?? loaded;
1083
1119
  if (doc) {
1084
1120
  keywords = Array.isArray(doc.keywords) ? doc.keywords : [];
1085
1121
  description = doc.usage?.description || doc.description || '';
@@ -1105,20 +1141,57 @@ async function gatherCoreComponents(coreDir) {
1105
1141
  * usage/description from each component's .doc.mjs, same as core. Without
1106
1142
  * this, an integration component is invisible to `search`/`build` even
1107
1143
  * though `component --list`/`component <Name>` already resolve it — the two
1108
- * discovery paths silently disagreed.
1144
+ * discovery paths silently disagreed. Also returns, per name, the docs that
1145
+ * hold it by their file name and the loadable docs that only record it.
1109
1146
  * @param {import('../../foundation/integrations/integrations.mjs').LoadedIntegration[]} loadedIntegrations
1110
- * @returns {Promise<Candidate[]>}
1147
+ * @returns {Promise<{candidates: Candidate[], fileNamed: Map<string, Set<string>>, documented: Map<string, Set<string>>}>}
1111
1148
  */
1112
1149
  async function gatherIntegrationComponents(loadedIntegrations) {
1113
1150
  /** @type {Candidate[]} */
1114
1151
  const candidates = [];
1152
+ // What the owner rule reads: the docs that hold a name by their file name,
1153
+ // loadable or not and hidden ones too, and the loadable docs that only
1154
+ // record it.
1155
+ /** @type {Map<string, Set<string>>} */
1156
+ const fileNamed = new Map();
1157
+ /** @type {Map<string, Set<string>>} */
1158
+ const documented = new Map();
1159
+ /** @param {Map<string, Set<string>>} map @param {string} name @param {string} docPath */
1160
+ const hold = (map, name, docPath) =>
1161
+ map.set(name, (map.get(name) ?? new Set()).add(docPath));
1115
1162
  for (const integration of loadedIntegrations) {
1116
- const {components} = await discoverValidIntegrationComponents(integration);
1163
+ const {components, discovered} =
1164
+ await discoverValidIntegrationComponents(integration);
1165
+ const renamed = new Set(
1166
+ discovered
1167
+ .filter(rec => integrationRecordHold(rec) === 'doc')
1168
+ .filter(rec => !isIntegrationMember(rec))
1169
+ .map(rec => rec.docPath),
1170
+ );
1171
+ forEachIntegrationDoc(integration.components, (docPath, stem) => {
1172
+ if (!renamed.has(docPath)) hold(fileNamed, stem, docPath);
1173
+ });
1117
1174
  for (const rec of components) {
1118
- const doc = await loadModuleDoc(rec.docPath);
1175
+ const loaded = await loadModuleDoc(rec.docPath);
1176
+ // A family member is indexed from its own entry: the family's keywords
1177
+ // and guidance describe the family.
1178
+ const member = isIntegrationMember(rec);
1179
+ const doc = member ? integrationMemberEntry(loaded, rec) : loaded;
1180
+ // A doc that names another export of its file still answers to the
1181
+ // file's name, as `component <file name>` does.
1182
+ const fileName = docStem(rec.docPath);
1183
+ if (integrationRecordHold(rec) === 'doc') {
1184
+ hold(documented, rec.name, rec.docPath);
1185
+ }
1186
+ if (renamed.has(rec.docPath) && !member) {
1187
+ hold(documented, fileName, rec.docPath);
1188
+ }
1119
1189
  candidates.push({
1120
1190
  domain: 'component',
1121
1191
  name: rec.name,
1192
+ aliases: !member && fileName !== rec.name ? [fileName] : [],
1193
+ _hold: integrationRecordHold(rec),
1194
+ _docPath: rec.docPath,
1122
1195
  keywords: doc && Array.isArray(doc.keywords) ? doc.keywords : [],
1123
1196
  description: doc ? doc.usage?.description || doc.description || '' : '',
1124
1197
  guidance: guidanceFrom(doc),
@@ -1138,12 +1211,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
1138
1211
  packageName: rec.package,
1139
1212
  },
1140
1213
  rec.name,
1141
- doc?.import,
1214
+ loaded?.import,
1142
1215
  ),
1143
1216
  });
1144
1217
  }
1145
1218
  }
1146
- return candidates;
1219
+ return {candidates, fileNamed, documented};
1147
1220
  }
1148
1221
 
1149
1222
  /**
@@ -1155,11 +1228,12 @@ async function gatherIntegrationComponents(loadedIntegrations) {
1155
1228
  */
1156
1229
  async function gatherComponents(coreDir, cwd) {
1157
1230
  const loadedIntegrations = await loadIntegrationsSafely(cwd);
1158
- const [core, integrations, replacements] = await Promise.all([
1231
+ const [core, gathered, replacements] = await Promise.all([
1159
1232
  gatherCoreComponents(coreDir),
1160
1233
  gatherIntegrationComponents(loadedIntegrations),
1161
1234
  loadComponentReplacements(coreDir, loadedIntegrations),
1162
1235
  ]);
1236
+ const {candidates: integrations, fileNamed, documented} = gathered;
1163
1237
  // An active replacement answers to the Core name it replaces, so a search
1164
1238
  // for that name finds the replacement and not the Core original
1165
1239
  // (spec:AST-035 FR11).
@@ -1170,9 +1244,43 @@ async function gatherComponents(coreDir, cwd) {
1170
1244
  active.target,
1171
1245
  ]),
1172
1246
  );
1247
+ // The owner rule, as component detail applies it: a candidate `component
1248
+ // <name>` does not open names its package and follows the owner, and a
1249
+ // file-name alias that opens another owner is dropped.
1250
+ const coreNames = new Set(core.map(candidate => candidate.name));
1251
+ /** @param {string} name @param {Candidate} candidate */
1252
+ const opens = (name, candidate) =>
1253
+ !coreNames.has(name) &&
1254
+ findComponentReadme(coreDir, name) == null &&
1255
+ [...(fileNamed.get(name) ?? [])].every(doc => doc === candidate._docPath) &&
1256
+ [...(documented.get(name) ?? [])].every(doc => doc === candidate._docPath);
1257
+ // A legacy docs package keeps an exact name over an export name a doc records.
1258
+ /** @type {ReturnType<typeof discoverExternalPackages> | undefined} */
1259
+ let externals;
1260
+ /** @param {string} name */
1261
+ const legacyDocuments = name =>
1262
+ (externals ??= discoverExternalPackages(cwd)).some(
1263
+ ext => resolveLegacyExternalDoc(ext, name) != null,
1264
+ );
1265
+ const ruled = integrations.map(candidate => {
1266
+ const aliases = (candidate.aliases ?? []).filter(alias =>
1267
+ opens(alias, candidate),
1268
+ );
1269
+ return candidate._hold === 'doc' &&
1270
+ (!opens(candidate.name, candidate) || legacyDocuments(candidate.name))
1271
+ ? {
1272
+ ...candidate,
1273
+ aliases,
1274
+ _command: `astryx component ${candidate.name} --package ${candidate._package}`,
1275
+ }
1276
+ : {...candidate, aliases};
1277
+ });
1173
1278
  return [
1174
1279
  ...core.filter(candidate => !replacements.forTarget(candidate.name)),
1175
- ...integrations.map(candidate => {
1280
+ ...[
1281
+ ...ruled.filter(candidate => !candidate._command),
1282
+ ...ruled.filter(candidate => candidate._command),
1283
+ ].map(candidate => {
1176
1284
  const target = targetOf.get(`${candidate._package}\0${candidate.name}`);
1177
1285
  if (target != null) {
1178
1286
  return {...candidate, aliases: [...(candidate.aliases ?? []), target]};
@@ -1225,11 +1333,11 @@ export async function componentKeywords(coreDir, cwd) {
1225
1333
  .flat()
1226
1334
  .map(async name => {
1227
1335
  const readme = findComponentReadme(coreDir, name);
1228
- const doc =
1336
+ const loaded =
1229
1337
  readme && readme.endsWith('.doc.mjs')
1230
1338
  ? await loadModuleDoc(readme)
1231
1339
  : null;
1232
- return {name, keywords: keywordsOf(doc)};
1340
+ return {name, keywords: keywordsOf(familyMember(loaded, name) ?? loaded)};
1233
1341
  });
1234
1342
  const loadedIntegrations = await loadIntegrationsSafely(cwd);
1235
1343
  const replacements = await loadComponentReplacements(
@@ -1239,10 +1347,13 @@ export async function componentKeywords(coreDir, cwd) {
1239
1347
  const integrations = loadedIntegrations.map(async integration => {
1240
1348
  const {components} = await discoverValidIntegrationComponents(integration);
1241
1349
  return Promise.all(
1242
- components.map(async rec => ({
1243
- name: rec.name,
1244
- keywords: keywordsOf(await loadModuleDoc(rec.docPath)),
1245
- })),
1350
+ components.map(async rec => {
1351
+ const loaded = await loadModuleDoc(rec.docPath);
1352
+ return {
1353
+ name: rec.name,
1354
+ keywords: keywordsOf(integrationMemberEntry(loaded, rec) ?? loaded),
1355
+ };
1356
+ }),
1246
1357
  );
1247
1358
  });
1248
1359
  return [
@@ -1416,6 +1527,10 @@ async function gatherDocs(cwd) {
1416
1527
  ...defined,
1417
1528
  ...codeTerms({content}),
1418
1529
  ],
1530
+ _declared: [
1531
+ ...(Array.isArray(selfDoc?.keywords) ? selfDoc.keywords : []),
1532
+ ...(Array.isArray(node.keywords) ? node.keywords : []),
1533
+ ],
1419
1534
  description: node.summary || '',
1420
1535
  prose: sectionProse({title: node.title, content}),
1421
1536
  titles: [node.title],
@@ -1552,6 +1667,7 @@ function topicCandidates(
1552
1667
  ...(doc?.title || title ? [doc?.title || title] : []),
1553
1668
  ...(Array.isArray(doc?.keywords) ? doc.keywords : []),
1554
1669
  ],
1670
+ _declared: Array.isArray(doc?.keywords) ? doc.keywords : [],
1555
1671
  description: doc?.description || summary,
1556
1672
  prose: split
1557
1673
  ? sections.map(section => section.title).filter(Boolean)
@@ -616,6 +616,42 @@ describe('search leaf — a theme is found by its names; its description is pros
616
616
  );
617
617
  });
618
618
 
619
+ describe('search leaf — components whose docs live in a family folder', () => {
620
+ it("indexes a family member from its own entry, not the family's keywords", async () => {
621
+ // HStack and VStack are documented in Stack's doc, whose keywords
622
+ // (horizontal, vertical, row, column, ...) describe Stack.
623
+ const reasons = async (/** @type {string} */ query) =>
624
+ Object.fromEntries(
625
+ (await search(query, {cwd, type: 'component'})).data.results.map(r => [
626
+ r.name,
627
+ r.reason,
628
+ ]),
629
+ );
630
+ const column = await reasons('column');
631
+ expect(column.HStack ?? '').not.toMatch(/keyword/);
632
+ expect(column.VStack ?? '').not.toMatch(/keyword/);
633
+ expect(column.Stack).toMatch(/keyword/);
634
+ const vertical = (await search('vertical', {cwd, type: 'component'})).data.results.map(
635
+ r => r.name,
636
+ );
637
+ expect(vertical.indexOf('VStack')).toBeGreaterThanOrEqual(0);
638
+ if (vertical.includes('HStack')) {
639
+ expect(vertical.indexOf('VStack')).toBeLessThan(vertical.indexOf('HStack'));
640
+ }
641
+ }, SLOW);
642
+
643
+ it('ranks VStack, HStack and Heading first for their own names, with their own import', async () => {
644
+ for (const name of ['VStack', 'HStack', 'Heading']) {
645
+ const r = await search(name.toLowerCase(), {cwd, type: 'component'});
646
+ expect(r.data.results[0]).toMatchObject({
647
+ domain: 'component',
648
+ name,
649
+ import: `@astryxdesign/core/${name}`,
650
+ });
651
+ }
652
+ }, SLOW);
653
+ });
654
+
619
655
  describe('search leaf — integration components', () => {
620
656
  /**
621
657
  * A minimal consumer project: a stub `@astryxdesign/core` (so `findCoreDir`
@@ -21,8 +21,10 @@ import {checkGhCli} from '../_github.mjs';
21
21
  import {Project} from '../../../foundation/config/project.mjs';
22
22
  import {
23
23
  CORE_PACKAGE,
24
- findIntegrationComponentDoc,
25
- findIntegrationComponentSource,
24
+ docStem,
25
+ findComponentReadme,
26
+ findIntegrationComponent,
27
+ unscopedComponentOwners,
26
28
  } from '../../../foundation/discovery/component-discovery.mjs';
27
29
  import {ERROR_CODES} from '../../../foundation/response/error-codes.mjs';
28
30
  import {AstryxError, writeFailed} from '../../error.mjs';
@@ -126,33 +128,38 @@ async function loadConfigSafely(cwd) {
126
128
 
127
129
  /**
128
130
  * Build the set of OWNER packages that provide a component named `name` across
129
- * core + every loaded integration.
131
+ * core + every loaded integration. Core holds every name it documents, but has
132
+ * source to copy (`sourceDir`) only for a name with its own folder.
130
133
  * @param {string} coreDir
131
134
  * @param {Array<{name: string, components?: string, issuesUrl?: string}>} loadedIntegrations
132
135
  * @param {string} name
133
136
  * @param {string|undefined} coreIssuesUrl
134
- * @returns {Array<{package: string, sourceDir: string|null, ownerPackage: string, issuesUrl: string|undefined}>}
137
+ * @returns {Array<{package: string, sourceDir: string|null, docPath: string|null, ownerPackage: string, issuesUrl: string|undefined, hold: string}>}
135
138
  */
136
139
  function resolveOwners(coreDir, loadedIntegrations, name, coreIssuesUrl) {
137
140
  const owners = [];
138
141
  const coreComponentDir = path.join(coreDir, 'src', name);
139
- if (fs.existsSync(coreComponentDir)) {
142
+ const coreFolder = fs.existsSync(coreComponentDir);
143
+ if (coreFolder || findComponentReadme(coreDir, name)) {
140
144
  owners.push({
141
145
  package: CORE_PACKAGE,
142
- sourceDir: coreComponentDir,
146
+ sourceDir: coreFolder ? coreComponentDir : null,
147
+ docPath: null,
143
148
  ownerPackage: CORE_PACKAGE,
144
149
  issuesUrl: coreIssuesUrl || DEFAULT_ISSUES_URL,
150
+ hold: 'core',
145
151
  });
146
152
  }
147
153
  for (const integration of loadedIntegrations) {
148
- const docPath = findIntegrationComponentDoc(integration, name);
149
- if (!docPath) continue;
150
- const sourcePath = findIntegrationComponentSource(integration, name);
154
+ const found = findIntegrationComponent(integration, name);
155
+ if (!found) continue;
151
156
  owners.push({
152
157
  package: integration.name,
153
- sourceDir: sourcePath ? path.dirname(sourcePath) : null,
158
+ sourceDir: found.sourcePath ? path.dirname(found.sourcePath) : null,
159
+ docPath: found.docPath,
154
160
  ownerPackage: integration.name,
155
161
  issuesUrl: integration.issuesUrl,
162
+ hold: found.hold,
156
163
  });
157
164
  }
158
165
  return owners;
@@ -222,25 +229,32 @@ export async function swizzleCopy(component, options = {}) {
222
229
  : undefined;
223
230
  // An active integration replacement answers to the Core name it replaces,
224
231
  // bare or qualified by its own package (spec:AST-035 FR11): copy it under its
225
- // own name. `--package @astryxdesign/core` still copies the Core original.
232
+ // doc's file name, the folder it has always gone to. `--package
233
+ // @astryxdesign/core` still copies the Core original.
226
234
  if (pkg !== CORE_PACKAGE) {
227
235
  const replacement = await resolveComponentReplacement(
228
236
  coreDir,
229
237
  loadedIntegrations,
230
238
  dirName,
231
239
  );
240
+ const copyName = replacement ? docStem(replacement.docPath) : null;
232
241
  if (
233
242
  replacement &&
234
- (!pkg || (pkg === replacement.package && replacement.name !== dirName))
243
+ copyName &&
244
+ (!pkg || (pkg === replacement.package && copyName !== dirName))
235
245
  ) {
236
- return swizzleCopy(replacement.name, {
246
+ return swizzleCopy(copyName, {
237
247
  ...options,
238
248
  package: replacement.package,
239
249
  });
240
250
  }
241
251
  }
242
252
 
243
- const allOwners = resolveOwners(coreDir, loadedIntegrations, dirName, coreIssuesUrl);
253
+ const found = resolveOwners(coreDir, loadedIntegrations, dirName, coreIssuesUrl);
254
+ // The owner rule, then a Core name without its own folder drops out.
255
+ const allOwners = (pkg ? found : unscopedComponentOwners(found)).filter(
256
+ owner => owner.package !== CORE_PACKAGE || owner.sourceDir != null,
257
+ );
244
258
 
245
259
  if (allOwners.length === 0) {
246
260
  throw new AstryxError(