@astryxdesign/cli 0.7.2-canary.ebcc404 → 0.7.2-canary.f8f7cf1

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 (134) hide show
  1. package/README.md +9 -8
  2. package/api/build/help/help.mjs +1 -1
  3. package/api/component/_adapter.d.mts +14 -0
  4. package/api/component/_adapter.mjs +48 -0
  5. package/api/component/component.mjs +10 -2
  6. package/api/component/integration-export-name.test.mjs +233 -8
  7. package/api/docs/_adapter.d.mts +11 -0
  8. package/api/docs/_adapter.mjs +244 -1
  9. package/api/docs/integrationDocs.test.mjs +124 -0
  10. package/api/doctor/theme-checks.mjs +1 -1
  11. package/api/gap-report/gap-report.mjs +2 -1
  12. package/api/index.d.mts +2 -0
  13. package/api/index.mjs +2 -0
  14. package/api/init/run/run.mjs +2 -2
  15. package/api/integration/_adapter.d.mts +12 -0
  16. package/api/integration/_adapter.mjs +54 -0
  17. package/api/integration/add-contribution.component-names.test.mjs +66 -0
  18. package/api/integration/add-contribution.mjs +8 -4
  19. package/api/integration/add-contribution.test.mjs +4 -2
  20. package/api/integration/authoring-checks.mjs +11 -2
  21. package/api/integration/authoring-checks.test.mjs +39 -0
  22. package/api/integration/authoring-checks.type.d.mts +9 -0
  23. package/api/integration/authoring-checks.type.mjs +14 -1
  24. package/api/integration/check/check.d.mts +96 -0
  25. package/api/integration/check/check.mjs +357 -0
  26. package/api/integration/check/check.test.mjs +489 -0
  27. package/api/integration/integration-check.type.d.mts +118 -0
  28. package/api/integration/integration-check.type.mjs +84 -0
  29. package/api/integration/integrationCheck.doc.d.mts +10 -0
  30. package/api/integration/integrationCheck.doc.mjs +73 -0
  31. package/api/integration/integrationComponentConflicts.doc.mjs +36 -5
  32. package/api/integration/pack-check.mjs +48 -3
  33. package/api/integration/pack-check.test.mjs +247 -0
  34. package/api/integration/validate-integration.mjs +8 -0
  35. package/api/integration/validate-integration.test.mjs +11 -0
  36. package/api/integration/validate-integration.type.d.mts +8 -0
  37. package/api/integration/validate-integration.type.mjs +12 -1
  38. package/api/integration/validateIntegration.doc.mjs +17 -3
  39. package/api/search/search.integration-check.test.mjs +24 -0
  40. package/api/search/search.mjs +20 -8
  41. package/api/theme/build/build.family.test.mjs +54 -6
  42. package/api/theme/build/build.mjs +114 -17
  43. package/api/theme/themeBuild.doc.mjs +4 -4
  44. package/assets/docs/README.md +10 -0
  45. package/assets/docs/author-a-theme.doc.mjs +1 -1
  46. package/assets/docs/getting-started.doc.mjs +3 -3
  47. package/assets/docs/theme.doc.dense.mjs +1 -1
  48. package/assets/docs/theme.doc.mjs +1 -1
  49. package/assets/docs/theme.doc.zh.mjs +1 -1
  50. package/assets/docs/tree/add-a-component.doc.mjs +10 -0
  51. package/assets/docs/tree/block-template.doc.mjs +2 -2
  52. package/assets/docs/tree/check-the-props.doc.mjs +196 -0
  53. package/assets/docs/tree/check-your-docs.doc.mjs +44 -3
  54. package/assets/docs/tree/checks.doc.mjs +21 -13
  55. package/assets/docs/tree/component-family.doc.mjs +5 -1
  56. package/assets/docs/tree/component-imports.doc.mjs +1 -1
  57. package/assets/docs/tree/describe-the-component.doc.mjs +1 -1
  58. package/assets/docs/tree/links.doc.mjs +2 -2
  59. package/assets/docs/tree/page-template.doc.mjs +1 -1
  60. package/assets/docs/tree/quick-start.doc.mjs +1 -1
  61. package/assets/docs/tree/replace-a-core-template.doc.mjs +1 -1
  62. package/assets/docs/tree/single-component.doc.mjs +3 -2
  63. package/assets/docs/tree/troubleshooting.doc.mjs +9 -1
  64. package/assets/docs/tree/type-scale.doc.mjs +3 -3
  65. package/assets/docs/tree/versioning.doc.mjs +1 -0
  66. package/assets/docs/tree/write-the-template-file.doc.mjs +2 -1
  67. package/assets/docs/use-a-theme.doc.mjs +2 -2
  68. package/assets/theme.template.ts +1 -1
  69. package/authoring/doctypes/component/component.doc.mjs +1 -1
  70. package/authoring/doctypes/template/integration-template.doc.d.mts +11 -0
  71. package/authoring/doctypes/template/integration-template.doc.mjs +49 -0
  72. package/authoring/doctypes/template/parse.d.mts +6 -2
  73. package/authoring/doctypes/template/parse.mjs +3 -1
  74. package/authoring/doctypes/template/template.doc.mjs +1 -1
  75. package/authoring/doctypes/template/type.ts +22 -0
  76. package/authoring/index.d.ts +1 -0
  77. package/authoring/integration/integration.doc.mjs +8 -1
  78. package/authoring/integration/parse.test.mjs +10 -0
  79. package/authoring/integration/schema.d.mts +2 -0
  80. package/authoring/integration/schema.mjs +1 -0
  81. package/authoring/integration/type.ts +4 -0
  82. package/clients/cli/commands/build-theme.batch-report.test.mjs +3 -1
  83. package/clients/cli/commands/build-theme.mjs +1 -1
  84. package/clients/cli/commands/build-theme.path-safety.test.mjs +155 -21
  85. package/clients/cli/commands/doctor-integration-components.doc.mjs +15 -4
  86. package/clients/cli/commands/doctor-integration-validate.doc.mjs +11 -2
  87. package/clients/cli/commands/doctor-integration.test.mjs +837 -0
  88. package/clients/cli/commands/doctor.mjs +190 -0
  89. package/clients/cli/commands/integration-check.doc.mjs +85 -0
  90. package/clients/cli/commands/integration-check.test.mjs +267 -0
  91. package/clients/cli/commands/integration.doc.mjs +6 -2
  92. package/clients/cli/commands/integration.mjs +107 -1
  93. package/clients/cli/commands/text-json-parity.test.mjs +5 -0
  94. package/clients/cli/commands/theme-build.doc.mjs +2 -2
  95. package/clients/cli/index.mjs +1 -0
  96. package/foundation/config/project.mjs +82 -0
  97. package/foundation/discovery/authoring-self-docs.mjs +1 -0
  98. package/foundation/discovery/component-discovery.d.mts +108 -17
  99. package/foundation/discovery/component-discovery.export-names.test.mjs +469 -2
  100. package/foundation/discovery/component-discovery.mjs +380 -47
  101. package/foundation/discovery/component-replacement.test.mjs +20 -0
  102. package/foundation/discovery/doc-types.d.mts +157 -0
  103. package/foundation/discovery/doc-types.mjs +1224 -0
  104. package/foundation/discovery/doc-types.test.mjs +652 -0
  105. package/foundation/discovery/docs-discovery.d.mts +81 -0
  106. package/foundation/discovery/docs-discovery.mjs +57 -0
  107. package/foundation/discovery/integration-doc-files.d.mts +22 -0
  108. package/foundation/discovery/integration-doc-files.mjs +231 -0
  109. package/foundation/discovery/integration-doc-files.test.mjs +246 -0
  110. package/foundation/discovery/module-exports.d.mts +6 -0
  111. package/foundation/discovery/module-exports.mjs +15 -2
  112. package/foundation/discovery/package-tsconfig.d.mts +14 -0
  113. package/foundation/discovery/package-tsconfig.mjs +228 -0
  114. package/foundation/discovery/package-tsconfig.test.mjs +210 -0
  115. package/foundation/discovery/project-typescript.d.mts +23 -0
  116. package/foundation/discovery/project-typescript.mjs +61 -0
  117. package/foundation/discovery/prop-drift.d.mts +278 -0
  118. package/foundation/discovery/prop-drift.mjs +2263 -0
  119. package/foundation/discovery/prop-drift.test.mjs +2219 -0
  120. package/foundation/discovery/type-match.d.mts +53 -0
  121. package/foundation/discovery/type-match.mjs +660 -0
  122. package/foundation/discovery/type-match.test.mjs +169 -0
  123. package/foundation/discovery/type-text.d.mts +68 -0
  124. package/foundation/discovery/type-text.mjs +658 -0
  125. package/foundation/discovery/type-text.test.mjs +196 -0
  126. package/foundation/discovery/unresolved-references.d.mts +62 -0
  127. package/foundation/discovery/unresolved-references.mjs +790 -0
  128. package/foundation/discovery/unresolved-references.test.mjs +567 -0
  129. package/foundation/doc-compiler/doc-loads.test.mjs +6 -0
  130. package/foundation/doc-compiler/links.d.mts +9 -5
  131. package/foundation/doc-compiler/links.mjs +4 -2
  132. package/foundation/integrations/contribution-inventory.mjs +6 -1
  133. package/foundation/response/response-types.doc.mjs +7 -2
  134. package/package.json +9 -9
