@astryxdesign/cli 0.6.3-canary.2c831e4 → 0.6.3-canary.3e8c2f1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/integration/add-contribution.mjs +3 -5
- package/api/integration/add-contribution.test.mjs +4 -4
- package/api/integration/pack-check.mjs +7 -49
- package/api/integration/pack-check.test.mjs +0 -249
- package/assets/codemods/integration-discovery.mjs +2 -40
- package/assets/codemods/integration-discovery.test.mjs +0 -58
- package/assets/docs/cli-integrations.doc.mjs +0 -9
- package/clients/cli/commands/integration-authoring.test.mjs +9 -13
- package/package.json +11 -9
|
@@ -250,8 +250,7 @@ async function addComponent(name, options) {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
const sourcePath = projectPath(path.relative(packageDir, sourceFile));
|
|
253
|
-
const
|
|
254
|
-
const importSpecifier = `${owner}/${extensionlessPath}`;
|
|
253
|
+
const importSpecifier = `${owner}/${sourcePath}`;
|
|
255
254
|
const docContents = `export default {\n type: 'component',\n name: '${name}',\n import: ${JSON.stringify(importSpecifier)},\n description: '${name} component.',\n props: [],\n};\n`;
|
|
256
255
|
const sourceContents = `export function ${name}() {\n return <div>${name}</div>;\n}\n`;
|
|
257
256
|
|
|
@@ -264,7 +263,7 @@ async function addComponent(name, options) {
|
|
|
264
263
|
packageFile,
|
|
265
264
|
rootPath,
|
|
266
265
|
path.basename(manifestFile),
|
|
267
|
-
[{subpath:
|
|
266
|
+
[{subpath: sourcePath, target: sourcePath}],
|
|
268
267
|
);
|
|
269
268
|
if (pkgUpdate != null) {
|
|
270
269
|
plans.push({
|
|
@@ -577,7 +576,6 @@ async function addTemplate(name, options) {
|
|
|
577
576
|
|
|
578
577
|
const pascalName = kebabToPascal(name);
|
|
579
578
|
const sourcePath = projectPath(path.relative(packageDir, sourceFile));
|
|
580
|
-
const extensionlessPath = sourcePath.replace(/\.tsx?$/u, '');
|
|
581
579
|
const specContents = `export default {\n type: '${templateType}',\n name: '${name}',\n description: '${kebabToTitle(name)} template.',\n};\n`;
|
|
582
580
|
const sourceContents = `export default function ${pascalName}() {\n return <div>${kebabToTitle(name)}</div>;\n}\n`;
|
|
583
581
|
|
|
@@ -590,7 +588,7 @@ async function addTemplate(name, options) {
|
|
|
590
588
|
packageFile,
|
|
591
589
|
rootPath,
|
|
592
590
|
path.basename(manifestFile),
|
|
593
|
-
[{subpath:
|
|
591
|
+
[{subpath: sourcePath, target: sourcePath}],
|
|
594
592
|
);
|
|
595
593
|
if (pkgUpdate != null) {
|
|
596
594
|
plans.push({
|
|
@@ -118,14 +118,14 @@ describe('integrationAdd component', () => {
|
|
|
118
118
|
);
|
|
119
119
|
expect(pkg.exports).toEqual({
|
|
120
120
|
'.': './index.mjs',
|
|
121
|
-
'./components/MyWidget': './components/MyWidget.tsx',
|
|
121
|
+
'./components/MyWidget.tsx': './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"');
|
|
128
|
+
).toContain('import: "@acme/integration/components/MyWidget.tsx"');
|
|
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': './different.tsx',
|
|
146
|
+
'./components/MyWidget.tsx': './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': './templates/my-widget.tsx',
|
|
424
|
+
'./templates/my-widget.tsx': './templates/my-widget.tsx',
|
|
425
425
|
});
|
|
426
426
|
});
|
|
427
427
|
|
|
@@ -388,14 +388,6 @@ 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
|
-
);
|
|
399
391
|
for (const {name, specifier} of components) {
|
|
400
392
|
const result = resolved.get(specifier);
|
|
401
393
|
if (!result?.url || result.error) {
|
|
@@ -435,27 +427,19 @@ async function validatePackedComponentExports(integration, scratchBase) {
|
|
|
435
427
|
*/
|
|
436
428
|
async function validatePackedTemplateExports(integration, scratchBase) {
|
|
437
429
|
const {templates} = await discoverIntegrationTemplatesForOne(integration);
|
|
438
|
-
const entries = templates.map(template => {
|
|
439
|
-
|
|
430
|
+
const entries = templates.map(template => ({
|
|
431
|
+
id: template.dirName,
|
|
432
|
+
specifier: `${integration.name}/${path
|
|
440
433
|
.relative(integration.__packageDir, template.filePath)
|
|
441
434
|
.split(path.sep)
|
|
442
|
-
.join('/')
|
|
443
|
-
|
|
444
|
-
return {
|
|
445
|
-
id: template.dirName,
|
|
446
|
-
specifier: `${integration.name}/${extensionless}`,
|
|
447
|
-
};
|
|
448
|
-
});
|
|
435
|
+
.join('/')}`,
|
|
436
|
+
}));
|
|
449
437
|
const resolved = resolveConsumerSpecifiers(
|
|
450
438
|
entries.map(entry => entry.specifier),
|
|
451
439
|
scratchBase,
|
|
452
440
|
);
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
label: `Template "${id}"`,
|
|
456
|
-
specifier,
|
|
457
|
-
})),
|
|
458
|
-
);
|
|
441
|
+
/** @type {Issue[]} */
|
|
442
|
+
const issues = [];
|
|
459
443
|
for (const {id, specifier} of entries) {
|
|
460
444
|
const result = resolved.get(specifier);
|
|
461
445
|
if (!result?.url || result.error) {
|
|
@@ -485,32 +469,6 @@ async function validatePackedTemplateExports(integration, scratchBase) {
|
|
|
485
469
|
return issues;
|
|
486
470
|
}
|
|
487
471
|
|
|
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
|
-
|
|
514
472
|
/**
|
|
515
473
|
* Verify an integration package is ready to publish.
|
|
516
474
|
*
|
|
@@ -567,252 +567,3 @@ 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,40 +32,6 @@ 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
|
-
|
|
69
35
|
/**
|
|
70
36
|
* Recursively collect codemod module files under a version folder. Returns
|
|
71
37
|
* entries of {id, file} where id is the extension-less relative path
|
|
@@ -83,18 +49,14 @@ function collectCodemodFiles(versionDir) {
|
|
|
83
49
|
for (const entry of entries) {
|
|
84
50
|
const full = path.join(dir, entry.name);
|
|
85
51
|
if (entry.isDirectory()) {
|
|
86
|
-
if (
|
|
52
|
+
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
|
87
53
|
walk(full);
|
|
88
54
|
continue;
|
|
89
55
|
}
|
|
90
56
|
const ext = path.extname(entry.name);
|
|
91
57
|
if (!CODEMOD_EXTENSIONS.includes(ext)) continue;
|
|
92
|
-
if (isTestFile(entry.name)) continue;
|
|
93
58
|
const rel = path.relative(versionDir, full);
|
|
94
|
-
const id = rel
|
|
95
|
-
.slice(0, rel.length - ext.length)
|
|
96
|
-
.split(path.sep)
|
|
97
|
-
.join('/');
|
|
59
|
+
const id = rel.slice(0, rel.length - ext.length).split(path.sep).join('/');
|
|
98
60
|
out.push({id, file: full});
|
|
99
61
|
}
|
|
100
62
|
}
|
|
@@ -221,61 +221,3 @@ 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,15 +320,6 @@ 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
|
-
},
|
|
332
323
|
{
|
|
333
324
|
type: 'code',
|
|
334
325
|
lang: 'typescript',
|
|
@@ -120,7 +120,7 @@ describe('integration authoring CLI', () => {
|
|
|
120
120
|
);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
it('
|
|
123
|
+
it('packs the generated contribution and proves the consumer inventory', async () => {
|
|
124
124
|
const added = await runCli(
|
|
125
125
|
['integration', 'add', 'component', 'AcmeWidget', '--json'],
|
|
126
126
|
tmpDir,
|
|
@@ -131,23 +131,19 @@ describe('integration authoring CLI', () => {
|
|
|
131
131
|
['integration', 'pack', '--check', '--json'],
|
|
132
132
|
tmpDir,
|
|
133
133
|
);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
expect(checked.status).not.toBe(0);
|
|
137
|
-
const envelope = parseEnvelope(checked.stdout);
|
|
138
|
-
expect(envelope).toMatchObject({
|
|
134
|
+
expect(checked.status).toBe(0);
|
|
135
|
+
expect(parseEnvelope(checked.stdout)).toMatchObject({
|
|
139
136
|
type: 'integration.pack-check',
|
|
140
137
|
data: {
|
|
141
138
|
name: '@acme/widgets',
|
|
142
|
-
|
|
139
|
+
version: '1.0.0',
|
|
140
|
+
packable: true,
|
|
141
|
+
contributions: {
|
|
142
|
+
local: {components: ['AcmeWidget']},
|
|
143
|
+
packed: {components: ['AcmeWidget']},
|
|
144
|
+
},
|
|
143
145
|
},
|
|
144
146
|
});
|
|
145
|
-
expect(envelope.data.issues).toContainEqual(
|
|
146
|
-
expect.objectContaining({
|
|
147
|
-
severity: 'error',
|
|
148
|
-
message: expect.stringContaining('AcmeWidget'),
|
|
149
|
-
}),
|
|
150
|
-
);
|
|
151
147
|
});
|
|
152
148
|
|
|
153
149
|
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.
|
|
3
|
+
"version": "0.6.3-canary.3e8c2f1",
|
|
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,10 +100,11 @@
|
|
|
100
100
|
"zod": "^4.4.3"
|
|
101
101
|
},
|
|
102
102
|
"peerDependencies": {
|
|
103
|
-
"@astryxdesign/charts": "0.6.3-canary.
|
|
104
|
-
"@astryxdesign/core": "0.6.3-canary.
|
|
105
|
-
"@astryxdesign/lab": "0.6.3-canary.
|
|
106
|
-
"@astryxdesign/theme-neutral": "0.6.3-canary.
|
|
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"
|
|
107
108
|
},
|
|
108
109
|
"peerDependenciesMeta": {
|
|
109
110
|
"@astryxdesign/charts": {
|
|
@@ -120,13 +121,14 @@
|
|
|
120
121
|
}
|
|
121
122
|
},
|
|
122
123
|
"devDependencies": {
|
|
123
|
-
"@astryxdesign/charts": "0.6.3-canary.
|
|
124
|
-
"@astryxdesign/core": "0.6.3-canary.
|
|
125
|
-
"@astryxdesign/lab": "0.6.3-canary.
|
|
126
|
-
"@astryxdesign/theme-neutral": "0.6.3-canary.
|
|
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",
|
|
127
128
|
"@heroicons/react": "^2.2.0",
|
|
128
129
|
"@stylexjs/stylex": "^0.19.0",
|
|
129
130
|
"@types/babel__core": "^7.20.5",
|
|
131
|
+
"gpt-tokenizer": "^3.4.0",
|
|
130
132
|
"lucide-react": "^1.18.0",
|
|
131
133
|
"recharts": "^3.9.2"
|
|
132
134
|
},
|