343 lines
10 KiB
TypeScript
343 lines
10 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';
|
|
import { createMatrixPackage } from '../helpers/package-fixtures.js';
|
|
|
|
describe('FP-30 mx publish preflight', () => {
|
|
const tempDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
while (tempDirs.length > 0) {
|
|
const dir = tempDirs.pop();
|
|
if (dir) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
it('requires login and enforces immutable version publish', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-'));
|
|
tempDirs.push(root);
|
|
|
|
const fixture = createMatrixPackage(root, {
|
|
packageName: '@acme/fp30-publish',
|
|
version: '1.0.0',
|
|
componentType: 'PublishActor',
|
|
mount: 'fp30.publish',
|
|
});
|
|
const registryDir = path.join(root, 'registry');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
|
|
const unauthenticatedPublish = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(unauthenticatedPublish, 1);
|
|
assert.equal(
|
|
unauthenticatedPublish.errors.some((line) => line.includes('MX_AUTH_REQUIRED')),
|
|
true
|
|
);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-abc',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const publishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(publishResult, 0);
|
|
const publishPayload = parseLastJson(publishResult.logs) as {
|
|
packageName: string;
|
|
version: string;
|
|
registryTarballPath?: string;
|
|
};
|
|
assert.equal(publishPayload.packageName, fixture.packageName);
|
|
assert.equal(publishPayload.version, fixture.version);
|
|
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
|
|
|
|
const republishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(republishResult, 1);
|
|
assert.equal(republishResult.errors.some((line) => line.includes('MX_VERSION_EXISTS')), true);
|
|
});
|
|
|
|
it('allows webapp-only packages without actor components', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-webapp-'));
|
|
tempDirs.push(root);
|
|
|
|
const packageDir = path.join(root, 'webapp-only');
|
|
const registryDir = path.join(root, 'registry');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
|
|
fs.writeFileSync(path.join(packageDir, 'dist', 'index.html'), '<!doctype html><title>Webapp only</title>\n', 'utf8');
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'matrix.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: '@acme/fp30-webapp-only',
|
|
version: '1.0.0',
|
|
runtime: { language: 'typescript', entry: 'dist/index.html' },
|
|
webapp: { distDir: 'dist', entry: 'index.html', appName: 'webapp-only' },
|
|
components: [],
|
|
permissions: { fsPolicy: 'none' },
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: '@acme/fp30-webapp-only',
|
|
version: '1.0.0',
|
|
type: 'module',
|
|
files: ['dist', 'matrix.json'],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-webapp-only',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: packageDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const publishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: packageDir });
|
|
assertExitCode(publishResult, 0);
|
|
const publishPayload = parseLastJson(publishResult.logs) as {
|
|
packageName: string;
|
|
version: string;
|
|
registryTarballPath?: string;
|
|
};
|
|
assert.equal(publishPayload.packageName, '@acme/fp30-webapp-only');
|
|
assert.equal(publishPayload.version, '1.0.0');
|
|
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
|
|
});
|
|
|
|
it('allows runtime-only packages with a root actor and no components', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-runtime-only-'));
|
|
tempDirs.push(root);
|
|
|
|
const packageDir = path.join(root, 'runtime-only');
|
|
const registryDir = path.join(root, 'registry');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
|
|
fs.writeFileSync(path.join(packageDir, 'dist', 'index.js'), 'export class RuntimeOnlyActor {}\n', 'utf8');
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'matrix.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: '@acme/fp30-runtime-only',
|
|
version: '1.0.0',
|
|
root: {
|
|
type: 'RuntimeOnlyActor',
|
|
export: 'RuntimeOnlyActor',
|
|
description: 'Runtime-only root actor',
|
|
},
|
|
runtime: { language: 'typescript', entry: 'dist/index.js' },
|
|
components: [],
|
|
permissions: { fsPolicy: 'none' },
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: '@acme/fp30-runtime-only',
|
|
version: '1.0.0',
|
|
type: 'module',
|
|
files: ['dist', 'matrix.json'],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-runtime-only',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: packageDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const publishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: packageDir });
|
|
assertExitCode(publishResult, 0);
|
|
const publishPayload = parseLastJson(publishResult.logs) as {
|
|
packageName: string;
|
|
version: string;
|
|
registryTarballPath?: string;
|
|
};
|
|
assert.equal(publishPayload.packageName, '@acme/fp30-runtime-only');
|
|
assert.equal(publishPayload.version, '1.0.0');
|
|
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
|
|
});
|
|
|
|
it('allows service factory packages declared by matrix.json runtime.factory', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-publish-service-'));
|
|
tempDirs.push(root);
|
|
|
|
const packageDir = path.join(root, 'service-factory');
|
|
const registryDir = path.join(root, 'registry');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
fs.mkdirSync(path.join(packageDir, 'dist'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'dist', 'index.js'),
|
|
'export async function createServiceFactory() { return { runtime: { shutdown() {} } }; }\n',
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'matrix.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: '@acme/fp30-service-factory',
|
|
version: '1.0.0',
|
|
runtime: {
|
|
language: 'typescript',
|
|
entry: 'dist/index.js',
|
|
factory: {
|
|
kind: 'factory',
|
|
export: 'createServiceFactory',
|
|
instance: {
|
|
id: 'RUNTIME-SERVICE-FACTORY',
|
|
class: 'service',
|
|
rootKind: 'package-root',
|
|
autoStart: true,
|
|
},
|
|
},
|
|
},
|
|
components: [],
|
|
permissions: { fsPolicy: 'none' },
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(packageDir, 'package.json'),
|
|
JSON.stringify(
|
|
{
|
|
name: '@acme/fp30-service-factory',
|
|
version: '1.0.0',
|
|
type: 'module',
|
|
files: ['dist', 'matrix.json'],
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
'utf8',
|
|
);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-service-factory',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: packageDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const publishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: packageDir });
|
|
assertExitCode(publishResult, 0);
|
|
const publishPayload = parseLastJson(publishResult.logs) as {
|
|
packageName: string;
|
|
version: string;
|
|
registryTarballPath?: string;
|
|
};
|
|
assert.equal(publishPayload.packageName, '@acme/fp30-service-factory');
|
|
assert.equal(publishPayload.version, '1.0.0');
|
|
assert.equal(Boolean(publishPayload.registryTarballPath && fs.existsSync(publishPayload.registryTarballPath)), true);
|
|
});
|
|
});
|