@astryxdesign/cli 0.7.2-canary.c9f2659 → 0.7.2-canary.cbb7fa3
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/_adapter.d.mts +41 -1
- package/api/component/_adapter.mjs +93 -7
- package/api/component/component.mjs +22 -5
- package/api/component/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +644 -0
- package/api/component/list/list.mjs +42 -4
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +7 -3
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +108 -16
- package/api/search/search.test.mjs +36 -0
- package/api/swizzle/copy/copy.mjs +28 -14
- 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/tree/add-a-component.doc.mjs +10 -0
- package/assets/docs/tree/checks.doc.mjs +1 -1
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/single-component.doc.mjs +1 -1
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/versioning.doc.mjs +1 -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 +4 -4
- package/authoring/doctypes/component/type.ts +6 -3
- 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 +172 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +863 -55
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +40 -0
- package/foundation/discovery/module-exports.d.mts +27 -0
- package/foundation/discovery/module-exports.mjs +356 -31
- package/foundation/discovery/module-exports.test.mjs +172 -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/integrations/contribution-inventory.mjs +6 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Every surface knows an integration component, or a family member, by
|
|
5
|
+
* the name its doc documents. Fixtures live under a repo-local temp dir: Vite
|
|
6
|
+
* refuses to import a module from outside the project root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
10
|
+
import * as fs from 'node:fs';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import {spawnSync} from 'node:child_process';
|
|
13
|
+
import {component} from './component.mjs';
|
|
14
|
+
import {search} from '../search/search.mjs';
|
|
15
|
+
import {validateIntegration} from '../integration/validate-integration.mjs';
|
|
16
|
+
import {integrationComponentConflicts} from '../integration/authoring-checks.mjs';
|
|
17
|
+
import {swizzleCopy} from '../swizzle/copy/copy.mjs';
|
|
18
|
+
import {gapReport} from '../gap-report/gap-report.mjs';
|
|
19
|
+
import {Project} from '../../foundation/config/project.mjs';
|
|
20
|
+
import {collectDocInputs} from '../../foundation/doc-compiler/inputs.mjs';
|
|
21
|
+
import {COMPONENT_REPLACES_CLI} from '../../foundation/integrations/cli-requirement.mjs';
|
|
22
|
+
import {scopeIntegrationMember} from './_adapter.mjs';
|
|
23
|
+
|
|
24
|
+
const SLOW = 30_000;
|
|
25
|
+
|
|
26
|
+
/** @type {string} */
|
|
27
|
+
let tmpDir;
|
|
28
|
+
/** @type {string} */
|
|
29
|
+
let pkgDir;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {string} file path under the package's src/, without extension
|
|
33
|
+
* @param {string} name
|
|
34
|
+
* @param {string} source
|
|
35
|
+
* @param {string} description
|
|
36
|
+
* @param {object} [extra] more doc fields
|
|
37
|
+
*/
|
|
38
|
+
function writeComponent(file, name, source, description, extra = {}) {
|
|
39
|
+
const target = path.join(pkgDir, 'src', file);
|
|
40
|
+
fs.mkdirSync(path.dirname(target), {recursive: true});
|
|
41
|
+
fs.writeFileSync(`${target}.tsx`, source);
|
|
42
|
+
fs.writeFileSync(
|
|
43
|
+
`${target}.doc.mjs`,
|
|
44
|
+
`export default ${JSON.stringify(
|
|
45
|
+
{
|
|
46
|
+
type: 'component',
|
|
47
|
+
name,
|
|
48
|
+
displayName: name,
|
|
49
|
+
import: `@acme/widgets/${file}`,
|
|
50
|
+
usage: {description},
|
|
51
|
+
props: [],
|
|
52
|
+
...extra,
|
|
53
|
+
},
|
|
54
|
+
null,
|
|
55
|
+
2,
|
|
56
|
+
)};\n`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
tmpDir = fs.mkdtempSync(
|
|
62
|
+
path.join(process.cwd(), '.astryx-export-name-test-'),
|
|
63
|
+
);
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
path.join(tmpDir, 'package.json'),
|
|
66
|
+
JSON.stringify({name: 'consumer', version: '1.0.0'}),
|
|
67
|
+
);
|
|
68
|
+
fs.writeFileSync(
|
|
69
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
70
|
+
"export default {integrations: ['@acme/widgets']};\n",
|
|
71
|
+
);
|
|
72
|
+
pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
|
|
73
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
path.join(pkgDir, 'package.json'),
|
|
76
|
+
JSON.stringify({
|
|
77
|
+
name: '@acme/widgets',
|
|
78
|
+
version: '1.0.0',
|
|
79
|
+
exports: {
|
|
80
|
+
'./gallery/Slides': './src/gallery/Slides.tsx',
|
|
81
|
+
'./gallery/Overlays': './src/gallery/Overlays.tsx',
|
|
82
|
+
'./AcmeBadge': './src/AcmeBadge.tsx',
|
|
83
|
+
'./tabs': './src/tabs/AcmeTabs.tsx',
|
|
84
|
+
},
|
|
85
|
+
}),
|
|
86
|
+
);
|
|
87
|
+
fs.writeFileSync(
|
|
88
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
89
|
+
"export default {components: './src', issuesUrl: 'https://example.com/acme/issues'};\n",
|
|
90
|
+
);
|
|
91
|
+
writeComponent(
|
|
92
|
+
'gallery/Slides',
|
|
93
|
+
'AcmeCarousel',
|
|
94
|
+
'export function AcmeCarousel() { return null; }\nexport function useSlides() { return []; }\n',
|
|
95
|
+
'Cycles through a row of slides.',
|
|
96
|
+
);
|
|
97
|
+
writeComponent(
|
|
98
|
+
'AcmeBadge',
|
|
99
|
+
'AcmeBadge',
|
|
100
|
+
'export function AcmeBadge() { return null; }\n',
|
|
101
|
+
'A small status label.',
|
|
102
|
+
);
|
|
103
|
+
// A family doc whose members the module it imports exports.
|
|
104
|
+
fs.mkdirSync(path.join(pkgDir, 'src', 'tabs'), {recursive: true});
|
|
105
|
+
fs.writeFileSync(
|
|
106
|
+
path.join(pkgDir, 'src', 'tabs', 'AcmeTabs.tsx'),
|
|
107
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\n',
|
|
108
|
+
);
|
|
109
|
+
fs.writeFileSync(
|
|
110
|
+
path.join(pkgDir, 'src', 'tabs', 'AcmeTabs.doc.mjs'),
|
|
111
|
+
`export default ${JSON.stringify(
|
|
112
|
+
{
|
|
113
|
+
type: 'component',
|
|
114
|
+
name: 'AcmeTabs',
|
|
115
|
+
displayName: 'Acme Tabs',
|
|
116
|
+
import: '@acme/widgets/tabs',
|
|
117
|
+
keywords: ['tabs', 'navigation'],
|
|
118
|
+
usage: {description: 'Switches between views.'},
|
|
119
|
+
components: [
|
|
120
|
+
{
|
|
121
|
+
name: 'AcmeTabs',
|
|
122
|
+
displayName: 'Acme Tabs',
|
|
123
|
+
description: 'The tab list.',
|
|
124
|
+
props: [],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: 'AcmeTab',
|
|
128
|
+
displayName: 'Acme Tab',
|
|
129
|
+
description: 'One labelled tab in the list.',
|
|
130
|
+
props: [
|
|
131
|
+
{name: 'label', type: 'string', description: 'The tab label.'},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
null,
|
|
137
|
+
2,
|
|
138
|
+
)};\n`,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** @param {any} list @returns {string[]} the integration's listed names */
|
|
147
|
+
function listedNames(list) {
|
|
148
|
+
return Object.values(list.data.components)
|
|
149
|
+
.flat()
|
|
150
|
+
.filter(entry => entry.package === '@acme/widgets')
|
|
151
|
+
.map(entry => entry.name)
|
|
152
|
+
.sort();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
describe('a component doc that names another export of its file', () => {
|
|
156
|
+
it(
|
|
157
|
+
"leaves a name a legacy docs package documents to that package's doc",
|
|
158
|
+
async () => {
|
|
159
|
+
const legacy = path.join(tmpDir, 'node_modules', '@legacy', 'docs');
|
|
160
|
+
fs.mkdirSync(path.join(legacy, 'docs'), {recursive: true});
|
|
161
|
+
fs.writeFileSync(
|
|
162
|
+
path.join(legacy, 'package.json'),
|
|
163
|
+
JSON.stringify({name: '@legacy/docs', astryx: {docs: './docs'}}),
|
|
164
|
+
);
|
|
165
|
+
for (const name of ['AcmeCarousel', 'Slides']) {
|
|
166
|
+
fs.writeFileSync(
|
|
167
|
+
path.join(legacy, 'docs', `${name}.doc.mjs`),
|
|
168
|
+
`export default {type: 'component', name: '${name}', usage: {description: 'Legacy.'}, props: []};\n`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const cwd = tmpDir;
|
|
172
|
+
expect((await component('AcmeCarousel', {cwd})).data.package).toBe(
|
|
173
|
+
'@legacy/docs',
|
|
174
|
+
);
|
|
175
|
+
const scoped = {cwd, package: '@acme/widgets'};
|
|
176
|
+
expect((await component('AcmeCarousel', scoped)).data.package).toBe(
|
|
177
|
+
'@acme/widgets',
|
|
178
|
+
);
|
|
179
|
+
// Slides.doc.mjs's file name still answers first, as on main.
|
|
180
|
+
expect((await component('Slides', {cwd})).data.package).toBe(
|
|
181
|
+
'@acme/widgets',
|
|
182
|
+
);
|
|
183
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
184
|
+
for (const name of ['AcmeCarousel', 'acmecarousel']) {
|
|
185
|
+
expect((await gapReport(name, report)).data.package).toBe(
|
|
186
|
+
'@astryxdesign/core',
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
const hits = (await search('AcmeCarousel', {cwd, type: 'component'})).data
|
|
190
|
+
.results;
|
|
191
|
+
expect(hits.find(hit => hit.package === '@acme/widgets')?.command).toBe(
|
|
192
|
+
'astryx component AcmeCarousel --package @acme/widgets',
|
|
193
|
+
);
|
|
194
|
+
},
|
|
195
|
+
SLOW,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
it(
|
|
199
|
+
'is listed, opened and found by the export name',
|
|
200
|
+
async () => {
|
|
201
|
+
expect(
|
|
202
|
+
listedNames(await component(undefined, {cwd: tmpDir, list: true})),
|
|
203
|
+
).toEqual(['AcmeBadge', 'AcmeCarousel', 'AcmeTab', 'AcmeTabs']);
|
|
204
|
+
|
|
205
|
+
const detail = await component('AcmeCarousel', {cwd: tmpDir});
|
|
206
|
+
expect(detail.type).toBe('component.detail');
|
|
207
|
+
expect(detail.data).toMatchObject({
|
|
208
|
+
name: 'AcmeCarousel',
|
|
209
|
+
package: '@acme/widgets',
|
|
210
|
+
import: '@acme/widgets/gallery/Slides',
|
|
211
|
+
sourceAvailable: true,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
const hits = (
|
|
215
|
+
await search('AcmeCarousel', {cwd: tmpDir, type: 'component'})
|
|
216
|
+
).data.results;
|
|
217
|
+
expect(hits[0]).toMatchObject({
|
|
218
|
+
name: 'AcmeCarousel',
|
|
219
|
+
package: '@acme/widgets',
|
|
220
|
+
import: '@acme/widgets/gallery/Slides',
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// The file name opens the same doc, as it did before, routes a gap
|
|
224
|
+
// report to the package, and search finds the component by it, a word
|
|
225
|
+
// of it included.
|
|
226
|
+
expect((await component('Slides', {cwd: tmpDir})).data.name).toBe(
|
|
227
|
+
'AcmeCarousel',
|
|
228
|
+
);
|
|
229
|
+
const report = {cwd: tmpDir, category: 'other', reason: 'Needs a size.'};
|
|
230
|
+
expect((await gapReport('Slides', report)).data.package).toBe(
|
|
231
|
+
'@acme/widgets',
|
|
232
|
+
);
|
|
233
|
+
for (const query of ['Slides', 'slide']) {
|
|
234
|
+
const byFileName = (
|
|
235
|
+
await search(query, {cwd: tmpDir, type: 'component'})
|
|
236
|
+
).data.results;
|
|
237
|
+
expect(byFileName[0]?.name).toBe('AcmeCarousel');
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
SLOW,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
it.each(['the export its doc names', 'a member its family doc lists'])(
|
|
244
|
+
"leaves a Core name to Core unless --package names it, when it is %s, and passes Doctor's package checks",
|
|
245
|
+
async kind => {
|
|
246
|
+
// Overlays.tsx exports Dialog, a Core name, and nothing named Overlays.
|
|
247
|
+
const members = [{name: 'Dialog', description: 'A dialog.', props: []}];
|
|
248
|
+
const member = kind === 'a member its family doc lists';
|
|
249
|
+
writeComponent(
|
|
250
|
+
'gallery/Overlays',
|
|
251
|
+
member ? 'AcmeOverlays' : 'Dialog',
|
|
252
|
+
'export function AcmeOverlays() { return null; }\nexport function Dialog() { return null; }\n',
|
|
253
|
+
'Overlays.',
|
|
254
|
+
member ? {components: members} : {},
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const validation = await validateIntegration(undefined, {cwd: pkgDir});
|
|
258
|
+
expect(validation.data.issues).toEqual([]);
|
|
259
|
+
|
|
260
|
+
const conflicts = await integrationComponentConflicts(undefined, {
|
|
261
|
+
cwd: pkgDir,
|
|
262
|
+
});
|
|
263
|
+
expect(conflicts.data.conflicts.map(conflict => conflict.name)).toEqual([
|
|
264
|
+
'Dialog',
|
|
265
|
+
]);
|
|
266
|
+
|
|
267
|
+
// Lookups by the bare name still answer with Core's Dialog, as before.
|
|
268
|
+
const cwd = tmpDir;
|
|
269
|
+
const core = '@astryxdesign/core';
|
|
270
|
+
expect((await component('Dialog', {cwd})).data.package).toBe(core);
|
|
271
|
+
expect((await component('Dialog', {cwd, props: true})).package).toBe(
|
|
272
|
+
core,
|
|
273
|
+
);
|
|
274
|
+
expect((await swizzleCopy('Dialog', {cwd})).package).toBe(core);
|
|
275
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
276
|
+
expect((await gapReport('Dialog', report)).data.package).toBe(core);
|
|
277
|
+
// The list and --package reach the integration's Dialog.
|
|
278
|
+
const scoped = {cwd, package: '@acme/widgets'};
|
|
279
|
+
expect((await component('Dialog', scoped)).data.import).toBe(
|
|
280
|
+
'@acme/widgets/gallery/Overlays',
|
|
281
|
+
);
|
|
282
|
+
expect(
|
|
283
|
+
listedNames(await component(undefined, {cwd, list: true})),
|
|
284
|
+
).toContain('Dialog');
|
|
285
|
+
// Search shows both; the package's hit names its package and follows
|
|
286
|
+
// Core's.
|
|
287
|
+
const hits = (await search('Dialog', {cwd, type: 'component'})).data
|
|
288
|
+
.results;
|
|
289
|
+
const ours = hits.findIndex(hit => hit.package === '@acme/widgets');
|
|
290
|
+
expect(hits[0]).toMatchObject({name: 'Dialog', package: core});
|
|
291
|
+
expect(hits[ours]).toMatchObject({
|
|
292
|
+
name: 'Dialog',
|
|
293
|
+
command: 'astryx component Dialog --package @acme/widgets',
|
|
294
|
+
});
|
|
295
|
+
},
|
|
296
|
+
SLOW,
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
it(
|
|
300
|
+
'leaves a Core name with no folder of its own unknown to swizzle, as before',
|
|
301
|
+
async () => {
|
|
302
|
+
// Core documents CheckboxIndicator inside Indicator's doc, with no folder.
|
|
303
|
+
// BaseTable, named after its file, keeps routing reports to its package.
|
|
304
|
+
writeComponent(
|
|
305
|
+
'BaseTable',
|
|
306
|
+
'BaseTable',
|
|
307
|
+
'export function BaseTable() { return null; }\n',
|
|
308
|
+
'A file-named table.',
|
|
309
|
+
);
|
|
310
|
+
writeComponent(
|
|
311
|
+
'gallery/Indicator',
|
|
312
|
+
'CheckboxIndicator',
|
|
313
|
+
'export function CheckboxIndicator() { return null; }\n',
|
|
314
|
+
'A header.',
|
|
315
|
+
);
|
|
316
|
+
const cwd = tmpDir;
|
|
317
|
+
expect((await component('CheckboxIndicator', {cwd})).data.package).toBe(
|
|
318
|
+
'@astryxdesign/core',
|
|
319
|
+
);
|
|
320
|
+
await expect(
|
|
321
|
+
swizzleCopy('CheckboxIndicator', {cwd}),
|
|
322
|
+
).rejects.toMatchObject({
|
|
323
|
+
code: 'ERR_UNKNOWN_COMPONENT',
|
|
324
|
+
});
|
|
325
|
+
const scoped = {cwd, package: '@acme/widgets'};
|
|
326
|
+
expect((await swizzleCopy('CheckboxIndicator', scoped)).package).toBe(
|
|
327
|
+
'@acme/widgets',
|
|
328
|
+
);
|
|
329
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
330
|
+
expect((await gapReport('CheckboxIndicator', report)).data.package).toBe(
|
|
331
|
+
'@astryxdesign/core',
|
|
332
|
+
);
|
|
333
|
+
const hit = (
|
|
334
|
+
await search('CheckboxIndicator', {cwd, type: 'component'})
|
|
335
|
+
).data.results.find(result => result.package === '@acme/widgets');
|
|
336
|
+
expect(hit?.command).toBe(
|
|
337
|
+
'astryx component CheckboxIndicator --package @acme/widgets',
|
|
338
|
+
);
|
|
339
|
+
expect((await gapReport('BaseTable', report)).data.package).toBe(
|
|
340
|
+
'@acme/widgets',
|
|
341
|
+
);
|
|
342
|
+
},
|
|
343
|
+
SLOW,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
it.each([
|
|
347
|
+
// props must be an array: the loader rejects this doc.
|
|
348
|
+
[
|
|
349
|
+
'invalid',
|
|
350
|
+
"export default {type: 'component', name: 'AcmeTab', props: 'x'};",
|
|
351
|
+
],
|
|
352
|
+
[
|
|
353
|
+
'hidden',
|
|
354
|
+
"export default {\n type: 'component',\n name: 'AcmeTab',\n hidden: true,\n usage: {description: 'x'},\n props: [],\n};",
|
|
355
|
+
],
|
|
356
|
+
])(
|
|
357
|
+
"leaves a name to another package's file-named doc even when that doc is %s",
|
|
358
|
+
async (kind, doc) => {
|
|
359
|
+
writeComponent(
|
|
360
|
+
'gallery/Strip',
|
|
361
|
+
'AcmeTab',
|
|
362
|
+
'export function AcmeTab() { return null; }\n',
|
|
363
|
+
'A strip of tabs.',
|
|
364
|
+
);
|
|
365
|
+
const beta = path.join(tmpDir, 'node_modules', '@beta', 'kit');
|
|
366
|
+
fs.mkdirSync(path.join(beta, 'src'), {recursive: true});
|
|
367
|
+
for (const [file, text] of Object.entries({
|
|
368
|
+
'package.json': '{"name": "@beta/kit", "version": "1.0.0"}',
|
|
369
|
+
'astryx.integration.mjs': "export default {components: './src'};",
|
|
370
|
+
'src/AcmeTab.tsx': 'export function AcmeTab() { return null; }',
|
|
371
|
+
'src/AcmeTab.doc.mjs': doc,
|
|
372
|
+
})) {
|
|
373
|
+
fs.writeFileSync(path.join(beta, file), `${text}\n`);
|
|
374
|
+
}
|
|
375
|
+
fs.writeFileSync(
|
|
376
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
377
|
+
"export default {integrations: ['@acme/widgets', '@beta/kit']};\n",
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const cwd = tmpDir;
|
|
381
|
+
expect((await component('AcmeTab', {cwd, source: true})).package).toBe(
|
|
382
|
+
'@beta/kit',
|
|
383
|
+
);
|
|
384
|
+
if (kind === 'invalid') {
|
|
385
|
+
await expect(component('AcmeTab', {cwd})).rejects.toMatchObject({
|
|
386
|
+
code: 'ERR_INVALID_DOC',
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
390
|
+
for (const name of ['AcmeTab', 'acmetab']) {
|
|
391
|
+
expect((await gapReport(name, report)).data.package).toBe(
|
|
392
|
+
'@astryxdesign/core',
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const hit = (
|
|
396
|
+
await search('AcmeTab', {cwd, type: 'component'})
|
|
397
|
+
).data.results.find(result => result.package === '@acme/widgets');
|
|
398
|
+
expect(hit?.command).toBe(
|
|
399
|
+
'astryx component AcmeTab --package @acme/widgets',
|
|
400
|
+
);
|
|
401
|
+
},
|
|
402
|
+
SLOW,
|
|
403
|
+
);
|
|
404
|
+
|
|
405
|
+
it(
|
|
406
|
+
'leaves a file name Core also has to Core in search, as in detail',
|
|
407
|
+
async () => {
|
|
408
|
+
// Badge.doc.mjs documents AcmeStatusBadge; its file name is Core's.
|
|
409
|
+
writeComponent(
|
|
410
|
+
'status/Badge',
|
|
411
|
+
'AcmeStatusBadge',
|
|
412
|
+
'export function AcmeStatusBadge() { return null; }\n',
|
|
413
|
+
'Shows a status.',
|
|
414
|
+
);
|
|
415
|
+
const cwd = tmpDir;
|
|
416
|
+
const [first] = (await search('Badge', {cwd, type: 'component'})).data
|
|
417
|
+
.results;
|
|
418
|
+
expect(first).toMatchObject({
|
|
419
|
+
name: 'Badge',
|
|
420
|
+
package: '@astryxdesign/core',
|
|
421
|
+
});
|
|
422
|
+
expect((await component('Badge', {cwd})).data.package).toBe(
|
|
423
|
+
'@astryxdesign/core',
|
|
424
|
+
);
|
|
425
|
+
const [own] = (await search('AcmeStatusBadge', {cwd, type: 'component'}))
|
|
426
|
+
.data.results;
|
|
427
|
+
expect(own).toMatchObject({
|
|
428
|
+
name: 'AcmeStatusBadge',
|
|
429
|
+
command: 'astryx component AcmeStatusBadge',
|
|
430
|
+
});
|
|
431
|
+
},
|
|
432
|
+
SLOW,
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
it(
|
|
436
|
+
'copies a replacement recorded under its export into its file name folder, as before',
|
|
437
|
+
async () => {
|
|
438
|
+
const manifest = path.join(pkgDir, 'package.json');
|
|
439
|
+
fs.writeFileSync(
|
|
440
|
+
manifest,
|
|
441
|
+
JSON.stringify({
|
|
442
|
+
...JSON.parse(fs.readFileSync(manifest, 'utf-8')),
|
|
443
|
+
peerDependencies: {
|
|
444
|
+
'@astryxdesign/cli': `>=${COMPONENT_REPLACES_CLI}`,
|
|
445
|
+
},
|
|
446
|
+
}),
|
|
447
|
+
);
|
|
448
|
+
writeComponent(
|
|
449
|
+
'gallery/Slides',
|
|
450
|
+
'AcmeCarousel',
|
|
451
|
+
'export function AcmeCarousel() { return null; }\n',
|
|
452
|
+
'Cycles through a row of slides.',
|
|
453
|
+
{replaces: 'Carousel'},
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
const copied = await swizzleCopy('Carousel', {cwd: tmpDir});
|
|
457
|
+
expect(copied.data).toMatchObject({
|
|
458
|
+
package: '@acme/widgets',
|
|
459
|
+
outputDir: path.join('components', 'astryx', 'Slides'),
|
|
460
|
+
});
|
|
461
|
+
},
|
|
462
|
+
SLOW,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
it(
|
|
466
|
+
"opens a member named like its family doc's own name as that member",
|
|
467
|
+
async () => {
|
|
468
|
+
// Overlays.tsx exports Overlays, so the doc keeps its file name, and
|
|
469
|
+
// AcmeChip, the doc's own name, is a member.
|
|
470
|
+
const label = {name: 'label', type: 'string', description: 'The label.'};
|
|
471
|
+
writeComponent(
|
|
472
|
+
'gallery/Overlays',
|
|
473
|
+
'AcmeChip',
|
|
474
|
+
'export function Overlays() { return null; }\nexport function AcmeChip() { return null; }\nexport function AcmeChipIcon() { return null; }\n',
|
|
475
|
+
'Chips.',
|
|
476
|
+
{
|
|
477
|
+
components: [
|
|
478
|
+
{name: 'AcmeChip', description: 'One chip.', props: [label]},
|
|
479
|
+
{name: 'AcmeChipIcon', description: 'An icon.', props: []},
|
|
480
|
+
],
|
|
481
|
+
},
|
|
482
|
+
);
|
|
483
|
+
// Every member names the family by the name it opens under.
|
|
484
|
+
expect(
|
|
485
|
+
(await component('AcmeChipIcon', {cwd: tmpDir})).data.parentDoc,
|
|
486
|
+
).toBe('Overlays');
|
|
487
|
+
const detail = await component('AcmeChip', {cwd: tmpDir});
|
|
488
|
+
expect(detail.data).toMatchObject({
|
|
489
|
+
name: 'AcmeChip',
|
|
490
|
+
description: 'One chip.',
|
|
491
|
+
parentDoc: 'Overlays',
|
|
492
|
+
});
|
|
493
|
+
const props = await component('AcmeChip', {cwd: tmpDir, props: true});
|
|
494
|
+
expect(props.data.map(prop => prop.name)).toEqual(['label']);
|
|
495
|
+
},
|
|
496
|
+
SLOW,
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
it(
|
|
500
|
+
'opens, lists and finds a family member as itself, with the import its family states',
|
|
501
|
+
async () => {
|
|
502
|
+
const list = await component(undefined, {cwd: tmpDir, list: true});
|
|
503
|
+
const entry = Object.values(list.data.components)
|
|
504
|
+
.flat()
|
|
505
|
+
.find(item => item.name === 'AcmeTab');
|
|
506
|
+
expect(entry).toEqual({
|
|
507
|
+
name: 'AcmeTab',
|
|
508
|
+
package: '@acme/widgets',
|
|
509
|
+
import: '@acme/widgets/tabs',
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const detail = await component('AcmeTab', {cwd: tmpDir});
|
|
513
|
+
expect(detail.data).toMatchObject({
|
|
514
|
+
name: 'AcmeTab',
|
|
515
|
+
description: 'One labelled tab in the list.',
|
|
516
|
+
parentDoc: 'AcmeTabs',
|
|
517
|
+
package: '@acme/widgets',
|
|
518
|
+
import: '@acme/widgets/tabs',
|
|
519
|
+
sourceAvailable: true,
|
|
520
|
+
});
|
|
521
|
+
expect(detail.data.props.map(prop => prop.name)).toEqual(['label']);
|
|
522
|
+
const props = await component('AcmeTab', {cwd: tmpDir, props: true});
|
|
523
|
+
expect(props.data.map(prop => prop.name)).toEqual(['label']);
|
|
524
|
+
// The family's own doc is unchanged.
|
|
525
|
+
expect((await component('AcmeTabs', {cwd: tmpDir})).data).toMatchObject({
|
|
526
|
+
name: 'AcmeTabs',
|
|
527
|
+
import: '@acme/widgets/tabs',
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const hits = (await search('AcmeTab', {cwd: tmpDir, type: 'component'}))
|
|
531
|
+
.data.results;
|
|
532
|
+
expect(hits[0]).toMatchObject({
|
|
533
|
+
name: 'AcmeTab',
|
|
534
|
+
description: 'One labelled tab in the list.',
|
|
535
|
+
import: '@acme/widgets/tabs',
|
|
536
|
+
});
|
|
537
|
+
// The member does not carry the family's keywords.
|
|
538
|
+
const tabs = (
|
|
539
|
+
await search('navigation', {cwd: tmpDir, type: 'component'})
|
|
540
|
+
).data.results;
|
|
541
|
+
expect(tabs.map(hit => hit.name)).toContain('AcmeTabs');
|
|
542
|
+
expect(tabs.map(hit => hit.name)).not.toContain('AcmeTab');
|
|
543
|
+
},
|
|
544
|
+
SLOW,
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
it(
|
|
548
|
+
'leaves a member named like a Core sub-component to Core, and swizzle unknown',
|
|
549
|
+
async () => {
|
|
550
|
+
const tabs = path.join(pkgDir, 'src', 'tabs');
|
|
551
|
+
const doc = path.join(tabs, 'AcmeTabs.doc.mjs');
|
|
552
|
+
fs.writeFileSync(
|
|
553
|
+
doc,
|
|
554
|
+
fs
|
|
555
|
+
.readFileSync(doc, 'utf-8')
|
|
556
|
+
.replace(
|
|
557
|
+
'"components": [',
|
|
558
|
+
'"components": [{"name": "DialogHeader", "description": "x"},',
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
fs.appendFileSync(
|
|
562
|
+
path.join(tabs, 'AcmeTabs.tsx'),
|
|
563
|
+
'export function DialogHeader() { return null; }\n',
|
|
564
|
+
);
|
|
565
|
+
const cwd = tmpDir;
|
|
566
|
+
expect(
|
|
567
|
+
listedNames(await component(undefined, {cwd, list: true})),
|
|
568
|
+
).toContain('DialogHeader');
|
|
569
|
+
expect((await component('DialogHeader', {cwd})).data.package).toBe(
|
|
570
|
+
'@astryxdesign/core',
|
|
571
|
+
);
|
|
572
|
+
await expect(swizzleCopy('DialogHeader', {cwd})).rejects.toMatchObject({
|
|
573
|
+
code: 'ERR_UNKNOWN_COMPONENT',
|
|
574
|
+
});
|
|
575
|
+
},
|
|
576
|
+
SLOW,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
it('fails a member its family doc, as this process loaded it, does not list', async () => {
|
|
580
|
+
const owner = {
|
|
581
|
+
package: '@acme/widgets',
|
|
582
|
+
docPath: path.join(pkgDir, 'src', 'tabs', 'AcmeTabs.doc.mjs'),
|
|
583
|
+
sourcePath: null,
|
|
584
|
+
};
|
|
585
|
+
// The doc this process loaded before AcmeTab was added.
|
|
586
|
+
const stale = {type: 'component', name: 'AcmeTabs', components: []};
|
|
587
|
+
expect(() =>
|
|
588
|
+
scopeIntegrationMember(/** @type {any} */ (stale), owner, 'AcmeTab', ''),
|
|
589
|
+
).toThrow(expect.objectContaining({code: 'ERR_UNKNOWN_COMPONENT'}));
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it(
|
|
593
|
+
'indexes a .doc.ts family member as a member when Node cannot load the doc',
|
|
594
|
+
() => {
|
|
595
|
+
const fam = path.join(pkgDir, 'src', 'fam');
|
|
596
|
+
fs.mkdirSync(fam, {recursive: true});
|
|
597
|
+
const manifest = path.join(pkgDir, 'package.json');
|
|
598
|
+
const pkg = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
599
|
+
pkg.exports['./fam'] = './src/fam/AcmeFamTs.tsx';
|
|
600
|
+
fs.writeFileSync(manifest, JSON.stringify(pkg));
|
|
601
|
+
fs.writeFileSync(
|
|
602
|
+
path.join(fam, 'AcmeFamTs.doc.ts'),
|
|
603
|
+
"export default {type: 'component', name: 'AcmeFamTs', import: '@acme/widgets/fam', usage: {description: 'A family.'}, components: [{name: 'AcmeFamTs', description: 'x'}, {name: 'AcmeFamItem', description: 'One item.'}]};\n",
|
|
604
|
+
);
|
|
605
|
+
fs.writeFileSync(
|
|
606
|
+
path.join(fam, 'AcmeFamTs.tsx'),
|
|
607
|
+
'export function AcmeFamTs() { return null; }\nexport function AcmeFamItem() { return null; }\n',
|
|
608
|
+
);
|
|
609
|
+
// The real CLI under Node, which does not strip types in node_modules.
|
|
610
|
+
const run = spawnSync(
|
|
611
|
+
process.execPath,
|
|
612
|
+
[
|
|
613
|
+
path.join(process.cwd(), 'packages/cli/clients/cli/bin/astryx.mjs'),
|
|
614
|
+
'search',
|
|
615
|
+
'AcmeFamTs',
|
|
616
|
+
'--json',
|
|
617
|
+
],
|
|
618
|
+
{cwd: tmpDir, encoding: 'utf-8'},
|
|
619
|
+
);
|
|
620
|
+
const hits = JSON.parse(run.stdout).data.results.filter(
|
|
621
|
+
(/** @type {any} */ hit) => hit.domain === 'component',
|
|
622
|
+
);
|
|
623
|
+
expect(hits.map((/** @type {any} */ hit) => hit.name)).toEqual([
|
|
624
|
+
'AcmeFamTs',
|
|
625
|
+
]);
|
|
626
|
+
},
|
|
627
|
+
SLOW,
|
|
628
|
+
);
|
|
629
|
+
|
|
630
|
+
it(
|
|
631
|
+
'reads a family doc once when the docs compile',
|
|
632
|
+
async () => {
|
|
633
|
+
const {inputs, problems} = await collectDocInputs(
|
|
634
|
+
await Project.load(tmpDir),
|
|
635
|
+
);
|
|
636
|
+
expect(problems).toEqual([]);
|
|
637
|
+
const tabsDoc = path.join(pkgDir, 'src', 'tabs', 'AcmeTabs.doc.mjs');
|
|
638
|
+
expect(
|
|
639
|
+
inputs.filter(input => input.file === tabsDoc).map(input => input.name),
|
|
640
|
+
).toEqual(['AcmeTabs']);
|
|
641
|
+
},
|
|
642
|
+
SLOW,
|
|
643
|
+
);
|
|
644
|
+
});
|