@astryxdesign/cli 0.6.4-canary.ed2e54e → 0.6.4
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/README.md +66 -64
- package/api/component/_adapter.d.mts +0 -25
- package/api/component/_adapter.mjs +5 -59
- package/api/component/component.d.mts +3 -6
- package/api/component/component.doc.mjs +10 -23
- package/api/component/component.mjs +9 -249
- package/api/component/component.type.d.mts +0 -25
- package/api/component/component.type.mjs +0 -44
- package/api/discover/_adapter.d.mts +6 -114
- package/api/discover/_adapter.mjs +17 -372
- package/api/discover/detail/detail.d.mts +6 -18
- package/api/discover/detail/detail.mjs +13 -67
- package/api/discover/detail/detail.test.mjs +0 -85
- package/api/discover/discover.d.mts +9 -3
- package/api/discover/discover.doc.mjs +18 -61
- package/api/discover/discover.mjs +36 -220
- package/api/discover/discover.test.mjs +2 -11
- package/api/discover/discover.type.d.mts +8 -147
- package/api/discover/discover.type.mjs +12 -102
- package/api/discover/list/list.d.mts +6 -20
- package/api/discover/list/list.mjs +12 -45
- package/api/discover/list/list.test.mjs +0 -46
- package/api/discover/search/search.d.mts +16 -18
- package/api/discover/search/search.mjs +56 -102
- package/api/discover/search/search.test.mjs +10 -144
- package/api/docs/docs.test.mjs +0 -2
- package/api/doctor/doctor.d.mts +3 -8
- package/api/doctor/doctor.mjs +9 -90
- package/api/doctor/doctor.test.mjs +10 -122
- package/api/index.d.mts +2 -1
- package/api/index.mjs +4 -4
- package/api/integration/add-helpers.d.mts +2 -5
- package/api/integration/add-helpers.mjs +9 -36
- package/api/integration/pack-check.mjs +3 -28
- package/api/json/index.ts +1 -0
- package/api/layout/_adapter.d.mts +34 -0
- package/api/layout/_adapter.mjs +148 -0
- package/api/layout/check/check.d.mts +16 -0
- package/api/layout/check/check.mjs +40 -0
- package/api/layout/expand/expand.d.mts +22 -0
- package/api/layout/expand/expand.mjs +155 -0
- package/api/layout/expand/expand.path-safety.test.mjs +53 -0
- package/api/layout/grammar/grammar.d.mts +13 -0
- package/api/layout/grammar/grammar.mjs +87 -0
- package/api/layout/layout.d.mts +6 -0
- package/api/layout/layout.mjs +17 -0
- package/api/layout/layout.test.mjs +297 -0
- package/api/layout/layout.type.d.mts +89 -0
- package/api/layout/layout.type.mjs +103 -0
- package/api/layout/layoutCheck.doc.d.mts +11 -0
- package/api/layout/layoutCheck.doc.mjs +85 -0
- package/api/layout/layoutExpand.doc.d.mts +11 -0
- package/api/layout/layoutExpand.doc.mjs +107 -0
- package/api/layout/layoutGrammar.doc.d.mts +11 -0
- package/api/layout/layoutGrammar.doc.mjs +57 -0
- package/api/search/search.test.mjs +0 -18
- package/api/template/template-integration.test.mjs +65 -1
- package/api/template/template.mjs +1 -1
- package/api/theme/add/add.mjs +25 -17
- package/api/theme/add/add.staging.test.mjs +23 -40
- package/api/theme/build/build.family.test.mjs +12 -7
- package/api/theme/build/build.mjs +18 -8
- package/api/upgrade/run/run.mjs +4 -6
- package/api/upgrade/upgrade.type.mjs +2 -2
- package/assets/codemods/__tests__/runner.test.mjs +1 -3
- package/assets/codemods/integration-runner.mjs +3 -3
- package/assets/codemods/runner.mjs +4 -5
- package/assets/docs/internationalization.doc.mjs +5 -7
- package/assets/docs/tree/integrations.doc.mjs +1 -20
- package/assets/templates/blocks/components/InternationalizationProvider/InternationalizationProvider01ShippedLocale.tsx +1 -1
- package/authoring/config/config.doc.mjs +1 -9
- package/authoring/config/parse.d.mts +0 -2
- package/authoring/config/parse.mjs +0 -19
- package/authoring/config/parse.test.mjs +0 -8
- package/authoring/config/type.ts +2 -13
- package/authoring/doctypes/command/command.doc.mjs +1 -1
- package/authoring/doctypes/command/type.ts +1 -1
- package/authoring/index.d.mts +0 -1
- package/authoring/index.d.ts +0 -10
- package/authoring/index.mjs +0 -1
- package/clients/cli/command-result-coverage.test.mjs +7 -7
- package/clients/cli/commands/component/index.mjs +55 -152
- package/clients/cli/commands/component-ownership.test.mjs +0 -89
- package/clients/cli/commands/component.doc.mjs +6 -23
- package/clients/cli/commands/debug-result-summary.test.mjs +2 -2
- package/clients/cli/commands/discover.doc.mjs +9 -53
- package/clients/cli/commands/discover.mjs +118 -393
- package/clients/cli/commands/docs.test.mjs +0 -29
- package/clients/cli/commands/layout-check.doc.mjs +65 -0
- package/clients/cli/commands/layout-expand.doc.mjs +83 -0
- package/clients/cli/commands/layout-grammar.doc.mjs +30 -0
- package/clients/cli/commands/layout.doc.mjs +34 -0
- package/clients/cli/commands/layout.error-codes.test.mjs +66 -0
- package/clients/cli/commands/layout.exit-parity.test.mjs +41 -0
- package/clients/cli/commands/layout.mjs +275 -0
- package/clients/cli/commands/layout.path-help.test.mjs +33 -0
- package/clients/cli/commands/layout.stdin-cap.test.mjs +47 -0
- package/clients/cli/commands/layout.text-fields.test.mjs +39 -0
- package/clients/cli/commands/text-json-parity.test.mjs +17 -0
- package/clients/cli/index.mjs +4 -0
- package/clients/cli/lib/exit-codes.test.mjs +8 -1
- package/clients/cli/lib/json-shim.mjs +14 -24
- package/clients/cli/lib/json-shim.test.mjs +20 -6
- package/clients/cli/lib/manifest.mjs +8 -3
- package/clients/cli/lib/manifest.test.mjs +2 -5
- package/foundation/discovery/authoring-self-docs.mjs +0 -1
- package/foundation/discovery/template-adapter.mjs +1 -1
- package/foundation/doc-compiler/doc-loads.test.mjs +12 -0
- package/foundation/doc-compiler/tree.test.mjs +1 -9
- package/foundation/integrations/integrations.d.mts +1 -14
- package/foundation/integrations/integrations.mjs +1 -41
- package/foundation/integrations/integrations.test.mjs +0 -31
- package/foundation/response/response-types.doc.mjs +21 -15
- package/foundation/response/response-types.doc.test.mjs +0 -23
- package/foundation/xle/browser.d.mts +3 -3
- package/foundation/xle/browser.mjs +3 -3
- package/foundation/xle/expand.mjs +2 -2
- package/foundation/xle/parse.mjs +1 -1
- package/foundation/xle/print.mjs +2 -2
- package/foundation/xle/splice.mjs +1 -1
- package/package.json +9 -9
- package/api/discover/_adapter.test.mjs +0 -215
- package/api/discover/_catalog-view.d.mts +0 -115
- package/api/discover/_catalog-view.mjs +0 -203
- package/api/discover/_catalog-view.test.mjs +0 -128
- package/api/discover/detail/item/item.d.mts +0 -26
- package/api/discover/detail/item/item.mjs +0 -78
- package/api/discover/detail/item/item.test.mjs +0 -73
- package/api/integration/pack-check.lifecycle-output.test.mjs +0 -105
- package/api/theme/add/add.rollback.test.mjs +0 -158
- package/api/theme/build/build.rollback.test.mjs +0 -148
- package/api/upgrade/run/files-changed.test.mjs +0 -111
- package/assets/codemods/file-count.test.mjs +0 -163
- package/assets/docs/tree/component-lookups.doc.mjs +0 -149
- package/authoring/discover/discover.doc.d.mts +0 -13
- package/authoring/discover/discover.doc.mjs +0 -138
- package/authoring/discover/parse.d.mts +0 -24
- package/authoring/discover/parse.mjs +0 -128
- package/authoring/discover/parse.test.mjs +0 -124
- package/authoring/discover/type.ts +0 -87
- package/clients/cli/commands/component-batch.test.mjs +0 -341
- package/clients/cli/commands/discover.sources.test.mjs +0 -267
- package/clients/cli/lib/parse-error-format.test.mjs +0 -81
- package/foundation/response/batch.type.d.mts +0 -33
- package/foundation/response/batch.type.mjs +0 -34
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file Multi-selector component lookup contract: one ordered batch envelope,
|
|
5
|
-
* complete per-selector states, unchanged single-selector responses, and matching
|
|
6
|
-
* text/JSON exit behavior.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import {describe, it, expect} from 'vitest';
|
|
10
|
-
import * as path from 'node:path';
|
|
11
|
-
import {
|
|
12
|
-
component,
|
|
13
|
-
COMPONENT_BATCH_SELECTOR_LIMIT,
|
|
14
|
-
} from '../../../api/component/component.mjs';
|
|
15
|
-
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
16
|
-
|
|
17
|
-
const REPO_ROOT = path.resolve(import.meta.dirname, '../../../../..');
|
|
18
|
-
const SLOW = 60_000;
|
|
19
|
-
|
|
20
|
-
describe('component() batch lookup', () => {
|
|
21
|
-
it(
|
|
22
|
-
'keeps one found row per selector, in order, including duplicates',
|
|
23
|
-
async () => {
|
|
24
|
-
const result = await component(['Button', 'Badge', 'Button'], {
|
|
25
|
-
cwd: REPO_ROOT,
|
|
26
|
-
detail: 'brief',
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
expect(result.type).toBe('component.batch');
|
|
30
|
-
expect(result.data.count).toBe(3);
|
|
31
|
-
expect(result.data.results.map(row => row.selector)).toEqual([
|
|
32
|
-
'Button',
|
|
33
|
-
'Badge',
|
|
34
|
-
'Button',
|
|
35
|
-
]);
|
|
36
|
-
expect(result.data.results.map(row => row.status)).toEqual([
|
|
37
|
-
'found',
|
|
38
|
-
'found',
|
|
39
|
-
'found',
|
|
40
|
-
]);
|
|
41
|
-
expect(
|
|
42
|
-
result.data.results.map(row =>
|
|
43
|
-
row.status === 'found' ? row.result.type : null,
|
|
44
|
-
),
|
|
45
|
-
).toEqual(['component.detail', 'component.detail', 'component.detail']);
|
|
46
|
-
},
|
|
47
|
-
SLOW,
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
it(
|
|
51
|
-
'applies every focused projection to each found row',
|
|
52
|
-
async () => {
|
|
53
|
-
const projections = [
|
|
54
|
-
{options: {props: true}, expectedType: 'component.detail.props'},
|
|
55
|
-
{options: {source: true}, expectedType: 'component.detail.source'},
|
|
56
|
-
{options: {showcase: true}, expectedType: 'component.detail.showcase'},
|
|
57
|
-
{options: {blocks: true}, expectedType: 'component.detail.blocks'},
|
|
58
|
-
];
|
|
59
|
-
for (const projection of projections) {
|
|
60
|
-
const result = await component(['Button', 'Card'], {
|
|
61
|
-
cwd: REPO_ROOT,
|
|
62
|
-
...projection.options,
|
|
63
|
-
});
|
|
64
|
-
expect(result.type).toBe('component.batch');
|
|
65
|
-
expect(
|
|
66
|
-
result.data.results.map(row =>
|
|
67
|
-
row.status === 'found' ? row.result.type : null,
|
|
68
|
-
),
|
|
69
|
-
).toEqual([projection.expectedType, projection.expectedType]);
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
SLOW,
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
it(
|
|
76
|
-
'uses input shape to distinguish catalog, single, and batch calls',
|
|
77
|
-
async () => {
|
|
78
|
-
expect((await component(undefined, {cwd: REPO_ROOT})).type).toBe(
|
|
79
|
-
'component.list',
|
|
80
|
-
);
|
|
81
|
-
expect((await component('Button', {cwd: REPO_ROOT})).type).toBe(
|
|
82
|
-
'component.detail',
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const emptyBatch = await component([], {cwd: REPO_ROOT});
|
|
86
|
-
expect(emptyBatch).toEqual({
|
|
87
|
-
type: 'component.batch',
|
|
88
|
-
data: {count: 0, results: []},
|
|
89
|
-
});
|
|
90
|
-
expect(
|
|
91
|
-
await component([], {
|
|
92
|
-
cwd: REPO_ROOT,
|
|
93
|
-
list: true,
|
|
94
|
-
category: 'Form',
|
|
95
|
-
}),
|
|
96
|
-
).toEqual(emptyBatch);
|
|
97
|
-
|
|
98
|
-
const oneRowBatch = await component(['Button'], {
|
|
99
|
-
cwd: REPO_ROOT,
|
|
100
|
-
detail: 'brief',
|
|
101
|
-
});
|
|
102
|
-
expect(oneRowBatch).toMatchObject({
|
|
103
|
-
type: 'component.batch',
|
|
104
|
-
data: {
|
|
105
|
-
count: 1,
|
|
106
|
-
results: [
|
|
107
|
-
{
|
|
108
|
-
selector: 'Button',
|
|
109
|
-
status: 'found',
|
|
110
|
-
result: {type: 'component.detail'},
|
|
111
|
-
},
|
|
112
|
-
],
|
|
113
|
-
},
|
|
114
|
-
});
|
|
115
|
-
},
|
|
116
|
-
SLOW,
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
it('rejects an oversized array before component resolution starts', async () => {
|
|
120
|
-
const selectors = Array.from(
|
|
121
|
-
{length: COMPONENT_BATCH_SELECTOR_LIMIT + 1},
|
|
122
|
-
() => 'Button',
|
|
123
|
-
);
|
|
124
|
-
await expect(
|
|
125
|
-
component(selectors, {
|
|
126
|
-
cwd: path.join(REPO_ROOT, 'missing-batch-cwd'),
|
|
127
|
-
}),
|
|
128
|
-
).rejects.toMatchObject({
|
|
129
|
-
code: 'ERR_INVALID_ARGUMENT',
|
|
130
|
-
message: `Component batch accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors; received ${selectors.length}`,
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it(
|
|
135
|
-
'keeps receipt invariants across a deterministic selector chaos matrix',
|
|
136
|
-
async () => {
|
|
137
|
-
const found = new Set(['Button', 'Card']);
|
|
138
|
-
const selectorCases = [
|
|
139
|
-
[],
|
|
140
|
-
['Button'],
|
|
141
|
-
['ZzzNope99'],
|
|
142
|
-
['Button', 'Button'],
|
|
143
|
-
['ZzzNope98', 'ZzzNope99'],
|
|
144
|
-
['Button', 'ZzzNope99', 'Card'],
|
|
145
|
-
['ZzzNope99', 'Button', 'ZzzNope99', 'Button'],
|
|
146
|
-
['Card', 'Button', 'Card', 'ZzzNope98', 'Button'],
|
|
147
|
-
];
|
|
148
|
-
|
|
149
|
-
for (const selectors of selectorCases) {
|
|
150
|
-
const result = await component(selectors, {
|
|
151
|
-
cwd: REPO_ROOT,
|
|
152
|
-
detail: 'brief',
|
|
153
|
-
});
|
|
154
|
-
expect(result.type).toBe('component.batch');
|
|
155
|
-
expect(result.data.count).toBe(selectors.length);
|
|
156
|
-
expect(result.data.results.map(row => row.selector)).toEqual(selectors);
|
|
157
|
-
expect(result.data.results.map(row => row.status)).toEqual(
|
|
158
|
-
selectors.map(selector =>
|
|
159
|
-
found.has(selector) ? 'found' : 'not_found',
|
|
160
|
-
),
|
|
161
|
-
);
|
|
162
|
-
for (const row of result.data.results) {
|
|
163
|
-
if (row.status === 'found') {
|
|
164
|
-
expect(row.result.type).toBe('component.detail');
|
|
165
|
-
} else {
|
|
166
|
-
expect(row.code).toBe('ERR_UNKNOWN_COMPONENT');
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
},
|
|
171
|
-
SLOW,
|
|
172
|
-
);
|
|
173
|
-
|
|
174
|
-
it(
|
|
175
|
-
'composes every detail and language control over a batch',
|
|
176
|
-
async () => {
|
|
177
|
-
/** @type {Array<{detail?: 'full'|'compact'|'brief'; lang?: string; zh?: boolean; dense?: boolean}>} */
|
|
178
|
-
const controls = [
|
|
179
|
-
{detail: 'brief'},
|
|
180
|
-
{detail: 'compact'},
|
|
181
|
-
{detail: 'full'},
|
|
182
|
-
{lang: 'en'},
|
|
183
|
-
{lang: 'dense'},
|
|
184
|
-
{zh: true},
|
|
185
|
-
{dense: true},
|
|
186
|
-
];
|
|
187
|
-
for (const control of controls) {
|
|
188
|
-
const result = await component(['Button', 'Card'], {
|
|
189
|
-
cwd: REPO_ROOT,
|
|
190
|
-
...control,
|
|
191
|
-
});
|
|
192
|
-
expect(result.type).toBe('component.batch');
|
|
193
|
-
expect(result.data.results).toHaveLength(2);
|
|
194
|
-
expect(
|
|
195
|
-
result.data.results.every(
|
|
196
|
-
row =>
|
|
197
|
-
row.status === 'found' && row.result.type === 'component.detail',
|
|
198
|
-
),
|
|
199
|
-
).toBe(true);
|
|
200
|
-
}
|
|
201
|
-
},
|
|
202
|
-
SLOW,
|
|
203
|
-
);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
describe('astryx component batch lookup', () => {
|
|
207
|
-
it('documents variadic selector grammar in --help', async () => {
|
|
208
|
-
const run = await runCli(['component', '--help'], REPO_ROOT);
|
|
209
|
-
expect(run.code).toBe(0);
|
|
210
|
-
expect(run.stdout).toContain(
|
|
211
|
-
'Usage: astryx component [options] [names...]',
|
|
212
|
-
);
|
|
213
|
-
expect(run.stdout).toContain('@scope/package@version/Name');
|
|
214
|
-
expect(run.stdout).toContain('Two or more return one ordered batch');
|
|
215
|
-
expect(run.stdout).toContain('At most 100 selectors');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it(
|
|
219
|
-
'keeps catalog and single-result CLI envelopes at zero and one selectors',
|
|
220
|
-
async () => {
|
|
221
|
-
const catalog = await runCli(['--json', 'component'], REPO_ROOT);
|
|
222
|
-
expect(catalog.code).toBe(0);
|
|
223
|
-
expect(JSON.parse(catalog.stdout).type).toBe('component.list');
|
|
224
|
-
|
|
225
|
-
const single = await runCli(
|
|
226
|
-
['--json', '--detail', 'brief', 'component', 'Button'],
|
|
227
|
-
REPO_ROOT,
|
|
228
|
-
);
|
|
229
|
-
expect(single.code).toBe(0);
|
|
230
|
-
expect(JSON.parse(single.stdout).type).toBe('component.detail');
|
|
231
|
-
},
|
|
232
|
-
SLOW,
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
it(
|
|
236
|
-
'emits one component.batch JSON envelope for several found selectors',
|
|
237
|
-
async () => {
|
|
238
|
-
const run = await runCli(
|
|
239
|
-
['--json', '--detail', 'brief', 'component', 'Button', 'Badge'],
|
|
240
|
-
REPO_ROOT,
|
|
241
|
-
);
|
|
242
|
-
expect(run.code).toBe(0);
|
|
243
|
-
const envelope = JSON.parse(run.stdout);
|
|
244
|
-
expect(envelope.type).toBe('component.batch');
|
|
245
|
-
expect(envelope.data.count).toBe(2);
|
|
246
|
-
expect(envelope.data.results.map(row => row.selector)).toEqual([
|
|
247
|
-
'Button',
|
|
248
|
-
'Badge',
|
|
249
|
-
]);
|
|
250
|
-
expect(envelope.data.results.every(row => row.status === 'found')).toBe(
|
|
251
|
-
true,
|
|
252
|
-
);
|
|
253
|
-
},
|
|
254
|
-
SLOW,
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
it(
|
|
258
|
-
'rejects an oversized batch before emitting JSON or text results',
|
|
259
|
-
async () => {
|
|
260
|
-
const selectors = Array.from(
|
|
261
|
-
{length: COMPONENT_BATCH_SELECTOR_LIMIT + 1},
|
|
262
|
-
() => 'Button',
|
|
263
|
-
);
|
|
264
|
-
const json = await runCli(
|
|
265
|
-
['--json', 'component', ...selectors],
|
|
266
|
-
REPO_ROOT,
|
|
267
|
-
);
|
|
268
|
-
expect(json.code).toBe(1);
|
|
269
|
-
expect(JSON.parse(json.stdout)).toMatchObject({
|
|
270
|
-
code: 'ERR_INVALID_ARGUMENT',
|
|
271
|
-
error: `Component batch accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors; received ${selectors.length}`,
|
|
272
|
-
});
|
|
273
|
-
expect(JSON.parse(json.stdout).type).toBeUndefined();
|
|
274
|
-
|
|
275
|
-
const text = await runCli(['component', ...selectors], REPO_ROOT);
|
|
276
|
-
expect(text.code).toBe(1);
|
|
277
|
-
expect(text.stdout + text.stderr).toContain(
|
|
278
|
-
`accepts at most ${COMPONENT_BATCH_SELECTOR_LIMIT} selectors`,
|
|
279
|
-
);
|
|
280
|
-
},
|
|
281
|
-
SLOW,
|
|
282
|
-
);
|
|
283
|
-
|
|
284
|
-
it(
|
|
285
|
-
'emits every row and exits 1 when one selector is not found',
|
|
286
|
-
async () => {
|
|
287
|
-
const run = await runCli(
|
|
288
|
-
['--json', '--detail', 'brief', 'component', 'Button', 'ZzzNope99'],
|
|
289
|
-
REPO_ROOT,
|
|
290
|
-
);
|
|
291
|
-
expect(run.code).toBe(1);
|
|
292
|
-
const envelope = JSON.parse(run.stdout);
|
|
293
|
-
expect(envelope.type).toBe('component.batch');
|
|
294
|
-
expect(envelope.data.results).toMatchObject([
|
|
295
|
-
{selector: 'Button', status: 'found'},
|
|
296
|
-
{
|
|
297
|
-
selector: 'ZzzNope99',
|
|
298
|
-
status: 'not_found',
|
|
299
|
-
code: 'ERR_UNKNOWN_COMPONENT',
|
|
300
|
-
},
|
|
301
|
-
]);
|
|
302
|
-
},
|
|
303
|
-
SLOW,
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
it(
|
|
307
|
-
'emits every failed row and exits 1 when no selector resolves',
|
|
308
|
-
async () => {
|
|
309
|
-
const run = await runCli(
|
|
310
|
-
['--json', 'component', 'ZzzNope98', 'ZzzNope99'],
|
|
311
|
-
REPO_ROOT,
|
|
312
|
-
);
|
|
313
|
-
expect(run.code).toBe(1);
|
|
314
|
-
const envelope = JSON.parse(run.stdout);
|
|
315
|
-
expect(envelope.type).toBe('component.batch');
|
|
316
|
-
expect(envelope.data.count).toBe(2);
|
|
317
|
-
expect(envelope.data.results).toMatchObject([
|
|
318
|
-
{selector: 'ZzzNope98', status: 'not_found'},
|
|
319
|
-
{selector: 'ZzzNope99', status: 'not_found'},
|
|
320
|
-
]);
|
|
321
|
-
},
|
|
322
|
-
SLOW,
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
it(
|
|
326
|
-
'projects every row in text and keeps the same failing exit status',
|
|
327
|
-
async () => {
|
|
328
|
-
const run = await runCli(
|
|
329
|
-
['--detail', 'brief', 'component', 'Button', 'ZzzNope99'],
|
|
330
|
-
REPO_ROOT,
|
|
331
|
-
);
|
|
332
|
-
expect(run.code).toBe(1);
|
|
333
|
-
expect(run.stdout).toContain('count: 2');
|
|
334
|
-
expect(run.stdout).toMatch(/selector: Button\nstatus:\s+found/);
|
|
335
|
-
expect(run.stdout).toMatch(
|
|
336
|
-
/selector: ZzzNope99\nstatus:\s+not_found\ncode:\s+ERR_UNKNOWN_COMPONENT/,
|
|
337
|
-
);
|
|
338
|
-
},
|
|
339
|
-
SLOW,
|
|
340
|
-
);
|
|
341
|
-
});
|
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file `astryx discover` with a discover source: the list, a package page, a
|
|
5
|
-
* search, and the refused option pair, driven through the real CLI against a
|
|
6
|
-
* hermetic project whose astryx.config sets `discover`. Saved copies go to a
|
|
7
|
-
* temp cache directory, never the project.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import {describe, it, expect, beforeEach, afterEach} from 'vitest';
|
|
11
|
-
import * as fs from 'node:fs';
|
|
12
|
-
import * as path from 'node:path';
|
|
13
|
-
import * as os from 'node:os';
|
|
14
|
-
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
15
|
-
|
|
16
|
-
const CONFIG = `
|
|
17
|
-
const packages = [
|
|
18
|
-
{
|
|
19
|
-
package: '@test/kit',
|
|
20
|
-
integration: 'test-kit',
|
|
21
|
-
aliases: [],
|
|
22
|
-
latest: '3.2.0',
|
|
23
|
-
versions: [
|
|
24
|
-
{version: '3.2.0', publishedAt: '2026-09-29T00:00:00.000Z', prerelease: false, status: 'ok'},
|
|
25
|
-
{version: '3.1.4', publishedAt: '2026-09-01T00:00:00.000Z', prerelease: false, status: 'ok'},
|
|
26
|
-
],
|
|
27
|
-
contributions: [{kind: 'component', name: 'Dial'}],
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
package: '@test/charts',
|
|
31
|
-
integration: 'test-charts',
|
|
32
|
-
aliases: [],
|
|
33
|
-
latest: '2.0.0',
|
|
34
|
-
versions: [
|
|
35
|
-
{version: '2.1.0-beta.1', publishedAt: '2026-09-25T00:00:00.000Z', prerelease: true, status: 'ok'},
|
|
36
|
-
{version: '2.0.0', publishedAt: '2026-09-20T00:00:00.000Z', prerelease: false, status: 'ok'},
|
|
37
|
-
],
|
|
38
|
-
contributions: [
|
|
39
|
-
{kind: 'component', name: 'Chart'},
|
|
40
|
-
{kind: 'template', name: 'pages/Report'},
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
package: '@test/boards',
|
|
45
|
-
integration: 'test-boards',
|
|
46
|
-
aliases: [],
|
|
47
|
-
latest: '1.0.0',
|
|
48
|
-
versions: [
|
|
49
|
-
{version: '1.0.0', publishedAt: '2026-09-10T00:00:00.000Z', prerelease: false, status: 'ok'},
|
|
50
|
-
],
|
|
51
|
-
contributions: [{kind: 'template', name: 'pages/DialBoard'}],
|
|
52
|
-
},
|
|
53
|
-
];
|
|
54
|
-
|
|
55
|
-
export default {
|
|
56
|
-
integrations: ['@test/kit'],
|
|
57
|
-
async discover({package: name}) {
|
|
58
|
-
return {
|
|
59
|
-
schemaVersion: 1,
|
|
60
|
-
source: {name: 'Test catalog', generatedAt: '2026-09-30T00:00:00.000Z', complete: true},
|
|
61
|
-
packages: packages.filter(p => name == null || p.package === name),
|
|
62
|
-
};
|
|
63
|
-
},
|
|
64
|
-
};
|
|
65
|
-
`;
|
|
66
|
-
|
|
67
|
-
let tmpDir;
|
|
68
|
-
let project;
|
|
69
|
-
/** @type {Record<string, string | undefined>} */
|
|
70
|
-
let savedEnv;
|
|
71
|
-
|
|
72
|
-
beforeEach(() => {
|
|
73
|
-
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-discover-sources-'));
|
|
74
|
-
project = path.join(tmpDir, 'project');
|
|
75
|
-
const pkgDir = path.join(project, 'node_modules', '@test', 'kit');
|
|
76
|
-
fs.mkdirSync(path.join(pkgDir, 'components'), {recursive: true});
|
|
77
|
-
fs.writeFileSync(
|
|
78
|
-
path.join(project, 'package.json'),
|
|
79
|
-
JSON.stringify({
|
|
80
|
-
name: 'proj',
|
|
81
|
-
version: '1.0.0',
|
|
82
|
-
dependencies: {'@test/kit': '3.1.4'},
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
fs.writeFileSync(path.join(project, 'astryx.config.mjs'), CONFIG);
|
|
86
|
-
fs.writeFileSync(
|
|
87
|
-
path.join(pkgDir, 'package.json'),
|
|
88
|
-
JSON.stringify({name: '@test/kit', version: '3.1.4'}),
|
|
89
|
-
);
|
|
90
|
-
fs.writeFileSync(
|
|
91
|
-
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
92
|
-
`export default {components: './components'};\n`,
|
|
93
|
-
);
|
|
94
|
-
fs.writeFileSync(
|
|
95
|
-
path.join(pkgDir, 'components', 'Dial.doc.mjs'),
|
|
96
|
-
`export const docs = {name: 'Dial', usage: {description: 'A dial.'}};\n`,
|
|
97
|
-
);
|
|
98
|
-
fs.writeFileSync(
|
|
99
|
-
path.join(pkgDir, 'components', 'Dial.tsx'),
|
|
100
|
-
`export function Dial() { return null; }\n`,
|
|
101
|
-
);
|
|
102
|
-
// The saved copy lives in the per-user cache; point it at the temp dir.
|
|
103
|
-
savedEnv = {
|
|
104
|
-
HOME: process.env.HOME,
|
|
105
|
-
XDG_CACHE_HOME: process.env.XDG_CACHE_HOME,
|
|
106
|
-
LOCALAPPDATA: process.env.LOCALAPPDATA,
|
|
107
|
-
};
|
|
108
|
-
process.env.HOME = path.join(tmpDir, 'home');
|
|
109
|
-
process.env.XDG_CACHE_HOME = path.join(tmpDir, 'cache');
|
|
110
|
-
process.env.LOCALAPPDATA = path.join(tmpDir, 'localappdata');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
afterEach(() => {
|
|
114
|
-
for (const [key, value] of Object.entries(savedEnv)) {
|
|
115
|
-
if (value === undefined) delete process.env[key];
|
|
116
|
-
else process.env[key] = value;
|
|
117
|
-
}
|
|
118
|
-
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
/** The per-user cache directory this platform uses under the redirected env. */
|
|
122
|
-
function userCacheDir() {
|
|
123
|
-
if (process.platform === 'win32') {
|
|
124
|
-
return path.join(tmpDir, 'localappdata', 'astryx', 'Cache');
|
|
125
|
-
}
|
|
126
|
-
if (process.platform === 'darwin') {
|
|
127
|
-
return path.join(tmpDir, 'home', 'Library', 'Caches', 'astryx');
|
|
128
|
-
}
|
|
129
|
-
return path.join(tmpDir, 'cache', 'astryx');
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
describe('astryx discover with a discover source', () => {
|
|
133
|
-
it('lists what the project has and what it could add, in --json', async () => {
|
|
134
|
-
const {status, stdout} = await runCli(['discover', '--json'], {
|
|
135
|
-
cwd: project,
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
expect(status).toBe(0);
|
|
139
|
-
const {data, meta} = JSON.parse(stdout);
|
|
140
|
-
expect(data).toEqual([
|
|
141
|
-
expect.objectContaining({
|
|
142
|
-
name: '@test/kit',
|
|
143
|
-
components: ['Dial'],
|
|
144
|
-
version: '3.1.4',
|
|
145
|
-
latest: '3.2.0',
|
|
146
|
-
}),
|
|
147
|
-
]);
|
|
148
|
-
expect(meta.available).toEqual([
|
|
149
|
-
{
|
|
150
|
-
name: '@test/charts',
|
|
151
|
-
components: ['Chart'],
|
|
152
|
-
version: '2.0.0',
|
|
153
|
-
templates: ['pages/Report'],
|
|
154
|
-
source: 'Test catalog',
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
name: '@test/boards',
|
|
158
|
-
components: [],
|
|
159
|
-
version: '1.0.0',
|
|
160
|
-
templates: ['pages/DialBoard'],
|
|
161
|
-
source: 'Test catalog',
|
|
162
|
-
},
|
|
163
|
-
]);
|
|
164
|
-
expect(meta.sources).toEqual([
|
|
165
|
-
expect.objectContaining({
|
|
166
|
-
name: 'Test catalog',
|
|
167
|
-
from: 'astryx.config',
|
|
168
|
-
status: 'fresh',
|
|
169
|
-
}),
|
|
170
|
-
]);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('prints both sides as records', async () => {
|
|
174
|
-
const {status, stdout} = await runCli(['discover'], {cwd: project});
|
|
175
|
-
|
|
176
|
-
expect(status).toBe(0);
|
|
177
|
-
expect(stdout).toMatch(/^Installed$/m);
|
|
178
|
-
expect(stdout).toMatch(/^Available$/m);
|
|
179
|
-
expect(stdout).toMatch(/^name:\s+@test\/charts$/m);
|
|
180
|
-
expect(stdout).toMatch(/^latest:\s+3\.2\.0$/m);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('shows a package it does not have with its releases and the command that adds it, and never runs it', async () => {
|
|
184
|
-
const {status, stdout} = await runCli(['discover', '@test/charts'], {
|
|
185
|
-
cwd: project,
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
expect(status).toBe(0);
|
|
189
|
-
expect(stdout).toMatch(/^installed:\s+false$/m);
|
|
190
|
-
expect(stdout).toContain('- 2.0.0 2026-09-20 latest');
|
|
191
|
-
expect(stdout).not.toContain('2.1.0-beta.1 ');
|
|
192
|
-
expect(stdout).toContain('1 prerelease is not listed.');
|
|
193
|
-
// The verb follows the detected package manager.
|
|
194
|
-
expect(stdout).toMatch(
|
|
195
|
-
/^(npm install|pnpm add|yarn add|bun add) @test\/charts$/m,
|
|
196
|
-
);
|
|
197
|
-
expect(
|
|
198
|
-
fs.existsSync(path.join(project, 'node_modules', '@test', 'charts')),
|
|
199
|
-
).toBe(false);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('says which item is missing from a package only a source lists', async () => {
|
|
203
|
-
const {status, stdout} = await runCli(
|
|
204
|
-
['discover', '@test/charts/Nope', '--json'],
|
|
205
|
-
{cwd: project},
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
expect(status).toBe(1);
|
|
209
|
-
const {code, error} = JSON.parse(stdout);
|
|
210
|
-
expect(code).toBe('ERR_UNKNOWN_COMPONENT');
|
|
211
|
-
expect(error).toBe('"Nope" not found in @test/charts');
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('searches every kind in every source', async () => {
|
|
215
|
-
const {status, stdout} = await runCli(['discover', 'report', '--json'], {
|
|
216
|
-
cwd: project,
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
expect(status).toBe(0);
|
|
220
|
-
expect(JSON.parse(stdout).data.matches).toEqual([
|
|
221
|
-
{
|
|
222
|
-
package: '@test/charts',
|
|
223
|
-
component: 'pages/Report',
|
|
224
|
-
kind: 'template',
|
|
225
|
-
installed: false,
|
|
226
|
-
},
|
|
227
|
-
]);
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('refuses --installed with --available', async () => {
|
|
231
|
-
const {status, stdout} = await runCli(
|
|
232
|
-
['discover', '--installed', '--available', '--json'],
|
|
233
|
-
{cwd: project},
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
expect(status).toBe(1);
|
|
237
|
-
expect(JSON.parse(stdout).code).toBe('ERR_INVALID_OPTION');
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
it('lists every match for a free-text query, even an exact component name, and opens one by its package path', async () => {
|
|
241
|
-
const exact = await runCli(['discover', 'Dial', '--json'], {cwd: project});
|
|
242
|
-
const {type, data} = JSON.parse(exact.stdout);
|
|
243
|
-
expect(type).toBe('discover.search');
|
|
244
|
-
expect(data.matches.map(m => [m.component, m.installed])).toEqual([
|
|
245
|
-
['Dial', true],
|
|
246
|
-
['pages/DialBoard', false],
|
|
247
|
-
]);
|
|
248
|
-
|
|
249
|
-
const opened = await runCli(['discover', '@test/kit/Dial', '--json'], {
|
|
250
|
-
cwd: project,
|
|
251
|
-
});
|
|
252
|
-
expect(JSON.parse(opened.stdout).type).toBe('discover.detail.doc');
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('saves the answer in the user cache and writes nothing into the project', async () => {
|
|
256
|
-
const before = fs.readdirSync(project).sort();
|
|
257
|
-
await runCli(['discover', '--json'], {cwd: project});
|
|
258
|
-
|
|
259
|
-
expect(fs.readdirSync(project).sort()).toEqual(before);
|
|
260
|
-
expect(fs.existsSync(path.join(project, 'node_modules', '.cache'))).toBe(
|
|
261
|
-
false,
|
|
262
|
-
);
|
|
263
|
-
expect(fs.readdirSync(path.join(userCacheDir(), 'discover'))).toHaveLength(
|
|
264
|
-
1,
|
|
265
|
-
);
|
|
266
|
-
});
|
|
267
|
-
});
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* @file A parse failure reads like every other Astryx error in text mode.
|
|
5
|
-
*
|
|
6
|
-
* `astryx theme list --lang zh-Hans` printed Commander's own line —
|
|
7
|
-
* `error: option '--lang <locale>' argument 'zh-Hans' is invalid…` — while
|
|
8
|
-
* every other CLI error prints `Error: …`. `--json` was already correct
|
|
9
|
-
* (ERR_INVALID_LANG), so text and JSON disagreed on everything except the exit
|
|
10
|
-
* code. Commander writes that line before any Astryx code runs, so the shim is
|
|
11
|
-
* the only place that can speak for it.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {describe, it, expect} from 'vitest';
|
|
15
|
-
import {Command} from 'commander';
|
|
16
|
-
import {runCli} from '../../../test-utils/run-cli.mjs';
|
|
17
|
-
import {installJsonShim} from './json-shim.mjs';
|
|
18
|
-
|
|
19
|
-
describe('a parse error prints the Astryx error format', () => {
|
|
20
|
-
it.each([
|
|
21
|
-
['invalid --lang value', ['theme', 'list', '--lang', 'zh-Hans'], 'ERR_INVALID_LANG'],
|
|
22
|
-
['invalid --detail value', ['theme', 'list', '--detail', 'nope'], 'ERR_INVALID_DETAIL'],
|
|
23
|
-
['unknown option', ['component', '--bogus-flag'], 'ERR_INVALID_OPTION'],
|
|
24
|
-
['missing argument', ['theme', 'build'], 'ERR_MISSING_ARGUMENT'],
|
|
25
|
-
])('%s', async (_label, args, code) => {
|
|
26
|
-
const human = await runCli(args);
|
|
27
|
-
|
|
28
|
-
expect(human.status).toBe(1);
|
|
29
|
-
expect(human.stderr).toContain('Error: ');
|
|
30
|
-
// Commander's own lowercase line must not reach the user.
|
|
31
|
-
expect(human.stderr).not.toMatch(/(^|\n)error: /);
|
|
32
|
-
|
|
33
|
-
// --json is unchanged, and the two modes agree on the exit code.
|
|
34
|
-
const json = await runCli(['--json', ...args]);
|
|
35
|
-
expect(json.status).toBe(human.status);
|
|
36
|
-
expect(JSON.parse(json.stdout).code).toBe(code);
|
|
37
|
-
expect(json.stderr).toBe('');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('carries Commander\'s explanation, not just a generic line', async () => {
|
|
41
|
-
const {stderr} = await runCli(['theme', 'list', '--lang', 'zh-Hans']);
|
|
42
|
-
expect(stderr).toContain("'zh-Hans' is invalid");
|
|
43
|
-
expect(stderr).toContain('en, zh, dense');
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('leaves --help and --version at exit 0 with nothing on stderr', async () => {
|
|
47
|
-
for (const args of [['--help'], ['theme', '--help']]) {
|
|
48
|
-
const r = await runCli(args);
|
|
49
|
-
expect(r.status).toBe(0);
|
|
50
|
-
expect(r.stderr).toBe('');
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
// Commander writes help through the same stderr channel when it shows help
|
|
55
|
-
// BECAUSE the invocation failed, so suppressing that channel wholesale would
|
|
56
|
-
// have taken the help with it. Driven on a throwaway program rather than a
|
|
57
|
-
// real command, so the guard outlives whichever command happens to have a
|
|
58
|
-
// subcommand group today.
|
|
59
|
-
it('still lets help reach stderr when help IS the failure report', () => {
|
|
60
|
-
const program = new Command('probe');
|
|
61
|
-
const group = program.command('group');
|
|
62
|
-
group.command('leaf').action(() => {});
|
|
63
|
-
installJsonShim(program);
|
|
64
|
-
|
|
65
|
-
/** @type {string[]} */
|
|
66
|
-
const written = [];
|
|
67
|
-
const original = process.stderr.write;
|
|
68
|
-
// @ts-expect-error test double for the write signature
|
|
69
|
-
process.stderr.write = str => {
|
|
70
|
-
written.push(String(str));
|
|
71
|
-
return true;
|
|
72
|
-
};
|
|
73
|
-
try {
|
|
74
|
-
group.outputHelp({error: true});
|
|
75
|
-
} finally {
|
|
76
|
-
process.stderr.write = original;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
expect(written.join('')).toMatch(/Usage: probe group/);
|
|
80
|
-
});
|
|
81
|
-
});
|