a post-component library for building user-interfaces on the web.
at main 108 lines 3.1 kB view raw
1import { createBirpc } from 'birpc' 2import * as fs from 'node:fs/promises' 3import * as path from 'node:path' 4import { fileURLToPath } from 'node:url' 5import { parseArgs, styleText } from 'node:util' 6import { create_browser_runtime } from './browser-runtime.ts' 7import { handle_coverage, type Coverage } from './coverage.ts' 8import * as devalue from './devalue.ts' 9import { create_node_runtime } from './node-runtime.ts' 10import type { ClientFunctions, TestResult } from './runtime.ts' 11 12export interface ServerFunctions { 13 report_result(result: TestResult): void 14} 15 16export interface Runtime { 17 port: MessagePort 18 coverage(): Promise<Coverage[]> 19 [Symbol.asyncDispose](): Promise<void> 20} 21 22const args = parseArgs({ 23 options: { 24 bench: { type: 'boolean', short: 'b', default: false }, 25 prod: { type: 'boolean', short: 'p', default: false }, 26 filter: { type: 'string', short: 'f' }, 27 }, 28 allowPositionals: true, 29}) 30 31const filter = args.values.filter !== undefined ? new RegExp(args.values.filter) : undefined 32 33const all_files: { [runtime: string]: string[] } = {} 34for (const arg of args.positionals) { 35 for await (const file of fs.glob(arg)) { 36 const runtime = file.includes('server') ? 'node' : 'browser' 37 ;(all_files[runtime] ??= []).push(file) 38 } 39} 40 41const results: TestResult[] = [] 42const coverage: Coverage[] = [] 43 44for (const [runtime, files] of Object.entries(all_files)) { 45 const collect_coverage = !args.values.bench 46 const rt = 47 runtime === 'node' 48 ? await create_node_runtime({ collect_coverage }) 49 : await create_browser_runtime({ collect_coverage }) 50 await using _ = rt // workaround for https://issues.chromium.org/issues/409478039 51 52 const client = createBirpc<ClientFunctions, ServerFunctions>( 53 { 54 report_result(run) { 55 if (run.result === 'pass') { 56 console.log(styleText('green', 'PASS'), run.name, styleText('dim', `(${run.duration.toFixed(1)}ms)`)) 57 } else { 58 console.log(styleText('red', 'FAIL'), run.name) 59 console.log(run.reason) 60 console.log() 61 } 62 63 results.push(run) 64 }, 65 }, 66 { 67 post: data => rt.port.postMessage(data), 68 on: fn => { 69 rt.port.onmessage = e => fn(e.data) 70 }, 71 serialize: devalue.stringify, 72 deserialize: devalue.parse, 73 }, 74 ) 75 76 await client.define('__DEV__', !args.values.prod) 77 78 const here = path.join(fileURLToPath(import.meta.url), '..') 79 await Promise.all(files.map(file => client.import('./' + path.relative(here, file)))) 80 81 if (args.values.bench) { 82 await client.run_benchmarks({ filter }) 83 } else { 84 await client.run_tests({ filter }) 85 await client.stop_coverage() 86 coverage.push(...(await rt.coverage())) 87 } 88} 89 90if (args.values.bench) { 91} else { 92 await handle_coverage(coverage) 93 94 if (results.length === 0) { 95 console.log('no tests found') 96 process.exitCode = 1 97 } else { 98 const passed = results.reduce((count, { result }) => count + (result === 'pass' ? 1 : 0), 0) 99 const failed = results.reduce((count, { result }) => count + (result === 'fail' ? 1 : 0), 0) 100 101 console.log() 102 console.log(`${passed} passed`) 103 if (failed) { 104 console.log(`${failed} failed`) 105 process.exitCode = 1 106 } 107 } 108}