@@ -1,8 +1,8 @@
1
1
  // Copyright (c) Meta Platforms, Inc. and affiliates.
2
2
 
3
3
  /**
4
- * @file Integration components are found by the names their docs document;
5
- * every doc that works today reads exactly as before.
4
+ * @file Integration components, and the members a family doc lists, are found
5
+ * by the names their docs document; every doc that works today reads as before.
6
6
  */
7
7
 
8
8
  import {afterEach, beforeEach, describe, expect, it} from 'vitest';
@@ -291,3 +291,470 @@ describe('integration components whose doc names another export of its file', ()
291
291
  expect(names()).toEqual([]);
292
292
  });
293
293
  });
294
+
295
+ /**
296
+ * Write a family doc and its source.
297
+ * @param {string} file path under the root, without extension
298
+ * @param {{name: string, members: string, source: string, extra?: string}} options
299
+ * `members` is the `components` array, written as JavaScript
300
+ */
301
+ function writeFamily(file, {name, members, source, extra = ''}) {
302
+ const target = path.join(root, file);
303
+ fs.mkdirSync(path.dirname(target), {recursive: true});
304
+ fs.writeFileSync(
305
+ `${target}.doc.mjs`,
306
+ `export default {\n type: 'component',\n name: '${name}',\n usage: {description: 'A family.'},${extra}\n components: ${members},\n};\n`,
307
+ );
308
+ fs.writeFileSync(`${target}.tsx`, source);
309
+ }
310
+
311
+ describe('integration components a family doc lists in components', () => {
312
+ it('records each exported full entry under its own name, with the file that declares it', async () => {
313
+ writeFamily('carousel/AcmeCarousel', {
314
+ name: 'AcmeCarousel',
315
+ members:
316
+ "[{name: 'AcmeCarousel', description: 'The carousel.'}, {name: 'AcmeSlide', description: 'One slide.'}, {name: 'AcmeDots', description: 'The dots.'}]",
317
+ source:
318
+ "export function AcmeCarousel() { return null; }\nexport function AcmeSlide() { return null; }\nexport {AcmeDots} from './dots';\n",
319
+ });
320
+ fs.writeFileSync(
321
+ path.join(root, 'carousel', 'dots.tsx'),
322
+ 'export const AcmeDots = () => null;\n',
323
+ );
324
+
325
+ const records = discoverIntegrationComponents(integration());
326
+ const docPath = path.join(root, 'carousel', 'AcmeCarousel.doc.mjs');
327
+ expect(records.map(record => record.name)).toEqual([
328
+ 'AcmeCarousel',
329
+ 'AcmeSlide',
330
+ 'AcmeDots',
331
+ ]);
332
+ expect(records.find(record => record.name === 'AcmeSlide')).toMatchObject({
333
+ docPath,
334
+ sourcePath: path.join(root, 'carousel', 'AcmeCarousel.tsx'),
335
+ });
336
+ expect(findIntegrationComponentDoc(integration(), 'AcmeDots')).toBe(
337
+ docPath,
338
+ );
339
+ expect(findIntegrationComponentSource(integration(), 'AcmeDots')).toBe(
340
+ path.join(root, 'carousel', 'dots.tsx'),
341
+ );
342
+ const {components, errors} =
343
+ await discoverValidIntegrationComponents(integration());
344
+ expect(errors).toEqual([]);
345
+ expect(components.map(record => record.name)).toEqual([
346
+ 'AcmeCarousel',
347
+ 'AcmeSlide',
348
+ 'AcmeDots',
349
+ ]);
350
+ });
351
+
352
+ it('reads members from the module the doc imports, through the package exports', () => {
353
+ // The components root inside a package whose exports publish the module.
354
+ const componentsRoot = path.join(root, 'src');
355
+ fs.mkdirSync(path.join(componentsRoot, 'carousel'), {recursive: true});
356
+ fs.writeFileSync(
357
+ path.join(root, 'package.json'),
358
+ JSON.stringify({
359
+ name: '@acme/widgets',
360
+ exports: {'./carousel': './src/carousel/index.ts'},
361
+ }),
362
+ );
363
+ fs.writeFileSync(
364
+ path.join(componentsRoot, 'carousel', 'AcmeCarousel.doc.mjs'),
365
+ "export default {type: 'component', name: 'AcmeCarousel', import: '@acme/widgets/carousel', usage: {description: 'x'}, components: [{name: 'AcmeCarousel', description: 'x'}, {name: 'AcmeSlide', description: 'x'}]};\n",
366
+ );
367
+ fs.writeFileSync(
368
+ path.join(componentsRoot, 'carousel', 'AcmeCarousel.tsx'),
369
+ 'export function AcmeCarousel() { return null; }\n',
370
+ );
371
+ fs.writeFileSync(
372
+ path.join(componentsRoot, 'carousel', 'index.ts'),
373
+ "export * from './AcmeCarousel';\nexport {AcmeSlide} from './slide';\n",
374
+ );
375
+ fs.writeFileSync(
376
+ path.join(componentsRoot, 'carousel', 'slide.tsx'),
377
+ 'export function AcmeSlide() { return null; }\n',
378
+ );
379
+
380
+ const loaded = {
381
+ name: '@acme/widgets',
382
+ components: componentsRoot,
383
+ __packageDir: root,
384
+ };
385
+ expect(
386
+ discoverIntegrationComponents(loaded).find(
387
+ record => record.name === 'AcmeSlide',
388
+ ),
389
+ ).toMatchObject({
390
+ sourcePath: path.join(componentsRoot, 'carousel', 'slide.tsx'),
391
+ });
392
+ // Without the package, only the doc's own source is read, and it lacks
393
+ // the member.
394
+ expect(
395
+ discoverIntegrationComponents({...loaded, __packageDir: undefined}).map(
396
+ record => record.name,
397
+ ),
398
+ ).toEqual(['AcmeCarousel']);
399
+ });
400
+
401
+ it('leaves every other entry as before', () => {
402
+ writeFamily('Gallery', {
403
+ name: 'AcmeGallery',
404
+ extra: "\n hiddenComponents: ['AcmeHidden'],",
405
+ members: [
406
+ '[',
407
+ " {name: 'AcmeGallery', description: 'x'},",
408
+ // Not exported by the source.
409
+ " {name: 'AcmeMissing', description: 'x'},",
410
+ // Hidden by the doc.
411
+ " {name: 'AcmeHidden', description: 'x'},",
412
+ // A hook.
413
+ " {name: 'useGallery', description: 'x'},",
414
+ // A name-only entry points at the member's own doc.
415
+ " {name: 'AcmeThumb'},",
416
+ // Another file already has the name.
417
+ " {name: 'AcmeBadge', description: 'x'},",
418
+ // A computed name.
419
+ " {name: `${'Acme'}Tile`, description: 'x'},",
420
+ ']',
421
+ ].join('\n'),
422
+ source: [
423
+ 'export function AcmeGallery() { return null; }',
424
+ 'export function AcmeHidden() { return null; }',
425
+ 'export function useGallery() { return []; }',
426
+ 'export function AcmeThumb() { return null; }',
427
+ 'export function AcmeBadge() { return null; }',
428
+ 'export function AcmeTile() { return null; }',
429
+ '',
430
+ ].join('\n'),
431
+ });
432
+ writeComponent('AcmeBadge', {
433
+ name: "'AcmeBadge'",
434
+ source: 'export function AcmeBadge() { return null; }\n',
435
+ });
436
+
437
+ // Gallery.tsx exports no `Gallery`, so the doc's own name is its export.
438
+ expect(names()).toEqual(['AcmeBadge', 'AcmeGallery']);
439
+ for (const name of [
440
+ 'AcmeMissing',
441
+ 'AcmeHidden',
442
+ 'useGallery',
443
+ 'AcmeThumb',
444
+ 'AcmeTile',
445
+ ]) {
446
+ expect(findIntegrationComponentDoc(integration(), name)).toBeNull();
447
+ }
448
+ });
449
+
450
+ it("does not hand a family's replaces to its members", async () => {
451
+ writeFamily('AcmeCarousel', {
452
+ name: 'AcmeCarousel',
453
+ extra: "\n replaces: 'Carousel',",
454
+ members:
455
+ "[{name: 'AcmeCarousel', description: 'x'}, {name: 'AcmeSlide', description: 'x'}]",
456
+ source:
457
+ 'export function AcmeCarousel() { return null; }\nexport function AcmeSlide() { return null; }\n',
458
+ });
459
+ const {components} =
460
+ await discoverValidIntegrationComponents(integration());
461
+ expect(components.map(record => [record.name, record.replaces])).toEqual([
462
+ ['AcmeCarousel', 'Carousel'],
463
+ ['AcmeSlide', undefined],
464
+ ]);
465
+ });
466
+
467
+ it('drops a member once the file its source re-exports stops exporting it', () => {
468
+ writeFamily('AcmeTabs', {
469
+ name: 'AcmeTabs',
470
+ members: "[{name: 'AcmeTab', description: 'x'}]",
471
+ source:
472
+ "export function AcmeTabs() { return null; }\nexport * from './tab';\n",
473
+ });
474
+ const tab = path.join(root, 'tab.tsx');
475
+ fs.writeFileSync(tab, 'export function AcmeTab() { return null; }\n');
476
+ expect(names()).toEqual(['AcmeTab', 'AcmeTabs']);
477
+ fs.writeFileSync(tab, 'export function AcmeTabLegacy() { return null; }\n');
478
+ expect(names()).toEqual(['AcmeTabs']);
479
+ });
480
+
481
+ it('reads members a doc lists through a shorthand property', () => {
482
+ fs.writeFileSync(
483
+ path.join(root, 'AcmeTabs.tsx'),
484
+ 'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\n',
485
+ );
486
+ fs.writeFileSync(
487
+ path.join(root, 'AcmeTabs.doc.mjs'),
488
+ "const components = [{name: 'AcmeTab', description: 'x'}];\nexport default {type: 'component', name: 'AcmeTabs', usage: {description: 'x'}, components};\n",
489
+ );
490
+ expect(names()).toEqual(['AcmeTab', 'AcmeTabs']);
491
+ });
492
+
493
+ it('reports a family doc the loader rejects once, not once per member', async () => {
494
+ writeFamily('AcmeStrip', {
495
+ name: 'AcmeStrip',
496
+ extra: '\n displayName: 7,',
497
+ members:
498
+ "[{name: 'AcmeStrip', description: 'x'}, {name: 'AcmeStripItem', description: 'x'}]",
499
+ source:
500
+ 'export function AcmeStrip() { return null; }\nexport function AcmeStripItem() { return null; }\n',
501
+ });
502
+ const {components, errors} =
503
+ await discoverValidIntegrationComponents(integration());
504
+ expect(components).toEqual([]);
505
+ expect(errors.map(error => error.name)).toEqual(['AcmeStrip']);
506
+ });
507
+
508
+ /**
509
+ * A package around the components root `src/`, with these files.
510
+ * @param {Record<string, unknown>} manifest package.json fields
511
+ * @param {Record<string, string>} files paths under the package
512
+ */
513
+ function writePackage(manifest, files) {
514
+ fs.writeFileSync(
515
+ path.join(root, 'package.json'),
516
+ JSON.stringify({name: '@acme/widgets', ...manifest}),
517
+ );
518
+ for (const [file, text] of Object.entries(files)) {
519
+ fs.mkdirSync(path.dirname(path.join(root, file)), {recursive: true});
520
+ fs.writeFileSync(path.join(root, file), text);
521
+ }
522
+ return {
523
+ name: '@acme/widgets',
524
+ components: path.join(root, 'src'),
525
+ __packageDir: root,
526
+ };
527
+ }
528
+
529
+ /** @param {object} loaded */
530
+ const namesIn = loaded =>
531
+ discoverIntegrationComponents(/** @type {any} */ (loaded))
532
+ .map(record => record.name)
533
+ .sort();
534
+
535
+ const family = (/** @type {string} */ name, /** @type {string} */ entries) =>
536
+ `export default {type: 'component', name: '${name}', usage: {description: 'x'}, components: [${entries}]};\n`;
537
+
538
+ it('reads members from the main file of a package without exports, and none it cannot resolve', () => {
539
+ const loaded = writePackage(
540
+ {main: './src/index.ts'},
541
+ {
542
+ 'src/list/AcmeList.doc.mjs': family(
543
+ 'AcmeList',
544
+ "{name: 'AcmeListRow', description: 'x'}, {name: 'AcmeListCell', description: 'x'}",
545
+ ).replace('usage:', "import: '@acme/widgets', usage:"),
546
+ 'src/list/AcmeList.tsx':
547
+ 'export function AcmeList() { return null; }\nexport function AcmeListRow() { return null; }\n',
548
+ 'src/list/cell.tsx':
549
+ 'export function AcmeListCell() { return null; }\n',
550
+ 'src/index.ts':
551
+ "export {AcmeList} from './list/AcmeList';\nexport {AcmeListCell} from './list/cell';\n",
552
+ },
553
+ );
554
+ expect(namesIn(loaded)).toEqual(['AcmeList', 'AcmeListCell']);
555
+ // An import the package cannot resolve lists no members, not the
556
+ // same-stem file's.
557
+ fs.rmSync(path.join(root, 'src', 'index.ts'));
558
+ expect(namesIn(loaded)).toEqual(['AcmeList']);
559
+ });
560
+
561
+ it("takes a member's source from the family's folder, never a bundle or the package root", () => {
562
+ const loaded = writePackage(
563
+ {exports: {'.': {import: './dist/index.mjs'}}},
564
+ {
565
+ 'src/tabs/AcmeTabs.doc.mjs': family(
566
+ 'AcmeTabs',
567
+ "{name: 'AcmeTab', description: 'x'}",
568
+ ),
569
+ 'src/tabs/AcmeTabs.tsx':
570
+ 'export function AcmeTabs() { return null; }\nexport function AcmeTab() { return null; }\n',
571
+ 'dist/index.mjs':
572
+ 'function AcmeTabs() { return null; }\nfunction AcmeTab() { return null; }\nexport {AcmeTabs, AcmeTab};\n',
573
+ // A member only the package's root file and the bundle declare is
574
+ // listed, with no source.
575
+ 'src/cards/AcmeCards.doc.mjs': family(
576
+ 'AcmeCards',
577
+ "{name: 'AcmeCard', description: 'x'}",
578
+ ),
579
+ 'src/cards/AcmeCards.tsx':
580
+ 'export function AcmeCards() { return null; }\n',
581
+ 'src/index.tsx':
582
+ "export {AcmeCards} from './cards/AcmeCards';\nexport function AcmeCard() { return null; }\n",
583
+ },
584
+ );
585
+ fs.writeFileSync(
586
+ path.join(root, 'dist', 'index.mjs'),
587
+ `${fs.readFileSync(path.join(root, 'dist', 'index.mjs'), 'utf-8')}export function AcmeCard() { return null; }\n`,
588
+ );
589
+
590
+ const records = discoverIntegrationComponents(loaded);
591
+ expect(records.map(record => record.name).sort()).toEqual([
592
+ 'AcmeCard',
593
+ 'AcmeCards',
594
+ 'AcmeTab',
595
+ 'AcmeTabs',
596
+ ]);
597
+ expect(findIntegrationComponentSource(loaded, 'AcmeTab')).toBe(
598
+ path.join(root, 'src', 'tabs', 'AcmeTabs.tsx'),
599
+ );
600
+ expect(findIntegrationComponentDoc(loaded, 'AcmeCard')).toBe(
601
+ path.join(root, 'src', 'cards', 'AcmeCards.doc.mjs'),
602
+ );
603
+ expect(findIntegrationComponentSource(loaded, 'AcmeCard')).toBeNull();
604
+ expect(
605
+ records.find(record => record.name === 'AcmeCard')?.sourcePath,
606
+ ).toBeNull();
607
+ });
608
+
609
+ it('lists a member only a bundle declares, with no source, when the family doc sits at the package root', () => {
610
+ const loaded = {
611
+ ...writePackage(
612
+ {exports: {'.': './dist/index.mjs'}},
613
+ {
614
+ 'AcmeStrip.doc.mjs': family(
615
+ 'AcmeStrip',
616
+ "{name: 'AcmeStripItem', description: 'x'}",
617
+ ).replace('usage:', "import: '@acme/widgets', usage:"),
618
+ 'AcmeStrip.tsx': 'export function AcmeStrip() { return null; }\n',
619
+ 'dist/index.mjs':
620
+ 'export function AcmeStrip() { return null; }\nexport function AcmeStripItem() { return null; }\n',
621
+ },
622
+ ),
623
+ components: root,
624
+ };
625
+ expect(namesIn(loaded)).toEqual(['AcmeStrip', 'AcmeStripItem']);
626
+ expect(findIntegrationComponentSource(loaded, 'AcmeStripItem')).toBeNull();
627
+ });
628
+
629
+ it('lists the members a family doc without its own source imports', () => {
630
+ const loaded = writePackage(
631
+ {exports: {'./group': './src/group/index.ts'}},
632
+ {
633
+ 'src/group/AcmeGroup.doc.mjs': family(
634
+ 'AcmeGroup',
635
+ "{name: 'AcmeGroupItem', description: 'x'}",
636
+ ).replace('usage:', "import: '@acme/widgets/group', usage:"),
637
+ 'src/group/index.ts':
638
+ "export {AcmeGroupItem} from './AcmeGroupItem';\n",
639
+ 'src/group/AcmeGroupItem.tsx':
640
+ 'export function AcmeGroupItem() { return null; }\n',
641
+ },
642
+ );
643
+ expect(namesIn(loaded)).toEqual(['AcmeGroup', 'AcmeGroupItem']);
644
+ expect(findIntegrationComponentSource(loaded, 'AcmeGroupItem')).toBe(
645
+ path.join(root, 'src', 'group', 'AcmeGroupItem.tsx'),
646
+ );
647
+ });
648
+
649
+ it("finds a member's source in the family's folder when the import names built output", () => {
650
+ const loaded = writePackage(
651
+ {exports: {'./list': './dist/list/index.js'}},
652
+ {
653
+ 'src/list/AcmeList.doc.mjs': family(
654
+ 'AcmeList',
655
+ "{name: 'AcmeListRow', description: 'x'}",
656
+ ).replace('usage:', "import: '@acme/widgets/list', usage:"),
657
+ 'src/list/AcmeList.tsx':
658
+ 'export function AcmeList() { return null; }\n',
659
+ 'src/list/row.tsx': 'export function AcmeListRow() { return null; }\n',
660
+ 'src/list/row.test.tsx':
661
+ 'export function AcmeListRow() { return null; }\n',
662
+ 'dist/list/index.js':
663
+ "export {AcmeList} from './list.js';\nexport * from './row.js';\n",
664
+ 'dist/list/list.js': 'export function AcmeList() { return null; }\n',
665
+ 'dist/list/row.js': 'export function AcmeListRow() { return null; }\n',
666
+ },
667
+ );
668
+ expect(namesIn(loaded)).toEqual(['AcmeList', 'AcmeListRow']);
669
+ expect(findIntegrationComponentSource(loaded, 'AcmeListRow')).toBe(
670
+ path.join(root, 'src', 'list', 'row.tsx'),
671
+ );
672
+ // Until the output exists, the import exports nothing.
673
+ fs.rmSync(path.join(root, 'dist'), {recursive: true});
674
+ expect(namesIn(loaded)).toEqual(['AcmeList']);
675
+ });
676
+
677
+ it('lists a member another folder declares, with no source', async () => {
678
+ const loaded = writePackage(
679
+ {exports: {'./menu': './src/menu/index.ts'}},
680
+ {
681
+ 'src/menu/AcmeMenu.doc.mjs': family(
682
+ 'AcmeMenu',
683
+ "{name: 'AcmeMenuItem', description: 'x'}",
684
+ ).replace('usage:', "import: '@acme/widgets/menu', usage:"),
685
+ 'src/menu/AcmeMenu.tsx':
686
+ 'export function AcmeMenu() { return null; }\n',
687
+ 'src/menu/index.ts':
688
+ "export {AcmeMenu} from './AcmeMenu';\nexport {AcmeMenuItem} from '../shared/AcmeMenuItem';\n",
689
+ 'src/shared/AcmeMenuItem.tsx':
690
+ 'export function AcmeMenuItem() { return null; }\n',
691
+ },
692
+ );
693
+ expect(namesIn(loaded)).toEqual(['AcmeMenu', 'AcmeMenuItem']);
694
+ expect(findIntegrationComponentDoc(loaded, 'AcmeMenuItem')).toBe(
695
+ path.join(root, 'src', 'menu', 'AcmeMenu.doc.mjs'),
696
+ );
697
+ expect(findIntegrationComponentSource(loaded, 'AcmeMenuItem')).toBeNull();
698
+ // A member without a source is valid: only a doc's own component needs
699
+ // its same-stem file.
700
+ const {components, errors} = await discoverValidIntegrationComponents(
701
+ /** @type {any} */ (loaded),
702
+ );
703
+ expect(errors).toEqual([]);
704
+ expect(components.map(component => component.name).sort()).toEqual([
705
+ 'AcmeMenu',
706
+ 'AcmeMenuItem',
707
+ ]);
708
+ });
709
+
710
+ it('reads members a binding holds and entries with computed fields', () => {
711
+ fs.writeFileSync(
712
+ path.join(root, 'AcmeRows.doc.mjs'),
713
+ "const describe = text => text;\nconst MEMBERS = [{name: 'AcmeRow', description: describe('A row.')}];\nexport default {type: 'component', name: 'AcmeRows', usage: {description: 'x'}, components: MEMBERS};\n",
714
+ );
715
+ fs.writeFileSync(
716
+ path.join(root, 'AcmeRows.tsx'),
717
+ 'export function AcmeRows() { return null; }\nexport function AcmeRow() { return null; }\n',
718
+ );
719
+ expect(names()).toEqual(['AcmeRow', 'AcmeRows']);
720
+ });
721
+
722
+ it("lists the doc's own name when its file keeps its own, and declares replaces once", async () => {
723
+ writeFamily('Chip', {
724
+ name: 'AcmeChip',
725
+ extra: "\n replaces: 'Badge',",
726
+ members:
727
+ "[{name: 'AcmeChip', description: 'x'}, {name: 'AcmeChipIcon', description: 'x'}]",
728
+ source:
729
+ 'export function Chip() { return null; }\nexport function AcmeChip() { return null; }\nexport function AcmeChipIcon() { return null; }\n',
730
+ });
731
+ const {components} =
732
+ await discoverValidIntegrationComponents(integration());
733
+ expect(components.map(record => [record.name, record.replaces])).toEqual([
734
+ ['Chip', 'Badge'],
735
+ ['AcmeChip', undefined],
736
+ ['AcmeChipIcon', undefined],
737
+ ]);
738
+ });
739
+
740
+ it('takes no member name a hidden doc or a replacement in the package claims', () => {
741
+ writeFamily('menu/AcmeMenu', {
742
+ name: 'AcmeMenu',
743
+ members:
744
+ "[{name: 'AcmeMenuDivider', description: 'x'}, {name: 'Carousel', description: 'x'}, {name: 'AcmeMenuItem', description: 'x'}]",
745
+ source:
746
+ 'export function AcmeMenu() { return null; }\nexport function AcmeMenuDivider() { return null; }\nexport function Carousel() { return null; }\nexport function AcmeMenuItem() { return null; }\n',
747
+ });
748
+ writeComponent('menu/AcmeMenuDivider', {
749
+ name: "'AcmeMenuDivider'",
750
+ extra: '\n hidden: true,',
751
+ });
752
+ writeComponent('Slides', {
753
+ name: "'AcmeCarousel'",
754
+ extra: "\n replaces: 'Carousel',",
755
+ source: 'export function AcmeCarousel() { return null; }\n',
756
+ });
757
+ expect(names()).toEqual(['AcmeCarousel', 'AcmeMenu', 'AcmeMenuItem']);
758
+ expect(findIntegrationComponentDoc(integration(), 'Carousel')).toBeNull();
759
+ });
760
+ });