@astryxdesign/cli 0.6.3-canary.ea2f048 → 0.6.3-canary.ebaebc4

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 (188) hide show
  1. package/README.md +2 -1
  2. package/api/build/build.type.d.mts +2 -2
  3. package/api/build/build.type.mjs +2 -2
  4. package/api/component/component.type.d.mts +6 -6
  5. package/api/component/component.type.mjs +19 -19
  6. package/api/discover/discover.type.d.mts +4 -4
  7. package/api/discover/discover.type.mjs +10 -10
  8. package/api/docs/_adapter.d.mts +37 -24
  9. package/api/docs/_adapter.mjs +169 -83
  10. package/api/docs/compiled-topics.test.mjs +78 -0
  11. package/api/docs/detail/detail.mjs +14 -63
  12. package/api/docs/detail/section/section.d.mts +1 -1
  13. package/api/docs/detail/section/section.mjs +44 -20
  14. package/api/docs/detail/section/section.test.mjs +41 -0
  15. package/api/docs/docs.d.mts +7 -2
  16. package/api/docs/docs.doc.mjs +27 -10
  17. package/api/docs/docs.mjs +16 -9
  18. package/api/docs/docs.test.mjs +6 -0
  19. package/api/docs/docs.type.d.mts +40 -3
  20. package/api/docs/docs.type.mjs +36 -8
  21. package/api/docs/index/index.d.mts +18 -0
  22. package/api/docs/index/index.mjs +32 -0
  23. package/api/docs/index/index.test.mjs +62 -0
  24. package/api/docs/integrationDocs.test.mjs +106 -0
  25. package/api/doctor/doctor.d.mts +48 -0
  26. package/api/doctor/doctor.mjs +232 -0
  27. package/api/doctor/doctor.test.mjs +196 -0
  28. package/api/hook/hook.type.d.mts +3 -3
  29. package/api/hook/hook.type.mjs +11 -11
  30. package/api/hook/list/list.d.mts +1 -1
  31. package/api/integration/add-contribution.mjs +5 -3
  32. package/api/integration/add-contribution.test.mjs +4 -4
  33. package/api/integration/integration-authoring.type.d.mts +1 -1
  34. package/api/integration/pack-check.mjs +49 -7
  35. package/api/integration/pack-check.test.mjs +249 -0
  36. package/api/search/search.d.mts +1 -1
  37. package/api/search/search.mjs +5 -5
  38. package/api/search/search.type.d.mts +2 -2
  39. package/api/search/search.type.mjs +1 -1
  40. package/api/swizzle/swizzle.type.d.mts +2 -2
  41. package/api/swizzle/swizzle.type.mjs +2 -2
  42. package/api/template/template.d.mts +1 -1
  43. package/api/template/template.type.d.mts +6 -6
  44. package/api/template/template.type.mjs +12 -12
  45. package/api/theme/build/build.mjs +20 -6
  46. package/api/theme/build/build.test.mjs +127 -0
  47. package/api/theme/palette/generate/generate.mjs +1 -1
  48. package/api/theme/palette/generate/generator.d.mts +10 -13
  49. package/api/theme/palette/generate/generator.mjs +7 -3
  50. package/api/theme/theme.type.d.mts +170 -11
  51. package/api/theme/theme.type.mjs +94 -27
  52. package/api/upgrade/_adapter.mjs +71 -5
  53. package/api/upgrade/project-context.test.mjs +272 -0
  54. package/api/upgrade/upgrade.doc.mjs +4 -3
  55. package/api/upgrade/upgrade.type.d.mts +5 -5
  56. package/api/upgrade/upgrade.type.mjs +11 -11
  57. package/assets/codemods/integration-discovery.mjs +40 -2
  58. package/assets/codemods/integration-discovery.test.mjs +58 -0
  59. package/assets/codemods/transforms/v0.3.0/__tests__/unwrap-authoring-factories.test.mjs +27 -5
  60. package/assets/codemods/transforms/v0.3.0/unwrap-authoring-factories.mjs +20 -5
  61. package/assets/docs/README.md +9 -0
  62. package/assets/docs/authoring.doc.mjs +14 -0
  63. package/assets/docs/cli-integrations.doc.mjs +86 -15
  64. package/assets/docs/styling-libraries.doc.mjs +1 -1
  65. package/assets/docs/working-with-ai.doc.mjs +1 -1
  66. package/authoring/_shared/contract.ts +22 -0
  67. package/authoring/codemod/codemod.doc.mjs +6 -1
  68. package/authoring/codemod/parse.d.mts +8 -8
  69. package/authoring/codemod/parse.mjs +8 -6
  70. package/authoring/config/parse.d.mts +13 -13
  71. package/authoring/config/parse.mjs +8 -8
  72. package/authoring/config/type.ts +3 -3
  73. package/authoring/debug/parse.d.mts +5 -5
  74. package/authoring/debug/parse.mjs +3 -3
  75. package/authoring/doctypes/_schema.d.mts +788 -23
  76. package/authoring/doctypes/_schema.mjs +492 -39
  77. package/authoring/doctypes/base/graph-fields.doc.d.mts +9 -0
  78. package/authoring/doctypes/base/graph-fields.doc.mjs +62 -0
  79. package/authoring/doctypes/base/type.ts +40 -0
  80. package/authoring/doctypes/command/command.doc.mjs +3 -2
  81. package/authoring/doctypes/command/parse.d.mts +2 -2
  82. package/authoring/doctypes/command/parse.mjs +1 -1
  83. package/authoring/doctypes/command/type.ts +3 -2
  84. package/authoring/doctypes/component/component.doc.mjs +6 -3
  85. package/authoring/doctypes/component/parse.d.mts +2 -2
  86. package/authoring/doctypes/component/parse.mjs +1 -1
  87. package/authoring/doctypes/component/type.ts +4 -3
  88. package/authoring/doctypes/enum/parse.d.mts +2 -2
  89. package/authoring/doctypes/enum/parse.mjs +1 -1
  90. package/authoring/doctypes/enum/type.ts +3 -1
  91. package/authoring/doctypes/function/function.doc.mjs +4 -0
  92. package/authoring/doctypes/function/parse.d.mts +2 -2
  93. package/authoring/doctypes/function/parse.mjs +1 -1
  94. package/authoring/doctypes/function/type.ts +6 -2
  95. package/authoring/doctypes/hook/hook.doc.mjs +4 -0
  96. package/authoring/doctypes/hook/parse.d.mts +2 -2
  97. package/authoring/doctypes/hook/parse.mjs +1 -1
  98. package/authoring/doctypes/hook/type.ts +3 -2
  99. package/authoring/doctypes/legacy.d.mts +8 -6
  100. package/authoring/doctypes/legacy.mjs +5 -4
  101. package/authoring/doctypes/load-contract.test.mjs +207 -0
  102. package/authoring/doctypes/namespace/namespace.doc.d.mts +9 -0
  103. package/authoring/doctypes/namespace/namespace.doc.mjs +132 -0
  104. package/authoring/doctypes/namespace/parse.d.mts +12 -0
  105. package/authoring/doctypes/namespace/parse.mjs +25 -0
  106. package/authoring/doctypes/namespace/parse.test.mjs +165 -0
  107. package/authoring/doctypes/namespace/type.ts +71 -0
  108. package/authoring/doctypes/parse.d.mts +20 -18
  109. package/authoring/doctypes/parse.mjs +16 -10
  110. package/authoring/doctypes/parse.test.mjs +77 -3
  111. package/authoring/doctypes/reference/parse.d.mts +2 -2
  112. package/authoring/doctypes/reference/parse.mjs +8 -5
  113. package/authoring/doctypes/reference/reference.doc.mjs +17 -4
  114. package/authoring/doctypes/reference/type.ts +51 -5
  115. package/authoring/doctypes/schema/parse.d.mts +2 -2
  116. package/authoring/doctypes/schema/parse.mjs +1 -1
  117. package/authoring/doctypes/schema/type.ts +3 -2
  118. package/authoring/doctypes/template/parse.d.mts +92 -1
  119. package/authoring/doctypes/template/parse.mjs +36 -2
  120. package/authoring/doctypes/template/parse.test.mjs +8 -2
  121. package/authoring/doctypes/template/template.doc.mjs +4 -0
  122. package/authoring/doctypes/template/type.ts +5 -2
  123. package/authoring/doctypes/types.ts +10 -9
  124. package/authoring/gap-report/parse.d.mts +10 -10
  125. package/authoring/gap-report/parse.mjs +6 -6
  126. package/authoring/gap-report/type.ts +1 -1
  127. package/authoring/identity/identity.doc.d.mts +9 -0
  128. package/authoring/identity/identity.doc.mjs +61 -0
  129. package/authoring/identity/type.ts +132 -0
  130. package/authoring/index.d.mts +1 -0
  131. package/authoring/index.d.ts +49 -17
  132. package/authoring/index.mjs +1 -0
  133. package/authoring/integration/integration.doc.mjs +13 -6
  134. package/authoring/integration/parse.d.mts +2 -2
  135. package/authoring/integration/parse.mjs +1 -1
  136. package/authoring/integration/parse.test.mjs +10 -1
  137. package/authoring/integration/schema.d.mts +6 -4
  138. package/authoring/integration/schema.mjs +9 -3
  139. package/authoring/integration/type.ts +23 -6
  140. package/authoring/shadcn/receipt.d.mts +6 -6
  141. package/clients/cli/commands/docs.doc.mjs +13 -3
  142. package/clients/cli/commands/docs.mjs +121 -21
  143. package/clients/cli/commands/docs.test.mjs +88 -0
  144. package/clients/cli/commands/integration-authoring.test.mjs +13 -9
  145. package/clients/cli/commands/theme-palette-generate.doc.mjs +8 -4
  146. package/clients/cli/commands/upgrade.doc.mjs +2 -2
  147. package/clients/cli/formatters/index.mjs +162 -1
  148. package/clients/cli/formatters/index.test.mjs +91 -0
  149. package/clients/cli/lib/manifest.mjs +7 -2
  150. package/foundation/config/project.mjs +21 -6
  151. package/foundation/discovery/authoring-self-docs.d.mts +69 -0
  152. package/foundation/discovery/authoring-self-docs.mjs +214 -0
  153. package/foundation/discovery/authoring-self-docs.test.mjs +154 -0
  154. package/foundation/discovery/component-discovery.d.mts +1 -1
  155. package/foundation/discovery/component-discovery.mjs +2 -1
  156. package/foundation/discovery/docs-discovery.d.mts +11 -4
  157. package/foundation/discovery/docs-discovery.mjs +208 -88
  158. package/foundation/discovery/docs-discovery.test.mjs +279 -13
  159. package/foundation/discovery/docs-output-budget.d.mts +28 -0
  160. package/foundation/discovery/docs-output-budget.mjs +50 -0
  161. package/foundation/discovery/docs-section-key.d.mts +98 -0
  162. package/foundation/discovery/docs-section-key.mjs +221 -0
  163. package/foundation/discovery/docs-section-key.test.mjs +224 -0
  164. package/foundation/discovery/template-adapter.mjs +2 -1
  165. package/foundation/discovery/theming-targets.test.mjs +4 -0
  166. package/foundation/doc-compiler/compile.d.mts +162 -0
  167. package/foundation/doc-compiler/compile.mjs +262 -0
  168. package/foundation/doc-compiler/doc-compiler.test.mjs +687 -0
  169. package/foundation/doc-compiler/ir.d.mts +9 -0
  170. package/foundation/doc-compiler/ir.mjs +287 -0
  171. package/foundation/doc-compiler/lenses.d.mts +33 -0
  172. package/foundation/doc-compiler/lenses.mjs +127 -0
  173. package/foundation/identity/provider-identity.d.mts +90 -0
  174. package/foundation/identity/provider-identity.mjs +320 -0
  175. package/foundation/identity/provider-identity.test.mjs +254 -0
  176. package/foundation/identity/providers.d.mts +7 -0
  177. package/foundation/identity/providers.mjs +16 -0
  178. package/foundation/integrations/autolink.mjs +12 -5
  179. package/foundation/integrations/integration-warnings.mjs +6 -0
  180. package/foundation/integrations/integrations.d.mts +46 -2
  181. package/foundation/integrations/integrations.mjs +167 -8
  182. package/foundation/integrations/integrations.test.mjs +384 -1
  183. package/foundation/integrations/provider-conflicts.test.mjs +125 -0
  184. package/foundation/integrations/validate-contributions.d.mts +2 -0
  185. package/foundation/integrations/validate-contributions.mjs +10 -0
  186. package/foundation/response/json-contract.test.mjs +46 -17
  187. package/foundation/response/response-types.doc.mjs +6 -1
  188. package/package.json +9 -11
