hivecast-sdk/scripts/prove-fresh-samples.mjs

203 lines
6.3 KiB
JavaScript
Raw Normal View History

2026-06-08 06:41:34 +00:00
#!/usr/bin/env node
import { spawn, spawnSync } from 'node:child_process';
import net from 'node:net';
import { existsSync, readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { chromium } from 'playwright';
const root = resolve(new URL('..', import.meta.url).pathname);
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd ?? root,
stdio: options.stdio ?? 'inherit',
env: process.env,
encoding: 'utf8',
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}`);
}
return result;
}
function delay(ms) {
return new Promise((resolveDelay) => setTimeout(resolveDelay, ms));
}
function freePort() {
return new Promise((resolvePort, reject) => {
const server = net.createServer();
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
const port = typeof address === 'object' && address ? address.port : 0;
server.close(() => resolvePort(port));
});
});
}
async function waitForHttp(url, timeoutMs) {
const deadline = Date.now() + timeoutMs;
let lastError = null;
while (Date.now() < deadline) {
try {
const response = await fetch(url);
if (response.ok) return;
lastError = new Error(`${url} returned ${response.status}`);
} catch (error) {
lastError = error;
}
await delay(100);
}
throw lastError ?? new Error(`Timed out waiting for ${url}`);
}
function hasHiveCastEnv() {
return Boolean(process.env.MATRIX_API_KEY && process.env.MATRIX_CLOUD && process.env.MATRIX_SPACE);
}
function chromiumLaunchOptions() {
const explicit = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE;
if (explicit) return { headless: true, executablePath: explicit };
for (const candidate of ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser']) {
if (existsSync(candidate)) return { headless: true, executablePath: candidate };
}
return { headless: true };
}
function assertContains(file, text) {
const body = readFileSync(resolve(root, file), 'utf8');
if (!body.includes(text)) {
throw new Error(`${file} does not contain expected text: ${text}`);
}
}
function parseLastJsonObject(stdout) {
const text = stdout.trim();
for (let i = 0; i < text.length; i += 1) {
if (text[i] !== '{') continue;
try {
return JSON.parse(text.slice(i));
} catch {
// Keep scanning later object starts.
}
}
throw new Error(`No JSON proof object found in stdout:\n${stdout}`);
}
async function proveWebLive() {
const port = await freePort();
const origin = `http://127.0.0.1:${port}`;
const vite = spawn('pnpm', [
'--filter',
'matrix-sdk-example-fresh-web-page',
'exec',
'vite',
'--host',
'127.0.0.1',
'--port',
String(port),
'--strictPort',
], {
cwd: root,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
vite.stdout.on('data', (chunk) => {
stdout += String(chunk);
});
vite.stderr.on('data', (chunk) => {
stderr += String(chunk);
});
try {
await waitForHttp(origin, 15_000);
const browser = await chromium.launch(chromiumLaunchOptions());
const page = await browser.newPage();
const consoleErrors = [];
const failedRequests = [];
page.on('console', (msg) => {
if (msg.type() === 'error') consoleErrors.push(msg.text());
});
page.on('requestfailed', (request) => {
failedRequests.push({
url: request.url(),
failure: request.failure()?.errorText ?? 'unknown',
});
});
try {
await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 15_000 });
await page.waitForFunction(() => {
const text = document.querySelector('pre')?.textContent ?? '';
return text.includes('"ok": true') && text.includes('fresh-web-page');
}, { timeout: 20_000 });
const body = await page.locator('pre').innerText();
const proof = JSON.parse(body);
if (proof.ok !== true || proof.demo !== 'fresh-web-page') {
throw new Error(`Unexpected fresh web page proof: ${body}`);
}
if (consoleErrors.length > 0 || failedRequests.length > 0) {
throw new Error(`Browser proof had errors: ${JSON.stringify({ consoleErrors, failedRequests })}`);
}
return {
...proof,
origin,
consoleErrors,
failedRequests,
};
} finally {
await browser.close();
}
} catch (error) {
throw new Error([
error instanceof Error ? error.message : String(error),
stdout ? `vite stdout:\n${stdout}` : '',
stderr ? `vite stderr:\n${stderr}` : '',
].filter(Boolean).join('\n'));
} finally {
vite.kill('SIGTERM');
await delay(250);
if (!vite.killed) vite.kill('SIGKILL');
}
}
run('pnpm', ['--filter', '@open-matrix/core', 'build']);
run('pnpm', ['--filter', '@open-matrix/sdk', 'build']);
run('pnpm', ['--filter', 'matrix-sdk-example-fresh-server-actor', 'build']);
run('pnpm', ['--filter', 'matrix-sdk-example-fresh-web-page', 'build']);
assertContains('examples/fresh-server-actor/src/server.ts', "from '@open-matrix/sdk'");
assertContains('examples/fresh-server-actor/src/server.ts', 'exchangeHiveCastActorCredential');
assertContains('examples/fresh-web-page/src/main.ts', "from '@open-matrix/sdk'");
assertContains('examples/fresh-web-page/vite.config.ts', 'exchangeHiveCastActorCredential');
assertContains('examples/fresh-web-page/README.md', 'served by Vite, not by Matrix Web');
const live = hasHiveCastEnv();
let serverLive = null;
let webLive = null;
if (live) {
const result = run('pnpm', ['--filter', 'matrix-sdk-example-fresh-server-actor', 'check'], { stdio: 'pipe' });
serverLive = parseLastJsonObject(result.stdout);
if (serverLive.ok !== true || serverLive.demo !== 'fresh-server-actor') {
throw new Error(`Unexpected fresh server actor proof: ${result.stdout}`);
}
webLive = await proveWebLive();
}
console.log(JSON.stringify({
ok: true,
proof: 'fresh-samples',
built: [
'matrix-sdk-example-fresh-server-actor',
'matrix-sdk-example-fresh-web-page',
],
hivecastEnvPresent: live,
serverLive,
webLive,
}, null, 2));