@astryxdesign/cli 0.1.6 → 0.1.7-canary.04cd8f7

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 (107) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +115 -19
  3. package/docs/cli-integrations.doc.mjs +150 -0
  4. package/docs/getting-started.doc.mjs +9 -9
  5. package/docs/internationalization.doc.mjs +243 -0
  6. package/docs/layout.doc.dense.mjs +5 -0
  7. package/docs/migration.doc.mjs +18 -18
  8. package/docs/principles.doc.dense.mjs +5 -5
  9. package/docs/principles.doc.mjs +6 -6
  10. package/docs/principles.doc.zh.mjs +5 -5
  11. package/docs/styling-libraries.doc.mjs +3 -3
  12. package/docs/styling.doc.mjs +4 -4
  13. package/docs/theme.doc.dense.mjs +12 -12
  14. package/docs/theme.doc.mjs +7 -7
  15. package/docs/theme.doc.zh.mjs +10 -10
  16. package/docs/tokens.doc.dense.mjs +6 -7
  17. package/docs/tokens.doc.mjs +1 -1
  18. package/docs/tokens.doc.zh.mjs +6 -7
  19. package/docs/working-with-ai.doc.mjs +18 -18
  20. package/package.json +13 -10
  21. package/src/api/docOverlays.test.mjs +133 -0
  22. package/src/api/docs.mjs +14 -2
  23. package/src/api/doctor.mjs +3 -3
  24. package/src/api/integration-block-exports.test.mjs +240 -0
  25. package/src/api/template-suffix.test.mjs +246 -0
  26. package/src/api/template.mjs +104 -28
  27. package/src/api/validate-integration.mjs +0 -8
  28. package/src/codemods/__tests__/registry.test.mjs +1 -0
  29. package/src/codemods/ensure-jscodeshift.mjs +11 -27
  30. package/src/codemods/registry.mjs +1 -0
  31. package/src/codemods/run-codemod.mjs +1 -1
  32. package/src/codemods/runner.mjs +2 -2
  33. package/src/codemods/transforms/v0.1.7/__tests__/migrate-table-tableprops-to-direct-props.test.mjs +120 -0
  34. package/src/codemods/transforms/v0.1.7/__tests__/rename-table-renderprops-styles-to-xstyle.test.mjs +112 -0
  35. package/src/codemods/transforms/v0.1.7/index.mjs +27 -0
  36. package/src/codemods/transforms/v0.1.7/migrate-table-tableprops-to-direct-props.mjs +188 -0
  37. package/src/codemods/transforms/v0.1.7/rename-table-renderprops-styles-to-xstyle.mjs +197 -0
  38. package/src/commands/agent-docs.mjs +21 -8
  39. package/src/commands/agent-docs.test.mjs +31 -4
  40. package/src/commands/build-theme.mjs +10 -71
  41. package/src/commands/build.mjs +15 -15
  42. package/src/commands/component/index.mjs +4 -4
  43. package/src/commands/discover.mjs +7 -5
  44. package/src/commands/docs.mjs +4 -4
  45. package/src/commands/hook/index.mjs +4 -4
  46. package/src/commands/init.mjs +48 -152
  47. package/src/commands/init.next-steps.test.mjs +1 -1
  48. package/src/commands/interactive-guard.test.mjs +19 -22
  49. package/src/commands/json-contract.test.mjs +1 -1
  50. package/src/commands/layout.mjs +1 -1
  51. package/src/commands/search.mjs +4 -4
  52. package/src/commands/swizzle.mjs +11 -34
  53. package/src/commands/template.mjs +11 -31
  54. package/src/commands/upgrade.mjs +9 -6
  55. package/src/commands/upgrade.test.mjs +1 -1
  56. package/src/config.mjs +5 -14
  57. package/src/doc.mjs +27 -0
  58. package/src/doc.test.mjs +383 -0
  59. package/src/index.mjs +5 -6
  60. package/src/integration.mjs +4 -15
  61. package/src/lib/component-discovery.importpath.test.mjs +59 -0
  62. package/src/lib/component-discovery.mjs +15 -5
  63. package/src/lib/component-format.mjs +47 -14
  64. package/src/lib/component-format.test.mjs +95 -1
  65. package/src/lib/component-loader.mjs +104 -2
  66. package/src/lib/componentDocOverlay.test.mjs +111 -0
  67. package/src/lib/config-schema.mjs +0 -30
  68. package/src/lib/hook-format.mjs +8 -3
  69. package/src/lib/term-log.mjs +48 -0
  70. package/src/lib/xle/registry.mjs +0 -5
  71. package/src/schemas/doc-schema.mjs +226 -0
  72. package/src/schemas/template-schema.mjs +47 -0
  73. package/src/template.mjs +9 -67
  74. package/src/types/config.d.ts +11 -66
  75. package/src/types/doc.d.ts +23 -0
  76. package/src/types/integration.d.ts +7 -18
  77. package/src/types/template-api.d.ts +14 -50
  78. package/src/utils/package-manager.mjs +78 -0
  79. package/src/utils/package-manager.test.mjs +108 -1
  80. package/src/utils/path-safety.mjs +0 -18
  81. package/src/utils/update-check.mjs +2 -1
  82. package/templates/blocks/components/Avatar/AvatarGroup.tsx +5 -7
  83. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +4 -6
  84. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +3 -5
  85. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +8 -6
  86. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +3 -5
  87. package/templates/blocks/components/ChatComposerInput/ChatComposerInputControlledInput.tsx +1 -1
  88. package/templates/blocks/components/ChatComposerInput/ChatComposerInputDisabled.tsx +1 -1
  89. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMentionTrigger.tsx +1 -1
  90. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMultipleTriggers.tsx +1 -1
  91. package/templates/blocks/components/ChatComposerInput/ChatComposerInputShowcase.tsx +1 -1
  92. package/templates/blocks/components/ChatComposerInput/ChatComposerInputSlashCommands.tsx +1 -1
  93. package/templates/blocks/components/TabList/TabListTabsWithActions.doc.mjs +1 -1
  94. package/templates/blocks/components/TabList/TabListTabsWithActions.tsx +2 -7
  95. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +14 -0
  96. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +41 -0
  97. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +13 -0
  98. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +78 -0
  99. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +14 -0
  100. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +38 -0
  101. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +14 -0
  102. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +47 -0
  103. package/templates/pages/ide/page.tsx +35 -41
  104. package/templates/pages/theme-showcase/page.tsx +7 -7
  105. package/templates/themes/neutral/neutralTheme.ts +63 -32
  106. package/src/utils/interactive.mjs +0 -76
  107. package/src/utils/interactive.test.mjs +0 -70