@@ -388,6 +388,14 @@ async function validatePackedComponentExports(integration, scratchBase) {
388
388
  components.map(component => component.specifier),
389
389
  scratchBase,
390
390
  );
391
+ issues.push(
392
+ ...validateSpecifierExtensions(
393
+ components.map(({name, specifier}) => ({
394
+ label: `Component "${name}"`,
395
+ specifier,
396
+ })),
397
+ ),
398
+ );
391
399
  for (const {name, specifier} of components) {
392
400
  const result = resolved.get(specifier);
393
401
  if (!result?.url || result.error) {
@@ -427,19 +435,27 @@ async function validatePackedComponentExports(integration, scratchBase) {
427
435
  */
428
436
  async function validatePackedTemplateExports(integration, scratchBase) {
429
437
  const {templates} = await discoverIntegrationTemplatesForOne(integration);
430
- const entries = templates.map(template => ({
431
- id: template.dirName,
432
- specifier: `${integration.name}/${path
438
+ const entries = templates.map(template => {
439
+ const relPath = path
433
440
  .relative(integration.__packageDir, template.filePath)
434
441
  .split(path.sep)
435
- .join('/')}`,
436
- }));
442
+ .join('/');
443
+ const extensionless = relPath.replace(/\.tsx?$/u, '');
444
+ return {
445
+ id: template.dirName,
446
+ specifier: `${integration.name}/${extensionless}`,
447
+ };
448
+ });
437
449
  const resolved = resolveConsumerSpecifiers(
438
450
  entries.map(entry => entry.specifier),
439
451
  scratchBase,
440
452
  );
441
- /** @type {Issue[]} */
442
- const issues = [];
453
+ const issues = validateSpecifierExtensions(
454
+ entries.map(({id, specifier}) => ({
455
+ label: `Template "${id}"`,
456
+ specifier,
457
+ })),
458
+ );
443
459
  for (const {id, specifier} of entries) {
444
460
  const result = resolved.get(specifier);
445
461
  if (!result?.url || result.error) {
@@ -469,6 +485,32 @@ async function validatePackedTemplateExports(integration, scratchBase) {
469
485
  return issues;
470
486
  }
471
487
 
488
+ const TS_EXTENSION_RE = /\.tsx?$/u;
489
+
490
+ /**
491
+ * Reject specifiers that end in a TypeScript extension — they fail under
492
+ * default `moduleResolution` with TS5097/TS2307 unless the consumer enables
493
+ * `allowImportingTsExtensions`.
494
+ *
495
+ * @param {Array<{label: string, specifier: string}>} entries
496
+ * @returns {Issue[]}
497
+ */
498
+ function validateSpecifierExtensions(entries) {
499
+ /** @type {Issue[]} */
500
+ const issues = [];
501
+ for (const {label, specifier} of entries) {
502
+ if (TS_EXTENSION_RE.test(specifier)) {
503
+ issues.push(
504
+ error(
505
+ 'typescript_extension_in_specifier',
506
+ `${label} advertises import "${specifier}", which ends in a TypeScript extension. A consumer with default moduleResolution will reject it (TS5097). Use an extensionless specifier mapped through the package exports.`,
507
+ ),
508
+ );
509
+ }
510
+ }
511
+ return issues;
512
+ }
513
+
472
514
  /**
473
515
  * Verify an integration package is ready to publish.
474
516
  *
@@ -567,3 +567,252 @@ describe('pack-check mutation tests', () => {
567
567
  }
568
568
  });
569
569
  });
570
+
571
+ describe('extensionless subpath resolution', () => {
572
+ it('rejects .tsx public specifiers before publishing', async () => {
573
+ // Manually construct a package with OLD-style .tsx exports keys
574
+ writePackage({
575
+ manifest: "export default {components: './components'};\n",
576
+ files: ['astryx.integration.mjs', 'components', 'index.mjs'],
577
+ themes: false,
578
+ });
579
+ const pkgFile = path.join(tmpDir, 'package.json');
580
+ const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
581
+ pkg.exports = {
582
+ '.': './index.mjs',
583
+ './components/AcmeWidget.tsx': './components/AcmeWidget.tsx',
584
+ };
585
+ fs.writeFileSync(pkgFile, `${JSON.stringify(pkg, null, 2)}\n`);
586
+ fs.writeFileSync(
587
+ path.join(tmpDir, 'index.mjs'),
588
+ 'export const existing = 1;\n',
589
+ );
590
+ fs.mkdirSync(path.join(tmpDir, 'components'));
591
+ fs.writeFileSync(
592
+ path.join(tmpDir, 'components', 'AcmeWidget.doc.mjs'),
593
+ `export default {type: 'component', name: 'AcmeWidget', import: '@acme/widgets/components/AcmeWidget.tsx', description: 'Widget.', props: []};\n`,
594
+ );
595
+ fs.writeFileSync(
596
+ path.join(tmpDir, 'components', 'AcmeWidget.tsx'),
597
+ 'export function AcmeWidget() { return null; }\n',
598
+ );
599
+
600
+ const result = await integrationPackCheck({cwd: tmpDir});
601
+
602
+ // The .tsx extension in the specifier must be caught
603
+ expect(result.data.packable).toBe(false);
604
+ expect(result.data.issues).toContainEqual(
605
+ expect.objectContaining({
606
+ code: 'typescript_extension_in_specifier',
607
+ message: expect.stringContaining('.tsx'),
608
+ }),
609
+ );
610
+ });
611
+
612
+ it('generated extensionless component specifier resolves through exports map', async () => {
613
+ fs.writeFileSync(
614
+ path.join(tmpDir, 'package.json'),
615
+ `${JSON.stringify({
616
+ name: '@acme/widgets',
617
+ version: '1.0.0',
618
+ files: ['index.mjs'],
619
+ exports: {'.': './index.mjs'},
620
+ })}\n`,
621
+ );
622
+ fs.writeFileSync(
623
+ path.join(tmpDir, 'index.mjs'),
624
+ 'export const existing = 1;\n',
625
+ );
626
+ await integrationAddComponent('AcmeWidget', {cwd: tmpDir});
627
+
628
+ // Verify the exports map has extensionless key → .tsx target
629
+ const pkg = JSON.parse(
630
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
631
+ );
632
+ expect(pkg.exports['./components/AcmeWidget']).toBe(
633
+ './components/AcmeWidget.tsx',
634
+ );
635
+ expect(pkg.exports['./components/AcmeWidget.tsx']).toBeUndefined();
636
+
637
+ const result = await integrationPackCheck({cwd: tmpDir});
638
+
639
+ expect(result.data.packable).toBe(true);
640
+ expect(result.data.issues).not.toContainEqual(
641
+ expect.objectContaining({code: 'typescript_extension_in_specifier'}),
642
+ );
643
+ expect(result.data.issues).not.toContainEqual(
644
+ expect.objectContaining({code: 'component_import_unresolvable'}),
645
+ );
646
+ });
647
+
648
+ it('validates generated imports without project-local TypeScript', async () => {
649
+ fs.rmSync(tmpDir, {recursive: true, force: true});
650
+ tmpDir = fs.mkdtempSync(
651
+ path.join(os.tmpdir(), 'astryx-pack-check-no-typescript-'),
652
+ );
653
+ fs.writeFileSync(
654
+ path.join(tmpDir, 'package.json'),
655
+ `${JSON.stringify({
656
+ name: '@acme/widgets',
657
+ version: '1.0.0',
658
+ files: ['index.mjs'],
659
+ exports: {'.': './index.mjs'},
660
+ })}\n`,
661
+ );
662
+ fs.writeFileSync(
663
+ path.join(tmpDir, 'index.mjs'),
664
+ 'export const existing = 1;\n',
665
+ );
666
+ await integrationAddComponent('AcmeWidget', {cwd: tmpDir});
667
+
668
+ const result = await integrationPackCheck({cwd: tmpDir});
669
+
670
+ expect(result.data.packable).toBe(true);
671
+ expect(result.data.issues).not.toContainEqual(
672
+ expect.objectContaining({code: 'component_import_unresolvable'}),
673
+ );
674
+ });
675
+
676
+ it('generated extensionless template specifiers resolve for page and block types', async () => {
677
+ fs.writeFileSync(
678
+ path.join(tmpDir, 'package.json'),
679
+ `${JSON.stringify({
680
+ name: '@acme/widgets',
681
+ version: '1.0.0',
682
+ files: ['index.mjs'],
683
+ exports: {'.': './index.mjs'},
684
+ })}\n`,
685
+ );
686
+ fs.writeFileSync(
687
+ path.join(tmpDir, 'index.mjs'),
688
+ 'export const existing = 1;\n',
689
+ );
690
+ await integrationAddTemplate('account-page', {cwd: tmpDir, type: 'page'});
691
+ await integrationAddTemplate('info-card', {cwd: tmpDir, type: 'block'});
692
+
693
+ const pkg = JSON.parse(
694
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
695
+ );
696
+ expect(pkg.exports['./templates/account-page']).toBe(
697
+ './templates/account-page.tsx',
698
+ );
699
+ expect(pkg.exports['./templates/info-card']).toBe(
700
+ './templates/info-card.tsx',
701
+ );
702
+
703
+ const result = await integrationPackCheck({cwd: tmpDir});
704
+
705
+ expect(result.data.packable).toBe(true);
706
+ expect(result.data.issues).not.toContainEqual(
707
+ expect.objectContaining({code: 'template_import_unresolvable'}),
708
+ );
709
+ expect(result.data.issues).not.toContainEqual(
710
+ expect.objectContaining({code: 'typescript_extension_in_specifier'}),
711
+ );
712
+ });
713
+
714
+ it('preserves condition-style exports sugar without creating a map', async () => {
715
+ // Package with condition-only exports (sugar, no subpaths)
716
+ fs.writeFileSync(
717
+ path.join(tmpDir, 'package.json'),
718
+ `${JSON.stringify({
719
+ name: '@acme/widgets',
720
+ version: '1.0.0',
721
+ files: ['index.mjs'],
722
+ exports: {import: './index.mjs', require: './index.cjs'},
723
+ })}\n`,
724
+ );
725
+ fs.writeFileSync(
726
+ path.join(tmpDir, 'index.mjs'),
727
+ 'export const existing = 1;\n',
728
+ );
729
+ fs.writeFileSync(path.join(tmpDir, 'index.cjs'), 'exports.existing = 1;\n');
730
+ fs.writeFileSync(
731
+ path.join(tmpDir, 'astryx.integration.mjs'),
732
+ 'export default {};\n',
733
+ );
734
+
735
+ await integrationAddComponent('AcmeWidget', {cwd: tmpDir});
736
+
737
+ const pkg = JSON.parse(
738
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
739
+ );
740
+ // Condition sugar wrapped into subpath map with the component added
741
+ expect(pkg.exports['.']).toEqual({
742
+ import: './index.mjs',
743
+ require: './index.cjs',
744
+ });
745
+ expect(pkg.exports['./components/AcmeWidget']).toBe(
746
+ './components/AcmeWidget.tsx',
747
+ );
748
+ });
749
+
750
+ it('no-map: pack-check fails when generated component has no exports map', async () => {
751
+ fs.writeFileSync(
752
+ path.join(tmpDir, 'package.json'),
753
+ `${JSON.stringify({
754
+ name: '@acme/widgets',
755
+ version: '1.0.0',
756
+ files: ['dist'],
757
+ })}\n`,
758
+ );
759
+ fs.writeFileSync(
760
+ path.join(tmpDir, 'astryx.integration.mjs'),
761
+ 'export default {};\n',
762
+ );
763
+
764
+ await integrationAddComponent('AcmeWidget', {cwd: tmpDir});
765
+ await integrationAddTemplate('hero-page', {cwd: tmpDir, type: 'page'});
766
+
767
+ const pkg = JSON.parse(
768
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
769
+ );
770
+ // No exports map created
771
+ expect(pkg.exports).toBeUndefined();
772
+
773
+ const result = await integrationPackCheck({cwd: tmpDir});
774
+
775
+ // The extensionless import cannot resolve without an exports map —
776
+ // pack-check must fail closed, not false-green.
777
+ expect(result.data.packable).toBe(false);
778
+ expect(result.data.issues).toContainEqual(
779
+ expect.objectContaining({
780
+ severity: 'error',
781
+ message: expect.stringContaining('AcmeWidget'),
782
+ }),
783
+ );
784
+ });
785
+
786
+ it('idempotent when extensionless export already exists', async () => {
787
+ fs.writeFileSync(
788
+ path.join(tmpDir, 'package.json'),
789
+ `${JSON.stringify({
790
+ name: '@acme/widgets',
791
+ version: '1.0.0',
792
+ files: ['index.mjs'],
793
+ exports: {
794
+ '.': './index.mjs',
795
+ './components/AcmeWidget': './components/AcmeWidget.tsx',
796
+ },
797
+ })}\n`,
798
+ );
799
+ fs.writeFileSync(
800
+ path.join(tmpDir, 'index.mjs'),
801
+ 'export const existing = 1;\n',
802
+ );
803
+ fs.writeFileSync(
804
+ path.join(tmpDir, 'astryx.integration.mjs'),
805
+ 'export default {};\n',
806
+ );
807
+
808
+ await integrationAddComponent('AcmeWidget', {cwd: tmpDir});
809
+
810
+ const pkg = JSON.parse(
811
+ fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf-8'),
812
+ );
813
+ // Export preserved, no duplicate
814
+ expect(
815
+ Object.keys(pkg.exports).filter(k => k.includes('AcmeWidget')),
816
+ ).toEqual(['./components/AcmeWidget']);
817
+ });
818
+ });
@@ -99,5 +99,5 @@ export type Candidate = {
99
99
  _import?: string | undefined;
100
100
  _title?: string | undefined;
101
101
  _displayName?: string | undefined;
102
- _kind?: "block" | "page" | undefined;
102
+ _kind?: "page" | "block" | undefined;
103
103
  };
@@ -66,7 +66,7 @@ import {
66
66
  import {loadIntegrationsSafely} from '../component/_adapter.mjs';
67
67
  import {levenshteinDistance} from '../../foundation/text/string-utils.mjs';
68
68
  import {discoverTemplates, extractComponents} from '../template/template.mjs';
69
- import {loadDocsCatalog, loadTopicDoc} from '../docs/_adapter.mjs';
69
+ import {loadDocsCatalog, lowerTopic} from '../docs/_adapter.mjs';
70
70
  import {AstryxError} from '../error.mjs';
71
71
  import {ERROR_CODES} from '../../foundation/response/error-codes.mjs';
72
72
  import {setResultCoverage} from './coverage.mjs';
@@ -732,16 +732,16 @@ async function gatherHooks(coreDir) {
732
732
  async function gatherDocs(cwd) {
733
733
  /** @type {Candidate[]} */
734
734
  const candidates = [];
735
- let entries;
735
+ let catalog;
736
736
  try {
737
- entries = (await loadDocsCatalog(cwd)).entries();
737
+ catalog = await loadDocsCatalog(cwd);
738
738
  } catch {
739
739
  return candidates;
740
740
  }
741
- for (const entry of entries) {
741
+ for (const entry of catalog.entries()) {
742
742
  let doc = null;
743
743
  try {
744
- doc = await loadTopicDoc(entry);
744
+ doc = (await lowerTopic(catalog, entry)).doc;
745
745
  } catch {
746
746
  // A topic that cannot be loaded is reported by the commands that own
747
747
  // integration issues; search just cannot index it.
@@ -48,10 +48,10 @@ export type SearchResultEntry = {
48
48
  /**
49
49
  * - Template kind (`page` | `block`) — present for template results.
50
50
  */
51
- kind?: "block" | "page" | undefined;
51
+ kind?: "page" | "block" | undefined;
52
52
  };
53
53
  /**
54
- * xds --json search <query>
54
+ * astryx --json search <query>
55
55
  */
56
56
  export type SearchResponse = {
57
57
  type: "search";
@@ -28,7 +28,7 @@
28
28
  */
29
29
 
30
30
  /**
31
- * xds --json search <query>
31
+ * astryx --json search <query>
32
32
  * @typedef {object} SearchResponse
33
33
  * @property {'search'} type
34
34
  * @property {object} data
@@ -2,7 +2,7 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  /**
5
- * xds --json swizzle [--list]
5
+ * astryx --json swizzle [--list]
6
6
  */
7
7
  export type SwizzleListResponse = {
8
8
  type: "swizzle.list";
@@ -22,7 +22,7 @@ export type SwizzleFeedback = {
22
22
  ghCommand?: string | undefined;
23
23
  };
24
24
  /**
25
- * xds --json swizzle <component>
25
+ * astryx --json swizzle <component>
26
26
  */
27
27
  export type SwizzleCopyResponse = {
28
28
  type: "swizzle.copy";
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  /**
10
- * xds --json swizzle [--list]
10
+ * astryx --json swizzle [--list]
11
11
  *
12
12
  * @typedef {object} SwizzleListResponse
13
13
  * @property {'swizzle.list'} type
@@ -23,7 +23,7 @@
23
23
  */
24
24
 
25
25
  /**
26
- * xds --json swizzle <component>
26
+ * astryx --json swizzle <component>
27
27
  *
28
28
  * @typedef {object} SwizzleCopyResponse
29
29
  * @property {'swizzle.copy'} type
@@ -31,7 +31,7 @@ export function template(name?: string, options?: {
31
31
  skeleton?: boolean | undefined;
32
32
  cdn?: string | boolean | undefined;
33
33
  show?: boolean | undefined;
34
- type?: "block" | "page" | undefined;
34
+ type?: "page" | "block" | undefined;
35
35
  package?: string | undefined;
36
36
  cwd?: string | undefined;
37
37
  }): Promise<{
@@ -2,7 +2,7 @@
2
2
  // DO NOT EDIT — run `pnpm sync:api-types` to regenerate.
3
3
 
4
4
  /**
5
- * xds --json template [--list]
5
+ * astryx --json template [--list]
6
6
  */
7
7
  export type TemplateListResponse = {
8
8
  type: "template.list";
@@ -53,7 +53,7 @@ export type TemplateListEntry = {
53
53
  scaffold?: boolean | undefined;
54
54
  };
55
55
  /**
56
- * xds --json template <name>
56
+ * astryx --json template <name>
57
57
  */
58
58
  export type TemplateShowResponse = {
59
59
  type: "template.show";
@@ -66,7 +66,7 @@ export type TemplateShowResponse = {
66
66
  };
67
67
  };
68
68
  /**
69
- * xds --json template <name> --skeleton
69
+ * astryx --json template <name> --skeleton
70
70
  */
71
71
  export type TemplateSkeletonResponse = {
72
72
  type: "template.skeleton";
@@ -78,7 +78,7 @@ export type TemplateSkeletonResponse = {
78
78
  };
79
79
  };
80
80
  /**
81
- * xds --json template <name> [path]
81
+ * astryx --json template <name> [path]
82
82
  */
83
83
  export type TemplateCopyResponse = {
84
84
  type: "template.copy";
@@ -90,7 +90,7 @@ export type TemplateCopyResponse = {
90
90
  };
91
91
  };
92
92
  /**
93
- * xds --json template --cdn [path]
93
+ * astryx --json template --cdn [path]
94
94
  * `written: false` with `reason: 'exists'` is a success: the command is safe to
95
95
  * re-run, and an edited page is the consumer's file to keep. `version` is the
96
96
  * Astryx version every CDN URL in the file was pinned to.
@@ -118,7 +118,7 @@ export type TemplateOptions = {
118
118
  /**
119
119
  * Filter templates by kind: 'page' or 'block'. Only applies to list views.
120
120
  */
121
- type?: "block" | "page" | undefined;
121
+ type?: "page" | "block" | undefined;
122
122
  /**
123
123
  * Narrow to templates from a specific package (id-only lookups across packages are ambiguous).
124
124
  */
@@ -6,18 +6,18 @@
6
6
  *
7
7
  * Each template is exactly two files: page.tsx (code) + template.doc.mjs (metadata).
8
8
  *
9
- * Invocation -> type discriminator
9
+ * Invocation -> type discriminator
10
10
  * ------------------------------------------------------------------
11
- * xds --json template [--list] -> template.list
12
- * xds --json template <name> -> template.show
13
- * xds --json template <name> --skeleton -> template.skeleton
14
- * xds --json template <name> [path] -> template.copy
15
- * xds --json template --cdn [path] -> template.cdn
16
- * (unknown template) -> CLIError
11
+ * astryx --json template [--list] -> template.list
12
+ * astryx --json template <name> -> template.show
13
+ * astryx --json template <name> --skeleton -> template.skeleton
14
+ * astryx --json template <name> [path] -> template.copy
15
+ * astryx --json template --cdn [path] -> template.cdn
16
+ * (unknown template) -> CLIError
17
17
  */
18
18
 
19
19
  /**
20
- * xds --json template [--list]
20
+ * astryx --json template [--list]
21
21
  * @typedef {object} TemplateListResponse
22
22
  * @property {'template.list'} type
23
23
  * @property {TemplateListEntry[]} data
@@ -43,7 +43,7 @@
43
43
  */
44
44
 
45
45
  /**
46
- * xds --json template <name>
46
+ * astryx --json template <name>
47
47
  * @typedef {object} TemplateShowResponse
48
48
  * @property {'template.show'} type
49
49
  * @property {object} data
@@ -55,7 +55,7 @@
55
55
  */
56
56
 
57
57
  /**
58
- * xds --json template <name> --skeleton
58
+ * astryx --json template <name> --skeleton
59
59
  * @typedef {object} TemplateSkeletonResponse
60
60
  * @property {'template.skeleton'} type
61
61
  * @property {object} data
@@ -66,7 +66,7 @@
66
66
  */
67
67
 
68
68
  /**
69
- * xds --json template <name> [path]
69
+ * astryx --json template <name> [path]
70
70
  * @typedef {object} TemplateCopyResponse
71
71
  * @property {'template.copy'} type
72
72
  * @property {object} data
@@ -77,7 +77,7 @@
77
77
  */
78
78
 
79
79
  /**
80
- * xds --json template --cdn [path]
80
+ * astryx --json template --cdn [path]
81
81
  * `written: false` with `reason: 'exists'` is a success: the command is safe to
82
82
  * re-run, and an edited page is the consumer's file to keep. `version` is the
83
83
  * Astryx version every CDN URL in the file was pinned to.
@@ -12,9 +12,10 @@
12
12
  * - A .d.ts (plus an optional .variants.d.ts for custom prop values)
13
13
  *
14
14
  * It performs the writes and returns a `theme.build` receipt — its `warnings`
15
- * carry override problems and any fonts the theme names but does not load
16
- * (font-warning.mjs) — or `null` when the theme produced no CSS (nothing to
17
- * build). Errors throw AstryxError (with
15
+ * carry override problems, every declaration core's generator dropped because
16
+ * its value could not stay one CSS declaration, and any fonts the theme names
17
+ * but does not load (font-warning.mjs) — or `null` when the theme produced no
18
+ * CSS (nothing to build). Errors throw AstryxError (with
18
19
  * a stable code). Human progress is emitted through the shared `logger`
19
20
  * (silent by default), so the CLI keeps its exact output while a programmatic
20
21
  * caller stays quiet.
@@ -2303,7 +2304,15 @@ async function themeBuildInternal(
2303
2304
  const scopeSelector = themeScopeStart(themeDef.name);
2304
2305
  const scopeTo = THEME_SCOPE_TO;
2305
2306
 
2306
- const {component, prose} = _generateThemeRulesSplit(resolvedTheme);
2307
+ // Older cores ignore this optional collector. Current core writes the same
2308
+ // warning text here that runtime callers receive on the console.
2309
+ /** @type {string[]} */
2310
+ const droppedDeclarations = [];
2311
+
2312
+ const {component, prose} = _generateThemeRulesSplit(
2313
+ resolvedTheme,
2314
+ droppedDeclarations,
2315
+ );
2307
2316
  const cssParts = [];
2308
2317
  // Prose element defaults always ship — the `<Theme>` runtime
2309
2318
  // (generateThemeCSS) always emits them, so the build must too, or the
@@ -2322,7 +2331,7 @@ async function themeBuildInternal(
2322
2331
  let adaptationCss;
2323
2332
  try {
2324
2333
  adaptationCss = _generateAdaptationCSS
2325
- ? _generateAdaptationCSS(resolvedTheme)
2334
+ ? _generateAdaptationCSS(resolvedTheme, droppedDeclarations)
2326
2335
  : {component: '', prose: ''};
2327
2336
  } catch (error) {
2328
2337
  const message =
@@ -2368,11 +2377,16 @@ async function themeBuildInternal(
2368
2377
  // adaptations on the same resolved leaf.
2369
2378
  let onMediaCss = '';
2370
2379
  if (_generateOnMediaCSS) {
2371
- onMediaCss = _generateOnMediaCSS(resolvedTheme);
2380
+ onMediaCss = _generateOnMediaCSS(resolvedTheme, droppedDeclarations);
2372
2381
  if (onMediaCss) {
2373
2382
  cssParts.push(`@layer astryx-theme {\n${onMediaCss}\n}`);
2374
2383
  }
2375
2384
  }
2385
+ for (const message of droppedDeclarations) {
2386
+ const w = `Declaration ${message}. The generated CSS omits it; fix the value in the theme source.`;
2387
+ warningMessages.push(w);
2388
+ logger.warn(` ⚠ ${w}`);
2389
+ }
2376
2390
  if (cssParts.length === 0) {
2377
2391
  logger.log('No overrides found — nothing to build.');
2378
2392
  return null;