@astryxdesign/cli 0.7.2-canary.f16dc9f → 0.7.2-canary.fbc03f7
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/api/component/component.test.mjs +420 -167
- package/api/component/list/list.mjs +42 -4
- package/api/integration/pack-check.mjs +10 -10
- package/api/search/search.mjs +8 -3
- package/api/search/search.test.mjs +36 -0
- package/api/template/template.test.mjs +120 -3
- package/api/theme/build/build.family.test.mjs +16 -0
- package/api/theme/build/build.mjs +77 -18
- package/api/theme/themeBuild.doc.mjs +3 -3
- package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
- package/assets/codemods/transforms/next/index.mjs +11 -1
- package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
- package/assets/docs/working-with-ai.doc.mjs +43 -4
- package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
- package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
- package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
- package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
- package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
- package/assets/templates/pages/detail-page/page.tsx +3 -3
- package/assets/templates/pages/table-inbox/page.tsx +10 -8
- package/authoring/doctypes/base/type.ts +6 -0
- package/authoring/doctypes/component/component.doc.mjs +2 -2
- package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
- package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/discovery/component-discovery.d.mts +12 -0
- package/foundation/discovery/component-discovery.mjs +211 -0
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/doc-compiler/doc-loads.test.mjs +5 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- 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(
|
|
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(
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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(
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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(
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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('
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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(
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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(
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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(
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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(
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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(
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
()
|
|
177
|
-
()
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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(
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
});
|