@@ -0,0 +1,383 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ import {afterEach, beforeEach, describe, expect, it} from 'vitest';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import {
7
+ createComponentDoc,
8
+ createFunctionDoc,
9
+ createDoc,
10
+ ComponentDocSchema,
11
+ ComponentDocKindSchema,
12
+ FunctionDocKindSchema,
13
+ GenericDocKindSchema,
14
+ } from './doc.mjs';
15
+ import {loadComponentDoc} from './lib/component-loader.mjs';
16
+
17
+ // The factories are stamp-only (like createConfig / createBlockTemplate): they
18
+ // inject a `type` discriminant and are otherwise identity, performing NO
19
+ // runtime validation. Validation happens at the LOAD boundary
20
+ // (loadComponentDoc runs the loaded value through ComponentDocSchema), so the
21
+ // rejection cases assert the schema rejects rather than the factory throwing.
22
+
23
+ const goodComponent = {
24
+ name: 'Widget',
25
+ displayName: 'Widget',
26
+ description: 'A small widget.',
27
+ props: [
28
+ {name: 'label', type: 'string', description: 'Visible label.', required: true},
29
+ {name: 'size', type: "'sm' | 'md'", description: 'Control size.', default: "'md'"},
30
+ ],
31
+ };
32
+
33
+ const goodFunction = {
34
+ name: 'useThing',
35
+ displayName: 'useThing',
36
+ description: 'A thing hook.',
37
+ params: [{name: 'input', type: 'string', description: 'The input.', required: true}],
38
+ returns: [{name: 'value', type: 'string', description: 'The result.'}],
39
+ };
40
+
41
+ const goodGeneric = {
42
+ name: 'Theming',
43
+ displayName: 'Theming',
44
+ description: 'How theming works.',
45
+ };
46
+
47
+ describe('doc factories (stamp-only)', () => {
48
+ it('createComponentDoc stamps type: component and is otherwise identity', () => {
49
+ const doc = createComponentDoc(goodComponent);
50
+ expect(doc.type).toBe('component');
51
+ const {type, ...rest} = doc;
52
+ expect(rest).toEqual(goodComponent);
53
+ });
54
+
55
+ it('createFunctionDoc stamps type: function and is otherwise identity', () => {
56
+ const doc = createFunctionDoc(goodFunction);
57
+ expect(doc.type).toBe('function');
58
+ const {type, ...rest} = doc;
59
+ expect(rest).toEqual(goodFunction);
60
+ });
61
+
62
+ it('createDoc stamps type: generic and is otherwise identity', () => {
63
+ const doc = createDoc(goodGeneric);
64
+ expect(doc.type).toBe('generic');
65
+ const {type, ...rest} = doc;
66
+ expect(rest).toEqual(goodGeneric);
67
+ });
68
+
69
+ it('does NOT validate — stamps invalid shapes unchanged', () => {
70
+ const bogus = {group: 'Buttons'}; // no name
71
+ expect(createComponentDoc(bogus)).toEqual({...bogus, type: 'component'});
72
+ });
73
+ });
74
+
75
+ describe('per-kind schemas (new stamped format)', () => {
76
+ it('ComponentDocKindSchema accepts a valid component doc', () => {
77
+ expect(() =>
78
+ ComponentDocKindSchema.parse(createComponentDoc(goodComponent)),
79
+ ).not.toThrow();
80
+ });
81
+
82
+ it('ComponentDocKindSchema rejects a missing name with a readable message', () => {
83
+ expect(() =>
84
+ ComponentDocKindSchema.parse({type: 'component', name: '', props: []}),
85
+ ).toThrow(/name is required/);
86
+ });
87
+
88
+ it('ComponentDocKindSchema rejects a prop missing its type with a readable message', () => {
89
+ expect(() =>
90
+ ComponentDocKindSchema.parse({
91
+ type: 'component',
92
+ name: 'Widget',
93
+ props: [{name: 'label', description: 'no type'}],
94
+ }),
95
+ ).toThrow(/type/);
96
+ });
97
+
98
+ it('ComponentDocKindSchema surfaces the custom message for an empty prop type', () => {
99
+ expect(() =>
100
+ ComponentDocKindSchema.parse({
101
+ type: 'component',
102
+ name: 'Widget',
103
+ props: [{name: 'label', type: '', description: 'empty type'}],
104
+ }),
105
+ ).toThrow(/prop type is required/);
106
+ });
107
+
108
+ it('FunctionDocKindSchema accepts a valid function doc', () => {
109
+ expect(() =>
110
+ FunctionDocKindSchema.parse(createFunctionDoc(goodFunction)),
111
+ ).not.toThrow();
112
+ });
113
+
114
+ it('FunctionDocKindSchema rejects a function doc missing returns', () => {
115
+ expect(() =>
116
+ FunctionDocKindSchema.parse({
117
+ type: 'function',
118
+ name: 'useThing',
119
+ params: [],
120
+ }),
121
+ ).toThrow();
122
+ });
123
+
124
+ it('GenericDocKindSchema accepts a valid generic doc', () => {
125
+ expect(() =>
126
+ GenericDocKindSchema.parse(createDoc(goodGeneric)),
127
+ ).not.toThrow();
128
+ });
129
+
130
+ it('keeps nested rich blobs loose (usage/theming/playground passthrough)', () => {
131
+ const doc = createComponentDoc({
132
+ ...goodComponent,
133
+ usage: {description: 'Use it.', anatomy: [{name: 'root'}]},
134
+ theming: {targets: [{className: 'astryx-widget'}]},
135
+ playground: {defaults: {label: 'Hi'}},
136
+ examples: [{title: 'Basic', code: '<Widget />'}],
137
+ });
138
+ expect(() => ComponentDocKindSchema.parse(doc)).not.toThrow();
139
+ });
140
+
141
+ it('accepts parent + relatedDocs on the shared base', () => {
142
+ const doc = createComponentDoc({
143
+ ...goodComponent,
144
+ parent: 'WidgetGroup',
145
+ relatedDocs: ['Gauge', 'useThing'],
146
+ group: 'Widgets',
147
+ });
148
+ const parsed = ComponentDocKindSchema.parse(doc);
149
+ expect(parsed.parent).toBe('WidgetGroup');
150
+ expect(parsed.relatedDocs).toEqual(['Gauge', 'useThing']);
151
+ });
152
+ });
153
+
154
+ describe('ComponentDocSchema (load-boundary, both formats)', () => {
155
+ it('accepts a stamped component doc via the per-kind schema', () => {
156
+ expect(() =>
157
+ ComponentDocSchema.parse(createComponentDoc(goodComponent)),
158
+ ).not.toThrow();
159
+ });
160
+
161
+ it('accepts a stamped function doc', () => {
162
+ expect(() =>
163
+ ComponentDocSchema.parse(createFunctionDoc(goodFunction)),
164
+ ).not.toThrow();
165
+ });
166
+
167
+ it('accepts a stamped generic doc', () => {
168
+ expect(() =>
169
+ ComponentDocSchema.parse(createDoc(goodGeneric)),
170
+ ).not.toThrow();
171
+ });
172
+
173
+ it('accepts the OLD loose single-component shape (no type)', () => {
174
+ expect(() => ComponentDocSchema.parse(goodComponent)).not.toThrow();
175
+ });
176
+
177
+ it('accepts the OLD loose multi-component shape (components[])', () => {
178
+ const multi = {
179
+ name: 'Table',
180
+ displayName: 'Table',
181
+ components: [{name: 'TableRow', displayName: 'Table Row', description: 'A row.'}],
182
+ };
183
+ expect(() => ComponentDocSchema.parse(multi)).not.toThrow();
184
+ });
185
+
186
+ it('accepts the OLD loose sub-component shape (subComponentOf)', () => {
187
+ const sub = {
188
+ name: 'GaugeItem',
189
+ subComponentOf: 'Gauge',
190
+ displayName: 'Gauge Item',
191
+ description: 'A gauge item.',
192
+ props: [{name: 'item', type: 'Item', description: 'The item.'}],
193
+ };
194
+ expect(() => ComponentDocSchema.parse(sub)).not.toThrow();
195
+ });
196
+
197
+ it('accepts the OLD loose standalone-hook shape (params + returns, no type)', () => {
198
+ const hook = {
199
+ name: 'useThing',
200
+ displayName: 'useThing',
201
+ params: [{name: 'q', type: 'string', description: 'query'}],
202
+ returns: [{name: 'value', type: 'boolean', description: 'match'}],
203
+ };
204
+ expect(() => ComponentDocSchema.parse(hook)).not.toThrow();
205
+ });
206
+
207
+ it('accepts BOTH parent and legacy subComponentOf', () => {
208
+ const withParent = {name: 'A', parent: 'B', props: []};
209
+ const withSubComponentOf = {
210
+ name: 'A',
211
+ subComponentOf: 'B',
212
+ description: 'sub',
213
+ props: [],
214
+ };
215
+ expect(() => ComponentDocSchema.parse(withParent)).not.toThrow();
216
+ expect(() => ComponentDocSchema.parse(withSubComponentOf)).not.toThrow();
217
+ });
218
+
219
+ it('accepts BOTH relatedDocs and legacy relatedComponents/relatedHooks', () => {
220
+ const legacy = {
221
+ name: 'useThing',
222
+ displayName: 'useThing',
223
+ params: [],
224
+ returns: [],
225
+ relatedComponents: ['Gauge'],
226
+ relatedHooks: ['useOther'],
227
+ };
228
+ const modern = {...goodComponent, relatedDocs: ['Gauge', 'useThing']};
229
+ expect(() => ComponentDocSchema.parse(legacy)).not.toThrow();
230
+ expect(() =>
231
+ ComponentDocSchema.parse(createComponentDoc(modern)),
232
+ ).not.toThrow();
233
+ });
234
+
235
+ it('passes through loose extras (usage, playground, theming, importPath, showcase)', () => {
236
+ const loose = {
237
+ ...goodComponent,
238
+ usage: {description: 'Use it wisely.'},
239
+ playground: {defaults: {label: 'Hi'}},
240
+ theming: {targets: [{className: 'astryx-widget'}]},
241
+ keywords: ['widget', 'thing'],
242
+ category: 'Content',
243
+ isHiddenFromOverview: true,
244
+ importPath: '@astryxdesign/core/Widget',
245
+ showcase: 'WidgetHero',
246
+ };
247
+ const parsed = ComponentDocSchema.parse(loose);
248
+ expect(parsed.importPath).toBe('@astryxdesign/core/Widget');
249
+ expect(parsed.showcase).toBe('WidgetHero');
250
+ });
251
+
252
+ it('rejects a doc with no name', () => {
253
+ expect(() => ComponentDocSchema.parse({props: []})).toThrow();
254
+ });
255
+
256
+ it('rejects an empty name with a readable message', () => {
257
+ expect(() => ComponentDocSchema.parse({name: '', props: []})).toThrow(
258
+ /name is required/,
259
+ );
260
+ });
261
+ });
262
+
263
+ describe('loadComponentDoc (load boundary)', () => {
264
+ let tmpDir;
265
+
266
+ beforeEach(() => {
267
+ tmpDir = fs.mkdtempSync(path.join(process.cwd(), '.astryx-doc-test-'));
268
+ });
269
+
270
+ afterEach(() => {
271
+ fs.rmSync(tmpDir, {recursive: true, force: true});
272
+ });
273
+
274
+ const docModule = () => JSON.stringify(path.resolve(import.meta.dirname, 'doc.mjs'));
275
+
276
+ it('loads a .doc.ts fixture via jiti (createComponentDoc default export)', async () => {
277
+ const file = path.join(tmpDir, 'Widget.doc.ts');
278
+ fs.writeFileSync(
279
+ file,
280
+ [
281
+ `import {createComponentDoc} from ${docModule()};`,
282
+ 'export default createComponentDoc({',
283
+ " name: 'Widget',",
284
+ " displayName: 'Widget',",
285
+ " description: 'A small widget.',",
286
+ ' props: [',
287
+ " {name: 'label', type: 'string', description: 'Visible label.', required: true},",
288
+ ' ],',
289
+ '});',
290
+ ].join('\n'),
291
+ );
292
+ const docs = await loadComponentDoc(file);
293
+ expect(docs.type).toBe('component');
294
+ expect(docs.name).toBe('Widget');
295
+ expect(docs.props).toHaveLength(1);
296
+ });
297
+
298
+ it('loads a .doc.ts fixture via jiti (createFunctionDoc default export)', async () => {
299
+ const file = path.join(tmpDir, 'useThing.doc.ts');
300
+ fs.writeFileSync(
301
+ file,
302
+ [
303
+ `import {createFunctionDoc} from ${docModule()};`,
304
+ 'export default createFunctionDoc({',
305
+ " name: 'useThing',",
306
+ " displayName: 'useThing',",
307
+ " params: [{name: 'input', type: 'string', description: 'The input.'}],",
308
+ " returns: [{name: 'value', type: 'string', description: 'The result.'}],",
309
+ '});',
310
+ ].join('\n'),
311
+ );
312
+ const docs = await loadComponentDoc(file);
313
+ expect(docs.type).toBe('function');
314
+ expect(docs.params).toHaveLength(1);
315
+ expect(docs.returns).toHaveLength(1);
316
+ });
317
+
318
+ it('loads a .doc.ts fixture via jiti (createDoc generic default export)', async () => {
319
+ const file = path.join(tmpDir, 'Theming.doc.ts');
320
+ fs.writeFileSync(
321
+ file,
322
+ [
323
+ `import {createDoc} from ${docModule()};`,
324
+ 'export default createDoc({',
325
+ " name: 'Theming',",
326
+ " displayName: 'Theming',",
327
+ " description: 'How theming works.',",
328
+ '});',
329
+ ].join('\n'),
330
+ );
331
+ const docs = await loadComponentDoc(file);
332
+ expect(docs.type).toBe('generic');
333
+ expect(docs.name).toBe('Theming');
334
+ });
335
+
336
+ it('REGRESSION: loads the OLD loose `export const docs = {}` format', async () => {
337
+ const file = path.join(tmpDir, 'Legacy.doc.mjs');
338
+ fs.writeFileSync(
339
+ file,
340
+ [
341
+ 'export const docs = {',
342
+ " name: 'Legacy',",
343
+ " displayName: 'Legacy',",
344
+ " description: 'A loose named-export doc.',",
345
+ " relatedComponents: ['Gauge'],",
346
+ " relatedHooks: ['useThing'],",
347
+ " importPath: '@astryxdesign/core/Legacy',",
348
+ " props: [{name: 'value', type: 'string', description: 'A value.'}],",
349
+ '};',
350
+ ].join('\n'),
351
+ );
352
+ const docs = await loadComponentDoc(file);
353
+ expect(docs.name).toBe('Legacy');
354
+ expect(docs.props[0].name).toBe('value');
355
+ expect(docs.relatedComponents).toEqual(['Gauge']);
356
+ expect(docs.importPath).toBe('@astryxdesign/core/Legacy');
357
+ });
358
+
359
+ it('REGRESSION: loads the OLD loose standalone-hook `docs` format', async () => {
360
+ const file = path.join(tmpDir, 'useLegacy.doc.mjs');
361
+ fs.writeFileSync(
362
+ file,
363
+ [
364
+ 'export const docs = {',
365
+ " name: 'useLegacy',",
366
+ " displayName: 'useLegacy',",
367
+ " params: [{name: 'q', type: 'string', description: 'query'}],",
368
+ " returns: [{name: 'value', type: 'boolean', description: 'match'}],",
369
+ " relatedHooks: ['useThing'],",
370
+ '};',
371
+ ].join('\n'),
372
+ );
373
+ const docs = await loadComponentDoc(file);
374
+ expect(docs.name).toBe('useLegacy');
375
+ expect(docs.returns[0].name).toBe('value');
376
+ });
377
+
378
+ it('throws a readable error for an invalid doc', async () => {
379
+ const file = path.join(tmpDir, 'Bad.doc.mjs');
380
+ fs.writeFileSync(file, 'export default {group: "Buttons"};\n');
381
+ await expect(loadComponentDoc(file)).rejects.toThrow(/is invalid|name/);
382
+ });
383
+ });
package/src/index.mjs CHANGED
@@ -12,7 +12,7 @@ import {fileURLToPath} from 'node:url';
12
12
  import * as fs from 'node:fs';
