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

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 (174) hide show
  1. package/README.md +9 -8
  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/theme-checks.mjs +1 -1
  13. package/api/gap-report/gap-report.mjs +67 -4
  14. package/api/index.d.mts +2 -0
  15. package/api/index.mjs +2 -0
  16. package/api/init/run/run.mjs +2 -2
  17. package/api/integration/_adapter.d.mts +12 -0
  18. package/api/integration/_adapter.mjs +54 -0
  19. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  20. package/api/integration/add-contribution.mjs +8 -4
  21. package/api/integration/add-contribution.test.mjs +4 -2
  22. package/api/integration/authoring-checks.mjs +11 -2
  23. package/api/integration/authoring-checks.test.mjs +39 -0
  24. package/api/integration/authoring-checks.type.d.mts +9 -0
  25. package/api/integration/authoring-checks.type.mjs +14 -1
  26. package/api/integration/check/check.d.mts +96 -0
  27. package/api/integration/check/check.mjs +357 -0
  28. package/api/integration/check/check.test.mjs +489 -0
  29. package/api/integration/integration-check.type.d.mts +118 -0
  30. package/api/integration/integration-check.type.mjs +84 -0
  31. package/api/integration/integrationCheck.doc.d.mts +10 -0
  32. package/api/integration/integrationCheck.doc.mjs +73 -0
  33. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  34. package/api/integration/pack-check.mjs +61 -107
  35. package/api/integration/pack-check.test.mjs +359 -0
  36. package/api/integration/validate-integration.mjs +8 -0
  37. package/api/integration/validate-integration.test.mjs +11 -0
  38. package/api/integration/validate-integration.type.d.mts +8 -0
  39. package/api/integration/validate-integration.type.mjs +12 -1
  40. package/api/integration/validateIntegration.doc.mjs +17 -3
  41. package/api/search/search.d.mts +9 -0
  42. package/api/search/search.integration-check.test.mjs +24 -0
  43. package/api/search/search.mjs +108 -16
  44. package/api/search/search.test.mjs +36 -0
  45. package/api/swizzle/copy/copy.mjs +28 -14
  46. package/api/template/template.test.mjs +120 -3
  47. package/api/theme/build/build.family.test.mjs +64 -0
  48. package/api/theme/build/build.mjs +180 -24
  49. package/api/theme/themeBuild.doc.mjs +4 -4
  50. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  51. package/assets/codemods/transforms/next/index.mjs +11 -1
  52. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  53. package/assets/docs/README.md +10 -0
  54. package/assets/docs/author-a-theme.doc.mjs +1 -1
  55. package/assets/docs/getting-started.doc.mjs +3 -3
  56. package/assets/docs/theme.doc.dense.mjs +1 -1
  57. package/assets/docs/theme.doc.mjs +1 -1
  58. package/assets/docs/theme.doc.zh.mjs +1 -1
  59. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  60. package/assets/docs/tree/block-template.doc.mjs +2 -2
  61. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  62. package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
  63. package/assets/docs/tree/checks.doc.mjs +21 -13
  64. package/assets/docs/tree/component-family.doc.mjs +5 -1
  65. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  66. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  67. package/assets/docs/tree/links.doc.mjs +2 -2
  68. package/assets/docs/tree/page-template.doc.mjs +1 -1
  69. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  70. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  71. package/assets/docs/tree/single-component.doc.mjs +3 -2
  72. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  73. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  74. package/assets/docs/tree/versioning.doc.mjs +1 -0
  75. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  76. package/assets/docs/use-a-theme.doc.mjs +2 -2
  77. package/assets/docs/working-with-ai.doc.mjs +43 -4
  78. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  79. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  80. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  81. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  82. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  83. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  84. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  85. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  86. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  87. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  88. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  89. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  90. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  91. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  92. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  93. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  94. package/assets/templates/pages/detail-page/page.tsx +3 -3
  95. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  96. package/assets/theme.template.ts +1 -1
  97. package/authoring/doctypes/base/type.ts +6 -0
  98. package/authoring/doctypes/component/component.doc.mjs +4 -4
  99. package/authoring/doctypes/component/type.ts +6 -3
  100. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  101. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  102. package/authoring/doctypes/template/parse.d.mts +6 -2
  103. package/authoring/doctypes/template/parse.mjs +3 -1
  104. package/authoring/doctypes/template/template.doc.mjs +1 -1
  105. package/authoring/doctypes/template/type.ts +22 -0
  106. package/authoring/index.d.ts +1 -0
  107. package/authoring/integration/integration.doc.mjs +8 -1
  108. package/authoring/integration/parse.test.mjs +10 -0
  109. package/authoring/integration/schema.d.mts +2 -0
  110. package/authoring/integration/schema.mjs +1 -0
  111. package/authoring/integration/type.ts +4 -0
  112. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  113. package/clients/cli/commands/build-theme.mjs +1 -1
  114. package/clients/cli/commands/build-theme.path-safety.test.mjs +224 -6
  115. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  116. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  117. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  118. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  119. package/clients/cli/commands/doctor.mjs +190 -0
  120. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  121. package/clients/cli/commands/integration-check.test.mjs +267 -0
  122. package/clients/cli/commands/integration.doc.mjs +6 -2
  123. package/clients/cli/commands/integration.mjs +107 -1
  124. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  125. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  126. package/clients/cli/index.mjs +1 -0
  127. package/clients/cli/lib/component-format.mjs +5 -1
  128. package/foundation/config/project.mjs +82 -0
  129. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  130. package/foundation/discovery/component-discovery.d.mts +190 -10
  131. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  132. package/foundation/discovery/component-discovery.mjs +864 -56
  133. package/foundation/discovery/component-discovery.test.mjs +96 -0
  134. package/foundation/discovery/component-replacement.mjs +18 -0
  135. package/foundation/discovery/component-replacement.test.mjs +40 -0
  136. package/foundation/discovery/doc-types.d.mts +157 -0
  137. package/foundation/discovery/doc-types.mjs +1224 -0
  138. package/foundation/discovery/doc-types.test.mjs +652 -0
  139. package/foundation/discovery/docs-discovery.d.mts +81 -0
  140. package/foundation/discovery/docs-discovery.mjs +57 -0
  141. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  142. package/foundation/discovery/integration-doc-files.mjs +231 -0
  143. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  144. package/foundation/discovery/module-exports.d.mts +27 -0
  145. package/foundation/discovery/module-exports.mjs +356 -31
  146. package/foundation/discovery/module-exports.test.mjs +172 -0
  147. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  148. package/foundation/discovery/package-tsconfig.mjs +228 -0
  149. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  150. package/foundation/discovery/project-typescript.d.mts +23 -0
  151. package/foundation/discovery/project-typescript.mjs +61 -0
  152. package/foundation/discovery/prop-drift.d.mts +278 -0
  153. package/foundation/discovery/prop-drift.mjs +2263 -0
  154. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  155. package/foundation/discovery/template-adapter.mjs +74 -5
  156. package/foundation/discovery/type-match.d.mts +53 -0
  157. package/foundation/discovery/type-match.mjs +660 -0
  158. package/foundation/discovery/type-match.test.mjs +169 -0
  159. package/foundation/discovery/type-text.d.mts +68 -0
  160. package/foundation/discovery/type-text.mjs +658 -0
  161. package/foundation/discovery/type-text.test.mjs +196 -0
  162. package/foundation/discovery/unresolved-references.d.mts +62 -0
  163. package/foundation/discovery/unresolved-references.mjs +790 -0
  164. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  165. package/foundation/doc-compiler/doc-loads.test.mjs +11 -1
  166. package/foundation/doc-compiler/inputs.mjs +23 -0
  167. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  168. package/foundation/doc-compiler/links.d.mts +9 -5
  169. package/foundation/doc-compiler/links.mjs +4 -2
  170. package/foundation/integrations/contribution-inventory.mjs +6 -1
  171. package/foundation/response/response-types.doc.mjs +7 -2
  172. package/foundation/xle/registry.mjs +4 -0
  173. package/foundation/xle/registry.test.mjs +28 -0
  174. package/package.json +10 -10
