612 lines
19 KiB
TypeScript
612 lines
19 KiB
TypeScript
import { afterEach, describe, it } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { createHash } from 'node:crypto';
|
|
import * as fs from 'node:fs';
|
|
import * as http from 'node:http';
|
|
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';
|
|
|
|
async function startNpmRegistryFixture(params: {
|
|
readonly packageName: string;
|
|
readonly version: string;
|
|
readonly tarballPath: string;
|
|
readonly expectedToken?: string;
|
|
}): Promise<{
|
|
readonly registryUrl: string;
|
|
readonly metadataRequests: () => number;
|
|
readonly tarballRequests: () => number;
|
|
readonly authorizationHeaders: () => readonly string[];
|
|
close(): Promise<void>;
|
|
}> {
|
|
const tarball = fs.readFileSync(params.tarballPath);
|
|
const shasum = createHash('sha1').update(tarball).digest('hex');
|
|
const integrity = `sha512-${createHash('sha512').update(tarball).digest('base64')}`;
|
|
let metadataRequestCount = 0;
|
|
let tarballRequestCount = 0;
|
|
const authorizationHeaders: string[] = [];
|
|
|
|
const server = http.createServer((request, response) => {
|
|
const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
|
|
const decodedPath = decodeURIComponent(requestUrl.pathname);
|
|
const authorization = request.headers.authorization;
|
|
if (typeof authorization === 'string') {
|
|
authorizationHeaders.push(authorization);
|
|
} else if (Array.isArray(authorization)) {
|
|
authorizationHeaders.push(...authorization);
|
|
}
|
|
|
|
if (decodedPath === `/${params.packageName}`) {
|
|
metadataRequestCount += 1;
|
|
const registryUrl = `http://127.0.0.1:${addressPort(server)}/`;
|
|
const metadata = {
|
|
name: params.packageName,
|
|
'dist-tags': { latest: params.version },
|
|
versions: {
|
|
[params.version]: {
|
|
name: params.packageName,
|
|
version: params.version,
|
|
dist: {
|
|
tarball: new URL('tarballs/package.tgz', registryUrl).toString(),
|
|
shasum,
|
|
integrity,
|
|
},
|
|
},
|
|
},
|
|
};
|
|
response.writeHead(200, { 'content-type': 'application/json' });
|
|
response.end(JSON.stringify(metadata));
|
|
return;
|
|
}
|
|
|
|
if (decodedPath === '/tarballs/package.tgz') {
|
|
tarballRequestCount += 1;
|
|
response.writeHead(200, {
|
|
'content-type': 'application/octet-stream',
|
|
'content-length': String(tarball.byteLength),
|
|
});
|
|
response.end(tarball);
|
|
return;
|
|
}
|
|
|
|
response.writeHead(404, { 'content-type': 'application/json' });
|
|
response.end(JSON.stringify({ error: `Not found: ${decodedPath}` }));
|
|
});
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.once('error', reject);
|
|
server.listen(0, '127.0.0.1', () => {
|
|
server.off('error', reject);
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
return {
|
|
registryUrl: `http://127.0.0.1:${addressPort(server)}/`,
|
|
metadataRequests: () => metadataRequestCount,
|
|
tarballRequests: () => tarballRequestCount,
|
|
authorizationHeaders: () => [...authorizationHeaders],
|
|
async close(): Promise<void> {
|
|
await new Promise<void>((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
function addressPort(server: http.Server): number {
|
|
const address = server.address();
|
|
if (!address || typeof address === 'string') {
|
|
throw new Error('Registry fixture is not listening on a TCP port');
|
|
}
|
|
return address.port;
|
|
}
|
|
|
|
function npmPackPackage(packageDir: string, outDir: string): string {
|
|
fs.mkdirSync(outDir, { recursive: true });
|
|
const result = spawnSync('npm', ['pack', packageDir, '--pack-destination', outDir], {
|
|
encoding: 'utf8',
|
|
});
|
|
assert.equal(
|
|
result.status,
|
|
0,
|
|
`npm pack failed\nstdout:\n${result.stdout ?? ''}\nstderr:\n${result.stderr ?? ''}`,
|
|
);
|
|
const fileName = result.stdout.trim().split(/\r?\n/).filter(Boolean).pop();
|
|
assert.equal(Boolean(fileName), true);
|
|
return path.join(outDir, fileName!);
|
|
}
|
|
|
|
describe('FP-30 mx install source resolution', () => {
|
|
const tempDirs: string[] = [];
|
|
const closeFns: Array<() => Promise<void>> = [];
|
|
const restoreEnvFns: Array<() => void> = [];
|
|
|
|
afterEach(async () => {
|
|
while (restoreEnvFns.length > 0) {
|
|
const restore = restoreEnvFns.pop();
|
|
if (restore) {
|
|
restore();
|
|
}
|
|
}
|
|
while (closeFns.length > 0) {
|
|
const close = closeFns.pop();
|
|
if (close) {
|
|
await close();
|
|
}
|
|
}
|
|
while (tempDirs.length > 0) {
|
|
const dir = tempDirs.pop();
|
|
if (dir) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
function useMatrixHome(matrixHome: string): void {
|
|
const previousMatrixHome = process.env.MATRIX_HOME;
|
|
const previousCliConfig = process.env.MATRIX_CLI_CONFIG;
|
|
const previousPackageRegistry = process.env.MATRIX_PACKAGE_REGISTRY;
|
|
const previousRegistryUrl = process.env.MATRIX_REGISTRY_URL;
|
|
process.env.MATRIX_HOME = matrixHome;
|
|
delete process.env.MATRIX_CLI_CONFIG;
|
|
delete process.env.MATRIX_PACKAGE_REGISTRY;
|
|
delete process.env.MATRIX_REGISTRY_URL;
|
|
restoreEnvFns.push(() => {
|
|
restoreEnv('MATRIX_HOME', previousMatrixHome);
|
|
restoreEnv('MATRIX_CLI_CONFIG', previousCliConfig);
|
|
restoreEnv('MATRIX_PACKAGE_REGISTRY', previousPackageRegistry);
|
|
restoreEnv('MATRIX_REGISTRY_URL', previousRegistryUrl);
|
|
});
|
|
}
|
|
|
|
function restoreEnv(key: string, value: string | undefined): void {
|
|
if (value === undefined) {
|
|
delete process.env[key];
|
|
return;
|
|
}
|
|
process.env[key] = value;
|
|
}
|
|
|
|
it('installs package from local directory, tarball, and registry name', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-sources-'));
|
|
tempDirs.push(root);
|
|
const workspaceDir = path.join(root, 'workspace');
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
|
const registryDir = path.join(root, '.matrix', 'registry');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
|
|
const fixture = createMatrixPackage(path.join(root, 'source'), {
|
|
packageName: '@acme/install-source',
|
|
version: '1.0.0',
|
|
componentType: 'InstallSourceActor',
|
|
mount: 'install.source',
|
|
});
|
|
|
|
const installFromDirectory = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
fixture.packageDir,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installFromDirectory, 0);
|
|
const installedFromDir = parseLastJson(installFromDirectory.logs) as { targetDir: string };
|
|
assert.equal(fs.existsSync(installedFromDir.targetDir), true);
|
|
|
|
const uninstallAfterDirectory = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'uninstall',
|
|
fixture.packageName,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(uninstallAfterDirectory, 0);
|
|
|
|
const packResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'pack',
|
|
'--out-dir',
|
|
path.join(root, 'packed'),
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(packResult, 0);
|
|
const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
|
|
assert.equal(fs.existsSync(packPayload.tarballPath), true);
|
|
|
|
const installFromTarball = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
packPayload.tarballPath,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installFromTarball, 0);
|
|
const installedFromTarball = parseLastJson(installFromTarball.logs) as { targetDir: string };
|
|
assert.equal(fs.existsSync(installedFromTarball.targetDir), true);
|
|
|
|
const uninstallAfterTarball = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'uninstall',
|
|
fixture.packageName,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(uninstallAfterTarball, 0);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-install',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const publishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(publishResult, 0);
|
|
|
|
const installFromRegistry = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
`${fixture.packageName}@${fixture.version}`,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installFromRegistry, 0);
|
|
const installedFromRegistry = parseLastJson(installFromRegistry.logs) as { source: string; targetDir: string };
|
|
assert.equal(installedFromRegistry.source, `${fixture.packageName}@${fixture.version}`);
|
|
assert.equal(fs.existsSync(installedFromRegistry.targetDir), true);
|
|
});
|
|
|
|
it('installs package from an npm-compatible registry URL with stored registry credentials', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-npm-registry-'));
|
|
tempDirs.push(root);
|
|
const workspaceDir = path.join(root, 'workspace');
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
const fixture = createMatrixPackage(path.join(root, 'source'), {
|
|
packageName: '@acme/remote-install',
|
|
version: '1.2.3',
|
|
componentType: 'RemoteInstallActor',
|
|
mount: 'remote.install',
|
|
});
|
|
fs.mkdirSync(path.join(fixture.packageDir, 'vendor', 'runtime-helper'), { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(fixture.packageDir, 'vendor', 'runtime-helper', 'package.json'),
|
|
JSON.stringify({
|
|
name: '@acme/runtime-helper',
|
|
version: '1.0.0',
|
|
type: 'module',
|
|
main: 'index.js',
|
|
}, null, 2),
|
|
'utf8',
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(fixture.packageDir, 'vendor', 'runtime-helper', 'index.js'),
|
|
'export const helper = true;\n',
|
|
'utf8',
|
|
);
|
|
const packageJsonPath = path.join(fixture.packageDir, 'package.json');
|
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')) as Record<string, unknown>;
|
|
packageJson.dependencies = {
|
|
'@acme/runtime-helper': 'file:./vendor/runtime-helper',
|
|
};
|
|
packageJson.devDependencies = {
|
|
'@open-matrix/core': '*',
|
|
};
|
|
packageJson.peerDependencies = {
|
|
'@open-matrix/core': '*',
|
|
};
|
|
packageJson.peerDependenciesMeta = {
|
|
'@open-matrix/core': { optional: true },
|
|
};
|
|
fs.writeFileSync(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf8');
|
|
|
|
const packResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'pack',
|
|
'--out-dir',
|
|
path.join(root, 'packed'),
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(packResult, 0);
|
|
const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
|
|
|
|
const registry = await startNpmRegistryFixture({
|
|
packageName: fixture.packageName,
|
|
version: fixture.version,
|
|
tarballPath: packPayload.tarballPath,
|
|
expectedToken: 'token-remote-install',
|
|
});
|
|
closeFns.push(registry.close);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-remote-install',
|
|
'--registry',
|
|
registry.registryUrl,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const installFromRemoteRegistry = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
`${fixture.packageName}@${fixture.version}`,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--registry',
|
|
registry.registryUrl,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installFromRemoteRegistry, 0);
|
|
|
|
const installed = parseLastJson(installFromRemoteRegistry.logs) as {
|
|
source: string;
|
|
targetDir: string;
|
|
version: string;
|
|
};
|
|
assert.equal(installed.source, `${fixture.packageName}@${fixture.version}`);
|
|
assert.equal(installed.version, fixture.version);
|
|
assert.equal(fs.existsSync(installed.targetDir), true);
|
|
assert.equal(
|
|
fs.existsSync(path.join(installed.targetDir, 'node_modules', '@acme', 'runtime-helper', 'package.json')),
|
|
true,
|
|
);
|
|
assert.equal(
|
|
fs.existsSync(path.join(installed.targetDir, 'node_modules', '@open-matrix', 'core', 'package.json')),
|
|
false,
|
|
);
|
|
assert.equal(registry.metadataRequests() > 0, true);
|
|
assert.equal(registry.tarballRequests() > 0, true);
|
|
assert.equal(
|
|
registry.authorizationHeaders().some((header) => header.includes('token-remote-install')),
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('installs package names from the configured npm registry without a per-command registry flag', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-config-registry-'));
|
|
tempDirs.push(root);
|
|
const workspaceDir = path.join(root, 'workspace');
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
useMatrixHome(path.join(root, '.matrix'));
|
|
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
const fixture = createMatrixPackage(path.join(root, 'source'), {
|
|
packageName: '@acme/configured-remote-install',
|
|
version: '1.2.3',
|
|
componentType: 'ConfiguredRemoteInstallActor',
|
|
mount: 'configured.remote.install',
|
|
});
|
|
|
|
const packResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'pack',
|
|
'--out-dir',
|
|
path.join(root, 'packed'),
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(packResult, 0);
|
|
const packPayload = parseLastJson(packResult.logs) as { tarballPath: string };
|
|
|
|
const registry = await startNpmRegistryFixture({
|
|
packageName: fixture.packageName,
|
|
version: fixture.version,
|
|
tarballPath: packPayload.tarballPath,
|
|
expectedToken: 'token-configured-install',
|
|
});
|
|
closeFns.push(registry.close);
|
|
|
|
const setConfig = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'config',
|
|
'set',
|
|
'registry',
|
|
registry.registryUrl,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(setConfig, 0);
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-configured-install',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const installFromConfiguredRegistry = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
`${fixture.packageName}@${fixture.version}`,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installFromConfiguredRegistry, 0);
|
|
|
|
const installed = parseLastJson(installFromConfiguredRegistry.logs) as {
|
|
source: string;
|
|
targetDir: string;
|
|
version: string;
|
|
};
|
|
assert.equal(installed.source, `${fixture.packageName}@${fixture.version}`);
|
|
assert.equal(installed.version, fixture.version);
|
|
assert.equal(fs.existsSync(installed.targetDir), true);
|
|
assert.equal(registry.metadataRequests() > 0, true);
|
|
assert.equal(registry.tarballRequests() > 0, true);
|
|
assert.equal(
|
|
registry.authorizationHeaders().some((header) => header.includes('token-configured-install')),
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('rejects npm registry packages whose matrix.json and package.json versions disagree', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-version-mismatch-'));
|
|
tempDirs.push(root);
|
|
const workspaceDir = path.join(root, 'workspace');
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
|
const fixture = createMatrixPackage(path.join(root, 'source'), {
|
|
packageName: '@acme/version-mismatch',
|
|
version: '1.0.0',
|
|
componentType: 'VersionMismatchActor',
|
|
mount: 'version.mismatch',
|
|
});
|
|
fs.writeFileSync(
|
|
path.join(fixture.packageDir, 'package.json'),
|
|
JSON.stringify({
|
|
name: fixture.packageName,
|
|
version: '9.9.9',
|
|
type: 'module',
|
|
main: 'src/index.ts',
|
|
}, null, 2),
|
|
'utf8',
|
|
);
|
|
const tarballPath = npmPackPackage(fixture.packageDir, path.join(root, 'packed'));
|
|
const registry = await startNpmRegistryFixture({
|
|
packageName: fixture.packageName,
|
|
version: '9.9.9',
|
|
tarballPath,
|
|
});
|
|
closeFns.push(registry.close);
|
|
|
|
const installResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
`${fixture.packageName}@9.9.9`,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--registry',
|
|
registry.registryUrl,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installResult, 1);
|
|
assert.equal(
|
|
installResult.errors.some((line) => line.includes('MX_PACKAGE_VERSION_MISMATCH')),
|
|
true,
|
|
);
|
|
});
|
|
|
|
it('normalizes whitespace-padded registry specifiers before install', async () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'mx-fp30-install-specifier-trim-'));
|
|
tempDirs.push(root);
|
|
const workspaceDir = path.join(root, 'workspace');
|
|
fs.mkdirSync(workspaceDir, { recursive: true });
|
|
|
|
const packagesDir = path.join(root, '.matrix', 'packages');
|
|
const registryDir = path.join(root, '.matrix', 'registry');
|
|
const credentialsFile = path.join(root, '.matrix', 'credentials.json');
|
|
|
|
const fixture = createMatrixPackage(path.join(root, 'source'), {
|
|
packageName: '@acme/install-source',
|
|
version: '1.0.0',
|
|
componentType: 'InstallSourceActor',
|
|
mount: 'install.source',
|
|
});
|
|
|
|
const loginResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'login',
|
|
'--username',
|
|
'alice',
|
|
'--token',
|
|
'token-install',
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(loginResult, 0);
|
|
|
|
const publishResult = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'publish',
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--credentials-file',
|
|
credentialsFile,
|
|
'--json',
|
|
], { cwd: fixture.packageDir });
|
|
assertExitCode(publishResult, 0);
|
|
|
|
const installFromRegistry = await runMxCli([
|
|
'node',
|
|
'mx',
|
|
'install',
|
|
` ${fixture.packageName}@${fixture.version} `,
|
|
'--packages-dir',
|
|
packagesDir,
|
|
'--registry-dir',
|
|
registryDir,
|
|
'--json',
|
|
], { cwd: workspaceDir });
|
|
assertExitCode(installFromRegistry, 0);
|
|
|
|
const installedFromRegistry = parseLastJson(installFromRegistry.logs) as { source: string; targetDir: string };
|
|
assert.equal(installedFromRegistry.source, `${fixture.packageName}@${fixture.version}`);
|
|
assert.equal(fs.existsSync(installedFromRegistry.targetDir), true);
|
|
});
|
|
});
|