@astryxdesign/cli 0.1.6-canary.fec872b → 0.1.6

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 (54) hide show
  1. package/package.json +9 -13
  2. package/src/api/template.mjs +28 -104
  3. package/src/api/validate-integration.mjs +8 -0
  4. package/src/codemods/__tests__/registry.test.mjs +0 -1
  5. package/src/codemods/registry.mjs +0 -1
  6. package/src/config.mjs +14 -5
  7. package/src/integration.mjs +15 -4
  8. package/src/lib/component-discovery.mjs +5 -15
  9. package/src/lib/component-format.mjs +13 -45
  10. package/src/lib/component-format.test.mjs +1 -95
  11. package/src/lib/component-loader.mjs +2 -104
  12. package/src/lib/config-schema.mjs +30 -0
  13. package/src/lib/hook-format.mjs +3 -8
  14. package/src/lib/xle/registry.mjs +5 -0
  15. package/src/template.mjs +67 -9
  16. package/src/types/config.d.ts +66 -11
  17. package/src/types/integration.d.ts +18 -7
  18. package/src/types/template-api.d.ts +50 -14
  19. package/templates/blocks/components/Avatar/AvatarGroup.tsx +7 -5
  20. package/templates/blocks/components/Avatar/AvatarShowcase.tsx +6 -4
  21. package/templates/blocks/components/Avatar/AvatarUserCard.tsx +5 -3
  22. package/templates/blocks/components/Avatar/AvatarWithImage.tsx +6 -8
  23. package/templates/blocks/components/Avatar/AvatarWithStatus.tsx +5 -3
  24. package/templates/blocks/components/ChatComposerInput/ChatComposerInputControlledInput.tsx +1 -1
  25. package/templates/blocks/components/ChatComposerInput/ChatComposerInputDisabled.tsx +1 -1
  26. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMentionTrigger.tsx +1 -1
  27. package/templates/blocks/components/ChatComposerInput/ChatComposerInputMultipleTriggers.tsx +1 -1
  28. package/templates/blocks/components/ChatComposerInput/ChatComposerInputShowcase.tsx +1 -1
  29. package/templates/blocks/components/ChatComposerInput/ChatComposerInputSlashCommands.tsx +1 -1
  30. package/templates/pages/ide/page.tsx +41 -35
  31. package/templates/pages/theme-showcase/page.tsx +7 -7
  32. package/templates/themes/neutral/neutralTheme.ts +32 -63
  33. package/docs/integration-authoring.md +0 -105
  34. package/docs/internationalization.doc.mjs +0 -243
  35. package/src/api/integration-block-exports.test.mjs +0 -240
  36. package/src/api/template-suffix.test.mjs +0 -246
  37. package/src/codemods/transforms/v0.1.7/__tests__/migrate-table-tableprops-to-direct-props.test.mjs +0 -120
  38. package/src/codemods/transforms/v0.1.7/index.mjs +0 -19
  39. package/src/codemods/transforms/v0.1.7/migrate-table-tableprops-to-direct-props.mjs +0 -188
  40. package/src/doc.mjs +0 -27
  41. package/src/doc.test.mjs +0 -383
  42. package/src/lib/component-discovery.importpath.test.mjs +0 -59
  43. package/src/lib/componentDocOverlay.test.mjs +0 -111
  44. package/src/schemas/doc-schema.mjs +0 -226
  45. package/src/schemas/template-schema.mjs +0 -47
  46. package/src/types/doc.d.ts +0 -23
  47. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +0 -14
  48. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +0 -41
  49. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +0 -13
  50. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +0 -78
  51. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +0 -14
  52. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +0 -38
  53. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +0 -14
  54. package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +0 -47