@@ -13,221 +13,474 @@ import {describe, it, expect} from 'vitest';
13
13
  import * as path from 'node:path';
14
14
  import {fileURLToPath} from 'node:url';
15
15
  import {component} from './component.mjs';
16
+ import {search} from '../search/search.mjs';
16
17
  import {AstryxError} from '../error.mjs';
17
18
  import {runCli} from '../../test-utils/run-cli.mjs';
18
19
 
19
20
  // api/component/ -> up 4 = repo root (has packages/core, which findCoreDir walks to).
20
- const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../..');
21
+ const REPO = path.resolve(
22
+ path.dirname(fileURLToPath(import.meta.url)),
23
+ '../../../..',
24
+ );
21
25
  const cwd = REPO;
22
26
  const SLOW = 30_000;
23
27
 
24
28
  describe('component dispatcher — routing', () => {
25
- it('routes a bare name to component.detail', async () => {
26
- const r = await component('Button', {cwd});
27
- expect(r.type).toBe('component.detail');
28
- expect(r.data.name).toBe('Button');
29
- }, SLOW);
29
+ it(
30
+ 'routes a bare name to component.detail',
31
+ async () => {
32
+ const r = await component('Button', {cwd});
33
+ expect(r.type).toBe('component.detail');
34
+ expect(r.data.name).toBe('Button');
35
+ },
36
+ SLOW,
37
+ );
30
38
 
31
- it('routes no-name to the list leaf (component.list, detail=names)', async () => {
32
- const r = await component(undefined, {cwd});
33
- expect(r.type).toBe('component.list');
34
- expect(r.data.detail).toBe('names');
35
- expect(Object.keys(r.data.components).length).toBeGreaterThan(0);
36
- }, SLOW);
39
+ it(
40
+ 'routes no-name to the list leaf (component.list, detail=names)',
41
+ async () => {
42
+ const r = await component(undefined, {cwd});
43
+ expect(r.type).toBe('component.list');
44
+ expect(r.data.detail).toBe('names');
45
+ expect(Object.keys(r.data.components).length).toBeGreaterThan(0);
46
+ },
47
+ SLOW,
48
+ );
37
49
 
38
- it('routes --category to the list leaf, filtered to that group', async () => {
39
- const r = await component(undefined, {cwd, category: 'Layout'});
40
- expect(r.type).toBe('component.list');
41
- expect(Object.keys(r.data.components)).toContain('Layout');
42
- }, SLOW);
50
+ it(
51
+ 'routes --category to the list leaf, filtered to that group',
52
+ async () => {
53
+ const r = await component(undefined, {cwd, category: 'Layout'});
54
+ expect(r.type).toBe('component.list');
55
+ expect(Object.keys(r.data.components)).toContain('Layout');
56
+ },
57
+ SLOW,
58
+ );
43
59
 
44
- it('treats an empty-string name as a list (not a not-found error)', async () => {
45
- const r = await component('', {cwd});
46
- expect(r.type).toBe('component.list');
47
- }, SLOW);
60
+ it(
61
+ 'treats an empty-string name as a list (not a not-found error)',
62
+ async () => {
63
+ const r = await component('', {cwd});
64
+ expect(r.type).toBe('component.list');
65
+ },
66
+ SLOW,
67
+ );
48
68
  });
