@astryxdesign/cli 0.1.6-canary.0121ab3 → 0.1.6-canary.02f2602
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.
- package/package.json +13 -9
- package/src/api/template.mjs +1 -1
- package/src/api/validate-integration.mjs +0 -8
- package/src/config.mjs +5 -14
- package/src/doc.mjs +27 -0
- package/src/doc.test.mjs +383 -0
- package/src/integration.mjs +4 -15
- package/src/lib/component-discovery.importpath.test.mjs +59 -0
- package/src/lib/component-discovery.mjs +15 -5
- package/src/lib/component-format.mjs +45 -13
- package/src/lib/component-format.test.mjs +95 -1
- package/src/lib/component-loader.mjs +104 -2
- package/src/lib/componentDocOverlay.test.mjs +111 -0
- package/src/lib/config-schema.mjs +0 -30
- package/src/lib/hook-format.mjs +8 -3
- package/src/lib/xle/registry.mjs +0 -5
- package/src/schemas/doc-schema.mjs +226 -0
- package/src/schemas/template-schema.mjs +47 -0
- package/src/template.mjs +9 -67
- package/src/types/config.d.ts +11 -66
- package/src/types/doc.d.ts +23 -0
- package/src/types/integration.d.ts +7 -18
- package/src/types/template-api.d.ts +14 -50
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenLiveRegion.tsx +41 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.doc.mjs +13 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenShowcase.tsx +78 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenStructuralHeading.tsx +38 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.doc.mjs +14 -0
- package/templates/blocks/components/VisuallyHidden/VisuallyHiddenSupplementaryContext.tsx +47 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@astryxdesign/cli",
|
|
3
|
-
"version": "0.1.6-canary.
|
|
3
|
+
"version": "0.1.6-canary.02f2602",
|
|
4
4
|
"displayName": "CLI",
|
|
5
5
|
"description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
|
|
6
6
|
"author": "Meta Open Source",
|
|
@@ -47,6 +47,10 @@
|
|
|
47
47
|
"types": "./src/types/integration.d.ts",
|
|
48
48
|
"import": "./src/integration.mjs"
|
|
49
49
|
},
|
|
50
|
+
"./doc": {
|
|
51
|
+
"types": "./src/types/doc.d.ts",
|
|
52
|
+
"import": "./src/doc.mjs"
|
|
53
|
+
},
|
|
50
54
|
"./template": {
|
|
51
55
|
"types": "./src/types/template-api.d.ts",
|
|
52
56
|
"import": "./src/template.mjs"
|
|
@@ -75,10 +79,10 @@
|
|
|
75
79
|
"zod": "^4.4.3"
|
|
76
80
|
},
|
|
77
81
|
"peerDependencies": {
|
|
78
|
-
"@astryxdesign/charts": "0.1.6-canary.
|
|
79
|
-
"@astryxdesign/core": "0.1.6-canary.
|
|
80
|
-
"@astryxdesign/lab": "0.1.6-canary.
|
|
81
|
-
"@astryxdesign/theme-neutral": "0.1.6-canary.
|
|
82
|
+
"@astryxdesign/charts": "0.1.6-canary.02f2602",
|
|
83
|
+
"@astryxdesign/core": "0.1.6-canary.02f2602",
|
|
84
|
+
"@astryxdesign/lab": "0.1.6-canary.02f2602",
|
|
85
|
+
"@astryxdesign/theme-neutral": "0.1.6-canary.02f2602",
|
|
82
86
|
"gpt-tokenizer": "^3.4.0"
|
|
83
87
|
},
|
|
84
88
|
"peerDependenciesMeta": {
|
|
@@ -96,10 +100,10 @@
|
|
|
96
100
|
}
|
|
97
101
|
},
|
|
98
102
|
"devDependencies": {
|
|
99
|
-
"@astryxdesign/charts": "0.1.6-canary.
|
|
100
|
-
"@astryxdesign/core": "0.1.6-canary.
|
|
101
|
-
"@astryxdesign/lab": "0.1.6-canary.
|
|
102
|
-
"@astryxdesign/theme-neutral": "0.1.6-canary.
|
|
103
|
+
"@astryxdesign/charts": "0.1.6-canary.02f2602",
|
|
104
|
+
"@astryxdesign/core": "0.1.6-canary.02f2602",
|
|
105
|
+
"@astryxdesign/lab": "0.1.6-canary.02f2602",
|
|
106
|
+
"@astryxdesign/theme-neutral": "0.1.6-canary.02f2602",
|
|
103
107
|
"gpt-tokenizer": "^3.4.0"
|
|
104
108
|
},
|
|
105
109
|
"scripts": {
|
package/src/api/template.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import * as fs from 'node:fs';
|
|
|
8
8
|
import * as path from 'node:path';
|
|
9
9
|
import {createJiti} from 'jiti';
|
|
10
10
|
import {loadModuleWithSchema} from '../lib/module-loader.mjs';
|
|
11
|
-
import {TemplateEnvelopeSchema} from '../template.mjs';
|
|
11
|
+
import {TemplateEnvelopeSchema} from '../schemas/template-schema.mjs';
|
|
12
12
|
import {CLI_ROOT, discoverExternalPackages} from '../utils/paths.mjs';
|
|
13
13
|
import {
|
|
14
14
|
assertWithin,
|
|
@@ -67,11 +67,6 @@ function error(code, message) {
|
|
|
67
67
|
return {code, severity: 'error', message};
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
/** @param {string} code @param {string} message @returns {Issue} */
|
|
71
|
-
function warning(code, message) {
|
|
72
|
-
return {code, severity: 'warning', message};
|
|
73
|
-
}
|
|
74
|
-
|
|
75
70
|
/**
|
|
76
71
|
* Verify each declared contribution root exists on disk. A declared-but-missing
|
|
77
72
|
* root is a `missing_root` error.
|
|
@@ -365,6 +360,3 @@ export function summarizeIssues(issues) {
|
|
|
365
360
|
}
|
|
366
361
|
return {errors, warnings};
|
|
367
362
|
}
|
|
368
|
-
|
|
369
|
-
// Re-export issue constructors for callers/tests that want them.
|
|
370
|
-
export {error as integrationError, warning as integrationWarning};
|
package/src/config.mjs
CHANGED
|
@@ -1,18 +1,9 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `@astryxdesign/cli/config`, so config files get editor/type feedback without
|
|
9
|
-
* coupling to CLI internals. Validation is NOT performed here — it happens at
|
|
10
|
-
* the load boundary (see `loadModuleWithSchema` + `AstryxConfigSchema`).
|
|
11
|
-
*
|
|
12
|
-
* @template {import('./types/config').AstryxConfig} T
|
|
13
|
-
* @param {T} config
|
|
14
|
-
* @returns {T}
|
|
4
|
+
* Re-export of the config-authoring helper, which now lives in
|
|
5
|
+
* `@astryxdesign/core/config` so an app's config file gets type feedback
|
|
6
|
+
* without depending on the CLI. Kept here so existing
|
|
7
|
+
* `@astryxdesign/cli/config` imports continue to work unchanged.
|
|
15
8
|
*/
|
|
16
|
-
export
|
|
17
|
-
return config;
|
|
18
|
-
}
|
|
9
|
+
export {createConfig} from '@astryxdesign/core/config';
|
package/src/doc.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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
ADDED
|
@@ -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/integration.mjs
CHANGED
|
@@ -1,19 +1,8 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
|
280
|
-
// on-disk convention, then the bare
|
|
281
|
-
// migration (P4) renames to.
|
|
282
|
-
//
|
|
283
|
-
const candidateFiles = [
|
|
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;
|