@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.
Files changed (142) hide show
  1. package/README.md +7 -7
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +41 -1
  4. package/api/component/_adapter.mjs +93 -7
  5. package/api/component/component.mjs +22 -5
  6. package/api/component/component.test.mjs +420 -167
  7. package/api/component/integration-export-name.test.mjs +644 -0
  8. package/api/component/list/list.mjs +42 -4
  9. package/api/docs/_adapter.mjs +207 -1
  10. package/api/docs/integrationDocs.test.mjs +124 -0
  11. package/api/doctor/theme-checks.mjs +1 -1
  12. package/api/gap-report/gap-report.mjs +67 -4
  13. package/api/init/run/run.mjs +2 -2
  14. package/api/integration/_adapter.d.mts +12 -0
  15. package/api/integration/_adapter.mjs +54 -0
  16. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  17. package/api/integration/add-contribution.mjs +7 -3
  18. package/api/integration/authoring-checks.mjs +11 -2
  19. package/api/integration/authoring-checks.test.mjs +39 -0
  20. package/api/integration/authoring-checks.type.d.mts +9 -0
  21. package/api/integration/authoring-checks.type.mjs +14 -1
  22. package/api/integration/integrationComponentConflicts.doc.mjs +21 -5
  23. package/api/integration/pack-check.mjs +61 -107
  24. package/api/integration/pack-check.test.mjs +359 -0
  25. package/api/integration/validate-integration.test.mjs +11 -0
  26. package/api/search/search.d.mts +9 -0
  27. package/api/search/search.mjs +108 -16
  28. package/api/search/search.test.mjs +36 -0
  29. package/api/swizzle/copy/copy.mjs +28 -14
  30. package/api/template/template.test.mjs +120 -3
  31. package/api/theme/build/build.family.test.mjs +16 -0
  32. package/api/theme/build/build.mjs +81 -22
  33. package/api/theme/themeBuild.doc.mjs +3 -3
  34. package/assets/codemods/transforms/next/__tests__/migrate-list-edge-compensation-to-edge.test.mjs +120 -0
  35. package/assets/codemods/transforms/next/index.mjs +11 -1
  36. package/assets/codemods/transforms/next/migrate-list-edge-compensation-to-edge.mjs +198 -0
  37. package/assets/docs/README.md +10 -0
  38. package/assets/docs/author-a-theme.doc.mjs +1 -1
  39. package/assets/docs/getting-started.doc.mjs +3 -3
  40. package/assets/docs/theme.doc.dense.mjs +1 -1
  41. package/assets/docs/theme.doc.mjs +1 -1
  42. package/assets/docs/theme.doc.zh.mjs +1 -1
  43. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  44. package/assets/docs/tree/check-the-props.doc.mjs +145 -0
  45. package/assets/docs/tree/checks.doc.mjs +4 -4
  46. package/assets/docs/tree/component-family.doc.mjs +5 -1
  47. package/assets/docs/tree/component-imports.doc.mjs +8 -0
  48. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  49. package/assets/docs/tree/links.doc.mjs +2 -2
  50. package/assets/docs/tree/single-component.doc.mjs +3 -2
  51. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  52. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  53. package/assets/docs/tree/versioning.doc.mjs +1 -0
  54. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  55. package/assets/docs/use-a-theme.doc.mjs +2 -2
  56. package/assets/docs/working-with-ai.doc.mjs +43 -4
  57. package/assets/templates/blocks/components/Drawer/DrawerForm.doc.mjs +24 -0
  58. package/assets/templates/blocks/components/Drawer/DrawerForm.tsx +62 -0
  59. package/assets/templates/blocks/components/Drawer/DrawerScrim.doc.mjs +22 -0
  60. package/assets/templates/blocks/components/Drawer/DrawerScrim.tsx +68 -0
  61. package/assets/templates/blocks/components/Drawer/DrawerShowcase.doc.mjs +22 -0
  62. package/assets/templates/blocks/components/Drawer/DrawerShowcase.tsx +41 -0
  63. package/assets/templates/blocks/components/Drawer/DrawerStart.doc.mjs +21 -0
  64. package/assets/templates/blocks/components/Drawer/DrawerStart.tsx +41 -0
  65. package/assets/templates/blocks/components/Drawer/DrawerWidths.doc.mjs +22 -0
  66. package/assets/templates/blocks/components/Drawer/DrawerWidths.tsx +54 -0
  67. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.doc.mjs +15 -0
  68. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderShowcase.tsx +73 -0
  69. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.doc.mjs +22 -0
  70. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderSubtitleEndContent.tsx +49 -0
  71. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.doc.mjs +21 -0
  72. package/assets/templates/blocks/components/DrawerHeader/DrawerHeaderWithCloseButton.tsx +42 -0
  73. package/assets/templates/pages/detail-page/page.tsx +3 -3
  74. package/assets/templates/pages/table-inbox/page.tsx +10 -8
  75. package/assets/theme.template.ts +1 -1
  76. package/authoring/doctypes/base/type.ts +6 -0
  77. package/authoring/doctypes/component/component.doc.mjs +4 -4
  78. package/authoring/doctypes/component/type.ts +6 -3
  79. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  80. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  81. package/authoring/doctypes/template/parse.d.mts +6 -2
  82. package/authoring/doctypes/template/parse.mjs +3 -1
  83. package/authoring/doctypes/template/type.ts +22 -0
  84. package/authoring/index.d.ts +1 -0
  85. package/authoring/integration/integration.doc.mjs +8 -1
  86. package/authoring/integration/parse.test.mjs +10 -0
  87. package/authoring/integration/schema.d.mts +2 -0
  88. package/authoring/integration/schema.mjs +1 -0
  89. package/authoring/integration/type.ts +4 -0
  90. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  91. package/clients/cli/commands/build-theme.mjs +1 -1
  92. package/clients/cli/commands/build-theme.path-safety.test.mjs +84 -0
  93. package/clients/cli/commands/build-theme.registry.test.mjs +5 -2
  94. package/clients/cli/commands/doctor-integration-components.doc.mjs +9 -3
  95. package/clients/cli/commands/doctor-integration.test.mjs +392 -0
  96. package/clients/cli/commands/doctor.mjs +74 -0
  97. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  98. package/clients/cli/lib/component-format.mjs +5 -1
  99. package/foundation/config/project.mjs +82 -0
  100. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  101. package/foundation/discovery/component-discovery.d.mts +190 -10
  102. package/foundation/discovery/component-discovery.export-names.test.mjs +760 -0
  103. package/foundation/discovery/component-discovery.mjs +864 -56
  104. package/foundation/discovery/component-discovery.test.mjs +96 -0
  105. package/foundation/discovery/component-replacement.mjs +18 -0
  106. package/foundation/discovery/component-replacement.test.mjs +40 -0
  107. package/foundation/discovery/doc-types.d.mts +157 -0
  108. package/foundation/discovery/doc-types.mjs +1224 -0
  109. package/foundation/discovery/doc-types.test.mjs +652 -0
  110. package/foundation/discovery/docs-discovery.d.mts +81 -0
  111. package/foundation/discovery/docs-discovery.mjs +57 -0
  112. package/foundation/discovery/module-exports.d.mts +27 -0
  113. package/foundation/discovery/module-exports.mjs +356 -31
  114. package/foundation/discovery/module-exports.test.mjs +172 -0
  115. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  116. package/foundation/discovery/package-tsconfig.mjs +228 -0
  117. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  118. package/foundation/discovery/project-typescript.d.mts +23 -0
  119. package/foundation/discovery/project-typescript.mjs +61 -0
  120. package/foundation/discovery/prop-drift.d.mts +220 -0
  121. package/foundation/discovery/prop-drift.mjs +1765 -0
  122. package/foundation/discovery/prop-drift.test.mjs +1197 -0
  123. package/foundation/discovery/template-adapter.mjs +74 -5
  124. package/foundation/discovery/type-match.d.mts +53 -0
  125. package/foundation/discovery/type-match.mjs +660 -0
  126. package/foundation/discovery/type-match.test.mjs +169 -0
  127. package/foundation/discovery/type-text.d.mts +68 -0
  128. package/foundation/discovery/type-text.mjs +658 -0
  129. package/foundation/discovery/type-text.test.mjs +196 -0
  130. package/foundation/discovery/unresolved-references.d.mts +62 -0
  131. package/foundation/discovery/unresolved-references.mjs +790 -0
  132. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  133. package/foundation/doc-compiler/doc-loads.test.mjs +9 -1
  134. package/foundation/doc-compiler/inputs.mjs +23 -0
  135. package/foundation/doc-compiler/inputs.test.mjs +0 -1
  136. package/foundation/doc-compiler/links.d.mts +9 -5
  137. package/foundation/doc-compiler/links.mjs +4 -2
  138. package/foundation/integrations/contribution-inventory.mjs +6 -1
  139. package/foundation/response/response-types.doc.mjs +1 -1
  140. package/foundation/xle/registry.mjs +4 -0
  141. package/foundation/xle/registry.test.mjs +28 -0
  142. 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
- // Discovery keys components by doc stem anywhere under the root, so a second
268
- // doc with this name would shadow one of the two.
269
- const existingDoc = findIntegrationComponentDoc({components: root}, name);
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 and replaces declarations against Core.',
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: ['integration', 'component', 'conflict', 'authoring', 'doctor'],
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
- {label: 'Check the local integration', code: 'await integrationComponentConflicts();'},
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 loadComponentDoc(record.docPath);
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.name}" doc at "${record.docPath}" could not be loaded: ${err instanceof Error ? err.message : String(err)}`,
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
- const receipt = await themeBuild(
664
- source,
665
- {out: expectedCss},
666
- {cwd: packageDir},
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
- const receipt = await themeBuild(
851
- source,
852
- {out: expectedCss},
853
- {cwd: localIntegration.__packageDir},
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
- issues.push(...compareIdentities(localIdentities, packedIdentities));
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
- localIdentities,
1324
+ comparedLocal,
1371
1325
  packedIdentities,
1372
1326
  issues,
1373
1327
  );