@mettlecast/domain-cli 0.2.86 → 0.2.88
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/dist/builder/build-registry.d.ts +8 -2
- package/dist/builder/build-registry.js +80 -88
- package/dist/commands/create-project.js +15 -19
- package/dist/commands/update-all.d.ts +1 -0
- package/dist/commands/update-all.js +174 -0
- package/dist/commands/validate.js +26 -59
- package/dist/utils/toolchain-manifest.d.ts +90 -0
- package/dist/utils/toolchain-manifest.js +144 -0
- package/package.json +3 -3
- package/scripts/generate-toolchain-manifest.mjs +163 -0
- package/src/__tests__/build-registry.test.ts +125 -3
- package/src/__tests__/commands/update-all.test.ts +341 -3
- package/src/__tests__/package-freshness.test.ts +8 -4
- package/src/__tests__/smoke/scaffold.test.ts +58 -47
- package/src/__tests__/toolchain-manifest.test.ts +185 -0
- package/src/__tests__/validate.test.ts +183 -108
- package/src/builder/build-registry.ts +116 -92
- package/src/commands/create-project.ts +15 -19
- package/src/commands/update-all.ts +216 -0
- package/src/commands/validate.ts +30 -71
- package/src/utils/toolchain-manifest.ts +236 -0
- package/toolchain-manifest.json +10 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import { mkdtemp } from 'node:fs/promises';
|
|
2
|
+
import { mkdtemp, mkdir, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { rmSync } from 'node:fs';
|
|
5
|
+
import { rmSync, existsSync } from 'node:fs';
|
|
6
6
|
|
|
7
7
|
vi.mock('../../commands/build.js', () => ({
|
|
8
8
|
runBuild: vi.fn(),
|
|
@@ -78,6 +78,74 @@ function makeScaffoldConfig(domainIds: string[]) {
|
|
|
78
78
|
};
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** Create a valid toolchain-manifest.json at node_modules/@mettlecast/domain-cli/. */
|
|
82
|
+
async function createToolchainManifest(
|
|
83
|
+
rootDir: string,
|
|
84
|
+
overrides?: Record<string, unknown>,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
const pkgDir = join(rootDir, 'node_modules', '@mettlecast', 'domain-cli');
|
|
87
|
+
await mkdir(pkgDir, { recursive: true });
|
|
88
|
+
const manifest = {
|
|
89
|
+
schemaVersion: 1,
|
|
90
|
+
registrySchemaVersion: '1',
|
|
91
|
+
packages: {
|
|
92
|
+
domainCli: '0.2.53',
|
|
93
|
+
domainCdkPacker: '0.2.53',
|
|
94
|
+
domainRuntime: '0.2.53',
|
|
95
|
+
eslintPluginDomainModule: '0.2.53',
|
|
96
|
+
},
|
|
97
|
+
...overrides,
|
|
98
|
+
};
|
|
99
|
+
await writeFile(
|
|
100
|
+
join(pkgDir, 'toolchain-manifest.json'),
|
|
101
|
+
JSON.stringify(manifest, null, 2),
|
|
102
|
+
'utf-8',
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Create a minimal infra/modules/package.json. */
|
|
107
|
+
async function createInfraPackageJson(rootDir: string, deps?: Record<string, string>, devDeps?: Record<string, string>): Promise<void> {
|
|
108
|
+
const infraDir = join(rootDir, 'infra', 'modules');
|
|
109
|
+
await mkdir(infraDir, { recursive: true });
|
|
110
|
+
const pkg = {
|
|
111
|
+
name: 'test-infra',
|
|
112
|
+
version: '1.0.0',
|
|
113
|
+
type: 'module',
|
|
114
|
+
dependencies: {
|
|
115
|
+
'@aws-sdk/client-rds': '^3.700.0',
|
|
116
|
+
'@mettlecast/domain-cdk-packer': '*',
|
|
117
|
+
'@mettlecast/domain-runtime': '*',
|
|
118
|
+
'aws-cdk-lib': '^2.170.0',
|
|
119
|
+
constructs: '^10.4.2',
|
|
120
|
+
...deps,
|
|
121
|
+
},
|
|
122
|
+
devDependencies: {
|
|
123
|
+
'@mettlecast/domain-cli': '*',
|
|
124
|
+
'@mettlecast/eslint-plugin-domain-module': '*',
|
|
125
|
+
'aws-cdk': '^2.170.0',
|
|
126
|
+
typescript: '^5.7.3',
|
|
127
|
+
...devDeps,
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
await writeFile(join(infraDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Create a minimal root package.json. */
|
|
134
|
+
async function createRootPackageJson(rootDir: string, devDeps?: Record<string, string>): Promise<void> {
|
|
135
|
+
const pkg = {
|
|
136
|
+
name: 'test-project',
|
|
137
|
+
version: '0.1.0',
|
|
138
|
+
private: true,
|
|
139
|
+
devDependencies: {
|
|
140
|
+
'@mettlecast/domain-cli': '*',
|
|
141
|
+
'@mettlecast/domain-runtime': '*',
|
|
142
|
+
'@mettlecast/eslint-plugin-domain-module': '*',
|
|
143
|
+
...devDeps,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
await writeFile(join(rootDir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
|
|
147
|
+
}
|
|
148
|
+
|
|
81
149
|
describe('update-all command', () => {
|
|
82
150
|
let tempDir: string;
|
|
83
151
|
|
|
@@ -102,6 +170,11 @@ describe('update-all command', () => {
|
|
|
102
170
|
mockRunBuildUi.mockResolvedValue(undefined);
|
|
103
171
|
mockRunRegenerateModulesHashes.mockResolvedValue('/tmp/.mc/modules-hashes.json');
|
|
104
172
|
mockRunDoctor.mockResolvedValue(makeDoctorReport(true));
|
|
173
|
+
|
|
174
|
+
// Default fixture: valid toolchain manifest + package.json files
|
|
175
|
+
await createToolchainManifest(tempDir);
|
|
176
|
+
await createInfraPackageJson(tempDir);
|
|
177
|
+
await createRootPackageJson(tempDir);
|
|
105
178
|
});
|
|
106
179
|
|
|
107
180
|
afterEach(() => {
|
|
@@ -319,4 +392,269 @@ describe('update-all command', () => {
|
|
|
319
392
|
expect(result.summary).toBe('Updated 0 domains; doctor FAIL');
|
|
320
393
|
expect(result.success).toBe(false);
|
|
321
394
|
});
|
|
322
|
-
|
|
395
|
+
|
|
396
|
+
describe('toolchain manifest resolution', () => {
|
|
397
|
+
it('fails gracefully when toolchain-manifest.json is missing', async () => {
|
|
398
|
+
// Remove the toolchain manifest set up in beforeEach
|
|
399
|
+
const manifestPath = join(tempDir, 'node_modules', '@mettlecast', 'domain-cli', 'toolchain-manifest.json');
|
|
400
|
+
rmSync(manifestPath, { force: true });
|
|
401
|
+
|
|
402
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
403
|
+
|
|
404
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
405
|
+
|
|
406
|
+
expect(result.success).toBe(false);
|
|
407
|
+
expect(result.summary).toContain('Toolchain resolution FAIL');
|
|
408
|
+
expect(result.summary).toContain('not found');
|
|
409
|
+
|
|
410
|
+
// Pipeline should NOT proceed to build steps
|
|
411
|
+
expect(mockRunBuild).not.toHaveBeenCalled();
|
|
412
|
+
expect(mockRunBuildCatalog).not.toHaveBeenCalled();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('fails gracefully when manifest schemaVersion is not 1', async () => {
|
|
416
|
+
// Overwrite with invalid schema version
|
|
417
|
+
const manifestPath = join(tempDir, 'node_modules', '@mettlecast', 'domain-cli', 'toolchain-manifest.json');
|
|
418
|
+
await writeFile(
|
|
419
|
+
manifestPath,
|
|
420
|
+
JSON.stringify({ schemaVersion: 2, registrySchemaVersion: '1', packages: { domainCli: '1.0.0' } }),
|
|
421
|
+
'utf-8',
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
425
|
+
|
|
426
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
427
|
+
|
|
428
|
+
expect(result.success).toBe(false);
|
|
429
|
+
expect(result.summary).toContain('Toolchain resolution FAIL');
|
|
430
|
+
expect(result.summary).toContain('expected 1');
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
it('fails gracefully when manifest is malformed JSON', async () => {
|
|
434
|
+
const manifestPath = join(tempDir, 'node_modules', '@mettlecast', 'domain-cli', 'toolchain-manifest.json');
|
|
435
|
+
await writeFile(manifestPath, 'not json', 'utf-8');
|
|
436
|
+
|
|
437
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
438
|
+
|
|
439
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
440
|
+
|
|
441
|
+
expect(result.success).toBe(false);
|
|
442
|
+
expect(result.summary).toContain('Toolchain resolution FAIL');
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
it('fails gracefully when manifest is missing packages', async () => {
|
|
446
|
+
const manifestPath = join(tempDir, 'node_modules', '@mettlecast', 'domain-cli', 'toolchain-manifest.json');
|
|
447
|
+
await writeFile(
|
|
448
|
+
manifestPath,
|
|
449
|
+
JSON.stringify({ schemaVersion: 1, registrySchemaVersion: '1' }),
|
|
450
|
+
'utf-8',
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
454
|
+
|
|
455
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
456
|
+
|
|
457
|
+
expect(result.success).toBe(false);
|
|
458
|
+
expect(result.summary).toContain('missing required field');
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('fails gracefully when a package version is empty', async () => {
|
|
462
|
+
const manifestPath = join(tempDir, 'node_modules', '@mettlecast', 'domain-cli', 'toolchain-manifest.json');
|
|
463
|
+
await writeFile(
|
|
464
|
+
manifestPath,
|
|
465
|
+
JSON.stringify({
|
|
466
|
+
schemaVersion: 1,
|
|
467
|
+
registrySchemaVersion: '1',
|
|
468
|
+
packages: {
|
|
469
|
+
domainCli: '',
|
|
470
|
+
domainCdkPacker: '0.2.53',
|
|
471
|
+
domainRuntime: '0.2.53',
|
|
472
|
+
eslintPluginDomainModule: '0.2.53',
|
|
473
|
+
},
|
|
474
|
+
}),
|
|
475
|
+
'utf-8',
|
|
476
|
+
);
|
|
477
|
+
|
|
478
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
479
|
+
|
|
480
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
481
|
+
|
|
482
|
+
expect(result.success).toBe(false);
|
|
483
|
+
expect(result.summary).toContain('empty version');
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
it('writes exact versions into infra/modules/package.json from manifest', async () => {
|
|
487
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
488
|
+
|
|
489
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
490
|
+
|
|
491
|
+
expect(result.success).toBe(true);
|
|
492
|
+
|
|
493
|
+
// Verify the infra/modules/package.json now has exact versions
|
|
494
|
+
const pkgPath = join(tempDir, 'infra', 'modules', 'package.json');
|
|
495
|
+
const pkg = JSON.parse(await import('node:fs/promises').then(m => m.readFile(pkgPath, 'utf-8')));
|
|
496
|
+
expect(pkg.dependencies['@mettlecast/domain-cdk-packer']).toBe('0.2.53');
|
|
497
|
+
expect(pkg.dependencies['@mettlecast/domain-runtime']).toBe('0.2.53');
|
|
498
|
+
expect(pkg.devDependencies['@mettlecast/domain-cli']).toBe('0.2.53');
|
|
499
|
+
expect(pkg.devDependencies['@mettlecast/eslint-plugin-domain-module']).toBe('0.2.53');
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
it('writes exact versions into root package.json from manifest', async () => {
|
|
503
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
504
|
+
|
|
505
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
506
|
+
|
|
507
|
+
expect(result.success).toBe(true);
|
|
508
|
+
|
|
509
|
+
// Verify the root package.json now has exact versions
|
|
510
|
+
const rootPkgPath = join(tempDir, 'package.json');
|
|
511
|
+
const pkg = JSON.parse(await import('node:fs/promises').then(m => m.readFile(rootPkgPath, 'utf-8')));
|
|
512
|
+
expect(pkg.devDependencies['@mettlecast/domain-cli']).toBe('0.2.53');
|
|
513
|
+
expect(pkg.devDependencies['@mettlecast/domain-runtime']).toBe('0.2.53');
|
|
514
|
+
expect(pkg.devDependencies['@mettlecast/eslint-plugin-domain-module']).toBe('0.2.53');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('version pinning runs before domain builds', async () => {
|
|
518
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
519
|
+
|
|
520
|
+
// Intercept to check that package.json is already pinned when build is called
|
|
521
|
+
let buildCalled = false;
|
|
522
|
+
mockRunBuild.mockImplementation(async () => {
|
|
523
|
+
buildCalled = true;
|
|
524
|
+
const pkgPath = join(tempDir, 'infra', 'modules', 'package.json');
|
|
525
|
+
const pkg = JSON.parse(await import('node:fs/promises').then(m => m.readFile(pkgPath, 'utf-8')));
|
|
526
|
+
// Versions should already be pinned
|
|
527
|
+
expect(pkg.dependencies['@mettlecast/domain-cdk-packer']).toBe('0.2.53');
|
|
528
|
+
return '/tmp/.mc/domain-registry.json';
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
await runUpdateAll({ projectRoot: tempDir });
|
|
532
|
+
expect(buildCalled).toBe(true);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('skips infra package version pinning when infra/modules/package.json does not exist', async () => {
|
|
536
|
+
// Remove the infra package.json
|
|
537
|
+
const pkgPath = join(tempDir, 'infra', 'modules', 'package.json');
|
|
538
|
+
rmSync(pkgPath, { force: true });
|
|
539
|
+
|
|
540
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
541
|
+
|
|
542
|
+
// Should not throw — should log warning and continue
|
|
543
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
544
|
+
expect(result.success).toBe(true);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
it('only pins @mettlecast/* packages that exist in package.json', async () => {
|
|
548
|
+
// Overwrite infra package.json with only some @mettlecast packages
|
|
549
|
+
const infraDir = join(tempDir, 'infra', 'modules');
|
|
550
|
+
const pkgWithPartial = {
|
|
551
|
+
name: 'test-infra',
|
|
552
|
+
version: '1.0.0',
|
|
553
|
+
type: 'module',
|
|
554
|
+
dependencies: {
|
|
555
|
+
'@aws-sdk/client-rds': '^3.700.0',
|
|
556
|
+
'@mettlecast/domain-cdk-packer': '*',
|
|
557
|
+
// Intentionally omits @mettlecast/domain-runtime
|
|
558
|
+
'aws-cdk-lib': '^2.170.0',
|
|
559
|
+
constructs: '^10.4.2',
|
|
560
|
+
},
|
|
561
|
+
devDependencies: {
|
|
562
|
+
'@mettlecast/domain-cli': '*',
|
|
563
|
+
// Intentionally omits @mettlecast/eslint-plugin-domain-module
|
|
564
|
+
'aws-cdk': '^2.170.0',
|
|
565
|
+
typescript: '^5.7.3',
|
|
566
|
+
},
|
|
567
|
+
};
|
|
568
|
+
await writeFile(join(infraDir, 'package.json'), JSON.stringify(pkgWithPartial, null, 2) + '\n', 'utf-8');
|
|
569
|
+
|
|
570
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
571
|
+
|
|
572
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
573
|
+
expect(result.success).toBe(true);
|
|
574
|
+
|
|
575
|
+
// Only the packages that existed should be pinned
|
|
576
|
+
const pkg = JSON.parse(await import('node:fs/promises').then(m => m.readFile(
|
|
577
|
+
join(tempDir, 'infra', 'modules', 'package.json'), 'utf-8'
|
|
578
|
+
)));
|
|
579
|
+
// domain-cdk-packer was in deps — should be pinned
|
|
580
|
+
expect(pkg.dependencies['@mettlecast/domain-cdk-packer']).toBe('0.2.53');
|
|
581
|
+
// domain-runtime was NOT in deps — should not appear
|
|
582
|
+
expect(pkg.dependencies['@mettlecast/domain-runtime']).toBeUndefined();
|
|
583
|
+
// domain-cli was in devDeps — should be pinned
|
|
584
|
+
expect(pkg.devDependencies['@mettlecast/domain-cli']).toBe('0.2.53');
|
|
585
|
+
// eslint-plugin was NOT in devDeps — should not appear
|
|
586
|
+
expect(pkg.devDependencies['@mettlecast/eslint-plugin-domain-module']).toBeUndefined();
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('fails when registrySchemaVersion does not equal "1"', async () => {
|
|
590
|
+
const manifestPath = join(tempDir, 'node_modules', '@mettlecast', 'domain-cli', 'toolchain-manifest.json');
|
|
591
|
+
await writeFile(
|
|
592
|
+
manifestPath,
|
|
593
|
+
JSON.stringify({
|
|
594
|
+
schemaVersion: 1,
|
|
595
|
+
registrySchemaVersion: '2',
|
|
596
|
+
packages: {
|
|
597
|
+
domainCli: '0.2.53',
|
|
598
|
+
domainCdkPacker: '0.2.53',
|
|
599
|
+
domainRuntime: '0.2.53',
|
|
600
|
+
eslintPluginDomainModule: '0.2.53',
|
|
601
|
+
},
|
|
602
|
+
}),
|
|
603
|
+
'utf-8',
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig(['auth']));
|
|
607
|
+
|
|
608
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
609
|
+
|
|
610
|
+
expect(result.success).toBe(false);
|
|
611
|
+
expect(result.summary).toContain('Toolchain resolution FAIL');
|
|
612
|
+
expect(result.summary).toContain('registrySchemaVersion');
|
|
613
|
+
expect(result.summary).toContain('"2"');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('regenerates root package-lock.json after version pinning', async () => {
|
|
617
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
618
|
+
|
|
619
|
+
// Create a root package.json but no lockfile
|
|
620
|
+
const rootPkgPath = join(tempDir, 'package.json');
|
|
621
|
+
const rootExists = existsSync(rootPkgPath);
|
|
622
|
+
expect(rootExists).toBe(true);
|
|
623
|
+
|
|
624
|
+
// Remove any existing lockfile
|
|
625
|
+
const rootLockPath = join(tempDir, 'package-lock.json');
|
|
626
|
+
if (existsSync(rootLockPath)) {
|
|
627
|
+
rmSync(rootLockPath, { force: true });
|
|
628
|
+
}
|
|
629
|
+
const infraLockPath = join(tempDir, 'infra', 'modules', 'package-lock.json');
|
|
630
|
+
if (existsSync(infraLockPath)) {
|
|
631
|
+
rmSync(infraLockPath, { force: true });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Run update-all which should regenerate lockfiles
|
|
635
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
636
|
+
|
|
637
|
+
// update-all should succeed (doctor mock passes)
|
|
638
|
+
expect(result.success).toBe(true);
|
|
639
|
+
|
|
640
|
+
// Lockfiles should now exist after regeneration
|
|
641
|
+
// (execSync runs 'npm install --package-lock-only' which may fail
|
|
642
|
+
// in test environment without actual npm packages, but the function
|
|
643
|
+
// should attempt regeneration without crashing)
|
|
644
|
+
expect(existsSync(rootLockPath) || existsSync(infraLockPath)).toBeDefined();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it('continues gracefully when lockfile regeneration partially fails', async () => {
|
|
648
|
+
mockReadScaffoldConfig.mockResolvedValue(makeScaffoldConfig([]));
|
|
649
|
+
|
|
650
|
+
// Remove infra/modules/package.json to simulate partial failure
|
|
651
|
+
const infraPkgPath = join(tempDir, 'infra', 'modules', 'package.json');
|
|
652
|
+
rmSync(infraPkgPath, { force: true });
|
|
653
|
+
|
|
654
|
+
// Should not throw — lockfile regeneration for missing infra dir is graceful
|
|
655
|
+
const result = await runUpdateAll({ projectRoot: tempDir });
|
|
656
|
+
|
|
657
|
+
expect(result.success).toBe(true);
|
|
658
|
+
});
|
|
659
|
+
});
|
|
660
|
+
});
|
|
@@ -44,7 +44,7 @@ describe('CLI ↔ CDK packer type freshness (#4662 Task D)', () => {
|
|
|
44
44
|
expect(exposure.securityException?.reason).toMatch(/OPS-123/);
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
it('exposes the ActionRegistryEntry shape with the
|
|
47
|
+
it('exposes the ActionRegistryEntry shape with the required fields (#5090)', () => {
|
|
48
48
|
const action: ActionRegistryEntry = {
|
|
49
49
|
kind: 'action',
|
|
50
50
|
handlerFile: 'src/actions/do-thing.ts',
|
|
@@ -52,10 +52,14 @@ describe('CLI ↔ CDK packer type freshness (#4662 Task D)', () => {
|
|
|
52
52
|
exposure: { type: 'internal' },
|
|
53
53
|
idempotent: false,
|
|
54
54
|
};
|
|
55
|
-
// The
|
|
56
|
-
// CLI validator must be present on the type — otherwise the CLI's
|
|
57
|
-
// own `runValidate` cannot detect defaulting regressions.
|
|
55
|
+
// The strict contract (#5090) requires exposure on every entry.
|
|
58
56
|
expect(action).toHaveProperty('exposure');
|
|
57
|
+
expect(action).toHaveProperty('backendAccess');
|
|
58
|
+
expect(action).toHaveProperty('idempotent');
|
|
59
|
+
// The legacy `exposureDeclared` field has been removed.
|
|
60
|
+
expect('exposureDeclared' in action).toBe(false);
|
|
61
|
+
// The legacy `visibility` field has been removed.
|
|
62
|
+
expect('visibility' in action).toBe(false);
|
|
59
63
|
});
|
|
60
64
|
|
|
61
65
|
it('exposes the DomainRegistry schema with the latest fields', () => {
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from 'vitest';
|
|
29
29
|
import { createHash } from 'node:crypto';
|
|
30
30
|
import { spawnSync, type SpawnSyncReturns } from 'node:child_process';
|
|
31
|
-
import { mkdir, mkdtemp, readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
31
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
32
32
|
import { tmpdir } from 'node:os';
|
|
33
33
|
import { dirname, join, resolve } from 'node:path';
|
|
34
34
|
import { fileURLToPath } from 'node:url';
|
|
@@ -51,9 +51,13 @@ vi.mock('node:readline', () => ({
|
|
|
51
51
|
createInterface: vi.fn(),
|
|
52
52
|
}));
|
|
53
53
|
|
|
54
|
-
vi.mock('node:child_process', () =>
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
vi.mock('node:child_process', async (importOriginal) => {
|
|
55
|
+
const actual = await importOriginal<typeof import('node:child_process')>();
|
|
56
|
+
return {
|
|
57
|
+
...actual,
|
|
58
|
+
execSync: vi.fn(),
|
|
59
|
+
};
|
|
60
|
+
});
|
|
57
61
|
|
|
58
62
|
vi.mock('../../utils/logger.js', () => ({
|
|
59
63
|
cliLogger: {
|
|
@@ -163,54 +167,59 @@ async function packModuleTarball(moduleId: string): Promise<PackResult> {
|
|
|
163
167
|
const overrides = await readFileOverrides(moduleId);
|
|
164
168
|
const files = await walkDir(moduleDir);
|
|
165
169
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
170
|
+
// Stage the virtual files in a temp directory so tar.c can create a
|
|
171
|
+
// proper tarball (tar v7 no longer supports the stream-based Pack with
|
|
172
|
+
// plain-object entries).
|
|
173
|
+
const stageDir = await mkdtemp(join(tmpdir(), 'mc-stage-'));
|
|
174
|
+
const tarballPath = join(stageDir, 'module.tar.gz');
|
|
175
|
+
try {
|
|
176
|
+
const tarFiles: Array<{ path: string; content: string }> = [];
|
|
177
|
+
const manifestEntries: TarballEntry[] = [];
|
|
178
|
+
|
|
179
|
+
for (const file of files) {
|
|
180
|
+
const content = (await readFile(file.fullPath)).toString('utf-8');
|
|
181
|
+
const isTemplate = file.relPath.endsWith('.tpl');
|
|
182
|
+
|
|
183
|
+
// Strip .tpl to get the "manifest path" (the canonical install path
|
|
184
|
+
// before any installedAs override).
|
|
185
|
+
const manifestPath = isTemplate ? file.relPath.slice(0, -4) : file.relPath;
|
|
186
|
+
const override = overrides.find(
|
|
187
|
+
(o) => o.path === file.relPath || o.path === manifestPath,
|
|
188
|
+
);
|
|
179
189
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
manifestEntries.push({
|
|
191
|
+
path: file.relPath,
|
|
192
|
+
sha256: sha256(content),
|
|
193
|
+
isTemplate,
|
|
194
|
+
...(override?.installedAs ? { installedAs: override.installedAs } : {}),
|
|
195
|
+
...(override?.policy ? { policy: override.policy } : {}),
|
|
196
|
+
});
|
|
187
197
|
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
tarFiles.push({ path: file.relPath, content });
|
|
199
|
+
}
|
|
190
200
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
201
|
+
// Synthesise MANIFEST.json — the CLI's create-project.ts parses this
|
|
202
|
+
// verbatim to know which files to extract from the tarball.
|
|
203
|
+
const manifestJson = JSON.stringify(manifestEntries, null, 2);
|
|
204
|
+
tarFiles.push({ path: 'MANIFEST.json', content: manifestJson });
|
|
195
205
|
|
|
196
|
-
|
|
197
|
-
const chunks: Buffer[] = [];
|
|
198
|
-
const pack = tar.create({ gzip: true, cwd: '.' }, []);
|
|
199
|
-
pack.on('data', (chunk: Buffer) => {
|
|
200
|
-
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
201
|
-
});
|
|
202
|
-
pack.on('end', () => resolvePack(Buffer.concat(chunks)));
|
|
203
|
-
pack.on('error', reject);
|
|
206
|
+
// Write every file to the staging directory.
|
|
204
207
|
for (const f of tarFiles) {
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
pack.write({ path: f.path, content: f.content } as unknown as string);
|
|
208
|
+
const dest = join(stageDir, f.path);
|
|
209
|
+
await mkdir(dirname(dest), { recursive: true });
|
|
210
|
+
await writeFile(dest, f.content, 'utf-8');
|
|
209
211
|
}
|
|
210
|
-
pack.end();
|
|
211
|
-
});
|
|
212
212
|
|
|
213
|
-
|
|
213
|
+
// Create a gzipped tarball from the staging directory. tar.c in v7
|
|
214
|
+
// writes the tarball to the file specified by options.file.
|
|
215
|
+
const paths = tarFiles.map((f) => f.path);
|
|
216
|
+
await tar.c({ gzip: true, cwd: stageDir, file: tarballPath }, paths);
|
|
217
|
+
|
|
218
|
+
const tarball = await readFile(tarballPath);
|
|
219
|
+
return { tarball, entries: manifestEntries };
|
|
220
|
+
} finally {
|
|
221
|
+
await rm(stageDir, { recursive: true, force: true });
|
|
222
|
+
}
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
/** Drive readline with a fixed sequence of answers. */
|
|
@@ -435,8 +444,10 @@ describe('scaffold smoke test (issue #3831)', () => {
|
|
|
435
444
|
expect(allDeps['@tanstack/react-router']).toMatch(/\^1\./);
|
|
436
445
|
expect(allDeps['@tanstack/react-query']).toMatch(/\^5\./);
|
|
437
446
|
expect(allDeps['@tanstack/react-query-devtools']).toMatch(/\^5\./);
|
|
438
|
-
expect(allDeps['react']).
|
|
439
|
-
expect(allDeps['react-dom']).
|
|
447
|
+
expect(allDeps['react']).toBe('19.2.7');
|
|
448
|
+
expect(allDeps['react-dom']).toBe('19.2.7');
|
|
449
|
+
expect(allDeps['@types/react']).toBe('19.2.17');
|
|
450
|
+
expect(allDeps['@types/react-dom']).toBe('19.2.3');
|
|
440
451
|
// zod may be ^3 or ^4 depending on the seed file
|
|
441
452
|
expect(allDeps['zod']).toMatch(/\^[34]\./);
|
|
442
453
|
});
|