@@ -1,188 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Codemod: Migrate Table tableProps to direct root props
5
- * @see https://github.com/facebook/astryx/issues/3679
6
- *
7
- * `tableProps` (typed HTMLAttributes<HTMLTableElement>, nesting HTML
8
- * attributes one level deep) is deprecated. As of v0.1.7, <Table> honors
9
- * className/style/xstyle directly and spreads all other BaseProps
10
- * (id, aria-*, data-*, event handlers, ...) onto the root <table>, with
11
- * direct props taking precedence over tableProps.
12
- *
13
- * This codemod lifts object-literal `tableProps` keys into sibling JSX
14
- * props:
15
- *
16
- * - tableProps={{className: 'x', style: s}} → className="x" style={s}
17
- * - String-literal keys ('aria-label', 'data-testid') become hyphenated
18
- * JSX attributes.
19
- * - Keys that collide with an existing sibling attribute (or fail the
20
- * attribute-name guard) are kept inside a shrunken tableProps with a
21
- * trailing TODO comment for manual migration.
22
- * - Dynamic values (tableProps={props}, tableProps={fn()}, objects with
23
- * spreads/computed keys/methods) are left untouched with a TODO comment.
24
- *
25
- * Only elements whose name resolves to a `Table` import (alias-aware)
26
- * from an Astryx core source are rewritten. No import changes are needed.
27
- */
28
-
29
- export const meta = {
30
- title: 'Migrate Table tableProps to direct root props',
31
- description:
32
- 'Lifts object-literal `tableProps` keys on <Table> into sibling JSX props ' +
33
- '(className, style, id, aria-*, data-*, event handlers). Dynamic or ' +
34
- 'colliding entries are kept and annotated with a TODO comment.',
35
- pr: '#3679',
36
- };
37
-
38
- /** Import sources that provide the Astryx Table component. */
39
- const TABLE_IMPORT_SOURCES = new Set([
40
- '@astryxdesign/core',
41
- '@astryxdesign/core/Table',
42
- '@xds/core',
43
- '@xds/core/Table',
44
- ]);
45
-
46
- /** Keys must be valid JSX attribute names to be lifted. */
47
- const LIFTABLE_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_-]*$/;
48
-
49
- const TODO_COMMENT =
50
- ' TODO(astryx): tableProps is deprecated — merge these into direct props manually ';
51
-
52
- /**
53
- * Extract the static key name from an object property, or null when the
54
- * property is not a simple liftable entry (spread, computed key, method,
55
- * getter/setter, non-string literal key).
56
- */
57
- function getPropertyKeyName(prop) {
58
- if (prop.type !== 'ObjectProperty' && prop.type !== 'Property') return null;
59
- // espree-style Property nodes: skip methods and accessors
60
- if (prop.method || (prop.kind != null && prop.kind !== 'init')) return null;
61
- if (prop.computed) return null;
62
- const key = prop.key;
63
- if (key.type === 'Identifier') return key.name;
64
- if (
65
- (key.type === 'StringLiteral' || key.type === 'Literal') &&
66
- typeof key.value === 'string'
67
- ) {
68
- return key.value;
69
- }
70
- return null;
71
- }
72
-
73
- export default function transformer(file, api) {
74
- const j = api.jscodeshift;
75
- const root = j(file.source);
76
- let hasChanges = false;
77
-
78
- // --- 1. Track local names for the Table import (alias-aware) ---
79
- const tableLocals = new Set();
80
- root.find(j.ImportDeclaration).forEach((path) => {
81
- if (!TABLE_IMPORT_SOURCES.has(path.node.source.value)) return;
82
- for (const spec of path.node.specifiers ?? []) {
83
- if (spec.type === 'ImportSpecifier' && spec.imported.name === 'Table') {
84
- tableLocals.add(spec.local.name);
85
- }
86
- }
87
- });
88
-
89
- if (tableLocals.size === 0) return undefined;
90
-
91
- function attachTodo(attr) {
92
- if (!attr.comments) attr.comments = [];
93
- if (attr.comments.some((c) => c.value === TODO_COMMENT)) return;
94
- attr.comments.push(j.commentBlock(TODO_COMMENT, false, true));
95
- hasChanges = true;
96
- }
97
-
98
- function buildAttributeValue(valueNode) {
99
- if (
100
- valueNode.type === 'StringLiteral' ||
101
- (valueNode.type === 'Literal' && typeof valueNode.value === 'string')
102
- ) {
103
- return j.stringLiteral(valueNode.value);
104
- }
105
- return j.jsxExpressionContainer(valueNode);
106
- }
107
-
108
- // --- 2. Rewrite tableProps on tracked <Table> elements ---
109
- root.find(j.JSXOpeningElement).forEach((path) => {
110
- const name = path.node.name;
111
- const componentName = name.type === 'JSXIdentifier' ? name.name : null;
112
- if (!componentName || !tableLocals.has(componentName)) return;
113
-
114
- const attrs = path.node.attributes;
115
- const tablePropsAttr = attrs.find(
116
- (a) => a.type === 'JSXAttribute' && a.name?.name === 'tableProps',
117
- );
118
- if (!tablePropsAttr) return;
119
-
120
- const value = tablePropsAttr.value;
121
- const isObjectLiteral =
122
- value?.type === 'JSXExpressionContainer' &&
123
- value.expression.type === 'ObjectExpression';
124
-
125
- // Dynamic case: tableProps={identifier}, tableProps={fn()}, ... —
126
- // leave the attribute untouched and warn via a trailing comment
127
- // (api.report is a stub; comments are the only warning channel).
128
- if (!isObjectLiteral) {
129
- attachTodo(tablePropsAttr);
130
- return;
131
- }
132
-
133
- const obj = value.expression;
134
-
135
- // Objects containing spreads, computed keys, or methods are treated
136
- // as dynamic: no partial lift, just the TODO comment.
137
- const allSimple = obj.properties.every(
138
- (prop) => getPropertyKeyName(prop) !== null,
139
- );
140
- if (!allSimple) {
141
- attachTodo(tablePropsAttr);
142
- return;
143
- }
144
-
145
- const lifted = [];
146
- const kept = [];
147
- for (const prop of obj.properties) {
148
- const keyName = getPropertyKeyName(prop);
149
- const collidesWithSibling = attrs.some(
150
- (a) => a.type === 'JSXAttribute' && a.name?.name === keyName,
151
- );
152
- const collidesWithLifted = lifted.some(
153
- (a) => a.name.name === keyName,
154
- );
155
- if (
156
- !LIFTABLE_KEY_RE.test(keyName) ||
157
- collidesWithSibling ||
158
- collidesWithLifted
159
- ) {
160
- kept.push(prop);
161
- continue;
162
- }
163
- lifted.push(
164
- j.jsxAttribute(
165
- j.jsxIdentifier(keyName),
166
- buildAttributeValue(prop.value),
167
- ),
168
- );
169
- }
170
-
171
- const tablePropsIdx = attrs.indexOf(tablePropsAttr);
172
- if (kept.length === 0) {
173
- // All keys lifted — replace tableProps with the sibling attributes.
174
- attrs.splice(tablePropsIdx, 1, ...lifted);
175
- hasChanges = true;
176
- } else {
177
- // Some keys collide or fail the name guard — keep only those in a
178
- // shrunken tableProps and flag it for manual migration.
179
- attrs.splice(tablePropsIdx, 0, ...lifted);
180
- obj.properties = kept;
181
- attachTodo(tablePropsAttr);
182
- if (lifted.length > 0) hasChanges = true;
183
- }
184
- });
185
-
186
- if (!hasChanges) return undefined;
187
- return root.toSource({quote: 'single'});
188
- }
package/src/doc.mjs DELETED
@@ -1,27 +0,0 @@
1
- // Copyright (c) Meta Platforms, Inc. and affiliates.
2
-
3
- /**
4
- * @file Doc-authoring API (public `@astryxdesign/cli/doc`).
5
- *
6
- * The `createComponentDoc`/`createFunctionDoc`/`createDoc` authoring helpers now
7
- * live in `@astryxdesign/core/authoring` and are re-exported here so existing
8
- * `@astryxdesign/cli/doc` imports keep working. The Zod load-boundary schemas
9
- * live in `./schemas/doc-schema.mjs` (core-free) and are re-exported here for
10
- * back-compat; internal hot-path code imports them from the schema module
11
- * directly so it never depends on core's built `dist/`.
12
- */
13
-
14
- export {
15
- createComponentDoc,
16
- createFunctionDoc,
17
- createDoc,
18
- } from '@astryxdesign/core/authoring';
19
-
20
- export {
21
- ComponentDocKindSchema,
22
- FunctionDocKindSchema,
23
- GenericDocKindSchema,
24
- StampedDocSchema,
25
- LegacyDocSchema,
26
- ComponentDocSchema,
27
- } from './schemas/doc-schema.mjs';
package/src/doc.test.mjs DELETED
@@ -1,383 +0,0 @@
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
- });
@@ -1,59 +0,0 @@
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
- });