@astryxdesign/cli 0.7.2-canary.e2909cf → 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/integration-export-name.test.mjs +419 -0
- package/api/gap-report/gap-report.mjs +66 -4
- package/api/integration/pack-check.mjs +3 -94
- package/api/integration/pack-check.test.mjs +112 -0
- package/api/search/search.d.mts +9 -0
- package/api/search/search.mjs +82 -7
- package/api/swizzle/copy/copy.mjs +28 -14
- package/assets/docs/tree/component-imports.doc.mjs +8 -0
- package/authoring/doctypes/component/component.doc.mjs +1 -1
- package/authoring/doctypes/component/type.ts +6 -3
- package/foundation/discovery/component-discovery.d.mts +83 -6
- package/foundation/discovery/component-discovery.export-names.test.mjs +293 -0
- package/foundation/discovery/component-discovery.mjs +313 -49
- 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/package.json +9 -9
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* @property {string|null} sourcePath
|
|
27
27
|
* @property {string|undefined} issuesUrl
|
|
28
28
|
* @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
|
|
29
|
+
* @property {import('../../foundation/discovery/component-discovery.mjs').NameHold} hold
|
|
30
|
+
* how the owner holds the name, for the owner rule
|
|
29
31
|
*/
|
|
30
32
|
/**
|
|
31
33
|
* What ownership shaping needs from an owner. The core and legacy-external
|
|
@@ -138,6 +140,25 @@ export function assertUnambiguousOwners(owners: ComponentOwner[], dirName: strin
|
|
|
138
140
|
* @returns {string | null}
|
|
139
141
|
*/
|
|
140
142
|
export function resolveLegacyExternalDoc(ext: ExternalPackageRef, dirName: string): string | null;
|
|
143
|
+
/**
|
|
144
|
+
* Whether a legacy external package documents `dirName`.
|
|
145
|
+
* @param {string} cwd
|
|
146
|
+
* @param {string} dirName
|
|
147
|
+
* @returns {boolean}
|
|
148
|
+
*/
|
|
149
|
+
export function legacyExternalDocuments(cwd: string, dirName: string): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Whether a legacy docs package keeps `dirName` over these owners, each of which
|
|
152
|
+
* holds it only as an export a doc records.
|
|
153
|
+
* @param {string} cwd
|
|
154
|
+
* @param {string} dirName
|
|
155
|
+
* @param {Array<{hold?: string, docPath?: string | null}>} owners
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
export function legacyExternalKeeps(cwd: string, dirName: string, owners: Array<{
|
|
159
|
+
hold?: string;
|
|
160
|
+
docPath?: string | null;
|
|
161
|
+
}>): boolean;
|
|
141
162
|
/**
|
|
142
163
|
* The core source file for `dirName` (the no-scope `--source` path reads core
|
|
143
164
|
* directly — it does NOT fall back to externals or fuzzy search).
|
|
@@ -210,7 +231,6 @@ export function scopeSubComponent(docs: LoadedComponentDoc, dirName: string, cor
|
|
|
210
231
|
};
|
|
211
232
|
matchingComponent: any;
|
|
212
233
|
} | null;
|
|
213
|
-
export { CORE_PACKAGE };
|
|
214
234
|
/**
|
|
215
235
|
* Internal ambiguity marker. Single-component callers still receive the same
|
|
216
236
|
* AstryxError code, message, and suggestions; batch callers can additionally
|
|
@@ -261,6 +281,10 @@ export type ComponentOwner = {
|
|
|
261
281
|
sourcePath: string | null;
|
|
262
282
|
issuesUrl: string | undefined;
|
|
263
283
|
integration: import("../../foundation/integrations/integrations.mjs").LoadedIntegration | null;
|
|
284
|
+
/**
|
|
285
|
+
* how the owner holds the name, for the owner rule
|
|
286
|
+
*/
|
|
287
|
+
hold: import("../../foundation/discovery/component-discovery.mjs").NameHold;
|
|
264
288
|
};
|
|
265
289
|
/**
|
|
266
290
|
* What ownership shaping needs from an owner. The core and legacy-external
|
|
@@ -303,4 +327,6 @@ export type ResolvedUnscopedDoc = {
|
|
|
303
327
|
resolvedSourcePath: string | null;
|
|
304
328
|
};
|
|
305
329
|
import { CORE_PACKAGE } from '../../foundation/discovery/component-discovery.mjs';
|
|
330
|
+
import { unscopedComponentOwners } from '../../foundation/discovery/component-discovery.mjs';
|
|
306
331
|
import { AstryxError } from '../error.mjs';
|
|
332
|
+
export { CORE_PACKAGE, unscopedComponentOwners };
|
|
@@ -28,13 +28,14 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
CORE_PACKAGE,
|
|
30
30
|
discoverComponents,
|
|
31
|
+
docStem,
|
|
31
32
|
findComponentReadme,
|
|
32
33
|
findComponentSource,
|
|
33
34
|
findExternalComponentDoc,
|
|
34
|
-
|
|
35
|
-
findIntegrationComponentSource,
|
|
35
|
+
findIntegrationComponent,
|
|
36
36
|
resolveImportPath,
|
|
37
37
|
resolveIntegrationImportPath as resolveIntegrationImport,
|
|
38
|
+
unscopedComponentOwners,
|
|
38
39
|
} from '../../foundation/discovery/component-discovery.mjs';
|
|
39
40
|
import {Project} from '../../foundation/config/project.mjs';
|
|
40
41
|
import {loadComponentDoc as loadValidatedComponentDoc} from '../../foundation/discovery/component-loader.mjs';
|
|
@@ -42,7 +43,7 @@ import {resolveComponentReplacements} from '../../foundation/discovery/component
|
|
|
42
43
|
import {searchComponents} from '../../foundation/text/string-utils.mjs';
|
|
43
44
|
import {AstryxError} from '../error.mjs';
|
|
44
45
|
|
|
45
|
-
export {CORE_PACKAGE};
|
|
46
|
+
export {CORE_PACKAGE, unscopedComponentOwners};
|
|
46
47
|
|
|
47
48
|
/**
|
|
48
49
|
* A loaded component doc. The shared validated loader accepts stamped and legacy
|
|
@@ -71,6 +72,8 @@ export {CORE_PACKAGE};
|
|
|
71
72
|
* @property {string|null} sourcePath
|
|
72
73
|
* @property {string|undefined} issuesUrl
|
|
73
74
|
* @property {import('../../foundation/integrations/integrations.mjs').LoadedIntegration|null} integration
|
|
75
|
+
* @property {import('../../foundation/discovery/component-discovery.mjs').NameHold} hold
|
|
76
|
+
* how the owner holds the name, for the owner rule
|
|
74
77
|
*/
|
|
75
78
|
|
|
76
79
|
/**
|
|
@@ -240,17 +243,19 @@ export function resolveOwners(coreDir, dirName, loadedIntegrations) {
|
|
|
240
243
|
sourcePath: findComponentSource(coreDir, dirName),
|
|
241
244
|
issuesUrl: undefined,
|
|
242
245
|
integration: null,
|
|
246
|
+
hold: 'core',
|
|
243
247
|
});
|
|
244
248
|
}
|
|
245
249
|
for (const integration of loadedIntegrations) {
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
250
|
+
const found = findIntegrationComponent(integration, dirName);
|
|
251
|
+
if (!found) continue;
|
|
248
252
|
owners.push({
|
|
249
253
|
package: integration.name,
|
|
250
|
-
docPath,
|
|
251
|
-
sourcePath:
|
|
254
|
+
docPath: found.docPath,
|
|
255
|
+
sourcePath: found.sourcePath,
|
|
252
256
|
issuesUrl: integration.issuesUrl,
|
|
253
257
|
integration,
|
|
258
|
+
hold: found.hold,
|
|
254
259
|
});
|
|
255
260
|
}
|
|
256
261
|
return owners;
|
|
@@ -361,6 +366,39 @@ export function resolveLegacyExternalDoc(ext, dirName) {
|
|
|
361
366
|
return extDocPath && extDocPath.endsWith('.doc.mjs') ? extDocPath : null;
|
|
362
367
|
}
|
|
363
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Whether a legacy external package documents `dirName`.
|
|
371
|
+
* @param {string} cwd
|
|
372
|
+
* @param {string} dirName
|
|
373
|
+
* @returns {boolean}
|
|
374
|
+
*/
|
|
375
|
+
export function legacyExternalDocuments(cwd, dirName) {
|
|
376
|
+
return discoverExternalPackages(cwd).some(
|
|
377
|
+
ext => resolveLegacyExternalDoc(ext, dirName) != null,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Whether a legacy docs package keeps `dirName` over these owners, each of which
|
|
383
|
+
* holds it only as an export a doc records.
|
|
384
|
+
* @param {string} cwd
|
|
385
|
+
* @param {string} dirName
|
|
386
|
+
* @param {Array<{hold?: string, docPath?: string | null}>} owners
|
|
387
|
+
* @returns {boolean}
|
|
388
|
+
*/
|
|
389
|
+
export function legacyExternalKeeps(cwd, dirName, owners) {
|
|
390
|
+
return (
|
|
391
|
+
owners.length > 0 &&
|
|
392
|
+
owners.every(
|
|
393
|
+
owner =>
|
|
394
|
+
owner.hold === 'doc' &&
|
|
395
|
+
owner.docPath != null &&
|
|
396
|
+
docStem(owner.docPath).toLowerCase() !== dirName.toLowerCase(),
|
|
397
|
+
) &&
|
|
398
|
+
legacyExternalDocuments(cwd, dirName)
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
364
402
|
/**
|
|
365
403
|
* The core source file for `dirName` (the no-scope `--source` path reads core
|
|
366
404
|
* directly — it does NOT fall back to externals or fuzzy search).
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
classifyScope,
|
|
23
23
|
assertUnambiguousOwners,
|
|
24
24
|
resolveLegacyExternalDoc,
|
|
25
|
+
legacyExternalKeeps,
|
|
25
26
|
resolveCoreSourcePath,
|
|
26
27
|
resolveUnscopedDoc,
|
|
27
28
|
loadComponentDoc,
|
|
@@ -29,6 +30,7 @@ import {
|
|
|
29
30
|
ComponentAmbiguityError,
|
|
30
31
|
installedComponentPackageVersion,
|
|
31
32
|
resolveComponentReplacement,
|
|
33
|
+
unscopedComponentOwners,
|
|
32
34
|
} from './_adapter.mjs';
|
|
33
35
|
import {componentList} from './list/list.mjs';
|
|
34
36
|
import {componentDetail} from './detail/detail.mjs';
|
|
@@ -481,12 +483,14 @@ export async function component(name, options = {}) {
|
|
|
481
483
|
});
|
|
482
484
|
}
|
|
483
485
|
|
|
486
|
+
// The owner rule runs first, over valid and invalid owners alike.
|
|
487
|
+
const ruled = unscopedComponentOwners(owners);
|
|
484
488
|
// Invalid integration metadata does not create ambiguity against a valid
|
|
485
|
-
// owner. Keep
|
|
489
|
+
// owner. Keep ruled owners only when no doc owner is valid, so an integration-
|
|
486
490
|
// only component still exposes its source and returns ERR_INVALID_DOC for
|
|
487
491
|
// detail instead of degrading to an unrelated unknown-component error.
|
|
488
492
|
const validOwners = [];
|
|
489
|
-
for (const owner of
|
|
493
|
+
for (const owner of ruled) {
|
|
490
494
|
if (owner.package === CORE_PACKAGE) {
|
|
491
495
|
validOwners.push(owner);
|
|
492
496
|
continue;
|
|
@@ -498,7 +502,12 @@ export async function component(name, options = {}) {
|
|
|
498
502
|
// Project/Doctor report invalid metadata; it is not an effective owner.
|
|
499
503
|
}
|
|
500
504
|
}
|
|
501
|
-
const
|
|
505
|
+
const loadable = validOwners.length > 0 ? validOwners : ruled;
|
|
506
|
+
// A legacy docs package's exact doc keeps its name over an export name a doc
|
|
507
|
+
// records, as Core and file-named owners do.
|
|
508
|
+
const effectiveOwners = legacyExternalKeeps(cwd, dirName, loadable)
|
|
509
|
+
? []
|
|
510
|
+
: loadable;
|
|
502
511
|
|
|
503
512
|
// ── Ambiguity: owned by MORE THAN ONE valid package, no --package ──
|
|
504
513
|
assertUnambiguousOwners(effectiveOwners, dirName);
|
|
@@ -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
|
+
});
|