#!/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));