147 lines
4.6 KiB
TypeScript
147 lines
4.6 KiB
TypeScript
|
|
import { afterEach, describe, it } from 'node:test';
|
||
|
|
import { strict as assert } from 'node:assert';
|
||
|
|
import * as fs from 'node:fs';
|
||
|
|
import * as os from 'node:os';
|
||
|
|
import * as path from 'node:path';
|
||
|
|
import { runMxCli, parseLastJson, assertExitCode } from '../helpers/cli-harness.js';
|
||
|
|
|
||
|
|
function createLifecyclePackage(rootDir: string, packageName: string, scriptBody: string): string {
|
||
|
|
const packageDir = path.join(rootDir, packageName.replace(/^@/, '').replace('/', '-'));
|
||
|
|
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'matrix.json'),
|
||
|
|
JSON.stringify(
|
||
|
|
{
|
||
|
|
name: packageName,
|
||
|
|
version: '1.0.0',
|
||
|
|
runtime: { language: 'javascript', entry: 'dist/index.js' },
|
||
|
|
components: [],
|
||
|
|
install: {
|
||
|
|
seed: {
|
||
|
|
script: 'dist/seed.js',
|
||
|
|
description: 'seed test hook',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
permissions: { fsPolicy: 'none' },
|
||
|
|
},
|
||
|
|
null,
|
||
|
|
2,
|
||
|
|
),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
fs.writeFileSync(
|
||
|
|
path.join(packageDir, 'package.json'),
|
||
|
|
JSON.stringify(
|
||
|
|
{
|
||
|
|
name: packageName,
|
||
|
|
version: '1.0.0',
|
||
|
|
type: 'module',
|
||
|
|
main: 'dist/index.js',
|
||
|
|
},
|
||
|
|
null,
|
||
|
|
2,
|
||
|
|
),
|
||
|
|
'utf8',
|
||
|
|
);
|
||
|
|
fs.writeFileSync(path.join(packageDir, 'dist', 'index.js'), 'export const ready = true;\n', 'utf8');
|
||
|
|
fs.writeFileSync(path.join(packageDir, 'dist', 'seed.js'), scriptBody, 'utf8');
|
||
|
|
return packageDir;
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('FP-30 mx install lifecycle', () => {
|
||
|
|
const tempDirs: string[] = [];
|
||
|
|
|
||
|
|
afterEach(() => {
|
||
|
|
delete process.env.MX_TEST_MARKER;
|
||
|
|
while (tempDirs.length > 0) {
|
||
|
|
const dir = tempDirs.pop();
|
||
|
|
if (dir) {
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('runs install lifecycle hooks and exposes install env', async () => {
|
||
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-lifecycle-'));
|
||
|
|
tempDirs.push(root);
|
||
|
|
const workspaceDir = path.join(root, 'workspace');
|
||
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
||
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
||
|
|
const markerPath = path.join(root, 'seed-marker.json');
|
||
|
|
process.env.MX_TEST_MARKER = markerPath;
|
||
|
|
|
||
|
|
const packageDir = createLifecyclePackage(
|
||
|
|
root,
|
||
|
|
'@acme/install-lifecycle',
|
||
|
|
`import { writeFileSync } from 'node:fs';
|
||
|
|
const marker = process.env.MX_TEST_MARKER;
|
||
|
|
if (!marker) throw new Error('missing marker');
|
||
|
|
writeFileSync(marker, JSON.stringify({
|
||
|
|
phase: process.env.MATRIX_INSTALL_PHASE,
|
||
|
|
packagesDir: process.env.MATRIX_PACKAGES_DIR,
|
||
|
|
packageDir: process.env.MATRIX_PACKAGE_DIR,
|
||
|
|
packageName: process.env.MATRIX_PACKAGE_NAME
|
||
|
|
}), 'utf8');
|
||
|
|
`,
|
||
|
|
);
|
||
|
|
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'install',
|
||
|
|
packageDir,
|
||
|
|
'--packages-dir',
|
||
|
|
packagesDir,
|
||
|
|
'--json',
|
||
|
|
], { cwd: workspaceDir });
|
||
|
|
assertExitCode(result, 0);
|
||
|
|
|
||
|
|
const payload = parseLastJson(result.logs) as {
|
||
|
|
targetDir: string;
|
||
|
|
lifecyclePhases: string[];
|
||
|
|
};
|
||
|
|
assert.deepEqual(payload.lifecyclePhases, ['seed']);
|
||
|
|
assert.equal(fs.existsSync(payload.targetDir), true);
|
||
|
|
assert.equal(fs.existsSync(markerPath), true);
|
||
|
|
|
||
|
|
const marker = JSON.parse(fs.readFileSync(markerPath, 'utf8')) as Record<string, string>;
|
||
|
|
assert.equal(marker.phase, 'seed');
|
||
|
|
assert.equal(path.resolve(marker.packagesDir), path.resolve(packagesDir));
|
||
|
|
assert.equal(path.resolve(marker.packageDir), path.resolve(payload.targetDir));
|
||
|
|
assert.equal(marker.packageName, '@acme/install-lifecycle');
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rolls back package install when a lifecycle hook fails', async () => {
|
||
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-lifecycle-fail-'));
|
||
|
|
tempDirs.push(root);
|
||
|
|
const workspaceDir = path.join(root, 'workspace');
|
||
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
||
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
||
|
|
|
||
|
|
const packageDir = createLifecyclePackage(
|
||
|
|
root,
|
||
|
|
'@acme/install-lifecycle-fail',
|
||
|
|
`throw new Error('seed failed');\n`,
|
||
|
|
);
|
||
|
|
|
||
|
|
const result = await runMxCli([
|
||
|
|
'node',
|
||
|
|
'mx',
|
||
|
|
'install',
|
||
|
|
packageDir,
|
||
|
|
'--packages-dir',
|
||
|
|
packagesDir,
|
||
|
|
'--json',
|
||
|
|
], { cwd: workspaceDir });
|
||
|
|
assertExitCode(result, 1);
|
||
|
|
assert.equal(
|
||
|
|
result.errors.some((line) => line.includes('install.seed failed')),
|
||
|
|
true,
|
||
|
|
`Expected lifecycle failure in stderr, got:\n${result.errors.join('\n')}`,
|
||
|
|
);
|
||
|
|
|
||
|
|
const installedPath = path.join(packagesDir, 'node_modules', '@acme', 'install-lifecycle-fail');
|
||
|
|
assert.equal(fs.existsSync(installedPath), false);
|
||
|
|
});
|
||
|
|
});
|