13
13
  import * as path from 'node:path';
14
14
  import {checkForUpdate} from './utils/update-check.mjs';
15
- import {getRunPrefix} from './utils/package-manager.mjs';
15
+ import {getCliInvocation} from './utils/package-manager.mjs';
16
16
  import {API_VERSION, setJsonMode} from './lib/json.mjs';
17
17
  import {buildManifest} from './lib/manifest.mjs';
18
18
  import {cliError} from './lib/cli-error.mjs';
@@ -174,7 +174,7 @@ function fullCommandName(actionCommand) {
174
174
  *
175
175
  * If --json is set on a command that is not on the JSON_SUPPORTED allowlist,
176
176
  * emit a structured error envelope and exit 1 — without running the command's
177
- * action (so no filesystem mutations, no clack prompts, no spawned processes).
177
+ * action (so no filesystem mutations, no interactive prompts, no spawned processes).
178
178
  *
179
179
  * This is the single source of truth for "command does not support --json".
180
180
  * Individual commands should NOT re-check this; they may assume that if their
@@ -305,15 +305,14 @@ program
305
305
  console.log(` ${c.name}${tag}`);
306
306
  if (c.description) console.log(` ${c.description}`);
307
307
  }
308
- console.log(`\nRun \`astryx manifest --json\` for the full structured manifest.\n`);
308
+ console.log(`\nRun \`${getCliInvocation()} manifest --json\` for the full structured manifest.\n`);
309
309
  });
