@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
|
@@ -98,6 +98,72 @@ describe('integrationAddComponent name collisions', () => {
|
|
|
98
98
|
expect(snapshot()).toEqual(before);
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
+
it('adds a name a family doc lists but the module it imports does not export', async () => {
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(tmpDir, 'package.json'),
|
|
104
|
+
`${JSON.stringify({name: '@acme/widgets', version: '1.0.0', files: ['dist'], exports: {'./tabs': './components/tabs/index.ts'}}, null, 2)}\n`,
|
|
105
|
+
);
|
|
106
|
+
const tabs = path.join(tmpDir, 'components', 'tabs');
|
|
107
|
+
fs.mkdirSync(tabs, {recursive: true});
|
|
108
|
+
fs.writeFileSync(
|
|
109
|
+
path.join(tabs, 'AcmeTabs.doc.mjs'),
|
|
110
|
+
"export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTabExtra', description: 'x'}]};\n",
|
|
111
|
+
);
|
|
112
|
+
fs.writeFileSync(
|
|
113
|
+
path.join(tabs, 'AcmeTabs.tsx'),
|
|
114
|
+
'export function AcmeTabs() { return null; }\nexport function AcmeTabExtra() { return null; }\n',
|
|
115
|
+
);
|
|
116
|
+
fs.writeFileSync(
|
|
117
|
+
path.join(tabs, 'index.ts'),
|
|
118
|
+
"export {AcmeTabs} from './AcmeTabs';\n",
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
const result = await integrationAdd('component', 'AcmeTabExtra', {
|
|
122
|
+
cwd: tmpDir,
|
|
123
|
+
dryRun: true,
|
|
124
|
+
});
|
|
125
|
+
expect(result.data.files).toContain('components/AcmeTabExtra.doc.mjs');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('refuses a member a family exports through built output', async () => {
|
|
129
|
+
fs.writeFileSync(
|
|
130
|
+
path.join(tmpDir, 'package.json'),
|
|
131
|
+
`${JSON.stringify({name: '@acme/widgets', version: '1.0.0', files: ['dist'], exports: {'./tabs': './dist/tabs/index.js'}}, null, 2)}\n`,
|
|
132
|
+
);
|
|
133
|
+
const tabs = path.join(tmpDir, 'components', 'tabs');
|
|
134
|
+
fs.mkdirSync(tabs, {recursive: true});
|
|
135
|
+
fs.writeFileSync(
|
|
136
|
+
path.join(tabs, 'AcmeTabs.doc.mjs'),
|
|
137
|
+
"export default {type: 'component', name: 'AcmeTabs', import: '@acme/widgets/tabs', description: 'x', components: [{name: 'AcmeTab', description: 'x'}]};\n",
|
|
138
|
+
);
|
|
139
|
+
fs.writeFileSync(
|
|
140
|
+
path.join(tabs, 'AcmeTabs.tsx'),
|
|
141
|
+
'export function AcmeTabs() { return null; }\n',
|
|
142
|
+
);
|
|
143
|
+
fs.writeFileSync(
|
|
144
|
+
path.join(tabs, 'AcmeTab.tsx'),
|
|
145
|
+
'export function AcmeTab() { return null; }\n',
|
|
146
|
+
);
|
|
147
|
+
const dist = path.join(tmpDir, 'dist', 'tabs');
|
|
148
|
+
fs.mkdirSync(dist, {recursive: true});
|
|
149
|
+
fs.writeFileSync(
|
|
150
|
+
path.join(dist, 'index.js'),
|
|
151
|
+
"export {AcmeTabs} from './tabs.js';\nexport {AcmeTab} from './tab.js';\n",
|
|
152
|
+
);
|
|
153
|
+
fs.writeFileSync(
|
|
154
|
+
path.join(dist, 'tabs.js'),
|
|
155
|
+
'export function AcmeTabs() { return null; }\n',
|
|
156
|
+
);
|
|
157
|
+
fs.writeFileSync(
|
|
158
|
+
path.join(dist, 'tab.js'),
|
|
159
|
+
'export function AcmeTab() { return null; }\n',
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
await expect(
|
|
163
|
+
integrationAdd('component', 'AcmeTab', {cwd: tmpDir, dryRun: true}),
|
|
164
|
+
).rejects.toThrow('Refusing to add component "AcmeTab"');
|
|
165
|
+
});
|
|
166
|
+
|
|
101
167
|
it('still adds a component whose name is free', async () => {
|
|
102
168
|
writeAuthoredComponent('components/AcmeWidget/AcmeWidget.doc.mjs');
|
|
103
169
|
|
|
@@ -264,9 +264,13 @@ async function addComponent(name, options) {
|
|
|
264
264
|
ERROR_CODES.ERR_FILE_EXISTS,
|
|
265
265
|
);
|
|
266
266
|
}
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
|
|
267
|
+
// A second doc under a name discovery already records, by a doc's file name
|
|
268
|
+
// anywhere under the root or by a name a doc documents, would shadow one of
|
|
269
|
+
// the two.
|
|
270
|
+
const existingDoc = findIntegrationComponentDoc(
|
|
271
|
+
{name: owner, components: root, __packageDir: packageDir},
|
|
272
|
+
name,
|
|
273
|
+
);
|
|
270
274
|
if (existingDoc != null) {
|
|
271
275
|
throw new AstryxError(
|
|
272
276
|
`Refusing to add component "${name}": ${projectPath(path.relative(packageDir, existingDoc))} already defines it.`,
|
|
@@ -3,6 +3,10 @@
|
|
|
3
3
|
/**
|
|
4
4
|
* @file Integration authoring diagnostics against the built-in Core catalog.
|
|
5
5
|
*
|
|
6
|
+
* The components check also compares each component doc's props with the
|
|
7
|
+
* component's TypeScript props (foundation/discovery/prop-drift.mjs); those
|
|
8
|
+
* findings are warnings under `propDrift`.
|
|
9
|
+
*
|
|
6
10
|
* Templates may intentionally replace Core identities; undeclared same-id
|
|
7
11
|
* overlaps stay fail-closed and require package selection. The CLI package
|
|
8
12
|
* version gates replacement-specific fields so 0.6.x keeps its released shape.
|
|
@@ -25,6 +29,7 @@ import {
|
|
|
25
29
|
discoverOwnedComponents,
|
|
26
30
|
} from '../../foundation/discovery/component-discovery.mjs';
|
|
27
31
|
import {loadComponentReplacements} from '../component/_adapter.mjs';
|
|
32
|
+
import {integrationPropDrift} from './_adapter.mjs';
|
|
28
33
|
import {
|
|
29
34
|
discoverBuiltinTopics,
|
|
30
35
|
discoverIntegrationDocs,
|
|
@@ -242,6 +247,10 @@ export async function integrationComponentConflicts(pkg, options = {}) {
|
|
|
242
247
|
};
|
|
243
248
|
}
|
|
244
249
|
|
|
250
|
+
// Each documented prop against the component's TypeScript props, read with
|
|
251
|
+
// the project's own compiler; without one the report says it was skipped.
|
|
252
|
+
const propDrift = await integrationPropDrift(resolved.integration, cwd);
|
|
253
|
+
|
|
245
254
|
const coreDir = findCoreDir(cwd);
|
|
246
255
|
if (!coreDir) {
|
|
247
256
|
issues.push({
|
|
@@ -252,7 +261,7 @@ export async function integrationComponentConflicts(pkg, options = {}) {
|
|
|
252
261
|
});
|
|
253
262
|
return {
|
|
254
263
|
type: 'integration.component-conflicts',
|
|
255
|
-
data: {validated, name, version, conflicts: [], issues},
|
|
264
|
+
data: {validated, name, version, conflicts: [], issues, propDrift},
|
|
256
265
|
};
|
|
257
266
|
}
|
|
258
267
|
|
|
@@ -296,7 +305,7 @@ export async function integrationComponentConflicts(pkg, options = {}) {
|
|
|
296
305
|
|
|
297
306
|
return {
|
|
298
307
|
type: 'integration.component-conflicts',
|
|
299
|
-
data: {validated, name, version, conflicts, issues},
|
|
308
|
+
data: {validated, name, version, conflicts, issues, propDrift},
|
|
300
309
|
};
|
|
301
310
|
}
|
|
302
311
|
|
|
@@ -417,6 +417,45 @@ describe('integrationComponentConflicts', () => {
|
|
|
417
417
|
expect(result.data.conflicts).toEqual([]);
|
|
418
418
|
expect(result.data.issues).toEqual([]);
|
|
419
419
|
});
|
|
420
|
+
|
|
421
|
+
it("compares each component doc's props with the component's TypeScript props, as warnings", async () => {
|
|
422
|
+
writeComponentIntegration('AcmeBadge');
|
|
423
|
+
fs.writeFileSync(
|
|
424
|
+
path.join(tmpDir, 'components', 'AcmeBadge.doc.mjs'),
|
|
425
|
+
`export default {type: 'component', name: 'AcmeBadge', displayName: 'AcmeBadge', description: 'Fixture component.', props: [{name: 'label', type: 'string', description: 'Text.', required: true}, {name: 'size', type: "'sm' | 'md'", description: 'Size.'}]};\n`,
|
|
426
|
+
);
|
|
427
|
+
fs.writeFileSync(
|
|
428
|
+
path.join(tmpDir, 'components', 'AcmeBadge.tsx'),
|
|
429
|
+
'export function AcmeBadge(props: {label: string; count?: number}) { return null; }\n',
|
|
430
|
+
);
|
|
431
|
+
|
|
432
|
+
const result = await integrationComponentConflicts(undefined, {
|
|
433
|
+
cwd: tmpDir,
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
expect(result.data.issues).toEqual([]);
|
|
437
|
+
expect(result.data.propDrift).toMatchObject({
|
|
438
|
+
status: 'checked',
|
|
439
|
+
components: 1,
|
|
440
|
+
unchecked: [],
|
|
441
|
+
});
|
|
442
|
+
expect(
|
|
443
|
+
result.data.propDrift?.findings.map(f => [f.prop, f.kind, f.severity]),
|
|
444
|
+
).toEqual([
|
|
445
|
+
['count', 'not_in_doc', 'warning'],
|
|
446
|
+
['size', 'not_in_code', 'warning'],
|
|
447
|
+
]);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
it('leaves propDrift out when the package has no components root', async () => {
|
|
451
|
+
writeDocIntegration({name: 'acme-setup'});
|
|
452
|
+
|
|
453
|
+
const result = await integrationComponentConflicts(undefined, {
|
|
454
|
+
cwd: tmpDir,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
expect(result.data).not.toHaveProperty('propDrift');
|
|
458
|
+
});
|
|
420
459
|
});
|
|
421
460
|
|
|
422
461
|
describe('integrationDocConflicts', () => {
|
|
@@ -47,6 +47,14 @@ export type IntegrationTemplateConflictResponse = {
|
|
|
47
47
|
issues: import("../../foundation/integrations/issue").AstryxIntegrationIssue[];
|
|
48
48
|
};
|
|
49
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Documented props compared with the component's TypeScript props. Present
|
|
52
|
+
* when the manifest loads and declares a components root. `status: 'skipped'`
|
|
53
|
+
* means nothing was compared (no TypeScript in the project) and `reason` says
|
|
54
|
+
* why. Every finding is a warning.
|
|
55
|
+
*/
|
|
56
|
+
export type PropDriftReport = import("../../foundation/discovery/prop-drift.mjs").PropDriftReport;
|
|
57
|
+
export type PropDriftFinding = import("../../foundation/discovery/prop-drift.mjs").PropDriftFinding;
|
|
50
58
|
export type IntegrationComponentConflictResponse = {
|
|
51
59
|
type: "integration.component-conflicts";
|
|
52
60
|
data: {
|
|
@@ -55,6 +63,7 @@ export type IntegrationComponentConflictResponse = {
|
|
|
55
63
|
version: string | null;
|
|
56
64
|
conflicts: IntegrationComponentConflict[];
|
|
57
65
|
issues: import("../../foundation/integrations/issue").AstryxIntegrationIssue[];
|
|
66
|
+
propDrift?: PropDriftReport;
|
|
58
67
|
};
|
|
59
68
|
};
|
|
60
69
|
export type IntegrationDocConflictResponse = {
|
|
@@ -58,10 +58,23 @@
|
|
|
58
58
|
* @property {{validated: boolean, name: string | null, version: string | null, conflicts: IntegrationTemplateConflict[], issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[]}} data
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Documented props compared with the component's TypeScript props. Present
|
|
63
|
+
* when the manifest loads and declares a components root. `status: 'skipped'`
|
|
64
|
+
* means nothing was compared (no TypeScript in the project) and `reason` says
|
|
65
|
+
* why. Every finding is a warning.
|
|
66
|
+
*
|
|
67
|
+
* @typedef {import('../../foundation/discovery/prop-drift.mjs').PropDriftReport} PropDriftReport
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* @typedef {import('../../foundation/discovery/prop-drift.mjs').PropDriftFinding} PropDriftFinding
|
|
72
|
+
*/
|
|
73
|
+
|
|
61
74
|
/**
|
|
62
75
|
* @typedef {object} IntegrationComponentConflictResponse
|
|
63
76
|
* @property {'integration.component-conflicts'} type
|
|
64
|
-
* @property {{validated: boolean, name: string | null, version: string | null, conflicts: IntegrationComponentConflict[], issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[]}} data
|
|
77
|
+
* @property {{validated: boolean, name: string | null, version: string | null, conflicts: IntegrationComponentConflict[], issues: import('../../foundation/integrations/issue').AstryxIntegrationIssue[], propDrift?: PropDriftReport}} data
|
|
65
78
|
*/
|
|
66
79
|
|
|
67
80
|
/**
|
|
@@ -10,17 +10,30 @@ export const doc = {
|
|
|
10
10
|
namespace: 'cli/api',
|
|
11
11
|
displayName: 'integrationComponentConflicts()',
|
|
12
12
|
summary:
|
|
13
|
-
'Check integration component names
|
|
13
|
+
'Check integration component names, replaces declarations, and documented props.',
|
|
14
14
|
description:
|
|
15
15
|
'Loads one local or installed integration, compares its component names with ' +
|
|
16
16
|
'Core, and returns non-blocking conflicts with the exact package-qualified ' +
|
|
17
17
|
"component command required to keep an intentional overlap. Each component's " +
|
|
18
18
|
'replaces is checked too: its findings are issues, errors for a package that ' +
|
|
19
|
-
'declares the CLI range that turns replacement on and warnings otherwise.'
|
|
19
|
+
'declares the CLI range that turns replacement on and warnings otherwise. ' +
|
|
20
|
+
"Each component doc's props are compared with the component's TypeScript props, " +
|
|
21
|
+
"read with the project's own `typescript`: a documented prop the code lacks, a " +
|
|
22
|
+
'prop the package declares that the doc omits, or a wrong `required`. Those ' +
|
|
23
|
+
'findings are warnings ' +
|
|
24
|
+
'in `propDrift`; without TypeScript it is `skipped` with a reason.',
|
|
20
25
|
importPath: '@astryxdesign/cli/api',
|
|
21
26
|
signature:
|
|
22
27
|
'integrationComponentConflicts(pkg?: string, options?: IntegrationAuthoringOptions): Promise<IntegrationComponentConflictResponse>',
|
|
23
|
-
keywords: [
|
|
28
|
+
keywords: [
|
|
29
|
+
'integration',
|
|
30
|
+
'component',
|
|
31
|
+
'conflict',
|
|
32
|
+
'authoring',
|
|
33
|
+
'doctor',
|
|
34
|
+
'props',
|
|
35
|
+
'drift',
|
|
36
|
+
],
|
|
24
37
|
params: [
|
|
25
38
|
{name: 'pkg', type: 'string', description: 'Installed package; omit for the local package.'},
|
|
26
39
|
{name: 'options.cwd', type: 'string', description: 'Resolution directory.'},
|
|
@@ -29,11 +42,14 @@ export const doc = {
|
|
|
29
42
|
{
|
|
30
43
|
type: 'integration.component-conflicts',
|
|
31
44
|
description:
|
|
32
|
-
'`validated` (false when no integration manifest was found, so nothing was inspected), integration identity, structural and replaces issues, and Core component-name conflicts with package-qualified commands. An active replacement that keeps the Core name it replaces is not a conflict.',
|
|
45
|
+
'`validated` (false when no integration manifest was found, so nothing was inspected), integration identity, structural and replaces issues, and Core component-name conflicts with package-qualified commands. An active replacement that keeps the Core name it replaces is not a conflict. When the manifest loads and declares a components root, `propDrift` holds the props comparison: `status` (`checked` | `skipped`), `reason` when skipped, `typescript`, `components` compared, `findings` ({component, prop, kind: `not_in_code` | `not_in_doc` | `required_mismatch`, severity: `warning`, message, doc, source, docType?, codeType?, docRequired?, codeRequired?}), `unchecked` ({component, doc, reason}), and `propsNotChecked` when a mapped type builds props from keys the check cannot trace to the package ({component, doc, reason, props}: no warning is drawn for those props).',
|
|
33
46
|
},
|
|
34
47
|
],
|
|
35
48
|
examples: [
|
|
36
|
-
{
|
|
49
|
+
{
|
|
50
|
+
label: 'Check the local integration',
|
|
51
|
+
code: 'await integrationComponentConflicts();',
|
|
52
|
+
},
|
|
37
53
|
],
|
|
38
54
|
command: 'doctor integration components',
|
|
39
55
|
related: ['integrationTemplateConflicts', 'component'],
|
|
@@ -24,7 +24,6 @@ import * as path from 'node:path';
|
|
|
24
24
|
import * as os from 'node:os';
|
|
25
25
|
import {fileURLToPath} from 'node:url';
|
|
26
26
|
import {spawnSync} from 'node:child_process';
|
|
27
|
-
import jscodeshift from 'jscodeshift';
|
|
28
27
|
import {assertWithin} from '../../foundation/fs/path-safety.mjs';
|
|
29
28
|
import {resolvePackageDir} from '../../foundation/integrations/integrations.mjs';
|
|
30
29
|
import {
|
|
@@ -41,6 +40,7 @@ import {
|
|
|
41
40
|
} from '../../foundation/discovery/docs-discovery.mjs';
|
|
42
41
|
import {discoverIntegrationThemes} from '../../foundation/discovery/theme-discovery.mjs';
|
|
43
42
|
import {inspectPackageExport} from '../../foundation/discovery/package-exports.mjs';
|
|
43
|
+
import {moduleExportsName} from '../../foundation/discovery/module-exports.mjs';
|
|
44
44
|
import {
|
|
45
45
|
ThemeImportError,
|
|
46
46
|
resolveThemeImports,
|
|
@@ -51,6 +51,9 @@ import {
|
|
|
51
51
|
} from '../theme/build/build.mjs';
|
|
52
52
|
import {
|
|
53
53
|
discoverIntegrationComponents,
|
|
54
|
+
docStem,
|
|
55
|
+
familyImports,
|
|
56
|
+
isIntegrationMember,
|
|
54
57
|
resolveIntegrationImportPath,
|
|
55
58
|
} from '../../foundation/discovery/component-discovery.mjs';
|
|
56
59
|
import {loadComponentReplacements} from '../component/_adapter.mjs';
|
|
@@ -307,98 +310,6 @@ function extractTarball(tgzPath, extractDir) {
|
|
|
307
310
|
}
|
|
308
311
|
}
|
|
309
312
|
|
|
310
|
-
const MODULE_EXTENSIONS = ['.mjs', '.js', '.mts', '.ts', '.tsx', '.jsx'];
|
|
311
|
-
const moduleParser = jscodeshift.withParser('tsx');
|
|
312
|
-
|
|
313
|
-
/** @param {unknown} specifier @param {string} fromFile */
|
|
314
|
-
function resolveLocalModule(specifier, fromFile) {
|
|
315
|
-
if (typeof specifier !== 'string' || !specifier.startsWith('.')) return null;
|
|
316
|
-
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
317
|
-
const candidates = [
|
|
318
|
-
base,
|
|
319
|
-
...MODULE_EXTENSIONS.map(extension => `${base}${extension}`),
|
|
320
|
-
...MODULE_EXTENSIONS.map(extension => path.join(base, `index${extension}`)),
|
|
321
|
-
];
|
|
322
|
-
return candidates.find(candidate => fs.existsSync(candidate)) ?? null;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/** @param {any} declaration @param {string} exportName */
|
|
326
|
-
function declarationExports(declaration, exportName) {
|
|
327
|
-
if (!declaration) return false;
|
|
328
|
-
if (['FunctionDeclaration', 'ClassDeclaration'].includes(declaration.type)) {
|
|
329
|
-
return declaration.id?.name === exportName;
|
|
330
|
-
}
|
|
331
|
-
if (declaration.type !== 'VariableDeclaration') return false;
|
|
332
|
-
return declaration.declarations.some(
|
|
333
|
-
(/** @type {any} */ item) =>
|
|
334
|
-
item.id?.type === 'Identifier' && item.id.name === exportName,
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/**
|
|
339
|
-
* Check one resolved module's static export surface without executing package
|
|
340
|
-
* code. Local ESM re-exports are followed recursively.
|
|
341
|
-
*
|
|
342
|
-
* @param {string} file
|
|
343
|
-
* @param {string} exportName
|
|
344
|
-
* @param {Set<string>} [seen]
|
|
345
|
-
*/
|
|
346
|
-
function moduleExportsName(file, exportName, seen = new Set()) {
|
|
347
|
-
const identity = `${file}\0${exportName}`;
|
|
348
|
-
if (seen.has(identity) || !fs.existsSync(file)) return false;
|
|
349
|
-
seen.add(identity);
|
|
350
|
-
|
|
351
|
-
let root;
|
|
352
|
-
try {
|
|
353
|
-
root = moduleParser(fs.readFileSync(file, 'utf-8'));
|
|
354
|
-
} catch {
|
|
355
|
-
return false;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
let found = false;
|
|
359
|
-
root
|
|
360
|
-
.find(moduleParser.ExportNamedDeclaration)
|
|
361
|
-
.forEach((/** @type {any} */ exportPath) => {
|
|
362
|
-
if (found) return;
|
|
363
|
-
const node = exportPath.node;
|
|
364
|
-
if (declarationExports(node.declaration, exportName)) {
|
|
365
|
-
found = true;
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
for (const specifier of node.specifiers ?? []) {
|
|
369
|
-
const exported = specifier.exported?.name ?? specifier.exported?.value;
|
|
370
|
-
if (exported !== exportName) continue;
|
|
371
|
-
if (!node.source) {
|
|
372
|
-
found = true;
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
const target = resolveLocalModule(node.source.value, file);
|
|
376
|
-
const imported =
|
|
377
|
-
specifier.local?.name ?? specifier.local?.value ?? exportName;
|
|
378
|
-
if (target && moduleExportsName(target, imported, seen)) {
|
|
379
|
-
found = true;
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
if (found) return true;
|
|
385
|
-
if (
|
|
386
|
-
exportName === 'default' &&
|
|
387
|
-
root.find(moduleParser.ExportDefaultDeclaration).size() > 0
|
|
388
|
-
) {
|
|
389
|
-
return true;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
root
|
|
393
|
-
.find(moduleParser.ExportAllDeclaration)
|
|
394
|
-
.forEach((/** @type {any} */ exportPath) => {
|
|
395
|
-
if (found) return;
|
|
396
|
-
const target = resolveLocalModule(exportPath.node.source?.value, file);
|
|
397
|
-
if (target && moduleExportsName(target, exportName, seen)) found = true;
|
|
398
|
-
});
|
|
399
|
-
return found;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
313
|
/**
|
|
403
314
|
* Whether any of these doc files has a section that sets `id`.
|
|
404
315
|
* @param {string[]} files
|
|
@@ -472,15 +383,20 @@ async function validatePackedComponentExports(integration, scratchBase) {
|
|
|
472
383
|
const issues = [];
|
|
473
384
|
/** @type {Array<{name: string, specifier: string}>} */
|
|
474
385
|
const components = [];
|
|
386
|
+
// A family doc backs several records: load it, and report it, once.
|
|
387
|
+
const loads = new Map();
|
|
475
388
|
for (const record of records) {
|
|
389
|
+
const first = !loads.has(record.docPath);
|
|
390
|
+
if (first) loads.set(record.docPath, loadComponentDoc(record.docPath));
|
|
476
391
|
let docs;
|
|
477
392
|
try {
|
|
478
|
-
docs = await
|
|
393
|
+
docs = await loads.get(record.docPath);
|
|
479
394
|
} catch (err) {
|
|
395
|
+
if (!first) continue;
|
|
480
396
|
issues.push(
|
|
481
397
|
error(
|
|
482
398
|
'component_doc_unloadable',
|
|
483
|
-
`Component "${record.
|
|
399
|
+
`Component "${docStem(record.docPath)}" doc at "${record.docPath}" could not be loaded: ${err instanceof Error ? err.message : String(err)}`,
|
|
484
400
|
),
|
|
485
401
|
);
|
|
486
402
|
continue;
|
|
@@ -660,11 +576,10 @@ async function validateIncompleteThemeExports(
|
|
|
660
576
|
const expectedCss = path.join(expectedDir, `${localTheme.slug}.css`);
|
|
661
577
|
try {
|
|
662
578
|
if (outputs.some(output => output.kind !== 'font stylesheet')) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
);
|
|
579
|
+
// The scratch directory is the write root; the package stays the cwd,
|
|
580
|
+
// so the build selects the package's Core and its paths as before.
|
|
581
|
+
const scratchBuild = {out: expectedCss, __writeRoot: expectedDir};
|
|
582
|
+
const receipt = await themeBuild(source, scratchBuild, {cwd: packageDir});
|
|
668
583
|
if (receipt == null || receipt.type !== 'theme.build') {
|
|
669
584
|
throw new Error('the theme build produced no complete receipt');
|
|
670
585
|
}
|
|
@@ -847,11 +762,12 @@ async function validatePackedThemeExports(
|
|
|
847
762
|
);
|
|
848
763
|
const expectedCss = path.join(expectedDir, `${localTheme.slug}.css`);
|
|
849
764
|
try {
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
765
|
+
// The scratch directory is the write root; the package stays the cwd,
|
|
766
|
+
// so the build selects the package's Core and its paths as before.
|
|
767
|
+
const scratchBuild = {out: expectedCss, __writeRoot: expectedDir};
|
|
768
|
+
const receipt = await themeBuild(source, scratchBuild, {
|
|
769
|
+
cwd: localIntegration.__packageDir,
|
|
770
|
+
});
|
|
855
771
|
if (receipt == null || receipt.type !== 'theme.build') {
|
|
856
772
|
issues.push(
|
|
857
773
|
error(
|
|
@@ -1085,6 +1001,24 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1085
1001
|
const fileInv = computeRequiredFiles(loaded);
|
|
1086
1002
|
const {identities: localIdentities, errors: localDiscoveryErrors} =
|
|
1087
1003
|
await collectIdentities(loaded);
|
|
1004
|
+
// A family lists its members from the module its import names, which the
|
|
1005
|
+
// pack lifecycle may build or rebuild, so they are compared as local
|
|
1006
|
+
// discovery lists them after the pack.
|
|
1007
|
+
const familiesBefore = familyImports(loaded).map(family => family.docPath);
|
|
1008
|
+
/**
|
|
1009
|
+
* @param {Array<{name: string, docPath: string|null}>} records
|
|
1010
|
+
* @param {Set<string>} families
|
|
1011
|
+
*/
|
|
1012
|
+
const membersOf = (records, families) =>
|
|
1013
|
+
records
|
|
1014
|
+
.filter(
|
|
1015
|
+
r =>
|
|
1016
|
+
r.docPath != null &&
|
|
1017
|
+
families.has(r.docPath) &&
|
|
1018
|
+
isIntegrationMember(r),
|
|
1019
|
+
)
|
|
1020
|
+
.map(r => r.name);
|
|
1021
|
+
const recordsBefore = discoverIntegrationComponents(loaded);
|
|
1088
1022
|
for (const discoveryError of localDiscoveryErrors) {
|
|
1089
1023
|
issues.push(
|
|
1090
1024
|
error(
|
|
@@ -1345,7 +1279,27 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1345
1279
|
);
|
|
1346
1280
|
}
|
|
1347
1281
|
|
|
1348
|
-
|
|
1282
|
+
const families = new Set([
|
|
1283
|
+
...familiesBefore,
|
|
1284
|
+
...familyImports(loaded).map(family => family.docPath),
|
|
1285
|
+
]);
|
|
1286
|
+
let localComponents = localIdentities.components;
|
|
1287
|
+
if (families.size > 0) {
|
|
1288
|
+
const stale = new Set(membersOf(recordsBefore, families));
|
|
1289
|
+
const built = new Set(
|
|
1290
|
+
membersOf(discoverIntegrationComponents(loaded), families),
|
|
1291
|
+
);
|
|
1292
|
+
const {identities: afterPack} = await collectIdentities(loaded);
|
|
1293
|
+
localComponents = [
|
|
1294
|
+
...new Set([
|
|
1295
|
+
...localComponents.filter(name => !stale.has(name)),
|
|
1296
|
+
...afterPack.components.filter(name => built.has(name)),
|
|
1297
|
+
]),
|
|
1298
|
+
].sort();
|
|
1299
|
+
}
|
|
1300
|
+
// The receipt reports the local inventory that was compared.
|
|
1301
|
+
const comparedLocal = {...localIdentities, components: localComponents};
|
|
1302
|
+
issues.push(...compareIdentities(comparedLocal, packedIdentities));
|
|
1349
1303
|
issues.push(
|
|
1350
1304
|
...(await validatePackedThemeExports(
|
|
1351
1305
|
loaded,
|
|
@@ -1367,7 +1321,7 @@ export async function integrationPackCheck(options = {}) {
|
|
|
1367
1321
|
pkgVersion,
|
|
1368
1322
|
fileInv,
|
|
1369
1323
|
packResult,
|
|
1370
|
-
|
|
1324
|
+
comparedLocal,
|
|
1371
1325
|
packedIdentities,
|
|
1372
1326
|
issues,
|
|
1373
1327
|
);
|