@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.ebcc404
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 +27 -1
- package/api/component/_adapter.mjs +45 -7
- package/api/component/component.mjs +12 -3
- package/api/component/component.test.mjs +420 -167
- package/api/component/integration-export-name.test.mjs +419 -0
- package/api/component/list/list.mjs +42 -4
- package/api/gap-report/gap-report.mjs +66 -4
- package/api/integration/pack-check.mjs +13 -104
- package/api/integration/pack-check.test.mjs +112 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +90 -10
- 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/component-imports.doc.mjs +8 -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 +3 -3
- 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 +95 -6
- package/foundation/discovery/component-discovery.export-names.test.mjs +293 -0
- package/foundation/discovery/component-discovery.mjs +524 -49
- package/foundation/discovery/component-discovery.test.mjs +96 -0
- package/foundation/discovery/component-replacement.mjs +18 -0
- package/foundation/discovery/component-replacement.test.mjs +20 -0
- package/foundation/discovery/module-exports.d.mts +21 -0
- package/foundation/discovery/module-exports.mjs +343 -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/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Every surface knows an integration component by the name its doc
|
|
5
|
+
* documents. Fixtures live under a repo-local temp dir: Vite refuses to import
|
|
6
|
+
* 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 {component} from './component.mjs';
|
|
13
|
+
import {search} from '../search/search.mjs';
|
|
14
|
+
import {validateIntegration} from '../integration/validate-integration.mjs';
|
|
15
|
+
import {integrationComponentConflicts} from '../integration/authoring-checks.mjs';
|
|
16
|
+
import {swizzleCopy} from '../swizzle/copy/copy.mjs';
|
|
17
|
+
import {gapReport} from '../gap-report/gap-report.mjs';
|
|
18
|
+
import {COMPONENT_REPLACES_CLI} from '../../foundation/integrations/cli-requirement.mjs';
|
|
19
|
+
|
|
20
|
+
const SLOW = 30_000;
|
|
21
|
+
|
|
22
|
+
/** @type {string} */
|
|
23
|
+
let tmpDir;
|
|
24
|
+
/** @type {string} */
|
|
25
|
+
let pkgDir;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} file path under the package's src/, without extension
|
|
29
|
+
* @param {string} name
|
|
30
|
+
* @param {string} source
|
|
31
|
+
* @param {string} description
|
|
32
|
+
* @param {object} [extra] more doc fields
|
|
33
|
+
*/
|
|
34
|
+
function writeComponent(file, name, source, description, extra = {}) {
|
|
35
|
+
const target = path.join(pkgDir, 'src', file);
|
|
36
|
+
fs.mkdirSync(path.dirname(target), {recursive: true});
|
|
37
|
+
fs.writeFileSync(`${target}.tsx`, source);
|
|
38
|
+
fs.writeFileSync(
|
|
39
|
+
`${target}.doc.mjs`,
|
|
40
|
+
`export default ${JSON.stringify(
|
|
41
|
+
{
|
|
42
|
+
type: 'component',
|
|
43
|
+
name,
|
|
44
|
+
displayName: name,
|
|
45
|
+
import: `@acme/widgets/${file}`,
|
|
46
|
+
usage: {description},
|
|
47
|
+
props: [],
|
|
48
|
+
...extra,
|
|
49
|
+
},
|
|
50
|
+
null,
|
|
51
|
+
2,
|
|
52
|
+
)};\n`,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
tmpDir = fs.mkdtempSync(
|
|
58
|
+
path.join(process.cwd(), '.astryx-export-name-test-'),
|
|
59
|
+
);
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(tmpDir, 'package.json'),
|
|
62
|
+
JSON.stringify({name: 'consumer', version: '1.0.0'}),
|
|
63
|
+
);
|
|
64
|
+
fs.writeFileSync(
|
|
65
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
66
|
+
"export default {integrations: ['@acme/widgets']};\n",
|
|
67
|
+
);
|
|
68
|
+
pkgDir = path.join(tmpDir, 'node_modules', '@acme', 'widgets');
|
|
69
|
+
fs.mkdirSync(pkgDir, {recursive: true});
|
|
70
|
+
fs.writeFileSync(
|
|
71
|
+
path.join(pkgDir, 'package.json'),
|
|
72
|
+
JSON.stringify({
|
|
73
|
+
name: '@acme/widgets',
|
|
74
|
+
version: '1.0.0',
|
|
75
|
+
exports: {
|
|
76
|
+
'./gallery/Slides': './src/gallery/Slides.tsx',
|
|
77
|
+
'./gallery/Overlays': './src/gallery/Overlays.tsx',
|
|
78
|
+
'./AcmeBadge': './src/AcmeBadge.tsx',
|
|
79
|
+
},
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
fs.writeFileSync(
|
|
83
|
+
path.join(pkgDir, 'astryx.integration.mjs'),
|
|
84
|
+
"export default {components: './src', issuesUrl: 'https://example.com/acme/issues'};\n",
|
|
85
|
+
);
|
|
86
|
+
writeComponent(
|
|
87
|
+
'gallery/Slides',
|
|
88
|
+
'AcmeCarousel',
|
|
89
|
+
'export function AcmeCarousel() { return null; }\nexport function useSlides() { return []; }\n',
|
|
90
|
+
'Cycles through a row of slides.',
|
|
91
|
+
);
|
|
92
|
+
writeComponent(
|
|
93
|
+
'AcmeBadge',
|
|
94
|
+
'AcmeBadge',
|
|
95
|
+
'export function AcmeBadge() { return null; }\n',
|
|
96
|
+
'A small status label.',
|
|
97
|
+
);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
afterEach(() => {
|
|
101
|
+
fs.rmSync(tmpDir, {recursive: true, force: true});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
/** @param {any} list @returns {string[]} the integration's listed names */
|
|
105
|
+
function listedNames(list) {
|
|
106
|
+
return Object.values(list.data.components)
|
|
107
|
+
.flat()
|
|
108
|
+
.filter(entry => entry.package === '@acme/widgets')
|
|
109
|
+
.map(entry => entry.name)
|
|
110
|
+
.sort();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
describe('a component doc that names another export of its file', () => {
|
|
114
|
+
it(
|
|
115
|
+
"leaves a name a legacy docs package documents to that package's doc",
|
|
116
|
+
async () => {
|
|
117
|
+
const legacy = path.join(tmpDir, 'node_modules', '@legacy', 'docs');
|
|
118
|
+
fs.mkdirSync(path.join(legacy, 'docs'), {recursive: true});
|
|
119
|
+
fs.writeFileSync(
|
|
120
|
+
path.join(legacy, 'package.json'),
|
|
121
|
+
JSON.stringify({name: '@legacy/docs', astryx: {docs: './docs'}}),
|
|
122
|
+
);
|
|
123
|
+
for (const name of ['AcmeCarousel', 'Slides']) {
|
|
124
|
+
fs.writeFileSync(
|
|
125
|
+
path.join(legacy, 'docs', `${name}.doc.mjs`),
|
|
126
|
+
`export default {type: 'component', name: '${name}', usage: {description: 'Legacy.'}, props: []};\n`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const cwd = tmpDir;
|
|
130
|
+
expect((await component('AcmeCarousel', {cwd})).data.package).toBe(
|
|
131
|
+
'@legacy/docs',
|
|
132
|
+
);
|
|
133
|
+
const scoped = {cwd, package: '@acme/widgets'};
|
|
134
|
+
expect((await component('AcmeCarousel', scoped)).data.package).toBe(
|
|
135
|
+
'@acme/widgets',
|
|
136
|
+
);
|
|
137
|
+
// Slides.doc.mjs's file name still answers first, as on main.
|
|
138
|
+
expect((await component('Slides', {cwd})).data.package).toBe(
|
|
139
|
+
'@acme/widgets',
|
|
140
|
+
);
|
|
141
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
142
|
+
for (const name of ['AcmeCarousel', 'acmecarousel']) {
|
|
143
|
+
expect((await gapReport(name, report)).data.package).toBe(
|
|
144
|
+
'@astryxdesign/core',
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const hits = (await search('AcmeCarousel', {cwd, type: 'component'})).data
|
|
148
|
+
.results;
|
|
149
|
+
expect(hits.find(hit => hit.package === '@acme/widgets')?.command).toBe(
|
|
150
|
+
'astryx component AcmeCarousel --package @acme/widgets',
|
|
151
|
+
);
|
|
152
|
+
},
|
|
153
|
+
SLOW,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
it(
|
|
157
|
+
'is listed, opened and found by the export name',
|
|
158
|
+
async () => {
|
|
159
|
+
expect(
|
|
160
|
+
listedNames(await component(undefined, {cwd: tmpDir, list: true})),
|
|
161
|
+
).toEqual(['AcmeBadge', 'AcmeCarousel']);
|
|
162
|
+
|
|
163
|
+
const detail = await component('AcmeCarousel', {cwd: tmpDir});
|
|
164
|
+
expect(detail.type).toBe('component.detail');
|
|
165
|
+
expect(detail.data).toMatchObject({
|
|
166
|
+
name: 'AcmeCarousel',
|
|
167
|
+
package: '@acme/widgets',
|
|
168
|
+
import: '@acme/widgets/gallery/Slides',
|
|
169
|
+
sourceAvailable: true,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const hits = (
|
|
173
|
+
await search('AcmeCarousel', {cwd: tmpDir, type: 'component'})
|
|
174
|
+
).data.results;
|
|
175
|
+
expect(hits[0]).toMatchObject({
|
|
176
|
+
name: 'AcmeCarousel',
|
|
177
|
+
package: '@acme/widgets',
|
|
178
|
+
import: '@acme/widgets/gallery/Slides',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// The file name opens the same doc, as it did before, routes a gap
|
|
182
|
+
// report to the package, and search finds the component by it, a word
|
|
183
|
+
// of it included.
|
|
184
|
+
expect((await component('Slides', {cwd: tmpDir})).data.name).toBe(
|
|
185
|
+
'AcmeCarousel',
|
|
186
|
+
);
|
|
187
|
+
const report = {cwd: tmpDir, category: 'other', reason: 'Needs a size.'};
|
|
188
|
+
expect((await gapReport('Slides', report)).data.package).toBe(
|
|
189
|
+
'@acme/widgets',
|
|
190
|
+
);
|
|
191
|
+
for (const query of ['Slides', 'slide']) {
|
|
192
|
+
const byFileName = (
|
|
193
|
+
await search(query, {cwd: tmpDir, type: 'component'})
|
|
194
|
+
).data.results;
|
|
195
|
+
expect(byFileName[0]?.name).toBe('AcmeCarousel');
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
SLOW,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
it(
|
|
202
|
+
"leaves a Core name to Core unless --package names it, and passes Doctor's package checks",
|
|
203
|
+
async () => {
|
|
204
|
+
// Overlays.tsx exports Dialog, a Core name, and nothing named Overlays.
|
|
205
|
+
writeComponent(
|
|
206
|
+
'gallery/Overlays',
|
|
207
|
+
'Dialog',
|
|
208
|
+
'export function AcmeOverlays() { return null; }\nexport function Dialog() { return null; }\n',
|
|
209
|
+
'Overlays.',
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const validation = await validateIntegration(undefined, {cwd: pkgDir});
|
|
213
|
+
expect(validation.data.issues).toEqual([]);
|
|
214
|
+
|
|
215
|
+
const conflicts = await integrationComponentConflicts(undefined, {
|
|
216
|
+
cwd: pkgDir,
|
|
217
|
+
});
|
|
218
|
+
expect(conflicts.data.conflicts.map(conflict => conflict.name)).toEqual([
|
|
219
|
+
'Dialog',
|
|
220
|
+
]);
|
|
221
|
+
|
|
222
|
+
// Lookups by the bare name still answer with Core's Dialog, as before.
|
|
223
|
+
const cwd = tmpDir;
|
|
224
|
+
const core = '@astryxdesign/core';
|
|
225
|
+
expect((await component('Dialog', {cwd})).data.package).toBe(core);
|
|
226
|
+
expect((await component('Dialog', {cwd, props: true})).package).toBe(
|
|
227
|
+
core,
|
|
228
|
+
);
|
|
229
|
+
expect((await swizzleCopy('Dialog', {cwd})).package).toBe(core);
|
|
230
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
231
|
+
expect((await gapReport('Dialog', report)).data.package).toBe(core);
|
|
232
|
+
// The list and --package reach the integration's Dialog.
|
|
233
|
+
const scoped = {cwd, package: '@acme/widgets'};
|
|
234
|
+
expect((await component('Dialog', scoped)).data.import).toBe(
|
|
235
|
+
'@acme/widgets/gallery/Overlays',
|
|
236
|
+
);
|
|
237
|
+
expect(
|
|
238
|
+
listedNames(await component(undefined, {cwd, list: true})),
|
|
239
|
+
).toContain('Dialog');
|
|
240
|
+
// Search shows both; the package's hit names its package and follows
|
|
241
|
+
// Core's.
|
|
242
|
+
const hits = (await search('Dialog', {cwd, type: 'component'})).data
|
|
243
|
+
.results;
|
|
244
|
+
const ours = hits.findIndex(hit => hit.package === '@acme/widgets');
|
|
245
|
+
expect(hits[0]).toMatchObject({name: 'Dialog', package: core});
|
|
246
|
+
expect(hits[ours]).toMatchObject({
|
|
247
|
+
name: 'Dialog',
|
|
248
|
+
command: 'astryx component Dialog --package @acme/widgets',
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
SLOW,
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
it(
|
|
255
|
+
'leaves a Core name with no folder of its own unknown to swizzle, as before',
|
|
256
|
+
async () => {
|
|
257
|
+
// Core documents CheckboxIndicator inside Indicator's doc, with no folder.
|
|
258
|
+
// BaseTable, named after its file, keeps routing reports to its package.
|
|
259
|
+
writeComponent(
|
|
260
|
+
'BaseTable',
|
|
261
|
+
'BaseTable',
|
|
262
|
+
'export function BaseTable() { return null; }\n',
|
|
263
|
+
'A file-named table.',
|
|
264
|
+
);
|
|
265
|
+
writeComponent(
|
|
266
|
+
'gallery/Indicator',
|
|
267
|
+
'CheckboxIndicator',
|
|
268
|
+
'export function CheckboxIndicator() { return null; }\n',
|
|
269
|
+
'A header.',
|
|
270
|
+
);
|
|
271
|
+
const cwd = tmpDir;
|
|
272
|
+
expect((await component('CheckboxIndicator', {cwd})).data.package).toBe(
|
|
273
|
+
'@astryxdesign/core',
|
|
274
|
+
);
|
|
275
|
+
await expect(
|
|
276
|
+
swizzleCopy('CheckboxIndicator', {cwd}),
|
|
277
|
+
).rejects.toMatchObject({
|
|
278
|
+
code: 'ERR_UNKNOWN_COMPONENT',
|
|
279
|
+
});
|
|
280
|
+
const scoped = {cwd, package: '@acme/widgets'};
|
|
281
|
+
expect((await swizzleCopy('CheckboxIndicator', scoped)).package).toBe(
|
|
282
|
+
'@acme/widgets',
|
|
283
|
+
);
|
|
284
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
285
|
+
expect((await gapReport('CheckboxIndicator', report)).data.package).toBe(
|
|
286
|
+
'@astryxdesign/core',
|
|
287
|
+
);
|
|
288
|
+
const hit = (
|
|
289
|
+
await search('CheckboxIndicator', {cwd, type: 'component'})
|
|
290
|
+
).data.results.find(result => result.package === '@acme/widgets');
|
|
291
|
+
expect(hit?.command).toBe(
|
|
292
|
+
'astryx component CheckboxIndicator --package @acme/widgets',
|
|
293
|
+
);
|
|
294
|
+
expect((await gapReport('BaseTable', report)).data.package).toBe(
|
|
295
|
+
'@acme/widgets',
|
|
296
|
+
);
|
|
297
|
+
},
|
|
298
|
+
SLOW,
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
it.each([
|
|
302
|
+
// props must be an array: the loader rejects this doc.
|
|
303
|
+
[
|
|
304
|
+
'invalid',
|
|
305
|
+
"export default {type: 'component', name: 'AcmeTab', props: 'x'};",
|
|
306
|
+
],
|
|
307
|
+
[
|
|
308
|
+
'hidden',
|
|
309
|
+
"export default {\n type: 'component',\n name: 'AcmeTab',\n hidden: true,\n usage: {description: 'x'},\n props: [],\n};",
|
|
310
|
+
],
|
|
311
|
+
])(
|
|
312
|
+
"leaves a name to another package's file-named doc even when that doc is %s",
|
|
313
|
+
async (kind, doc) => {
|
|
314
|
+
writeComponent(
|
|
315
|
+
'gallery/Strip',
|
|
316
|
+
'AcmeTab',
|
|
317
|
+
'export function AcmeTab() { return null; }\n',
|
|
318
|
+
'A strip of tabs.',
|
|
319
|
+
);
|
|
320
|
+
const beta = path.join(tmpDir, 'node_modules', '@beta', 'kit');
|
|
321
|
+
fs.mkdirSync(path.join(beta, 'src'), {recursive: true});
|
|
322
|
+
for (const [file, text] of Object.entries({
|
|
323
|
+
'package.json': '{"name": "@beta/kit", "version": "1.0.0"}',
|
|
324
|
+
'astryx.integration.mjs': "export default {components: './src'};",
|
|
325
|
+
'src/AcmeTab.tsx': 'export function AcmeTab() { return null; }',
|
|
326
|
+
'src/AcmeTab.doc.mjs': doc,
|
|
327
|
+
})) {
|
|
328
|
+
fs.writeFileSync(path.join(beta, file), `${text}\n`);
|
|
329
|
+
}
|
|
330
|
+
fs.writeFileSync(
|
|
331
|
+
path.join(tmpDir, 'astryx.config.mjs'),
|
|
332
|
+
"export default {integrations: ['@acme/widgets', '@beta/kit']};\n",
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const cwd = tmpDir;
|
|
336
|
+
expect((await component('AcmeTab', {cwd, source: true})).package).toBe(
|
|
337
|
+
'@beta/kit',
|
|
338
|
+
);
|
|
339
|
+
if (kind === 'invalid') {
|
|
340
|
+
await expect(component('AcmeTab', {cwd})).rejects.toMatchObject({
|
|
341
|
+
code: 'ERR_INVALID_DOC',
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
const report = {cwd, category: 'other', reason: 'Needs a size.'};
|
|
345
|
+
for (const name of ['AcmeTab', 'acmetab']) {
|
|
346
|
+
expect((await gapReport(name, report)).data.package).toBe(
|
|
347
|
+
'@astryxdesign/core',
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
const hit = (
|
|
351
|
+
await search('AcmeTab', {cwd, type: 'component'})
|
|
352
|
+
).data.results.find(result => result.package === '@acme/widgets');
|
|
353
|
+
expect(hit?.command).toBe(
|
|
354
|
+
'astryx component AcmeTab --package @acme/widgets',
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
SLOW,
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
it(
|
|
361
|
+
'leaves a file name Core also has to Core in search, as in detail',
|
|
362
|
+
async () => {
|
|
363
|
+
// Badge.doc.mjs documents AcmeStatusBadge; its file name is Core's.
|
|
364
|
+
writeComponent(
|
|
365
|
+
'status/Badge',
|
|
366
|
+
'AcmeStatusBadge',
|
|
367
|
+
'export function AcmeStatusBadge() { return null; }\n',
|
|
368
|
+
'Shows a status.',
|
|
369
|
+
);
|
|
370
|
+
const cwd = tmpDir;
|
|
371
|
+
const [first] = (await search('Badge', {cwd, type: 'component'})).data
|
|
372
|
+
.results;
|
|
373
|
+
expect(first).toMatchObject({
|
|
374
|
+
name: 'Badge',
|
|
375
|
+
package: '@astryxdesign/core',
|
|
376
|
+
});
|
|
377
|
+
expect((await component('Badge', {cwd})).data.package).toBe(
|
|
378
|
+
'@astryxdesign/core',
|
|
379
|
+
);
|
|
380
|
+
const [own] = (await search('AcmeStatusBadge', {cwd, type: 'component'}))
|
|
381
|
+
.data.results;
|
|
382
|
+
expect(own).toMatchObject({
|
|
383
|
+
name: 'AcmeStatusBadge',
|
|
384
|
+
command: 'astryx component AcmeStatusBadge',
|
|
385
|
+
});
|
|
386
|
+
},
|
|
387
|
+
SLOW,
|
|
388
|
+
);
|
|
389
|
+
|
|
390
|
+
it(
|
|
391
|
+
'copies a replacement recorded under its export into its file name folder, as before',
|
|
392
|
+
async () => {
|
|
393
|
+
const manifest = path.join(pkgDir, 'package.json');
|
|
394
|
+
fs.writeFileSync(
|
|
395
|
+
manifest,
|
|
396
|
+
JSON.stringify({
|
|
397
|
+
...JSON.parse(fs.readFileSync(manifest, 'utf-8')),
|
|
398
|
+
peerDependencies: {
|
|
399
|
+
'@astryxdesign/cli': `>=${COMPONENT_REPLACES_CLI}`,
|
|
400
|
+
},
|
|
401
|
+
}),
|
|
402
|
+
);
|
|
403
|
+
writeComponent(
|
|
404
|
+
'gallery/Slides',
|
|
405
|
+
'AcmeCarousel',
|
|
406
|
+
'export function AcmeCarousel() { return null; }\n',
|
|
407
|
+
'Cycles through a row of slides.',
|
|
408
|
+
{replaces: 'Carousel'},
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const copied = await swizzleCopy('Carousel', {cwd: tmpDir});
|
|
412
|
+
expect(copied.data).toMatchObject({
|
|
413
|
+
package: '@acme/widgets',
|
|
414
|
+
outputDir: path.join('components', 'astryx', 'Slides'),
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
SLOW,
|
|
418
|
+
);
|
|
419
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
discoverComponents,
|
|
17
17
|
discoverExternalComponentsGrouped,
|
|
18
18
|
discoverValidIntegrationComponents,
|
|
19
|
+
familyMember,
|
|
19
20
|
findComponentReadme,
|
|
20
21
|
findExternalComponentDoc,
|
|
21
22
|
resolveImportPath,
|
|
@@ -66,6 +67,33 @@ async function legacyImport(ext, name, coreDir, docOpts) {
|
|
|
66
67
|
*/
|
|
67
68
|
const coreEntry = doc => ({name: doc.name, package: CORE_PACKAGE, ...doc});
|
|
68
69
|
|
|
70
|
+
/**
|
|
71
|
+
* The full-level entry for a component documented as a member of its family's
|
|
72
|
+
* doc (VStack in Stack's doc): a component doc built from the member's own
|
|
73
|
+
* entry, under its own name, display name and import, shaped like every other
|
|
74
|
+
* full entry. Null when the doc is the component's own.
|
|
75
|
+
* @param {any} docs
|
|
76
|
+
* @param {string} name
|
|
77
|
+
* @param {string} coreDir
|
|
78
|
+
* @returns {any}
|
|
79
|
+
*/
|
|
80
|
+
function memberEntry(docs, name, coreDir) {
|
|
81
|
+
const member = familyMember(docs, name);
|
|
82
|
+
if (!member) return null;
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
package: CORE_PACKAGE,
|
|
86
|
+
displayName: member.displayName ?? name,
|
|
87
|
+
...(docs.group ? {group: docs.group} : {}),
|
|
88
|
+
...(docs.category ? {category: docs.category} : {}),
|
|
89
|
+
usage: member.usage ?? {description: member.description ?? ''},
|
|
90
|
+
props: member.props ?? [],
|
|
91
|
+
...(member.examples ? {examples: member.examples} : {}),
|
|
92
|
+
...(docs.theming ? {theming: docs.theming} : {}),
|
|
93
|
+
import: resolveImportPath(coreDir, name),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
/**
|
|
70
98
|
* The entry for a Core slot an integration component replaces
|
|
71
99
|
* (spec:AST-035 FR11), at the list's detail level: the replacement's own name,
|
|
@@ -177,7 +205,11 @@ export async function componentList(
|
|
|
177
205
|
entries.push({
|
|
178
206
|
name: comp,
|
|
179
207
|
package: CORE_PACKAGE,
|
|
180
|
-
description:
|
|
208
|
+
description:
|
|
209
|
+
familyMember(docs, comp)?.description ||
|
|
210
|
+
docs.usage?.description ||
|
|
211
|
+
docs.description ||
|
|
212
|
+
'',
|
|
181
213
|
import: resolveImportPath(coreDir, comp),
|
|
182
214
|
});
|
|
183
215
|
} catch {
|
|
@@ -215,7 +247,8 @@ export async function componentList(
|
|
|
215
247
|
const readme = findComponentReadme(coreDir, comp);
|
|
216
248
|
if (readme && readme.endsWith('.doc.mjs')) {
|
|
217
249
|
try {
|
|
218
|
-
|
|
250
|
+
const docs = await loadComponentDoc(readme, {zh, lang, dense});
|
|
251
|
+
entries.push(memberEntry(docs, comp, coreDir) ?? coreEntry(docs));
|
|
219
252
|
} catch {
|
|
220
253
|
entries.push({name: `XDS${comp}`, package: CORE_PACKAGE, description: ''});
|
|
221
254
|
}
|
|
@@ -269,7 +302,11 @@ export async function componentList(
|
|
|
269
302
|
result[cat].push({
|
|
270
303
|
name: comp,
|
|
271
304
|
package: CORE_PACKAGE,
|
|
272
|
-
description:
|
|
305
|
+
description:
|
|
306
|
+
familyMember(docs, comp)?.description ||
|
|
307
|
+
docs.usage?.description ||
|
|
308
|
+
docs.description ||
|
|
309
|
+
'',
|
|
273
310
|
import: resolveImportPath(coreDir, comp),
|
|
274
311
|
});
|
|
275
312
|
} catch {
|
|
@@ -310,7 +347,8 @@ export async function componentList(
|
|
|
310
347
|
const readme = findComponentReadme(coreDir, comp);
|
|
311
348
|
if (readme && readme.endsWith('.doc.mjs')) {
|
|
312
349
|
try {
|
|
313
|
-
|
|
350
|
+
const docs = await loadComponentDoc(readme, {zh, lang, dense});
|
|
351
|
+
result[cat].push(memberEntry(docs, comp, coreDir) ?? coreEntry(docs));
|
|
314
352
|
} catch {
|
|
315
353
|
result[cat].push({name: `XDS${comp}`, package: CORE_PACKAGE, description: ''});
|
|
316
354
|
}
|
|
@@ -17,7 +17,17 @@
|
|
|
17
17
|
import {Worker} from 'node:worker_threads';
|
|
18
18
|
import {AstryxError} from '../error.mjs';
|
|
19
19
|
import {Project} from '../../foundation/config/project.mjs';
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
CORE_PACKAGE,
|
|
22
|
+
discoverIntegrationComponents,
|
|
23
|
+
docStem,
|
|
24
|
+
findComponentReadme,
|
|
25
|
+
findIntegrationComponent,
|
|
26
|
+
integrationRecordHold,
|
|
27
|
+
unscopedComponentOwners,
|
|
28
|
+
} from '../../foundation/discovery/component-discovery.mjs';
|
|
29
|
+
import {legacyExternalKeeps} from '../component/_adapter.mjs';
|
|
30
|
+
import {findCoreDir} from '../../foundation/fs/paths.mjs';
|
|
21
31
|
import {captureEnv} from '../../foundation/debug/event.mjs';
|
|
22
32
|
import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
|
|
23
33
|
import {parseGapReportReceipt} from '../../authoring/gap-report/parse.mjs';
|
|
@@ -178,13 +188,65 @@ async function selectTarget(project, component, packageName) {
|
|
|
178
188
|
coreName,
|
|
179
189
|
);
|
|
180
190
|
if (replacement) return targetForPackage(project, replacement.package);
|
|
191
|
+
// The owner rule runs over every owner discovery finds, loadable or not
|
|
192
|
+
// (a doc recorded under another export still answers to its file's name);
|
|
193
|
+
// then only owners whose docs load route a report.
|
|
194
|
+
const wanted = bareComponent.toLowerCase();
|
|
195
|
+
const loads = new Set(records.map(r => `${r.package}\0${r.docPath}`));
|
|
196
|
+
const recorded = project.loadedIntegrations.flatMap(integration =>
|
|
197
|
+
discoverIntegrationComponents(integration).flatMap(r =>
|
|
198
|
+
r.name.toLowerCase() === wanted
|
|
199
|
+
? [{...r, hold: integrationRecordHold(r)}]
|
|
200
|
+
: docStem(r.docPath).toLowerCase() === wanted
|
|
201
|
+
? [{...r, hold: 'doc'}]
|
|
202
|
+
: [],
|
|
203
|
+
),
|
|
204
|
+
);
|
|
205
|
+
// The checks below find files by name, so they run on every spelling the
|
|
206
|
+
// case-blind match above found, as well as the one typed.
|
|
207
|
+
const spellings = new Set([
|
|
208
|
+
bareComponent,
|
|
209
|
+
...recorded.flatMap(r =>
|
|
210
|
+
[r.name, docStem(r.docPath)].filter(n => n.toLowerCase() === wanted),
|
|
211
|
+
),
|
|
212
|
+
]);
|
|
213
|
+
// Core also holds a name only its doc lookup finds, as in detail: a name a
|
|
214
|
+
// doc records yields to it, though it routes no report itself.
|
|
215
|
+
const coreDir = findCoreDir(project.cwd);
|
|
216
|
+
const coreDocs =
|
|
217
|
+
coreDir != null &&
|
|
218
|
+
[...spellings].some(name => findComponentReadme(coreDir, name) != null);
|
|
219
|
+
const ruled = unscopedComponentOwners([
|
|
220
|
+
...records
|
|
221
|
+
.filter(
|
|
222
|
+
r => r.package === CORE_PACKAGE && r.name.toLowerCase() === wanted,
|
|
223
|
+
)
|
|
224
|
+
.map(r => ({...r, hold: 'core'})),
|
|
225
|
+
...recorded,
|
|
226
|
+
// A hidden doc named after its file still holds that name, as the
|
|
227
|
+
// component lookup finds it; it routes no report.
|
|
228
|
+
...project.loadedIntegrations.flatMap(integration =>
|
|
229
|
+
[...spellings].flatMap(spelling => {
|
|
230
|
+
const found = findIntegrationComponent(integration, spelling);
|
|
231
|
+
return found?.hold === 'file'
|
|
232
|
+
? [{...found, package: integration.name}]
|
|
233
|
+
: [];
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
]).filter(r => !coreDocs || r.hold !== 'doc');
|
|
237
|
+
// A legacy docs package keeps an export name a doc records, as in detail.
|
|
181
238
|
const owners = [
|
|
182
239
|
...new Set(
|
|
183
|
-
|
|
240
|
+
([...spellings].some(name =>
|
|
241
|
+
legacyExternalKeeps(project.cwd, name, ruled),
|
|
242
|
+
)
|
|
243
|
+
? []
|
|
244
|
+
: ruled
|
|
245
|
+
)
|
|
184
246
|
.filter(
|
|
185
|
-
|
|
247
|
+
r => r.hold === 'core' || loads.has(`${r.package}\0${r.docPath}`),
|
|
186
248
|
)
|
|
187
|
-
.map(
|
|
249
|
+
.map(r => r.package),
|
|
188
250
|
),
|
|
189
251
|
];
|
|
190
252
|
|