310
310
 
311
311
  // Hidden command used by package.json postinstall scripts
312
312
  program
313
313
  .command('postinstall', {hidden: true})
314
314
  .action(() => {
315
- const run = getRunPrefix();
316
- const r = `${run} xds`;
315
+ const r = getCliInvocation();
317
316
  const pad = (s, len) => s + ' '.repeat(Math.max(0, len - s.length));
318
317
  const W = 49; // inner width of the box
319
318
  const line = (s) => ` │ ${pad(s, W)}│`;
@@ -323,7 +322,7 @@ ${line('')}
323
322
  ${line(' Design system installed!')}
324
323
  ${line('')}
325
324
  ${line(' Get started:')}
326
- ${line(` ${r} init Interactive setup`)}
325
+ ${line(` ${r} init Setup + AI agent docs`)}
327
326
  ${line(` ${r} --help See all commands`)}
328
327
  ${line('')}
329
328
  ${line(' Or run directly:')}
@@ -1,19 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * Type-preserving helper for an Astryx integration manifest.
5
- *
6
- * This is an intentionally tiny runtime identity function: it returns its
7
- * argument unchanged. Its value is the exported TypeScript surface from
8
- * `@astryxdesign/cli/integration`, so manifests get editor/type feedback
9
- * without coupling to CLI internals. Validation is NOT performed here — it
10
- * happens at the load boundary (see `loadModuleWithSchema` +
11
- * `AstryxIntegrationSchema`).
12
- *
13
- * @template {import('./types/integration').AstryxIntegration} T
14
- * @param {T} integration
15
- * @returns {T}
4
+ * Re-export of the integration-authoring helper, which now lives in
5
+ * `@astryxdesign/core/authoring`. Kept here so existing
6
+ * `@astryxdesign/cli/integration` imports continue to work unchanged.
16
7
  */
17
- export function createIntegration(integration) {
18
- return integration;
19
- }
8
+ export {createIntegration} from '@astryxdesign/core/authoring';
@@ -0,0 +1,59 @@
1
+ // Copyright (c) Meta Platforms, Inc. and affiliates.
2
+
3
+ /**
4
+ * @file Verifies that `.ts`-authored sources (hooks and other functions) are
5
+ * found by `findComponentSource`, so `resolveImportPath` derives a
6
+ * tree-shakeable subpath instead of falling back to the bare package root.
7
+ *
8
+ * Runs against the real packages/core source, not mocks.
9
+ */
10
+
11
+ import {describe, it, expect} from 'vitest';
12
+ import {
13
+ findComponentSource,
14
+ resolveImportPath,
15
+ } from './component-discovery.mjs';
16
+ import {findCoreDir} from '../utils/paths.mjs';
17
+
18
+ describe('findComponentSource resolves .ts-authored sources', () => {
19
+ const coreDir = findCoreDir();
20
+
21
+ it('finds a hook authored as a .ts file (not just .tsx)', () => {
22
+ const src = findComponentSource(coreDir, 'useMediaQuery');
23
+ expect(src).toBeTruthy();
24
+ expect(src.endsWith('useMediaQuery.ts')).toBe(true);
25
+ });
26
+
27
+ it('still finds a component authored as .tsx', () => {
28
+ const src = findComponentSource(coreDir, 'Button');
29
+ expect(src).toBeTruthy();
30
+ expect(src.endsWith('.tsx')).toBe(true);
31
+ });
32
+ });
33
+
34
+ describe('resolveImportPath reproduces authored hook importPaths', () => {
35
+ const coreDir = findCoreDir();
36
+
37
+ // Representative sample of core hooks whose sources are `.ts` files. Before
38
+ // the `.ts` fix these fell back to bare `@astryxdesign/core`; the derived
39
+ // subpath must now match the value each hook's doc currently authors.
40
+ const cases = [
41
+ ['useMediaQuery', '@astryxdesign/core/hooks'],
42
+ ['useResizable', '@astryxdesign/core/Resizable'],
43
+ ['useTheme', '@astryxdesign/core/theme'],
44
+ ['useFocusTrap', '@astryxdesign/core/hooks'],
45
+ ['useOverflow', '@astryxdesign/core/hooks'],
46
+ ];
47
+
48
+ for (const [name, expected] of cases) {
49
+ it(`${name} derives ${expected}`, () => {
50
+ expect(resolveImportPath(coreDir, name)).toBe(expected);
51
+ });
52
+ }
53
+
54
+ it('never falls back to the bare package root for a .ts hook', () => {
55
+ for (const [name] of cases) {
56
+ expect(resolveImportPath(coreDir, name)).not.toBe('@astryxdesign/core');
57
+ }
58
+ });
59
+ });
@@ -273,14 +273,24 @@ export function findComponentReadme(coreDir, name) {
273
273
  * For "Button" finds src/Button/XDSButton.tsx
274
274
  * For "Layout" finds src/Layout/XDSLayout/XDSLayout.tsx
275
275
  * For "Card" finds src/Layout/Container/XDSCard.tsx (deep search fallback)
276
+ *
277
+ * Hooks and other functions are authored as `.ts` (e.g. `useMediaQuery.ts`,
278
+ * `useResizable.ts`), so `.ts` candidates are searched alongside `.tsx`. Without
279
+ * this, deriving the import path for a `.ts`-authored function falls back to the
280
+ * bare `@astryxdesign/core` root instead of its tree-shakeable subpath.
276
281
  */
277
282
  export function findComponentSource(coreDir, name) {
278
283
  const srcDir = path.join(coreDir, 'src');
279
- // Try the prefixed form (`XDSButton.tsx`) first since that is the current
280
- // on-disk convention, then the bare form (`Button.tsx`) that the Astryx-prefix
281
- // migration (P4) renames to. Listing the prefixed name first keeps behavior
282
- // identical until files are actually renamed.
283
- const candidateFiles = [`XDS${name}.tsx`, `${name}.tsx`];
284
+ // Try the prefixed forms (`XDSButton.tsx`) first since that is the current
285
+ // on-disk convention, then the bare forms (`Button.tsx`) that the Astryx-prefix
286
+ // migration (P4) renames to. `.tsx` before `.ts` within each so a component's
287
+ // `.tsx` wins over a same-named `.ts` helper; `.ts` covers hooks/functions.
288
+ const candidateFiles = [
289
+ `XDS${name}.tsx`,
290
+ `${name}.tsx`,
291
+ `XDS${name}.ts`,
292
+ `${name}.ts`,
293
+ ];
284
294
 
285
295
  function searchDir(dirPath) {
286
296
  if (!fs.existsSync(dirPath)) return null;
@@ -6,6 +6,7 @@
6
6
 
7
7
  import {discoverComponents, findComponentReadme, resolveImportPath} from './component-discovery.mjs';
8
8
  import {loadDocs} from './component-loader.mjs';
9
+ import {getCliInvocation} from '../utils/package-manager.mjs';
9
10
 
10
11
  /**
11
12
  * Derive the `defineTheme` component-override key from a theming target.
@@ -34,15 +35,30 @@ function getTargetDataAttributes(target) {
34
35
  ];
35
36
  }
36
37
 
38
+ /**
39
+ * Escape a value for a markdown table cell.
40
+ *
41
+ * A prop type is a union, and a union is spelled with the same `|` that GFM
42
+ * uses to separate cells — backticks do not protect it. A row with more cells
43
+ * than the header has columns gets the excess *discarded*, so an unescaped
44
+ * `gap: 0 | 0.5 | ...` silently eats its own Default and Description columns.
45
+ * <!-- SYNC: packages/core/src/Markdown/parser.ts (splits on unescaped pipes) -->
46
+ */
47
+ export function mdCell(value) {
48
+ return String(value ?? '').replace(/\|/g, '\\|');
49
+ }
50
+
37
51
  function formatPropsTable(props) {
38
52
  if (!props || props.length === 0) return '';
39
53
  const lines = [];
40
54
  lines.push('| Prop | Type | Default | Description |');
41
55
  lines.push('|------|------|---------|-------------|');
42
56
  for (const p of props) {
43
- const def = p.default ? `\`${p.default}\`` : '—';
57
+ const def = p.default ? `\`${mdCell(p.default)}\`` : '—';
44
58
  const req = p.required ? ' **(required)**' : '';
45
- lines.push(`| \`${p.name}\` | \`${p.type}\` | ${def} | ${p.description}${req} |`);
59
+ lines.push(
60
+ `| \`${mdCell(p.name)}\` | \`${mdCell(p.type)}\` | ${def} | ${mdCell(p.description)}${req} |`,
61
+ );
46
62
  }
47
63
  return lines.join('\n');
48
64
  }
@@ -66,7 +82,7 @@ function formatSubComponent(comp) {
66
82
  if (table) {
67
83
  out.push(table + '\n');
68
84
  } else {
69
- out.push(`See \`astryx component ${comp.name}\` for props and usage.\n`);
85
+ out.push(`See \`${getCliInvocation()} component ${comp.name}\` for props and usage.\n`);
70
86
  }
71
87
  return out;
72
88
  }
@@ -177,7 +193,7 @@ export function formatFull(docs, options = {}) {
177
193
  sections.push('|---------|----------|-------------|');
178
194
  for (const el of docs.usage.anatomy) {
179
195
  const req = el.required ? 'Yes' : 'No';
180
- sections.push(`| ${el.name} | ${req} | ${el.description} |`);
196
+ sections.push(`| ${mdCell(el.name)} | ${req} | ${mdCell(el.description)} |`);
181
197
  }
182
198
  sections.push('');
183
199
  }
@@ -282,7 +298,9 @@ export function formatFull(docs, options = {}) {
282
298
  varLines.push('| CSS Variable | Default | Description |');
283
299
  varLines.push('|-------------|---------|-------------|');
284
300
  for (const v of publicVars) {
285
- varLines.push(`| \`${v.name}\` | \`${v.default}\` | ${v.description} |`);
301
+ varLines.push(
302
+ `| \`${mdCell(v.name)}\` | \`${mdCell(v.default)}\` | ${mdCell(v.description)} |`,
303
+ );
286
304
  }
287
305
  sections.push(varLines.join('\n') + '\n');
288
306
  }
@@ -378,8 +396,8 @@ export function formatCompact(docs, componentName, importHint) {
378
396
  propLines.push('| CSS Property | Sets |');
379
397
  propLines.push('|-------------|------|');
380
398
  for (const d of docs.theming.derived) {
381
- const target = d.expand === 'container' ? 'container layout tokens' : (d.vars || []).map(v => `\`${v}\``).join(', ');
382
- propLines.push(`| \`${d.property}\` | ${target} |`);
399
+ const target = d.expand === 'container' ? 'container layout tokens' : (d.vars || []).map(v => `\`${mdCell(v)}\``).join(', ');
400
+ propLines.push(`| \`${mdCell(d.property)}\` | ${target} |`);
383
401
  }
384
402
  sections.push(propLines.join('\n') + '\n');
385
403
  }
@@ -395,6 +413,19 @@ export function formatCompact(docs, componentName, importHint) {
395
413
  *
396
414
  * For multi-component docs, extracts the entry matching componentName.
397
415
  */
416
+ /**
417
+ * Longest union `--detail brief` will spell out inside the one-line signature.
418
+ *
419
+ * The signature is a glance, not a reference: a six-member enum like
420
+ * `start|center|end|between|around|evenly` reads at a glance, an eleven-member
421
+ * spacing scale does not — and Stack carries four of them (gap, padding,
422
+ * paddingInline, paddingBlock), so it printed the same scale four times. Longer
423
+ * unions fall through to the terse prop list, exactly as they did when the type
424
+ * was still the bare name `SpacingStep`. The full values are always one
425
+ * `astryx component <Name>` away.
426
+ */
427
+ const SIGNATURE_UNION_MAX_MEMBERS = 8;
428
+
398
429
  export function formatBrief(docs, componentName, importHint, options = {}) {
399
430
  const displayName = componentName.startsWith('XDS')
400
431
  ? componentName.slice(3)
@@ -421,13 +452,15 @@ export function formatBrief(docs, componentName, importHint, options = {}) {
421
452
  const otherProps = [];
422
453
 
423
454
  for (const prop of props) {
424
- if (prop.type.includes('|') && !prop.type.includes('ReactNode')) {
425
- const values = prop.type
426
- .replace(/['"]/g, '')
427
- .split('|')
428
- .map(v => v.trim())
429
- .join('|');
430
- signatureProps.push(`${prop.name}: ${values}`);
455
+ const values =
456
+ prop.type.includes('|') && !prop.type.includes('ReactNode')
457
+ ? prop.type
458
+ .replace(/['"]/g, '')
459
+ .split('|')
460
+ .map(v => v.trim())
461
+ : null;
462
+ if (values && values.length <= SIGNATURE_UNION_MAX_MEMBERS) {
463
+ signatureProps.push(`${prop.name}: ${values.join('|')}`);
431
464
  } else if (prop.required) {
432
465
  otherProps.unshift(`${prop.name}: ${prop.type.split('|')[0].trim()}`);
433
466
  } else {