@astryxdesign/cli 0.6.3-canary.3e8c2f1 → 0.6.3-canary.4435b62

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.
@@ -250,7 +250,8 @@ async function addComponent(name, options) {
250
250
  }
251
251
 
252
252
  const sourcePath = projectPath(path.relative(packageDir, sourceFile));
253
- const importSpecifier = `${owner}/${sourcePath}`;
253
+ const extensionlessPath = sourcePath.replace(/\.tsx?$/u, '');
254
+ const importSpecifier = `${owner}/${extensionlessPath}`;
254
255
  const docContents = `export default {\n type: 'component',\n name: '${name}',\n import: ${JSON.stringify(importSpecifier)},\n description: '${name} component.',\n props: [],\n};\n`;
255
256
  const sourceContents = `export function ${name}() {\n return <div>${name}</div>;\n}\n`;
256
257
 
@@ -263,7 +264,7 @@ async function addComponent(name, options) {
263
264
  packageFile,
264
265
  rootPath,
265
266
  path.basename(manifestFile),
266
- [{subpath: sourcePath, target: sourcePath}],
267
+ [{subpath: extensionlessPath, target: sourcePath}],
267
268
  );
268
269
  if (pkgUpdate != null) {
269
270
  plans.push({
@@ -576,6 +577,7 @@ async function addTemplate(name, options) {
576
577
 
577
578
  const pascalName = kebabToPascal(name);
578
579
  const sourcePath = projectPath(path.relative(packageDir, sourceFile));
580
+ const extensionlessPath = sourcePath.replace(/\.tsx?$/u, '');
579
581
  const specContents = `export default {\n type: '${templateType}',\n name: '${name}',\n description: '${kebabToTitle(name)} template.',\n};\n`;
580
582
  const sourceContents = `export default function ${pascalName}() {\n return <div>${kebabToTitle(name)}</div>;\n}\n`;
581
583
 
@@ -588,7 +590,7 @@ async function addTemplate(name, options) {
588
590
  packageFile,
589
591
  rootPath,
590
592
  path.basename(manifestFile),
591
- [{subpath: sourcePath, target: sourcePath}],
593
+ [{subpath: extensionlessPath, target: sourcePath}],
592
594
  );
593
595
  if (pkgUpdate != null) {
594
596
  plans.push({
@@ -118,14 +118,14 @@ describe('integrationAdd component', () => {
118
118
  );
119
119
  expect(pkg.exports).toEqual({
120
120
  '.': './index.mjs',
121
- './components/MyWidget.tsx': './components/MyWidget.tsx',
121
+ './components/MyWidget': './components/MyWidget.tsx',
122
122
  });
123
123
  expect(
124
124
  fs.readFileSync(
125
125
  path.join(tmpDir, 'components/MyWidget.doc.mjs'),
126
126
  'utf-8',
127
127
  ),
128
- ).toContain('import: "@acme/integration/components/MyWidget.tsx"');
128
+ ).toContain('import: "@acme/integration/components/MyWidget"');
129
129
  });
130
130
 
131
131
  it('does not create exports when the package has no exports map', async () => {
@@ -143,7 +143,7 @@ describe('integrationAdd component', () => {
143
143
  setup({
144
144
  exports: {
145
145
  '.': './index.mjs',
146
- './components/MyWidget.tsx': './different.tsx',
146
+ './components/MyWidget': './different.tsx',
147
147
  },
148
148
  });
149
149
 
@@ -421,7 +421,7 @@ describe('integrationAdd template', () => {
421
421
  );
422
422
  expect(pkg.exports).toEqual({
423
423
  '.': './index.mjs',
424
- './templates/my-widget.tsx': './templates/my-widget.tsx',
424
+ './templates/my-widget': './templates/my-widget.tsx',
425
425
  });
426
426
  });
427
427
 
@@ -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
+ });
@@ -32,6 +32,40 @@ import {semverCompare} from '../../foundation/env/semver.mjs';
32
32
  /** File extensions recognized as codemod modules. */
33
33
  const CODEMOD_EXTENSIONS = ['.ts', '.mjs', '.js'];
34
34
 
35
+ /**
36
+ * Directories never walked for codemods: a test directory beside a transform is
37
+ * the natural place to put its test, and every file found here is loaded and
38
+ * validated as a codemod.
39
+ */
40
+ const SKIP_DIRS = new Set([
41
+ 'node_modules',
42
+ '.git',
43
+ '__tests__',
44
+ '__fixtures__',
45
+ ]);
46
+
47
+ /**
48
+ * Whether a file name is a test or fixture rather than a codemod.
49
+ *
50
+ * The trap this closes: EVERY `.ts`/`.mjs`/`.js` under a version folder used to
51
+ * be loaded as a codemod, so a test file colocated with its transform failed
52
+ * validation and — because a definition error is a hard error — took every
53
+ * codemod in that package's version with it. `astryx upgrade` then applied no
54
+ * transforms and reported success, which is the worst shape a failure can take.
55
+ *
56
+ * Core's own codemods never hit this: they are enumerated in a registry, and
57
+ * their colocated tests are simply not in it. Only integrations are discovered
58
+ * by walking a directory, so only integrations carry the landmine — which is why
59
+ * this is a loader fix and not a documentation one. It protects the integrations
60
+ * that already exist, which no authoring tool can reach.
61
+ *
62
+ * @param {string} name a file's base name
63
+ * @returns {boolean}
64
+ */
65
+ function isTestFile(name) {
66
+ return /\.(test|spec)\.[^.]+$/.test(name) || /\.fixture\.[^.]+$/.test(name);
67
+ }
68
+
35
69
  /**
36
70
  * Recursively collect codemod module files under a version folder. Returns
37
71
  * entries of {id, file} where id is the extension-less relative path
@@ -49,14 +83,18 @@ function collectCodemodFiles(versionDir) {
49
83
  for (const entry of entries) {
50
84
  const full = path.join(dir, entry.name);
51
85
  if (entry.isDirectory()) {
52
- if (entry.name === 'node_modules' || entry.name === '.git') continue;
86
+ if (SKIP_DIRS.has(entry.name)) continue;
53
87
  walk(full);
54
88
  continue;
55
89
  }
56
90
  const ext = path.extname(entry.name);
57
91
  if (!CODEMOD_EXTENSIONS.includes(ext)) continue;
92
+ if (isTestFile(entry.name)) continue;
58
93
  const rel = path.relative(versionDir, full);
59
- const id = rel.slice(0, rel.length - ext.length).split(path.sep).join('/');
94
+ const id = rel
95
+ .slice(0, rel.length - ext.length)
96
+ .split(path.sep)
97
+ .join('/');
60
98
  out.push({id, file: full});
61
99
  }
62
100
  }
@@ -221,3 +221,61 @@ describe('integration codemod discovery', () => {
221
221
  ).rejects.toThrow(/across versions/i);
222
222
  });
223
223
  });
224
+
225
+ describe('test files beside a codemod', () => {
226
+ // The incident this closes: every .ts/.mjs/.js under a version folder was
227
+ // loaded AND VALIDATED as a codemod, so a test file colocated with its
228
+ // transform failed validation — and because a definition error is a hard
229
+ // error, it took every codemod in that version with it. `astryx upgrade`
230
+ // then applied nothing and reported success. Core is immune because its own
231
+ // codemods are enumerated in a registry rather than discovered by walking a
232
+ // directory, so its colocated tests are simply never visited. Only
233
+ // integrations carry the landmine.
234
+ const TRANSFORM = `
235
+ export default {
236
+ type: 'code',
237
+ title: 'Drop foo',
238
+ transform: (file) => file.source.replace(/foo/g, 'bar'),
239
+ };
240
+ `;
241
+ // Not a codemod: no default export of the right shape. Loading it throws.
242
+ const A_TEST = `
243
+ import {describe, it, expect} from 'vitest';
244
+ describe('drop-foo', () => {
245
+ it('drops foo', () => expect(1).toBe(1));
246
+ });
247
+ `;
248
+
249
+ it.each([
250
+ ['a .test. sibling', '0.2.0/drop-foo.test.mjs'],
251
+ ['a .spec. sibling', '0.2.0/drop-foo.spec.mjs'],
252
+ ['a fixture sibling', '0.2.0/drop-foo.fixture.mjs'],
253
+ ['a __tests__ directory', '0.2.0/__tests__/drop-foo.mjs'],
254
+ ['a __fixtures__ directory', '0.2.0/__fixtures__/input.mjs'],
255
+ ])('ignores %s and still discovers the codemod', async (_label, testPath) => {
256
+ scaffold({'0.2.0/drop-foo.mjs': TRANSFORM, [testPath]: A_TEST});
257
+
258
+ const project = await Project.load(tmpDir);
259
+ const byVersion = await discoverIntegrationCodemods(
260
+ project.loadedIntegrations,
261
+ );
262
+
263
+ expect([...byVersion.keys()]).toEqual(['0.2.0']);
264
+ expect(byVersion.get('0.2.0').map(entry => entry.id)).toEqual(['drop-foo']);
265
+ });
266
+
267
+ it('a nested helper directory is still walked', async () => {
268
+ // Only test and fixture names are skipped. A package that organises its
269
+ // transforms into subdirectories keeps working.
270
+ scaffold({'0.2.0/imports/drop-foo.mjs': TRANSFORM});
271
+
272
+ const project = await Project.load(tmpDir);
273
+ const byVersion = await discoverIntegrationCodemods(
274
+ project.loadedIntegrations,
275
+ );
276
+
277
+ expect(byVersion.get('0.2.0').map(entry => entry.id)).toEqual([
278
+ 'imports/drop-foo',
279
+ ]);
280
+ });
281
+ });
@@ -320,6 +320,15 @@ export const docs = {
320
320
  type: 'prose',
321
321
  text: 'Codemod ids (the extension-less relative path under the version folder, e.g. `rename-widget-prop`, `config/rename-integration`) must be unique within a package across all versions. A duplicate id across versions is a hard error.',
322
322
  },
323
+ {
324
+ type: 'prose',
325
+ text: 'The loader automatically skips test and fixture files so you can colocate tests with transforms. Reserved names: files matching `*.test.*`, `*.spec.*`, or `*.fixture.*`, and any file under a `__tests__/` or `__fixtures__/` directory. These are never loaded as codemods regardless of their extension.',
326
+ },
327
+ {
328
+ type: 'code',
329
+ lang: 'text',
330
+ code: 'codemods/\n 0.2.0/\n rename-widget-prop.ts # loaded as a codemod\n rename-widget-prop.test.ts # skipped (reserved name)\n __tests__/\n rename-widget-prop.test.ts # skipped (reserved directory)',
331
+ },
323
332
  {
324
333
  type: 'code',
325
334
  lang: 'typescript',
@@ -120,7 +120,7 @@ describe('integration authoring CLI', () => {
120
120
  );
121
121
  });
122
122
 
123
- it('packs the generated contribution and proves the consumer inventory', async () => {
123
+ it('pack-check rejects a generated component without an exports map (no-map false green)', async () => {
124
124
  const added = await runCli(
125
125
  ['integration', 'add', 'component', 'AcmeWidget', '--json'],
126
126
  tmpDir,
@@ -131,19 +131,23 @@ describe('integration authoring CLI', () => {
131
131
  ['integration', 'pack', '--check', '--json'],
132
132
  tmpDir,
133
133
  );
134
- expect(checked.status).toBe(0);
135
- expect(parseEnvelope(checked.stdout)).toMatchObject({
134
+ // Without an exports map, the extensionless import cannot resolve —
135
+ // pack-check must fail, not false-green.
136
+ expect(checked.status).not.toBe(0);
137
+ const envelope = parseEnvelope(checked.stdout);
138
+ expect(envelope).toMatchObject({
136
139
  type: 'integration.pack-check',
137
140
  data: {
138
141
  name: '@acme/widgets',
139
- version: '1.0.0',
140
- packable: true,
141
- contributions: {
142
- local: {components: ['AcmeWidget']},
143
- packed: {components: ['AcmeWidget']},
144
- },
142
+ packable: false,
145
143
  },
146
144
  });
145
+ expect(envelope.data.issues).toContainEqual(
146
+ expect.objectContaining({
147
+ severity: 'error',
148
+ message: expect.stringContaining('AcmeWidget'),
149
+ }),
150
+ );
147
151
  });
148
152
 
149
153
  it('requires the explicit --check gate on pack', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/cli",
3
- "version": "0.6.3-canary.3e8c2f1",
3
+ "version": "0.6.3-canary.4435b62",
4
4
  "displayName": "CLI",
5
5
  "description": "Scaffold projects, browse templates, generate themes, and get agent-ready docs from the command line.",
6
6
  "author": "Meta Open Source",
@@ -100,11 +100,10 @@
100
100
  "zod": "^4.4.3"
101
101
  },
102
102
  "peerDependencies": {
103
- "@astryxdesign/charts": "0.6.3-canary.3e8c2f1",
104
- "@astryxdesign/core": "0.6.3-canary.3e8c2f1",
105
- "@astryxdesign/lab": "0.6.3-canary.3e8c2f1",
106
- "@astryxdesign/theme-neutral": "0.6.3-canary.3e8c2f1",
107
- "gpt-tokenizer": "^3.4.0"
103
+ "@astryxdesign/charts": "0.6.3-canary.4435b62",
104
+ "@astryxdesign/core": "0.6.3-canary.4435b62",
105
+ "@astryxdesign/lab": "0.6.3-canary.4435b62",
106
+ "@astryxdesign/theme-neutral": "0.6.3-canary.4435b62"
108
107
  },
109
108
  "peerDependenciesMeta": {
110
109
  "@astryxdesign/charts": {
@@ -121,14 +120,13 @@
121
120
  }
122
121
  },
123
122
  "devDependencies": {
124
- "@astryxdesign/charts": "0.6.3-canary.3e8c2f1",
125
- "@astryxdesign/core": "0.6.3-canary.3e8c2f1",
126
- "@astryxdesign/lab": "0.6.3-canary.3e8c2f1",
127
- "@astryxdesign/theme-neutral": "0.6.3-canary.3e8c2f1",
123
+ "@astryxdesign/charts": "0.6.3-canary.4435b62",
124
+ "@astryxdesign/core": "0.6.3-canary.4435b62",
125
+ "@astryxdesign/lab": "0.6.3-canary.4435b62",
126
+ "@astryxdesign/theme-neutral": "0.6.3-canary.4435b62",
128
127
  "@heroicons/react": "^2.2.0",
129
128
  "@stylexjs/stylex": "^0.19.0",
130
129
  "@types/babel__core": "^7.20.5",
131
- "gpt-tokenizer": "^3.4.0",
132
130
  "lucide-react": "^1.18.0",
133
131
  "recharts": "^3.9.2"
134
132
  },