@astryxdesign/cli 0.7.2-canary.db3a2e7 → 0.7.2-canary.e26723d
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 +7 -7
- package/api/build/help/help.mjs +1 -1
- 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/docs/_adapter.mjs +207 -1
- package/api/docs/integrationDocs.test.mjs +124 -0
- package/api/doctor/theme-checks.mjs +1 -1
- package/api/gap-report/gap-report.mjs +67 -4
- package/api/init/run/run.mjs +2 -2
- package/api/integration/_adapter.d.mts +12 -0
- package/api/integration/_adapter.mjs +54 -0
- package/api/integration/add-contribution.component-names.test.mjs +66 -0
- package/api/integration/add-contribution.mjs +7 -3
- package/api/integration/authoring-checks.mjs +11 -2
- package/api/integration/authoring-checks.test.mjs +39 -0
- package/api/integration/authoring-checks.type.d.mts +9 -0
- package/api/integration/authoring-checks.type.mjs +14 -1
- package/api/integration/integrationComponentConflicts.doc.mjs +21 -5
- package/api/integration/pack-check.mjs +61 -107
- package/api/integration/pack-check.test.mjs +359 -0
- package/api/integration/validate-integration.test.mjs +11 -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 +81 -22
- 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/README.md +10 -0
- package/assets/docs/author-a-theme.doc.mjs +1 -1
- package/assets/docs/getting-started.doc.mjs +3 -3
- package/assets/docs/theme.doc.dense.mjs +1 -1
- package/assets/docs/theme.doc.mjs +1 -1
- package/assets/docs/theme.doc.zh.mjs +1 -1
- package/assets/docs/tree/add-a-component.doc.mjs +10 -0
- package/assets/docs/tree/check-the-props.doc.mjs +145 -0
- package/assets/docs/tree/checks.doc.mjs +4 -4
- package/assets/docs/tree/component-family.doc.mjs +5 -1
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
- package/assets/docs/tree/links.doc.mjs +2 -2
- package/assets/docs/tree/single-component.doc.mjs +3 -2
- package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
- package/assets/docs/tree/type-scale.doc.mjs +3 -3
- package/assets/docs/tree/versioning.doc.mjs +1 -0
- package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
- package/assets/docs/use-a-theme.doc.mjs +2 -2
- 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/assets/theme.template.ts +1 -1
- 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/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
- package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
- package/authoring/doctypes/template/parse.d.mts +6 -2
- package/authoring/doctypes/template/parse.mjs +3 -1
- package/authoring/doctypes/template/type.ts +22 -0
- package/authoring/index.d.ts +1 -0
- package/authoring/integration/integration.doc.mjs +8 -1
- package/authoring/integration/parse.test.mjs +10 -0
- package/authoring/integration/schema.d.mts +2 -0
- package/authoring/integration/schema.mjs +1 -0
- package/authoring/integration/type.ts +4 -0
- package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
- package/clients/cli/commands/build-theme.mjs +1 -1
- 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/doctor-integration-components.doc.mjs +9 -3
- package/clients/cli/commands/doctor-integration.test.mjs +392 -0
- package/clients/cli/commands/doctor.mjs +74 -0
- package/clients/cli/commands/theme-build.doc.mjs +2 -2
- package/clients/cli/lib/component-format.mjs +5 -1
- package/foundation/config/project.mjs +82 -0
- package/foundation/discovery/authoring-self-docs.mjs +1 -0
- package/foundation/discovery/component-discovery.d.mts +190 -10
- package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
- package/foundation/discovery/component-discovery.mjs +864 -56
- 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/doc-types.d.mts +157 -0
- package/foundation/discovery/doc-types.mjs +1224 -0
- package/foundation/discovery/doc-types.test.mjs +652 -0
- package/foundation/discovery/docs-discovery.d.mts +81 -0
- package/foundation/discovery/docs-discovery.mjs +57 -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/package-tsconfig.d.mts +14 -0
- package/foundation/discovery/package-tsconfig.mjs +228 -0
- package/foundation/discovery/package-tsconfig.test.mjs +210 -0
- package/foundation/discovery/project-typescript.d.mts +23 -0
- package/foundation/discovery/project-typescript.mjs +61 -0
- package/foundation/discovery/prop-drift.d.mts +220 -0
- package/foundation/discovery/prop-drift.mjs +1765 -0
- package/foundation/discovery/prop-drift.test.mjs +1197 -0
- package/foundation/discovery/template-adapter.mjs +74 -5
- package/foundation/discovery/type-match.d.mts +53 -0
- package/foundation/discovery/type-match.mjs +660 -0
- package/foundation/discovery/type-match.test.mjs +169 -0
- package/foundation/discovery/type-text.d.mts +68 -0
- package/foundation/discovery/type-text.mjs +658 -0
- package/foundation/discovery/type-text.test.mjs +196 -0
- package/foundation/discovery/unresolved-references.d.mts +62 -0
- package/foundation/discovery/unresolved-references.mjs +790 -0
- package/foundation/discovery/unresolved-references.test.mjs +567 -0
- package/foundation/doc-compiler/doc-loads.test.mjs +9 -1
- package/foundation/doc-compiler/inputs.mjs +23 -0
- package/foundation/doc-compiler/inputs.test.mjs +0 -1
- package/foundation/doc-compiler/links.d.mts +9 -5
- package/foundation/doc-compiler/links.mjs +4 -2
- package/foundation/integrations/contribution-inventory.mjs +6 -1
- package/foundation/response/response-types.doc.mjs +1 -1
- package/foundation/xle/registry.mjs +4 -0
- package/foundation/xle/registry.test.mjs +28 -0
- package/package.json +10 -10
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Integration components, and the members a family doc lists, are found
|
|
5
|
+
* by the names their docs document; every doc that works today reads as before.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
|
|
9
|
+
import * as fs from 'node:fs';
|
|
10
|
+
import * as os from 'node:os';
|
|
11
|
+
import * as path from 'node:path';
|
|
12
|
+
import {
|
|
13
|
+
discoverIntegrationComponents,
|
|
14
|
+
discoverValidIntegrationComponents,
|
|
15
|
+
findIntegrationComponent,
|
|
16
|
+
findIntegrationComponentDoc,
|
|
17
|
+
findIntegrationComponentSource,
|
|
18
|
+
unscopedComponentOwners,
|
|
19
|
+
} from './component-discovery.mjs';
|
|
20
|
+
|
|
21
|
+
/** @type {string} the integration's components root */
|
|
22
|
+
let root;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
root = fs.mkdtempSync(path.join(os.tmpdir(), 'astryx-export-names-'));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fs.rmSync(root, {recursive: true, force: true});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Write a component doc, and its same-stem source when given.
|
|
34
|
+
* @param {string} file path under the root, without extension
|
|
35
|
+
* @param {{name: string, source?: string, form?: 'default' | 'docs', suffix?: string, extra?: string}} options
|
|
36
|
+
* `name` is written as a JavaScript expression
|
|
37
|
+
*/
|
|
38
|
+
function writeComponent(
|
|
39
|
+
file,
|
|
40
|
+
{name, source, form = 'default', suffix = '.doc.mjs', extra = ''},
|
|
41
|
+
) {
|
|
42
|
+
const target = path.join(root, file);
|
|
43
|
+
fs.mkdirSync(path.dirname(target), {recursive: true});
|
|
44
|
+
const doc = `{\n type: 'component',\n name: ${name},\n usage: {description: 'A component.'},\n props: [],${extra}\n}`;
|
|
45
|
+
fs.writeFileSync(
|
|
46
|
+
`${target}${suffix}`,
|
|
47
|
+
form === 'default'
|
|
48
|
+
? `export default ${doc};\n`
|
|
49
|
+
: `export const docs = ${doc};\n`,
|
|
50
|
+
);
|
|
51
|
+
if (source != null) fs.writeFileSync(`${target}.tsx`, source);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const integration = () => ({name: '@acme/widgets', components: root});
|
|
55
|
+
|
|
56
|
+
/** @returns {string[]} */
|
|
57
|
+
const names = () =>
|
|
58
|
+
discoverIntegrationComponents(integration())
|
|
59
|
+
.map(record => record.name)
|
|
60
|
+
.sort();
|
|
61
|
+
|
|
62
|
+
describe('integration components whose doc names another export of its file', () => {
|
|
63
|
+
it('records a doc beside a kebab-case file under the export its doc names', async () => {
|
|
64
|
+
writeComponent('artifact-viewer', {
|
|
65
|
+
name: "'AcmeArtifactViewer'",
|
|
66
|
+
source: 'export function AcmeArtifactViewer() { return null; }\n',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const [record] = discoverIntegrationComponents(integration());
|
|
70
|
+
expect(record).toMatchObject({
|
|
71
|
+
name: 'AcmeArtifactViewer',
|
|
72
|
+
package: '@acme/widgets',
|
|
73
|
+
docPath: path.join(root, 'artifact-viewer.doc.mjs'),
|
|
74
|
+
sourcePath: path.join(root, 'artifact-viewer.tsx'),
|
|
75
|
+
});
|
|
76
|
+
expect(
|
|
77
|
+
(await discoverValidIntegrationComponents(integration())).components,
|
|
78
|
+
).toEqual([expect.objectContaining({name: 'AcmeArtifactViewer'})]);
|
|
79
|
+
expect(
|
|
80
|
+
findIntegrationComponentDoc(integration(), 'AcmeArtifactViewer'),
|
|
81
|
+
).toBe(record.docPath);
|
|
82
|
+
expect(findIntegrationComponentDoc(integration(), 'artifact-viewer')).toBe(
|
|
83
|
+
record.docPath,
|
|
84
|
+
);
|
|
85
|
+
expect(
|
|
86
|
+
findIntegrationComponentSource(integration(), 'AcmeArtifactViewer'),
|
|
87
|
+
).toBe(record.sourcePath);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('leaves a name to Core or a file-named component over one a doc records', () => {
|
|
91
|
+
const core = {package: '@astryxdesign/core', hold: 'core'};
|
|
92
|
+
const fileNamed = {package: '@beta/kit', hold: 'file'};
|
|
93
|
+
const documented = {package: '@acme/widgets', hold: 'doc'};
|
|
94
|
+
expect(unscopedComponentOwners([documented, fileNamed])).toEqual([
|
|
95
|
+
fileNamed,
|
|
96
|
+
]);
|
|
97
|
+
expect(unscopedComponentOwners([core, documented])).toEqual([core]);
|
|
98
|
+
expect(unscopedComponentOwners([documented])).toEqual([documented]);
|
|
99
|
+
|
|
100
|
+
// Badge.doc.mjs records AcmeBadge, so the doc holds both names only
|
|
101
|
+
// because it documents them; Panel.doc.mjs names an export another file
|
|
102
|
+
// has, so it keeps its file name.
|
|
103
|
+
writeComponent('Badge', {
|
|
104
|
+
name: "'AcmeBadge'",
|
|
105
|
+
source: 'export function AcmeBadge() { return null; }\n',
|
|
106
|
+
});
|
|
107
|
+
writeComponent('Panel', {
|
|
108
|
+
name: "'AcmeChip'",
|
|
109
|
+
source: 'export function AcmeChip() { return null; }\n',
|
|
110
|
+
});
|
|
111
|
+
writeComponent('AcmeChip', {
|
|
112
|
+
name: "'AcmeChip'",
|
|
113
|
+
source: 'export function AcmeChip() { return null; }\n',
|
|
114
|
+
});
|
|
115
|
+
const hold = (/** @type {string} */ name) =>
|
|
116
|
+
findIntegrationComponent(integration(), name)?.hold;
|
|
117
|
+
expect(hold('AcmeBadge')).toBe('doc');
|
|
118
|
+
expect(hold('Badge')).toBe('doc');
|
|
119
|
+
expect(hold('Panel')).toBe('file');
|
|
120
|
+
expect(hold('AcmeChip')).toBe('file');
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('takes no name a hidden doc or a replacement in the package claims', () => {
|
|
124
|
+
// A hidden doc is still the file named AcmeCarousel: its name stays its own.
|
|
125
|
+
writeComponent('AcmeCarousel', {
|
|
126
|
+
name: "'AcmeCarousel'",
|
|
127
|
+
extra: '\n hidden: true,',
|
|
128
|
+
source: 'export function AcmeCarousel() { return null; }\n',
|
|
129
|
+
});
|
|
130
|
+
writeComponent('gallery/Slides', {
|
|
131
|
+
name: "'AcmeCarousel'",
|
|
132
|
+
source: 'export function AcmeCarousel() { return null; }\n',
|
|
133
|
+
});
|
|
134
|
+
// A doc in the package replaces Carousel, so that name answers with it.
|
|
135
|
+
writeComponent('Pager', {
|
|
136
|
+
name: "'AcmePager'",
|
|
137
|
+
extra: "\n replaces: 'Carousel',",
|
|
138
|
+
source: 'export function AcmePager() { return null; }\n',
|
|
139
|
+
});
|
|
140
|
+
writeComponent('Kit', {
|
|
141
|
+
name: "'Carousel'",
|
|
142
|
+
source: 'export function Carousel() { return null; }\n',
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(names()).toEqual(['AcmePager', 'Kit', 'Slides']);
|
|
146
|
+
expect(findIntegrationComponentDoc(integration(), 'AcmeCarousel')).toBe(
|
|
147
|
+
path.join(root, 'AcmeCarousel.doc.mjs'),
|
|
148
|
+
);
|
|
149
|
+
expect(findIntegrationComponentDoc(integration(), 'Carousel')).toBeNull();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('settles a name two docs document by path, the same in every locale', () => {
|
|
153
|
+
// By UTF-16 code unit `B/` sorts before `a/`; a locale's collation puts
|
|
154
|
+
// `a/` first.
|
|
155
|
+
for (const [file, stem] of [
|
|
156
|
+
['a/Alpha', 'Alpha'],
|
|
157
|
+
['B/Beta', 'Beta'],
|
|
158
|
+
]) {
|
|
159
|
+
writeComponent(file, {
|
|
160
|
+
name: "'Twin'",
|
|
161
|
+
source: `export function Twin() { return null; }\n// ${stem}\n`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
expect(names()).toEqual(['Alpha', 'Twin']);
|
|
166
|
+
expect(findIntegrationComponentDoc(integration(), 'Twin')).toBe(
|
|
167
|
+
path.join(root, 'B', 'Beta.doc.mjs'),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('reads a name a spread can override, and reports an unreadable doc under its file name', async () => {
|
|
172
|
+
fs.writeFileSync(
|
|
173
|
+
path.join(root, 'Based.doc.mjs'),
|
|
174
|
+
"const base = {\n name: 'Based',\n type: 'component',\n usage: {description: 'x'},\n props: [],\n};\nexport default {...base, name: 'AcmeBased'};\n",
|
|
175
|
+
);
|
|
176
|
+
fs.writeFileSync(
|
|
177
|
+
path.join(root, 'Based.tsx'),
|
|
178
|
+
'export function AcmeBased() { return null; }\n',
|
|
179
|
+
);
|
|
180
|
+
writeComponent('Throws', {
|
|
181
|
+
name: "'AcmeThrows'",
|
|
182
|
+
source: 'export function AcmeThrows() { return null; }\n',
|
|
183
|
+
});
|
|
184
|
+
const throws = path.join(root, 'Throws.doc.mjs');
|
|
185
|
+
fs.writeFileSync(
|
|
186
|
+
throws,
|
|
187
|
+
`throw new Error('boom');\n${fs.readFileSync(throws, 'utf-8')}`,
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
expect(names()).toEqual(['AcmeBased', 'AcmeThrows']);
|
|
191
|
+
const {errors} = await discoverValidIntegrationComponents(integration());
|
|
192
|
+
expect(errors).toEqual([
|
|
193
|
+
{
|
|
194
|
+
name: 'Throws',
|
|
195
|
+
message: 'Component "Throws" has invalid metadata: boom',
|
|
196
|
+
},
|
|
197
|
+
]);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('reads a legacy export, a .doc.ts doc and a re-exported component the same way', () => {
|
|
201
|
+
writeComponent('Slides', {
|
|
202
|
+
name: "'AcmeCarousel'",
|
|
203
|
+
form: 'docs',
|
|
204
|
+
source: "export {AcmeCarousel} from './carousel/impl';\n",
|
|
205
|
+
});
|
|
206
|
+
fs.mkdirSync(path.join(root, 'carousel'));
|
|
207
|
+
fs.writeFileSync(
|
|
208
|
+
path.join(root, 'carousel', 'impl.tsx'),
|
|
209
|
+
'export function AcmeCarousel() { return null; }\n',
|
|
210
|
+
);
|
|
211
|
+
writeComponent('Pager', {
|
|
212
|
+
name: "'AcmePager'",
|
|
213
|
+
suffix: '.doc.ts',
|
|
214
|
+
source: 'export const AcmePager = () => null;\n',
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(names()).toEqual(['AcmeCarousel', 'AcmePager']);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('keeps the file name for every doc that reads as before', async () => {
|
|
221
|
+
// Named after its file.
|
|
222
|
+
writeComponent('AcmeBadge', {
|
|
223
|
+
name: "'AcmeBadge'",
|
|
224
|
+
source: 'export function AcmeBadge() { return null; }\n',
|
|
225
|
+
});
|
|
226
|
+
// The file exports its own name, so that name stays.
|
|
227
|
+
writeComponent('Chip', {
|
|
228
|
+
name: "'AcmeChip'",
|
|
229
|
+
source:
|
|
230
|
+
'export function Chip() { return null; }\nexport function AcmeChip() { return null; }\n',
|
|
231
|
+
});
|
|
232
|
+
// The doc names an export the file does not have.
|
|
233
|
+
writeComponent('Menu', {
|
|
234
|
+
name: "'AcmeMenu'",
|
|
235
|
+
source: 'export function Dropdown() { return null; }\n',
|
|
236
|
+
});
|
|
237
|
+
// A type is not a runtime export.
|
|
238
|
+
writeComponent('Shape', {
|
|
239
|
+
name: "'AcmeShape'",
|
|
240
|
+
source: 'export type AcmeShape = {id: string};\n',
|
|
241
|
+
});
|
|
242
|
+
// A name the doc computes is not read.
|
|
243
|
+
writeComponent('Tile', {
|
|
244
|
+
name: "`${'Acme'}Tile`",
|
|
245
|
+
source: 'export function AcmeTile() { return null; }\n',
|
|
246
|
+
});
|
|
247
|
+
// Another file already has the name.
|
|
248
|
+
writeComponent('Panel', {
|
|
249
|
+
name: "'AcmeBadge'",
|
|
250
|
+
source: 'export function AcmeBadge() { return null; }\n',
|
|
251
|
+
});
|
|
252
|
+
// No source beside it.
|
|
253
|
+
writeComponent('Orphan', {name: "'AcmeOrphan'"});
|
|
254
|
+
|
|
255
|
+
expect(names()).toEqual([
|
|
256
|
+
'AcmeBadge',
|
|
257
|
+
'Chip',
|
|
258
|
+
'Menu',
|
|
259
|
+
'Orphan',
|
|
260
|
+
'Panel',
|
|
261
|
+
'Shape',
|
|
262
|
+
'Tile',
|
|
263
|
+
]);
|
|
264
|
+
for (const name of [
|
|
265
|
+
'AcmeChip',
|
|
266
|
+
'AcmeMenu',
|
|
267
|
+
'AcmeShape',
|
|
268
|
+
'AcmeTile',
|
|
269
|
+
'AcmeOrphan',
|
|
270
|
+
]) {
|
|
271
|
+
expect(findIntegrationComponentDoc(integration(), name)).toBeNull();
|
|
272
|
+
}
|
|
273
|
+
expect(
|
|
274
|
+
(await discoverValidIntegrationComponents(integration())).errors,
|
|
275
|
+
).toEqual([
|
|
276
|
+
{
|
|
277
|
+
name: 'Orphan',
|
|
278
|
+
message:
|
|
279
|
+
'Component "Orphan" is missing its same-stem source file Orphan.tsx.',
|
|
280
|
+
},
|
|
281
|
+
]);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('still leaves a hidden doc out', () => {
|
|
285
|
+
writeComponent('Slides', {
|
|
286
|
+
name: "'AcmeCarousel'",
|
|
287
|
+
extra: '\n hidden: true,',
|
|
288
|
+
source: 'export function AcmeCarousel() { return null; }\n',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(names()).toEqual([]);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Write a family doc and its source.
|
|
297
|
+
* @param {string} file path under the root, without extension
|
|
298
|
+
* @param {{name: string, members: string, source: string, extra?: string}} options
|
|
299
|
+
* `members` is the `components` array, written as JavaScript
|
|
300
|
+
*/
|
|
301
|
+
function writeFamily(file, {name, members, source, extra = ''}) {
|
|
302
|
+
const target = path.join(root, file);
|
|
303
|
+
fs.mkdirSync(path.dirname(target), {recursive: true});
|
|
304
|
+
fs.writeFileSync(
|
|
305
|
+
`${target}.doc.mjs`,
|
|
306
|
+
`export default {\n type: 'component',\n name: '${name}',\n usage: {description: 'A family.'},${extra}\n components: ${members},\n};\n`,
|
|
307
|
+
);
|
|
308
|
+
fs.writeFileSync(`${target}.tsx`, source);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
describe('integration components a family doc lists in components', () => {
|
|
312
|
+
it('records each exported full entry under its own name, with the file that declares it', async () => {
|
|
313
|
+
writeFamily('carousel/AcmeCarousel', {
|
|
314
|
+
name: 'AcmeCarousel',
|
|
315
|
+
members:
|
|
316
|
+
"[{name: 'AcmeCarousel', description: 'The carousel.'}, {name: 'AcmeSlide', description: 'One slide.'}, {name: 'AcmeDots', description: 'The dots.'}]",
|
|
317
|
+
source:
|
|
318
|
+
"export function AcmeCarousel() { return null; }\nexport function AcmeSlide() { return null; }\nexport {AcmeDots} from './dots';\n",
|
|
319
|
+
});
|
|
320
|
+
fs.writeFileSync(
|
|
321
|
+
path.join(root, 'carousel', 'dots.tsx'),
|
|
322
|
+
'export const AcmeDots = () => null;\n',
|
|
323
|
+
);
|
|
324
|
+
|
|
325
|
+
const records = discoverIntegrationComponents(integration());
|
|
326
|
+
const docPath = path.join(root, 'carousel', 'AcmeCarousel.doc.mjs');
|
|
327
|
+
expect(records.map(record => record.name)).toEqual([
|
|
328
|
+
'AcmeCarousel',
|
|
329
|
+
'AcmeSlide',
|
|
330
|
+
'AcmeDots',
|
|
331
|
+
]);
|
|
332
|
+
expect(records.find(record => record.name === 'AcmeSlide')).toMatchObject({
|
|
333
|
+
docPath,
|
|
334
|
+
sourcePath: path.join(root, 'carousel', 'AcmeCarousel.tsx'),
|
|
335
|
+
});
|
|
336
|
+
expect(findIntegrationComponentDoc(integration(), 'AcmeDots')).toBe(
|
|
337
|
+
docPath,
|
|
338
|
+
);
|
|
339
|
+
expect(findIntegrationComponentSource(integration(), 'AcmeDots')).toBe(
|
|
340
|
+
path.join(root, 'carousel', 'dots.tsx'),
|
|
341
|
+
);
|
|
342
|
+
const {components, errors} =
|
|
343
|
+
await discoverValidIntegrationComponents(integration());
|
|
344
|
+
expect(errors).toEqual([]);
|
|
345
|
+
expect(components.map(record => record.name)).toEqual([
|
|
346
|
+
'AcmeCarousel',
|
|
347
|
+
'AcmeSlide',
|
|
348
|
+
'AcmeDots',
|
|
349
|
+
]);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
it('reads members from the module the doc imports, through the package exports', () => {
|
|
353
|
+
// The components root inside a package whose exports publish the module.
|
|
354
|
+
const componentsRoot = path.join(root, 'src');
|
|
355
|
+
fs.mkdirSync(path.join(componentsRoot, 'carousel'), {recursive: true});
|
|
356
|
+
fs.writeFileSync(
|
|
357
|
+
path.join(root, 'package.json'),
|
|
358
|
+
JSON.stringify({
|
|
359
|
+
name: '@acme/widgets',
|
|
360
|
+
exports: {'./carousel': './src/carousel/index.ts'},
|
|
361
|
+
}),
|
|
362
|
+
);
|
|
363
|
+
fs.writeFileSync(
|
|
364
|
+
path.join(componentsRoot, 'carousel', 'AcmeCarousel.doc.mjs'),
|
|
365
|
+
"export default {type: 'component', name: 'AcmeCarousel', import: '@acme/widgets/carousel', usage: {description: 'x'}, components: [{name: 'AcmeCarousel', description: 'x'}, {name: 'AcmeSlide', description: 'x'}]};\n",
|
|
366
|
+
);
|
|
367
|
+
fs.writeFileSync(
|
|
368
|
+
path.join(componentsRoot, 'carousel', 'AcmeCarousel.tsx'),
|
|
369
|
+
'export function AcmeCarousel() { return null; }\n',
|
|
370
|
+
);
|
|
371
|
+
fs.writeFileSync(
|
|
372
|
+
path.join(componentsRoot, 'carousel', 'index.ts'),
|
|
373
|
+
"export * from './AcmeCarousel';\nexport {AcmeSlide} from './slide';\n",
|
|
374
|
+
);
|
|
375
|
+
fs.writeFileSync(
|
|
376
|
+
path.join(componentsRoot, 'carousel', 'slide.tsx'),
|
|
377
|
+
'export function AcmeSlide() { return null; }\n',
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const loaded = {
|
|
381
|
+
name: '@acme/widgets',
|
|
382
|
+
components: componentsRoot,
|
|
383
|
+
__packageDir: root,
|
|
384
|
+
};
|
|
385
|
+
expect(
|
|
386
|
+
discoverIntegrationComponents(loaded).find(
|
|
387
|
+
record => record.name === 'AcmeSlide',
|
|
388
|
+
),
|
|
389
|
+
).toMatchObject({
|
|
390
|
+
sourcePath: path.join(componentsRoot, 'carousel', 'slide.tsx'),
|
|
391
|
+
});
|
|
392
|
+
// Without the package, only the doc's own source is read, and it lacks
|
|
393
|
+
// the member.
|
|
394
|
+
expect(
|
|
395
|
+
discoverIntegrationComponents({...loaded, __packageDir: undefined}).map(
|
|
396
|
+
record => record.name,
|
|
397
|
+
),
|
|
398
|
+
).toEqual(['AcmeCarousel']);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('leaves every other entry as before', () => {
|
|
402
|
+
writeFamily('Gallery', {
|
|
403
|
+
name: 'AcmeGallery',
|
|
404
|
+
extra: "\n hiddenComponents: ['AcmeHidden'],",
|
|
405
|
+
members: [
|
|
406
|
+
'[',
|
|
407
|
+
" {name: 'AcmeGallery', description: 'x'},",
|
|
408
|
+
// Not exported by the source.
|
|
409
|
+
" {name: 'AcmeMissing', description: 'x'},",
|
|
410
|
+
// Hidden by the doc.
|
|
411
|
+
" {name: 'AcmeHidden', description: 'x'},",
|
|
412
|
+
// A hook.
|
|
413
|
+
" {name: 'useGallery', description: 'x'},",
|
|
414
|
+
// A name-only entry points at the member's own doc.
|
|
415
|
+
" {name: 'AcmeThumb'},",
|
|
416
|
+
// Another file already has the name.
|
|
417
|
+
" {name: 'AcmeBadge', description: 'x'},",
|
|
418
|
+
// A computed name.
|
|
419
|
+
" {name: `${'Acme'}Tile`, description: 'x'},",
|
|
420
|
+
']',
|
|
421
|
+
].join('\n'),
|
|
422
|
+
source: [
|
|
423
|
+
'export function AcmeGallery() { return null; }',
|
|
424
|
+
'export function AcmeHidden() { return null; }',
|
|
425
|
+
'export function useGallery() { return []; }',
|
|
426
|
+
'export function AcmeThumb() { return null; }',
|
|
427
|
+
'export function AcmeBadge() { return null; }',
|
|
428
|
+
'export function AcmeTile() { return null; }',
|
|
429
|
+
'',
|
|
430
|
+
].join('\n'),
|
|
431
|
+
});
|
|
432
|
+
writeComponent('AcmeBadge', {
|
|
433
|
+
name: "'AcmeBadge'",
|
|
434
|
+
source: 'export function AcmeBadge() { return null; }\n',
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
// Gallery.tsx exports no `Gallery`, so the doc's own name is its export.
|
|
438
|
+
expect(names()).toEqual(['AcmeBadge', 'AcmeGallery']);
|
|
439
|
+
for (const name of [
|
|
440
|
+
'AcmeMissing',
|
|
441
|
+
'AcmeHidden',
|
|
442
|
+
'useGallery',
|
|
443
|
+
'AcmeThumb',
|
|
444
|
+
'AcmeTile',
|
|
445
|
+
]) {
|
|
446
|
+
expect(findIntegrationComponentDoc(integration(), name)).toBeNull();
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it("does not hand a family's replaces to its members", async () => {
|
|
451
|
+
writeFamily('AcmeCarousel', {
|
|
452
|
+
name: 'AcmeCarousel',
|
|
453
|
+
extra: "\n replaces: 'Carousel',",
|
|
454
|
+
members:
|
|
455
|
+
"[{name: 'AcmeCarousel', description: 'x'}, {name: 'AcmeSlide', description: 'x'}]",
|
|
456
|
+
source:
|
|
457
|
+
'export function AcmeCarousel() { return null; }\nexport function AcmeSlide() { return null; }\n',
|
|
458
|
+
});
|
|
459
|
+
const {components} =
|
|
460
|
+
await discoverValidIntegrationComponents(integration());
|
|
461
|
+
expect(components.map(record => [record.name, record.replaces])).toEqual([
|
|
462
|
+
['AcmeCarousel', 'Carousel'],
|
|
463
|
+
['AcmeSlide', undefined],
|
|
464
|
+
]);
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
it('drops a member once the file its source re-exports stops exporting it', () => {
|
|
468
|
+
writeFamily('AcmeTabs', {
|
|
469
|
+
name: 'AcmeTabs',
|
|
470
|
+
members: "[{name: 'AcmeTab', description: 'x'}]",
|
|
471
|
+
source:
|
|
472
|
+
"export function AcmeTabs() { return null; }\nexport * from './tab';\n",
|
|
473
|
+
});
|
|
474
|
+
const tab = path.join(root, 'tab.tsx');
|
|
475
|
+
fs.writeFileSync(tab, 'export function AcmeTab() { return null; }\n');
|
|
476
|
+
expect(names()).toEqual(['AcmeTab', 'AcmeTabs']);
|
|
477
|
+
fs.writeFileSync(tab, 'export function AcmeTabLegacy() { return null; }\n');
|
|
478
|
+
expect(names()).toEqual(['AcmeTabs']);
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
it('reads members a doc lists through a shorthand property', () => {
|
|
482
|
+
fs.writeFileSync(
|
|
483
|
+
path.join(root, 'AcmeTabs.tsx'),
|
|
484
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\n',
|
|
485
|
+
);
|
|
486
|
+
fs.writeFileSync(
|
|
487
|
+
path.join(root, 'AcmeTabs.doc.mjs'),
|
|
488
|
+
"const components = [{name: 'AcmeTab', description: 'x'}];\nexport default {type: 'component', name: 'AcmeTabs', usage: {description: 'x'}, components};\n",
|
|
489
|
+
);
|
|
490
|
+
expect(names()).toEqual(['AcmeTab', 'AcmeTabs']);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
it('reports a family doc the loader rejects once, not once per member', async () => {
|
|
494
|
+
writeFamily('AcmeStrip', {
|
|
495
|
+
name: 'AcmeStrip',
|
|
496
|
+
extra: '\n displayName: 7,',
|
|
497
|
+
members:
|
|
498
|
+
"[{name: 'AcmeStrip', description: 'x'}, {name: 'AcmeStripItem', description: 'x'}]",
|
|
499
|
+
source:
|
|
500
|
+
'export function AcmeStrip() { return null; }\nexport function AcmeStripItem() { return null; }\n',
|
|
501
|
+
});
|
|
502
|
+
const {components, errors} =
|
|
503
|
+
await discoverValidIntegrationComponents(integration());
|
|
504
|
+
expect(components).toEqual([]);
|
|
505
|
+
expect(errors.map(error => error.name)).toEqual(['AcmeStrip']);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* A package around the components root `src/`, with these files.
|
|
510
|
+
* @param {Record<string, unknown>} manifest package.json fields
|
|
511
|
+
* @param {Record<string, string>} files paths under the package
|
|
512
|
+
*/
|
|
513
|
+
function writePackage(manifest, files) {
|
|
514
|
+
fs.writeFileSync(
|
|
515
|
+
path.join(root, 'package.json'),
|
|
516
|
+
JSON.stringify({name: '@acme/widgets', ...manifest}),
|
|
517
|
+
);
|
|
518
|
+
for (const [file, text] of Object.entries(files)) {
|
|
519
|
+
fs.mkdirSync(path.dirname(path.join(root, file)), {recursive: true});
|
|
520
|
+
fs.writeFileSync(path.join(root, file), text);
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
name: '@acme/widgets',
|
|
524
|
+
components: path.join(root, 'src'),
|
|
525
|
+
__packageDir: root,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/** @param {object} loaded */
|
|
530
|
+
const namesIn = loaded =>
|
|
531
|
+
discoverIntegrationComponents(/** @type {any} */ (loaded))
|
|
532
|
+
.map(record => record.name)
|
|
533
|
+
.sort();
|
|
534
|
+
|
|
535
|
+
const family = (/** @type {string} */ name, /** @type {string} */ entries) =>
|
|
536
|
+
`export default {type: 'component', name: '${name}', usage: {description: 'x'}, components: [${entries}]};\n`;
|
|
537
|
+
|
|
538
|
+
it('reads members from the main file of a package without exports, and none it cannot resolve', () => {
|
|
539
|
+
const loaded = writePackage(
|
|
540
|
+
{main: './src/index.ts'},
|
|
541
|
+
{
|
|
542
|
+
'src/list/AcmeList.doc.mjs': family(
|
|
543
|
+
'AcmeList',
|
|
544
|
+
"{name: 'AcmeListRow', description: 'x'}, {name: 'AcmeListCell', description: 'x'}",
|
|
545
|
+
).replace('usage:', "import: '@acme/widgets', usage:"),
|
|
546
|
+
'src/list/AcmeList.tsx':
|
|
547
|
+
'export function AcmeList() { return null; }\nexport function AcmeListRow() { return null; }\n',
|
|
548
|
+
'src/list/cell.tsx':
|
|
549
|
+
'export function AcmeListCell() { return null; }\n',
|
|
550
|
+
'src/index.ts':
|
|
551
|
+
"export {AcmeList} from './list/AcmeList';\nexport {AcmeListCell} from './list/cell';\n",
|
|
552
|
+
},
|
|
553
|
+
);
|
|
554
|
+
expect(namesIn(loaded)).toEqual(['AcmeList', 'AcmeListCell']);
|
|
555
|
+
// An import the package cannot resolve lists no members, not the
|
|
556
|
+
// same-stem file's.
|
|
557
|
+
fs.rmSync(path.join(root, 'src', 'index.ts'));
|
|
558
|
+
expect(namesIn(loaded)).toEqual(['AcmeList']);
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
it("takes a member's source from the family's folder, never a bundle or the package root", () => {
|
|
562
|
+
const loaded = writePackage(
|
|
563
|
+
{exports: {'.': {import: './dist/index.mjs'}}},
|
|
564
|
+
{
|
|
565
|
+
'src/tabs/AcmeTabs.doc.mjs': family(
|
|
566
|
+
'AcmeTabs',
|
|
567
|
+
"{name: 'AcmeTab', description: 'x'}",
|
|
568
|
+
),
|
|
569
|
+
'src/tabs/AcmeTabs.tsx':
|
|
570
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\n',
|
|
571
|
+
'dist/index.mjs':
|
|
572
|
+
'function AcmeTabs() { return null; }\nfunction AcmeTab() { return null; }\nexport {AcmeTabs, AcmeTab};\n',
|
|
573
|
+
// A member only the package's root file and the bundle declare is
|
|
574
|
+
// listed, with no source.
|
|
575
|
+
'src/cards/AcmeCards.doc.mjs': family(
|
|
576
|
+
'AcmeCards',
|
|
577
|
+
"{name: 'AcmeCard', description: 'x'}",
|
|
578
|
+
),
|
|
579
|
+
'src/cards/AcmeCards.tsx':
|
|
580
|
+
'export function AcmeCards() { return null; }\n',
|
|
581
|
+
'src/index.tsx':
|
|
582
|
+
"export {AcmeCards} from './cards/AcmeCards';\nexport function AcmeCard() { return null; }\n",
|
|
583
|
+
},
|
|
584
|
+
);
|
|
585
|
+
fs.writeFileSync(
|
|
586
|
+
path.join(root, 'dist', 'index.mjs'),
|
|
587
|
+
`${fs.readFileSync(path.join(root, 'dist', 'index.mjs'), 'utf-8')}export function AcmeCard() { return null; }\n`,
|
|
588
|
+
);
|
|
589
|
+
|
|
590
|
+
const records = discoverIntegrationComponents(loaded);
|
|
591
|
+
expect(records.map(record => record.name).sort()).toEqual([
|
|
592
|
+
'AcmeCard',
|
|
593
|
+
'AcmeCards',
|
|
594
|
+
'AcmeTab',
|
|
595
|
+
'AcmeTabs',
|
|
596
|
+
]);
|
|
597
|
+
expect(findIntegrationComponentSource(loaded, 'AcmeTab')).toBe(
|
|
598
|
+
path.join(root, 'src', 'tabs', 'AcmeTabs.tsx'),
|
|
599
|
+
);
|
|
600
|
+
expect(findIntegrationComponentDoc(loaded, 'AcmeCard')).toBe(
|
|
601
|
+
path.join(root, 'src', 'cards', 'AcmeCards.doc.mjs'),
|
|
602
|
+
);
|
|
603
|
+
expect(findIntegrationComponentSource(loaded, 'AcmeCard')).toBeNull();
|
|
604
|
+
expect(
|
|
605
|
+
records.find(record => record.name === 'AcmeCard')?.sourcePath,
|
|
606
|
+
).toBeNull();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('lists a member only a bundle declares, with no source, when the family doc sits at the package root', () => {
|
|
610
|
+
const loaded = {
|
|
611
|
+
...writePackage(
|
|
612
|
+
{exports: {'.': './dist/index.mjs'}},
|
|
613
|
+
{
|
|
614
|
+
'AcmeStrip.doc.mjs': family(
|
|
615
|
+
'AcmeStrip',
|
|
616
|
+
"{name: 'AcmeStripItem', description: 'x'}",
|
|
617
|
+
).replace('usage:', "import: '@acme/widgets', usage:"),
|
|
618
|
+
'AcmeStrip.tsx': 'export function AcmeStrip() { return null; }\n',
|
|
619
|
+
'dist/index.mjs':
|
|
620
|
+
'export function AcmeStrip() { return null; }\nexport function AcmeStripItem() { return null; }\n',
|
|
621
|
+
},
|
|
622
|
+
),
|
|
623
|
+
components: root,
|
|
624
|
+
};
|
|
625
|
+
expect(namesIn(loaded)).toEqual(['AcmeStrip', 'AcmeStripItem']);
|
|
626
|
+
expect(findIntegrationComponentSource(loaded, 'AcmeStripItem')).toBeNull();
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it('lists the members a family doc without its own source imports', () => {
|
|
630
|
+
const loaded = writePackage(
|
|
631
|
+
{exports: {'./group': './src/group/index.ts'}},
|
|
632
|
+
{
|
|
633
|
+
'src/group/AcmeGroup.doc.mjs': family(
|
|
634
|
+
'AcmeGroup',
|
|
635
|
+
"{name: 'AcmeGroupItem', description: 'x'}",
|
|
636
|
+
).replace('usage:', "import: '@acme/widgets/group', usage:"),
|
|
637
|
+
'src/group/index.ts':
|
|
638
|
+
"export {AcmeGroupItem} from './AcmeGroupItem';\n",
|
|
639
|
+
'src/group/AcmeGroupItem.tsx':
|
|
640
|
+
'export function AcmeGroupItem() { return null; }\n',
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
expect(namesIn(loaded)).toEqual(['AcmeGroup', 'AcmeGroupItem']);
|
|
644
|
+
expect(findIntegrationComponentSource(loaded, 'AcmeGroupItem')).toBe(
|
|
645
|
+
path.join(root, 'src', 'group', 'AcmeGroupItem.tsx'),
|
|
646
|
+
);
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
it("finds a member's source in the family's folder when the import names built output", () => {
|
|
650
|
+
const loaded = writePackage(
|
|
651
|
+
{exports: {'./list': './dist/list/index.js'}},
|
|
652
|
+
{
|
|
653
|
+
'src/list/AcmeList.doc.mjs': family(
|
|
654
|
+
'AcmeList',
|
|
655
|
+
"{name: 'AcmeListRow', description: 'x'}",
|
|
656
|
+
).replace('usage:', "import: '@acme/widgets/list', usage:"),
|
|
657
|
+
'src/list/AcmeList.tsx':
|
|
658
|
+
'export function AcmeList() { return null; }\n',
|
|
659
|
+
'src/list/row.tsx': 'export function AcmeListRow() { return null; }\n',
|
|
660
|
+
'src/list/row.test.tsx':
|
|
661
|
+
'export function AcmeListRow() { return null; }\n',
|
|
662
|
+
'dist/list/index.js':
|
|
663
|
+
"export {AcmeList} from './list.js';\nexport * from './row.js';\n",
|
|
664
|
+
'dist/list/list.js': 'export function AcmeList() { return null; }\n',
|
|
665
|
+
'dist/list/row.js': 'export function AcmeListRow() { return null; }\n',
|
|
666
|
+
},
|
|
667
|
+
);
|
|
668
|
+
expect(namesIn(loaded)).toEqual(['AcmeList', 'AcmeListRow']);
|
|
669
|
+
expect(findIntegrationComponentSource(loaded, 'AcmeListRow')).toBe(
|
|
670
|
+
path.join(root, 'src', 'list', 'row.tsx'),
|
|
671
|
+
);
|
|
672
|
+
// Until the output exists, the import exports nothing.
|
|
673
|
+
fs.rmSync(path.join(root, 'dist'), {recursive: true});
|
|
674
|
+
expect(namesIn(loaded)).toEqual(['AcmeList']);
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
it('lists a member another folder declares, with no source', async () => {
|
|
678
|
+
const loaded = writePackage(
|
|
679
|
+
{exports: {'./menu': './src/menu/index.ts'}},
|
|
680
|
+
{
|
|
681
|
+
'src/menu/AcmeMenu.doc.mjs': family(
|
|
682
|
+
'AcmeMenu',
|
|
683
|
+
"{name: 'AcmeMenuItem', description: 'x'}",
|
|
684
|
+
).replace('usage:', "import: '@acme/widgets/menu', usage:"),
|
|
685
|
+
'src/menu/AcmeMenu.tsx':
|
|
686
|
+
'export function AcmeMenu() { return null; }\n',
|
|
687
|
+
'src/menu/index.ts':
|
|
688
|
+
"export {AcmeMenu} from './AcmeMenu';\nexport {AcmeMenuItem} from '../shared/AcmeMenuItem';\n",
|
|
689
|
+
'src/shared/AcmeMenuItem.tsx':
|
|
690
|
+
'export function AcmeMenuItem() { return null; }\n',
|
|
691
|
+
},
|
|
692
|
+
);
|
|
693
|
+
expect(namesIn(loaded)).toEqual(['AcmeMenu', 'AcmeMenuItem']);
|
|
694
|
+
expect(findIntegrationComponentDoc(loaded, 'AcmeMenuItem')).toBe(
|
|
695
|
+
path.join(root, 'src', 'menu', 'AcmeMenu.doc.mjs'),
|
|
696
|
+
);
|
|
697
|
+
expect(findIntegrationComponentSource(loaded, 'AcmeMenuItem')).toBeNull();
|
|
698
|
+
// A member without a source is valid: only a doc's own component needs
|
|
699
|
+
// its same-stem file.
|
|
700
|
+
const {components, errors} = await discoverValidIntegrationComponents(
|
|
701
|
+
/** @type {any} */ (loaded),
|
|
702
|
+
);
|
|
703
|
+
expect(errors).toEqual([]);
|
|
704
|
+
expect(components.map(component => component.name).sort()).toEqual([
|
|
705
|
+
'AcmeMenu',
|
|
706
|
+
'AcmeMenuItem',
|
|
707
|
+
]);
|
|
708
|
+
});
|
|
709
|
+
|
|
710
|
+
it('reads members a binding holds and entries with computed fields', () => {
|
|
711
|
+
fs.writeFileSync(
|
|
712
|
+
path.join(root, 'AcmeRows.doc.mjs'),
|
|
713
|
+
"const describe = text => text;\nconst MEMBERS = [{name: 'AcmeRow', description: describe('A row.')}];\nexport default {type: 'component', name: 'AcmeRows', usage: {description: 'x'}, components: MEMBERS};\n",
|
|
714
|
+
);
|
|
715
|
+
fs.writeFileSync(
|
|
716
|
+
path.join(root, 'AcmeRows.tsx'),
|
|
717
|
+
'export function AcmeRows() { return null; }\nexport function AcmeRow() { return null; }\n',
|
|
718
|
+
);
|
|
719
|
+
expect(names()).toEqual(['AcmeRow', 'AcmeRows']);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it("lists the doc's own name when its file keeps its own, and declares replaces once", async () => {
|
|
723
|
+
writeFamily('Chip', {
|
|
724
|
+
name: 'AcmeChip',
|
|
725
|
+
extra: "\n replaces: 'Badge',",
|
|
726
|
+
members:
|
|
727
|
+
"[{name: 'AcmeChip', description: 'x'}, {name: 'AcmeChipIcon', description: 'x'}]",
|
|
728
|
+
source:
|
|
729
|
+
'export function Chip() { return null; }\nexport function AcmeChip() { return null; }\nexport function AcmeChipIcon() { return null; }\n',
|
|
730
|
+
});
|
|
731
|
+
const {components} =
|
|
732
|
+
await discoverValidIntegrationComponents(integration());
|
|
733
|
+
expect(components.map(record => [record.name, record.replaces])).toEqual([
|
|
734
|
+
['Chip', 'Badge'],
|
|
735
|
+
['AcmeChip', undefined],
|
|
736
|
+
['AcmeChipIcon', undefined],
|
|
737
|
+
]);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it('takes no member name a hidden doc or a replacement in the package claims', () => {
|
|
741
|
+
writeFamily('menu/AcmeMenu', {
|
|
742
|
+
name: 'AcmeMenu',
|
|
743
|
+
members:
|
|
744
|
+
"[{name: 'AcmeMenuDivider', description: 'x'}, {name: 'Carousel', description: 'x'}, {name: 'AcmeMenuItem', description: 'x'}]",
|
|
745
|
+
source:
|
|
746
|
+
'export function AcmeMenu() { return null; }\nexport function AcmeMenuDivider() { return null; }\nexport function Carousel() { return null; }\nexport function AcmeMenuItem() { return null; }\n',
|
|
747
|
+
});
|
|
748
|
+
writeComponent('menu/AcmeMenuDivider', {
|
|
749
|
+
name: "'AcmeMenuDivider'",
|
|
750
|
+
extra: '\n hidden: true,',
|
|
751
|
+
});
|
|
752
|
+
writeComponent('Slides', {
|
|
753
|
+
name: "'AcmeCarousel'",
|
|
754
|
+
extra: "\n replaces: 'Carousel',",
|
|
755
|
+
source: 'export function AcmeCarousel() { return null; }\n',
|
|
756
|
+
});
|
|
757
|
+
expect(names()).toEqual(['AcmeCarousel', 'AcmeMenu', 'AcmeMenuItem']);
|
|
758
|
+
expect(findIntegrationComponentDoc(integration(), 'Carousel')).toBeNull();
|
|
759
|
+
});
|
|
760
|
+
});
|