258 lines
8.2 KiB
JavaScript
258 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
import { mkdtemp, rm, mkdir, writeFile, readdir, readFile, readlink } from 'node:fs/promises';
|
|
import { existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { spawnSync } from 'node:child_process';
|
|
|
|
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
const workspaceDir = path.resolve(scriptDir, '..');
|
|
|
|
const packages = [
|
|
'@open-matrix/contracts',
|
|
'@open-matrix/federation',
|
|
'@open-matrix/omega-core',
|
|
'@open-matrix/core',
|
|
'@open-matrix/browser-host',
|
|
'@open-matrix/browser-kit',
|
|
'@open-matrix/sdk',
|
|
'@open-matrix/cli',
|
|
];
|
|
|
|
function run(command, args, options = {}) {
|
|
const result = spawnSync(command, args, {
|
|
cwd: options.cwd ?? workspaceDir,
|
|
stdio: options.stdio ?? 'inherit',
|
|
env: { ...process.env, ...options.env },
|
|
encoding: 'utf8',
|
|
});
|
|
if (result.status !== 0) {
|
|
throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}`);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function findTarball(tarballDir, packageName) {
|
|
const packageDir = path.join(workspaceDir, 'packages', packageName.split('/').at(-1) === 'omega-core'
|
|
? 'omega-core'
|
|
: packageName.split('/').at(-1));
|
|
const packageJson = JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
|
|
const expectedPrefix = packageJson.name.replace('@', '').replace('/', '-');
|
|
const entries = await readdir(tarballDir);
|
|
const tarball = entries.find((entry) => entry.startsWith(expectedPrefix) && entry.endsWith('.tgz'));
|
|
if (!tarball) {
|
|
throw new Error(`Packed tarball missing for ${packageName} (${expectedPrefix}-*.tgz)`);
|
|
}
|
|
return path.join(tarballDir, tarball);
|
|
}
|
|
|
|
async function assertNoSymlinkToHarness(dir) {
|
|
const pending = [dir];
|
|
while (pending.length > 0) {
|
|
const current = pending.pop();
|
|
const entries = await readdir(current, { withFileTypes: true });
|
|
for (const entry of entries) {
|
|
const full = path.join(current, entry.name);
|
|
if (entry.isSymbolicLink()) {
|
|
const target = await readlink(full);
|
|
if (target.includes('matrix-work-harness')) {
|
|
throw new Error(`Consumer node_modules symlink points at harness: ${full}`);
|
|
}
|
|
continue;
|
|
}
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === '.pnpm' || entry.name === '.bin') continue;
|
|
pending.push(full);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
async function main() {
|
|
const proofRoot = await mkdtemp(path.join(tmpdir(), 'matrix-sdk-consumer-proof.'));
|
|
const tarballDir = path.join(proofRoot, 'tarballs');
|
|
const consumerDir = path.join(proofRoot, 'consumer');
|
|
let keep = process.env.MATRIX_SDK_KEEP_CONSUMER_PROOF === '1';
|
|
|
|
try {
|
|
await mkdir(tarballDir, { recursive: true });
|
|
await mkdir(path.join(consumerDir, 'src'), { recursive: true });
|
|
|
|
run('pnpm', ['run', 'clean']);
|
|
run('pnpm', ['run', 'build']);
|
|
for (const packageName of packages) {
|
|
run('pnpm', ['--filter', packageName, 'pack', '--pack-destination', tarballDir]);
|
|
}
|
|
|
|
const tarballs = Object.fromEntries(
|
|
await Promise.all(packages.map(async (packageName) => [packageName, await findTarball(tarballDir, packageName)]))
|
|
);
|
|
|
|
const tarballDependencies = Object.fromEntries(
|
|
Object.entries(tarballs).map(([packageName, tarballPath]) => [packageName, `file:${tarballPath}`])
|
|
);
|
|
|
|
await writeFile(path.join(consumerDir, 'package.json'), JSON.stringify({
|
|
name: 'matrix-sdk-external-consumer-proof',
|
|
version: '0.0.0',
|
|
private: true,
|
|
type: 'module',
|
|
scripts: {
|
|
build: 'tsc -p tsconfig.json',
|
|
run: 'node dist/index.js',
|
|
},
|
|
dependencies: tarballDependencies,
|
|
devDependencies: {
|
|
'@types/node': '^25.0.10',
|
|
typescript: '^5.7.0',
|
|
},
|
|
pnpm: {
|
|
overrides: tarballDependencies,
|
|
},
|
|
}, null, 2));
|
|
|
|
await writeFile(path.join(consumerDir, 'tsconfig.json'), JSON.stringify({
|
|
compilerOptions: {
|
|
target: 'ES2022',
|
|
module: 'NodeNext',
|
|
moduleResolution: 'NodeNext',
|
|
strict: true,
|
|
skipLibCheck: true,
|
|
outDir: 'dist',
|
|
},
|
|
include: ['src/**/*.ts'],
|
|
}, null, 2));
|
|
|
|
await writeFile(path.join(consumerDir, 'src', 'browser-actor.ts'), `import { MatrixActorHtmlElement } from '@open-matrix/core';
|
|
import type { MatrixAppReadyDetail } from '@open-matrix/browser-host/app-ready';
|
|
|
|
export class DemoBrowserActor extends MatrixActorHtmlElement {
|
|
static accepts = { 'ui.ping': {} };
|
|
|
|
describeReady(detail: MatrixAppReadyDetail): string {
|
|
return detail.appName;
|
|
}
|
|
}
|
|
`);
|
|
|
|
await writeFile(path.join(consumerDir, 'src', 'index.ts'), `import assert from 'node:assert/strict';
|
|
import {
|
|
InMemoryBroker,
|
|
InMemoryTransport,
|
|
MatrixActor,
|
|
MatrixRuntime,
|
|
TopicRouter,
|
|
} from '@open-matrix/core';
|
|
import { MatrixActor as SdkMatrixActor } from '@open-matrix/sdk';
|
|
|
|
class EchoActor extends MatrixActor {
|
|
static accepts = {
|
|
echo: {},
|
|
};
|
|
|
|
onEcho(payload: { message?: string }) {
|
|
return {
|
|
ok: true,
|
|
message: payload.message ?? null,
|
|
source: 'external-sdk-consumer',
|
|
};
|
|
}
|
|
}
|
|
|
|
async function request(transport: InMemoryTransport, mount: string, op: string, payload: unknown) {
|
|
const correlationId = 'consumer-proof-1';
|
|
const replyTo = '$replies.consumer-proof-1';
|
|
const response = new Promise<unknown>((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
unsubscribe();
|
|
reject(new Error('request timed out'));
|
|
}, 2000);
|
|
const unsubscribe = transport.subscribe(replyTo, (value) => {
|
|
clearTimeout(timeout);
|
|
unsubscribe();
|
|
resolve(value);
|
|
});
|
|
});
|
|
|
|
transport.publish(TopicRouter.inbox(mount), {
|
|
op,
|
|
payload,
|
|
replyTo,
|
|
correlationId,
|
|
});
|
|
|
|
return response;
|
|
}
|
|
|
|
const broker = new InMemoryBroker();
|
|
const transport = new InMemoryTransport(broker, { name: 'external-consumer' });
|
|
const runtime = new MatrixRuntime({ transport, logging: false });
|
|
|
|
try {
|
|
assert.equal(SdkMatrixActor, MatrixActor);
|
|
await runtime.create(EchoActor, 'demo.echo');
|
|
const response = await request(transport, 'demo.echo', 'echo', { message: 'hello-sdk' });
|
|
assert.deepEqual(response, {
|
|
ok: true,
|
|
correlationId: 'consumer-proof-1',
|
|
result: {
|
|
ok: true,
|
|
message: 'hello-sdk',
|
|
source: 'external-sdk-consumer',
|
|
},
|
|
});
|
|
console.log(JSON.stringify({ ok: true, mount: 'demo.echo', op: 'echo' }));
|
|
} finally {
|
|
await runtime.shutdown();
|
|
}
|
|
`);
|
|
|
|
run('pnpm', ['install', '--ignore-scripts'], { cwd: consumerDir });
|
|
run('pnpm', ['exec', 'matrix', '--help'], { cwd: consumerDir, stdio: 'pipe' });
|
|
run('pnpm', ['run', 'build'], { cwd: consumerDir });
|
|
const runtimeProof = run('pnpm', ['run', 'run'], { cwd: consumerDir, stdio: 'pipe' });
|
|
const stdout = runtimeProof.stdout.trim();
|
|
const parsed = JSON.parse(stdout.split('\n').at(-1));
|
|
if (parsed.ok !== true || parsed.mount !== 'demo.echo' || parsed.op !== 'echo') {
|
|
throw new Error(`Unexpected runtime proof output: ${stdout}`);
|
|
}
|
|
|
|
const consumerFiles = [
|
|
await readFile(path.join(consumerDir, 'package.json'), 'utf8'),
|
|
await readFile(path.join(consumerDir, 'tsconfig.json'), 'utf8'),
|
|
await readFile(path.join(consumerDir, 'src', 'index.ts'), 'utf8'),
|
|
await readFile(path.join(consumerDir, 'src', 'browser-actor.ts'), 'utf8'),
|
|
].join('\n');
|
|
if (consumerFiles.includes('matrix-work-harness') || consumerFiles.includes('projects/matrix-3')) {
|
|
throw new Error('Consumer proof contains a Matrix-3/harness path reference');
|
|
}
|
|
if (existsSync(path.join(consumerDir, 'node_modules'))) {
|
|
await assertNoSymlinkToHarness(path.join(consumerDir, 'node_modules'));
|
|
}
|
|
|
|
console.log(JSON.stringify({
|
|
ok: true,
|
|
proof: 'external-consumer',
|
|
consumerDir,
|
|
packages,
|
|
runtime: parsed,
|
|
matrix3PathReferences: false,
|
|
}, null, 2));
|
|
} catch (error) {
|
|
keep = true;
|
|
throw error;
|
|
} finally {
|
|
if (!keep) {
|
|
await rm(proofRoot, { recursive: true, force: true });
|
|
} else {
|
|
console.error(`[prove-external-consumer] kept proof directory: ${proofRoot}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.stack : error);
|
|
process.exit(1);
|
|
});
|