49
69
 
50
70
  describe('component dispatcher — detail levels on list', () => {
51
- it('--detail compact lists with detail=compact', async () => {
52
- const r = await component(undefined, {cwd, list: true, detail: 'compact'});
53
- expect(r.type).toBe('component.list');
54
- expect(r.data.detail).toBe('compact');
71
+ it(
72
+ '--detail compact lists with detail=compact',
73
+ async () => {
74
+ const r = await component(undefined, {
75
+ cwd,
76
+ list: true,
77
+ detail: 'compact',
78
+ });
79
+ expect(r.type).toBe('component.list');
80
+ expect(r.data.detail).toBe('compact');
81
+ },
82
+ SLOW,
83
+ );
84
+
85
+ it(
86
+ '--detail full lists with detail=full',
87
+ async () => {
88
+ const r = await component(undefined, {cwd, list: true, detail: 'full'});
89
+ expect(r.type).toBe('component.list');
90
+ expect(r.data.detail).toBe('full');
91
+ },
92
+ SLOW,
93
+ );
94
+ });
95
+
96
+ describe('component list — components whose docs live in a family folder', () => {
97
+ it('lists HStack, VStack and Heading at every detail level, each with its own import', async () => {
98
+ const {docs: stackDoc} = await import(
99
+ path.join(REPO, 'packages', 'core', 'src', 'Stack', 'Stack.doc.mjs')
100
+ );
101
+ const vstackMember = stackDoc.components.find(c => c.name === 'VStack');
102
+
103
+ const names = await component(undefined, {cwd, list: true});
104
+ const listed = Object.values(names.data.components).flat();
105
+ for (const name of ['HStack', 'VStack', 'Heading']) {
106
+ expect(listed).toContainEqual({name, package: '@astryxdesign/core'});
107
+ }
108
+
109
+ const compact = await component(undefined, {cwd, list: true, detail: 'compact'});
110
+ const brief = Object.values(compact.data.components).flat();
111
+ expect(brief.find(e => e.name === 'VStack')).toEqual({
112
+ name: 'VStack',
113
+ package: '@astryxdesign/core',
114
+ description: vstackMember.description,
115
+ import: '@astryxdesign/core/VStack',
116
+ });
117
+
118
+ const full = await component(undefined, {cwd, list: true, detail: 'full'});
119
+ const entry = Object.values(full.data.components)
120
+ .flat()
121
+ .find(e => e.name === 'VStack');
122
+ // Shaped like every other full entry (a component doc plus its package):
123
+ // the member's own name, display name and summary, and no scoping field
124
+ // the full-entry type does not describe.
125
+ expect(entry).toEqual({
126
+ name: 'VStack',
127
+ package: '@astryxdesign/core',
128
+ displayName: vstackMember.displayName,
129
+ group: stackDoc.group,
130
+ category: stackDoc.category,
131
+ usage: {description: vstackMember.description},
132
+ props: vstackMember.props,
133
+ theming: stackDoc.theming,
134
+ import: '@astryxdesign/core/VStack',
135
+ });
55
136
  }, SLOW);
56
137
 
57
- it('--detail full lists with detail=full', async () => {
58
- const r = await component(undefined, {cwd, list: true, detail: 'full'});
59
- expect(r.type).toBe('component.list');
60
- expect(r.data.detail).toBe('full');
138
+ it('prints each one\'s own import path in the list, the detail, and search', async () => {
139
+ // The path `component <Name>` prints is the one docs and examples teach:
140
+ // the component's own entry point, never its family's (/Stack, /Text) or
141
+ // a re-export (/Layout).
142
+ const compact = Object.values(
143
+ (await component(undefined, {cwd, list: true, detail: 'compact'})).data.components,
144
+ ).flat();
145
+ const full = Object.values(
146
+ (await component(undefined, {cwd, list: true, detail: 'full'})).data.components,
147
+ ).flat();
148
+ const textList = (await runCli(['component', '--list'], cwd)).stdout;
149
+ const textCompact = (
150
+ await runCli(['component', '--list', '--detail', 'compact'], cwd)
151
+ ).stdout;
152
+ /** The `import:` line of a text record, found by its `name:` line. */
153
+ const textImport = (text, name) =>
154
+ new RegExp(`^name:\\s+${name}\\n(?:(?!name:).*\\n)*?import:\\s+(\\S+)`, 'm').exec(
155
+ text,
156
+ )?.[1];
157
+ for (const name of ['VStack', 'HStack', 'Heading']) {
158
+ const expected = `@astryxdesign/core/${name}`;
159
+ expect(compact.find(e => e.name === name)?.import).toBe(expected);
160
+ expect((await component(name, {cwd})).data.import).toBe(expected);
161
+ const hits = (await search(name, {cwd, type: 'component'})).data.results;
162
+ expect(hits.find(r => r.name === name)?.import).toBe(expected);
163
+ expect(textImport(textList, name)).toBe(expected);
164
+ expect(textImport(textCompact, name)).toBe(expected);
165
+ }
166
+ // The full level spreads a component's own doc, which states no import
167
+ // (Button's entry has none either); a member's entry is scoped to the
168
+ // member and states its own.
169
+ for (const name of ['VStack', 'HStack']) {
170
+ expect(full.find(e => e.name === name)?.import).toBe(`@astryxdesign/core/${name}`);
171
+ }
61
172
  }, SLOW);
62
173
  });
63
174
 
64
175
  describe('component dispatcher — projection precedence (locked)', () => {
65
- it('--props routes to component.detail.props', async () => {
66
- const r = await component('Button', {cwd, props: true});
67
- expect(r.type).toBe('component.detail.props');
68
- }, SLOW);
176
+ it(
177
+ '--props routes to component.detail.props',
178
+ async () => {
179
+ const r = await component('Button', {cwd, props: true});
180
+ expect(r.type).toBe('component.detail.props');
181
+ },
182
+ SLOW,
183
+ );
69
184
 
70
- it('--source routes to component.detail.source', async () => {
71
- const r = await component('Button', {cwd, source: true});
72
- expect(r.type).toBe('component.detail.source');
73
- }, SLOW);
185
+ it(
186
+ '--source routes to component.detail.source',
187
+ async () => {
188
+ const r = await component('Button', {cwd, source: true});
189
+ expect(r.type).toBe('component.detail.source');
190
+ },
191
+ SLOW,
192
+ );
74
193
 
75
- it('--source WINS over --props when both are set (precedence lock)', async () => {
76
- const r = await component('Button', {cwd, props: true, source: true});
77
- expect(r.type).toBe('component.detail.source');
78
- }, SLOW);
194
+ it(
195
+ '--source WINS over --props when both are set (precedence lock)',
196
+ async () => {
197
+ const r = await component('Button', {cwd, props: true, source: true});
198
+ expect(r.type).toBe('component.detail.source');
199
+ },
200
+ SLOW,
201
+ );
79
202
 
80
- it('--list WINS over a given name (list dispatch precedence lock)', async () => {
81
- const r = await component('Button', {cwd, list: true});
82
- expect(r.type).toBe('component.list');
83
- }, SLOW);
203
+ it(
204
+ '--list WINS over a given name (list dispatch precedence lock)',
205
+ async () => {
206
+ const r = await component('Button', {cwd, list: true});
207
+ expect(r.type).toBe('component.list');
208
+ },
209
+ SLOW,
210
+ );
84
211
  });
85
212
 
86
213
  describe('component dispatcher — name resolution', () => {
87
- it('strips a legacy XDS prefix (XDSButton -> Button)', async () => {
88
- const r = await component('XDSButton', {cwd});
89
- expect(r.type).toBe('component.detail');
90
- expect(r.data.name).toBe('Button');
91
- }, SLOW);
214
+ it(
215
+ 'strips a legacy XDS prefix (XDSButton -> Button)',
216
+ async () => {
217
+ const r = await component('XDSButton', {cwd});
218
+ expect(r.type).toBe('component.detail');
219
+ expect(r.data.name).toBe('Button');
220
+ },
221
+ SLOW,
222
+ );
92
223
 
93
- it('is case-SENSITIVE: a lowercased name is unknown', async () => {
94
- let err;
95
- try {
96
- await component('button', {cwd});
97
- } catch (e) {
98
- err = e;
99
- }
100
- expect(err).toBeDefined();
101
- expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
102
- }, SLOW);
224
+ it(
225
+ 'is case-SENSITIVE: a lowercased name is unknown',
226
+ async () => {
227
+ let err;
228
+ try {
229
+ await component('button', {cwd});
230
+ } catch (e) {
231
+ err = e;
232
+ }
233
+ expect(err).toBeDefined();
234
+ expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
235
+ },
236
+ SLOW,
237
+ );
103
238
 
104
- it('throws ERR_UNKNOWN_COMPONENT for a genuinely unknown name', async () => {
105
- let err;
106
- try {
107
- await component('ZzzNope99', {cwd});
108
- } catch (e) {
109
- err = e;
110
- }
111
- expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
112
- }, SLOW);
239
+ it(
240
+ 'throws ERR_UNKNOWN_COMPONENT for a genuinely unknown name',
241
+ async () => {
242
+ let err;
243
+ try {
244
+ await component('ZzzNope99', {cwd});
245
+ } catch (e) {
246
+ err = e;
247
+ }
248
+ expect(err.code).toBe('ERR_UNKNOWN_COMPONENT');
249
+ },
250
+ SLOW,
251
+ );
113
252
 
114
- it('throws ERR_UNKNOWN_PACKAGE when --package names a missing package', async () => {
115
- let err;
116
- try {
117
- await component('Button', {cwd, package: '@nonexistent/pkg-xyz'});
118
- } catch (e) {
119
- err = e;
120
- }
121
- expect(err).toBeDefined();
122
- expect(err.code).toBe('ERR_UNKNOWN_PACKAGE');
123
- }, SLOW);
253
+ it(
254
+ 'throws ERR_UNKNOWN_PACKAGE when --package names a missing package',
255
+ async () => {
256
+ let err;
257
+ try {
258
+ await component('Button', {cwd, package: '@nonexistent/pkg-xyz'});
259
+ } catch (e) {
260
+ err = e;
261
+ }
262
+ expect(err).toBeDefined();
263
+ expect(err.code).toBe('ERR_UNKNOWN_PACKAGE');
264
+ },
265
+ SLOW,
266
+ );
124
267
  });
125
268
 
126
-
127
269
  describe('component dispatcher — scoped --package routes all projections', () => {
128
270
  const CORE = '@astryxdesign/core';
129
271
 
130
- it('--package (core) routes --showcase like the no-scope path', async () => {
131
- const ns = await component('Button', {cwd, showcase: true});
132
- const sc = await component('Button', {cwd, package: CORE, showcase: true});
133
- expect(sc.type).toBe(ns.type); // was silently component.detail before the fix
134
- }, SLOW);
272
+ it(
273
+ '--package (core) routes --showcase like the no-scope path',
274
+ async () => {
275
+ const ns = await component('Button', {cwd, showcase: true});
276
+ const sc = await component('Button', {
277
+ cwd,
278
+ package: CORE,
279
+ showcase: true,
280
+ });
281
+ expect(sc.type).toBe(ns.type); // was silently component.detail before the fix
282
+ },
283
+ SLOW,
284
+ );
135
285
 
136
- it('--package (core) routes --blocks like the no-scope path', async () => {
137
- const ns = await component('Button', {cwd, blocks: true});
138
- const sc = await component('Button', {cwd, package: CORE, blocks: true});
139
- expect(sc.type).toBe(ns.type);
140
- expect(sc.type).toBe('component.detail.blocks');
141
- }, SLOW);
286
+ it(
287
+ '--package (core) routes --blocks like the no-scope path',
288
+ async () => {
289
+ const ns = await component('Button', {cwd, blocks: true});
290
+ const sc = await component('Button', {cwd, package: CORE, blocks: true});
291
+ expect(sc.type).toBe(ns.type);
292
+ expect(sc.type).toBe('component.detail.blocks');
293
+ },
294
+ SLOW,
295
+ );
142
296
 
143
- it('--package (core) still routes --source and --props', async () => {
144
- expect((await component('Button', {cwd, package: CORE, source: true})).type).toBe(
145
- 'component.detail.source',
146
- );
147
- expect((await component('Button', {cwd, package: CORE, props: true})).type).toBe(
148
- 'component.detail.props',
149
- );
150
- }, SLOW);
297
+ it(
298
+ "--props carries a prop doc's stability marker (Drawer's experimental prop)",
299
+ async () => {
300
+ const r = await component('Drawer', {cwd, props: true});
301
+ expect(r.type).toBe('component.detail.props');
302
+ const experimental = r.data.find(p => p.name === 'experimental');
303
+ expect(experimental?.stability).toBe('experimental');
304
+ // Only the experimental prop object is marked; stable props carry no field.
305
+ expect(
306
+ r.data.filter(p => p.stability !== undefined).map(p => p.name),
307
+ ).toEqual(['experimental']);
308
+ },
309
+ SLOW,
310
+ );
311
+
312
+ it(
313
+ '--props --json and the text props table both show the stability marker',
314
+ async () => {
315
+ const json = await runCli(
316
+ ['component', 'Drawer', '--props', '--json'],
317
+ cwd,
318
+ );
319
+ expect(json.status).toBe(0);
320
+ const envelope = JSON.parse(json.stdout);
321
+ expect(envelope.type).toBe('component.detail.props');
322
+ expect(
323
+ envelope.data.find(p => p.name === 'experimental')?.stability,
324
+ ).toBe('experimental');
325
+
326
+ const text = await runCli(['component', 'Drawer', '--props'], cwd);
327
+ expect(text.status).toBe(0);
328
+ const rows = text.stdout
329
+ .split('\n')
330
+ .filter(line => line.startsWith('| `'));
331
+ const experimentalRow = rows.find(line =>
332
+ line.startsWith('| `experimental` |'),
333
+ );
334
+ expect(experimentalRow).toContain('**(experimental)**');
335
+ expect(
336
+ rows.filter(line => line.includes('**(experimental)**')),
337
+ ).toHaveLength(1);
338
+ },
339
+ SLOW,
340
+ );
341
+
342
+ it(
343
+ '--package (core) still routes --source and --props',
344
+ async () => {
345
+ expect(
346
+ (await component('Button', {cwd, package: CORE, source: true})).type,
347
+ ).toBe('component.detail.source');
348
+ expect(
349
+ (await component('Button', {cwd, package: CORE, props: true})).type,
350
+ ).toBe('component.detail.props');
351
+ },
352
+ SLOW,
353
+ );
151
354
  });
152
355
 
153
356
  describe('component dispatcher — category guard', () => {
154
- it('a non-string category throws a coded error (not a raw TypeError)', async () => {
155
- for (const bad of [123, {}, [1]]) {
156
- const err = await component(undefined, {
157
- cwd,
158
- category: /** @type {any} */ (bad),
159
- }).catch(e => e);
160
- expect(err).toBeInstanceOf(AstryxError);
161
- expect(err.code).toBe('ERR_UNKNOWN_CATEGORY');
162
- }
163
- }, SLOW);
357
+ it(
358
+ 'a non-string category throws a coded error (not a raw TypeError)',
359
+ async () => {
360
+ for (const bad of [123, {}, [1]]) {
361
+ const err = await component(undefined, {
362
+ cwd,
363
+ category: /** @type {any} */ (bad),
364
+ }).catch(e => e);
365
+ expect(err).toBeInstanceOf(AstryxError);
366
+ expect(err.code).toBe('ERR_UNKNOWN_CATEGORY');
367
+ }
368
+ },
369
+ SLOW,
370
+ );
164
371
  });
165
372
 
166
373
  describe('component dispatcher — detail and lang guards', () => {
167
374
  // `detail` and `lang` are the same controls as the CLI's --detail and --lang:
168
375
  // same accepted values, same error codes.
169
- it('rejects a detail level the CLI rejects, with the same code', async () => {
170
- const cliRun = await runCli(['--json', 'component', '--list', '--detail', 'bogus'], cwd);
171
- expect(cliRun.code).toBe(1);
172
- expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_DETAIL');
173
-
174
- const bogus = /** @type {any} */ ('bogus');
175
- for (const call of [
176
- () => component(undefined, {cwd, list: true, detail: bogus}),
177
- () => component('Button', {cwd, detail: bogus}),
178
- ]) {
179
- const err = await call().catch(e => e);
180
- expect(err).toBeInstanceOf(AstryxError);
181
- expect(err.code).toBe('ERR_INVALID_DETAIL');
182
- }
183
- }, SLOW);
376
+ it(
377
+ 'rejects a detail level the CLI rejects, with the same code',
378
+ async () => {
379
+ const cliRun = await runCli(
380
+ ['--json', 'component', '--list', '--detail', 'bogus'],
381
+ cwd,
382
+ );
383
+ expect(cliRun.code).toBe(1);
384
+ expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_DETAIL');
184
385
 
185
- it('rejects a lang the CLI rejects, with the same code', async () => {
186
- const cliRun = await runCli(['--json', 'component', 'Button', '--lang', 'fr'], cwd);
187
- expect(cliRun.code).toBe(1);
188
- expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_LANG');
189
-
190
- for (const call of [
191
- () => component('Button', {cwd, lang: 'fr'}),
192
- () => component(undefined, {cwd, list: true, detail: 'compact', lang: 'fr'}),
193
- ]) {
194
- const err = await call().catch(e => e);
195
- expect(err).toBeInstanceOf(AstryxError);
196
- expect(err.code).toBe('ERR_INVALID_LANG');
197
- }
198
- const zh = await component('Button', {cwd, lang: 'zh'});
199
- expect(zh.type).toBe('component.detail');
200
- }, SLOW);
386
+ const bogus = /** @type {any} */ ('bogus');
387
+ for (const call of [
388
+ () => component(undefined, {cwd, list: true, detail: bogus}),
389
+ () => component('Button', {cwd, detail: bogus}),
390
+ ]) {
391
+ const err = await call().catch(e => e);
392
+ expect(err).toBeInstanceOf(AstryxError);
393
+ expect(err.code).toBe('ERR_INVALID_DETAIL');
394
+ }
395
+ },
396
+ SLOW,
397
+ );
398
+
399
+ it(
400
+ 'rejects a lang the CLI rejects, with the same code',
401
+ async () => {
402
+ const cliRun = await runCli(
403
+ ['--json', 'component', 'Button', '--lang', 'fr'],
404
+ cwd,
405
+ );
406
+ expect(cliRun.code).toBe(1);
407
+ expect(JSON.parse(cliRun.stdout).code).toBe('ERR_INVALID_LANG');
408
+
409
+ for (const call of [
410
+ () => component('Button', {cwd, lang: 'fr'}),
411
+ () =>
412
+ component(undefined, {
413
+ cwd,
414
+ list: true,
415
+ detail: 'compact',
416
+ lang: 'fr',
417
+ }),
418
+ ]) {
419
+ const err = await call().catch(e => e);
420
+ expect(err).toBeInstanceOf(AstryxError);
421
+ expect(err.code).toBe('ERR_INVALID_LANG');
422
+ }
423
+ const zh = await component('Button', {cwd, lang: 'zh'});
424
+ expect(zh.type).toBe('component.detail');
425
+ },
426
+ SLOW,
427
+ );
201
428
  });
202
429
 
203
430
  describe('component dispatcher — detail levels on single component (text only)', () => {
204
- it('--detail compact returns less text than full', async () => {
205
- const full = await runCli(['component', 'Button'], cwd);
206
- const compact = await runCli(['component', 'Button', '--detail', 'compact'], cwd);
207
- expect(full.code).toBe(0);
208
- expect(compact.code).toBe(0);
209
- expect(compact.stdout.length).toBeLessThan(full.stdout.length);
210
- // Compact still shows the name and import
211
- expect(compact.stdout).toContain('Button');
212
- expect(compact.stdout).toContain('@astryxdesign/core');
213
- }, SLOW);
431
+ it(
432
+ '--detail compact returns less text than full',
433
+ async () => {
434
+ const full = await runCli(['component', 'Button'], cwd);
435
+ const compact = await runCli(
436
+ ['component', 'Button', '--detail', 'compact'],
437
+ cwd,
438
+ );
439
+ expect(full.code).toBe(0);
440
+ expect(compact.code).toBe(0);
441
+ expect(compact.stdout.length).toBeLessThan(full.stdout.length);
442
+ // Compact still shows the name and import
443
+ expect(compact.stdout).toContain('Button');
444
+ expect(compact.stdout).toContain('@astryxdesign/core');
445
+ },
446
+ SLOW,
447
+ );
214
448
 
215
- it('--detail brief returns less text than compact', async () => {
216
- const compact = await runCli(['component', 'Button', '--detail', 'compact'], cwd);
217
- const brief = await runCli(['component', 'Button', '--detail', 'brief'], cwd);
218
- expect(brief.code).toBe(0);
219
- expect(brief.stdout.length).toBeLessThan(compact.stdout.length);
220
- expect(brief.stdout).toContain('Button');
221
- }, SLOW);
449
+ it(
450
+ '--detail brief returns less text than compact',
451
+ async () => {
452
+ const compact = await runCli(
453
+ ['component', 'Button', '--detail', 'compact'],
454
+ cwd,
455
+ );
456
+ const brief = await runCli(
457
+ ['component', 'Button', '--detail', 'brief'],
458
+ cwd,
459
+ );
460
+ expect(brief.code).toBe(0);
461
+ expect(brief.stdout.length).toBeLessThan(compact.stdout.length);
462
+ expect(brief.stdout).toContain('Button');
463
+ },
464
+ SLOW,
465
+ );
222
466
 
223
- it('--json --detail compact returns the same full JSON as --json (contract)', async () => {
224
- const full = await runCli(['--json', 'component', 'Button'], cwd);
225
- const compact = await runCli(['--json', 'component', 'Button', '--detail', 'compact'], cwd);
226
- expect(full.code).toBe(0);
227
- expect(compact.code).toBe(0);
228
- const fullData = JSON.parse(full.stdout);
229
- const compactData = JSON.parse(compact.stdout);
230
- // JSON shape is identical — detail is a text-only projection
231
- expect(Object.keys(compactData.data).sort()).toEqual(Object.keys(fullData.data).sort());
232
- }, SLOW);
467
+ it(
468
+ '--json --detail compact returns the same full JSON as --json (contract)',
469
+ async () => {
470
+ const full = await runCli(['--json', 'component', 'Button'], cwd);
471
+ const compact = await runCli(
472
+ ['--json', 'component', 'Button', '--detail', 'compact'],
473
+ cwd,
474
+ );
475
+ expect(full.code).toBe(0);
476
+ expect(compact.code).toBe(0);
477
+ const fullData = JSON.parse(full.stdout);
478
+ const compactData = JSON.parse(compact.stdout);
479
+ // JSON shape is identical — detail is a text-only projection
480
+ expect(Object.keys(compactData.data).sort()).toEqual(
481
+ Object.keys(fullData.data).sort(),
482
+ );
483
+ },
484
+ SLOW,
485
+ );
233
486
  });