@napplet/conformance-cli 0.2.1 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { readFile as readFile4, writeFile, stat as stat4 } from "fs/promises";
5
- import { join as join4, dirname, resolve as resolve3 } from "path";
4
+ import { readFile as readFile4, writeFile, stat as stat5 } from "fs/promises";
5
+ import { join as join5, dirname, resolve as resolve4 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import { spawn } from "child_process";
8
8
  import { chromium } from "playwright";
@@ -259,6 +259,20 @@ async function scanForbiddenGlobals(dir) {
259
259
  return [...found];
260
260
  }
261
261
 
262
+ // src/resolve-napplet.ts
263
+ import { stat as stat4 } from "fs/promises";
264
+ import { join as join4, resolve as resolve3 } from "path";
265
+ async function isFile(path) {
266
+ return (await stat4(path).catch(() => null))?.isFile() ?? false;
267
+ }
268
+ async function resolveNappletDir(target) {
269
+ const base = resolve3(target);
270
+ const dist = join4(base, "dist");
271
+ if (await isFile(join4(dist, "index.html"))) return { dir: dist, indexHtml: join4(dist, "index.html") };
272
+ if (await isFile(join4(base, "index.html"))) return { dir: base, indexHtml: join4(base, "index.html") };
273
+ throw new Error(`No index.html found in ${join4(dist, "index.html")} or ${join4(base, "index.html")}. Build the napplet first.`);
274
+ }
275
+
262
276
  // src/cli.ts
263
277
  var HELP = `napplet-conformance \u2014 run NAP protocol conformance against a napplet
264
278
 
@@ -268,7 +282,7 @@ Usage:
268
282
  napplet-conformance --url https://my.napplet/ [options]
269
283
 
270
284
  Arguments:
271
- dir Built napplet directory (looks for ./index.html or ./dist/index.html)
285
+ dir Built napplet directory (prefers ./dist/index.html, falls back to ./index.html)
272
286
 
273
287
  Options:
274
288
  --url <url> Test a remotely-served napplet instead of a local dir (headless)
@@ -356,16 +370,6 @@ function parseArgs(argv) {
356
370
  }
357
371
  return opts;
358
372
  }
359
- async function isFile(path) {
360
- return (await stat4(path).catch(() => null))?.isFile() ?? false;
361
- }
362
- async function resolveNappletDir(target) {
363
- const base = resolve3(target);
364
- if (await isFile(join4(base, "index.html"))) return { dir: base, indexHtml: join4(base, "index.html") };
365
- const dist = join4(base, "dist");
366
- if (await isFile(join4(dist, "index.html"))) return { dir: dist, indexHtml: join4(dist, "index.html") };
367
- throw new Error(`No index.html found in ${base} or ${dist}. Build the napplet first.`);
368
- }
369
373
  async function collectBoot(hostUrl, allowSameOrigin, totalTimeoutMs) {
370
374
  const browser = await chromium.launch({ args: ["--no-sandbox"] });
371
375
  try {
@@ -389,7 +393,7 @@ async function collectBoot(hostUrl, allowSameOrigin, totalTimeoutMs) {
389
393
  }
390
394
  }
391
395
  async function isDir(path) {
392
- return (await stat4(path).catch(() => null))?.isDirectory() ?? false;
396
+ return (await stat5(path).catch(() => null))?.isDirectory() ?? false;
393
397
  }
394
398
  function openBrowser(url) {
395
399
  try {
@@ -416,14 +420,14 @@ ${HELP}`);
416
420
  }
417
421
  const resolved = await resolveNappletDir(opts.target);
418
422
  const here = dirname(fileURLToPath(import.meta.url));
419
- const appDir = join4(here, "ui");
423
+ const appDir = join5(here, "ui");
420
424
  if (!await isDir(appDir)) {
421
425
  process.stderr.write(`Conformance UI assets are missing at ${appDir}. Rebuild @napplet/conformance-cli.
422
426
  `);
423
427
  return 2;
424
428
  }
425
429
  const server = await startUiServer({ appDir, nappletDir: resolved.dir, port: opts.port });
426
- const child = opts.exec ? spawn(opts.exec, { cwd: resolve3(opts.target), shell: true, stdio: "inherit" }) : null;
430
+ const child = opts.exec ? spawn(opts.exec, { cwd: resolve4(opts.target), shell: true, stdio: "inherit" }) : null;
427
431
  process.stdout.write(
428
432
  `
429
433
  napplet conformance UI \u2192 ${server.appUrl}
@@ -484,7 +488,7 @@ ${HELP}`);
484
488
  bootUrl = "/";
485
489
  }
486
490
  const here = dirname(fileURLToPath(import.meta.url));
487
- const hostBundle = await readFile4(join4(here, "host-bundle.global.js"), "utf8");
491
+ const hostBundle = await readFile4(join5(here, "host-bundle.global.js"), "utf8");
488
492
  const server = await startHarnessServer({
489
493
  nappletDir,
490
494
  hostBundle,
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/static.ts","../src/ui-server.ts","../src/scan.ts"],"sourcesContent":["/**\n * @napplet/conformance-cli -- `napplet-conformance` entry point.\n *\n * Runs the @napplet/conformance engine against a napplet in real headless Chromium:\n *\n * 1. Resolve the napplet's built directory (or a remote `--url`).\n * 2. Serve it on loopback alongside a host harness page + the engine bundle.\n * 3. Drive Playwright Chromium to `bootAndCollect` inside the host page.\n * 4. Assemble a ConformanceContext on the node side (manifest HTML + static scan),\n * run the check catalog, print the chosen report, and set the exit code.\n *\n * Exit codes: `0` conformant, `1` non-conformant, `2` usage/runtime error.\n *\n * @packageDocumentation\n */\n\nimport { readFile, writeFile, stat } from 'node:fs/promises';\nimport { join, dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { spawn } from 'node:child_process';\nimport { chromium } from 'playwright';\nimport {\n buildContext,\n runConformance,\n report,\n type BootCollection,\n type ReporterFormat,\n} from '@napplet/conformance';\nimport { startHarnessServer } from './server.js';\nimport { startUiServer } from './ui-server.js';\nimport { scanForbiddenGlobals } from './scan.js';\n\ninterface CliOptions {\n target?: string;\n url?: string;\n reporter: ReporterFormat;\n out?: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n allowSameOrigin: boolean;\n ui: boolean;\n port?: number;\n open: boolean;\n exec?: string;\n help: boolean;\n}\n\nconst HELP = `napplet-conformance — run NAP protocol conformance against a napplet\n\nUsage:\n napplet-conformance [dir] [options] # headless, one-shot, CI exit code\n napplet-conformance --ui [dir] [options] # open the live web runtime (like vitest --ui)\n napplet-conformance --url https://my.napplet/ [options]\n\nArguments:\n dir Built napplet directory (looks for ./index.html or ./dist/index.html)\n\nOptions:\n --url <url> Test a remotely-served napplet instead of a local dir (headless)\n --reporter <fmt> pretty | json | junit (default: pretty)\n --out <file> Write the report to a file instead of stdout\n --ready-timeout <ms> Boot timeout waiting for shell.ready (default: 5000)\n --settle <ms> Envelope-collection window after boot (default: 600)\n --no-degraded Skip the no-capability graceful-degradation pass\n --allow-same-origin Debug: also grant allow-same-origin (a conformant napplet must not need it)\n\nUI / watch mode (--ui):\n --port <n> Port for the UI server (default: random)\n --no-open Do not open a browser automatically\n --exec <cmd> Run a command (e.g. \"vite build --watch\") so edits rebuild the served dir;\n the watcher then re-runs conformance live on every change\n -h, --help Show this help\n\nExit codes (headless): 0 conformant, 1 non-conformant, 2 usage/runtime error.\n`;\n\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = {\n reporter: 'pretty',\n readyTimeoutMs: 5000,\n settleMs: 600,\n runDegraded: true,\n allowSameOrigin: false,\n ui: false,\n open: true,\n help: false,\n };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n const value = (): string => {\n const v = argv[i + 1];\n if (v === undefined || v.startsWith('--')) throw new Error(`Missing value for ${arg}`);\n i += 1;\n return v;\n };\n switch (arg) {\n case '-h':\n case '--help':\n opts.help = true;\n break;\n case '--url':\n opts.url = value();\n break;\n case '--reporter': {\n const r = value();\n if (r !== 'pretty' && r !== 'json' && r !== 'junit') throw new Error(`Invalid reporter: ${r}`);\n opts.reporter = r;\n break;\n }\n case '--out':\n opts.out = value();\n break;\n case '--ready-timeout':\n opts.readyTimeoutMs = Number(value());\n break;\n case '--settle':\n opts.settleMs = Number(value());\n break;\n case '--no-degraded':\n opts.runDegraded = false;\n break;\n case '--allow-same-origin':\n opts.allowSameOrigin = true;\n break;\n case '--ui':\n opts.ui = true;\n break;\n case '--port':\n opts.port = Number(value());\n break;\n case '--no-open':\n opts.open = false;\n break;\n case '--exec':\n opts.exec = value();\n break;\n default:\n if (arg.startsWith('--')) throw new Error(`Unknown option: ${arg}`);\n if (opts.target) throw new Error(`Unexpected extra argument: ${arg}`);\n opts.target = arg;\n }\n }\n return opts;\n}\n\nasync function isFile(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isFile() ?? false;\n}\n\n/** Resolve the directory + index.html for a local napplet target. */\nasync function resolveNappletDir(target: string): Promise<{ dir: string; indexHtml: string }> {\n const base = resolve(target);\n if (await isFile(join(base, 'index.html'))) return { dir: base, indexHtml: join(base, 'index.html') };\n const dist = join(base, 'dist');\n if (await isFile(join(dist, 'index.html'))) return { dir: dist, indexHtml: join(dist, 'index.html') };\n throw new Error(`No index.html found in ${base} or ${dist}. Build the napplet first.`);\n}\n\nasync function collectBoot(hostUrl: string, allowSameOrigin: boolean, totalTimeoutMs: number): Promise<BootCollection> {\n const browser = await chromium.launch({ args: ['--no-sandbox'] });\n try {\n const page = await browser.newPage();\n page.on('pageerror', (e) => process.stderr.write(`[host page error] ${e.message}\\n`));\n await page.goto(hostUrl, { waitUntil: 'load' });\n await page.waitForFunction(\n () => window.__conformanceBoot__ !== undefined || window.__conformanceError__ !== undefined,\n undefined,\n { timeout: totalTimeoutMs },\n );\n const error = await page.evaluate(() => window.__conformanceError__);\n if (error) throw new Error(`Host harness failed: ${error}`);\n const boot = await page.evaluate(() => window.__conformanceBoot__);\n if (!boot) throw new Error('Host harness produced no result');\n void allowSameOrigin;\n return boot;\n } finally {\n await browser.close();\n }\n}\n\nasync function isDir(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isDirectory() ?? false;\n}\n\n/** Best-effort: open a URL in the default browser. */\nfunction openBrowser(url: string): void {\n try {\n if (process.platform === 'darwin') {\n spawn('open', [url], { stdio: 'ignore', detached: true }).unref();\n } else if (process.platform === 'win32') {\n spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();\n } else {\n spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();\n }\n } catch {\n /* best-effort — the URL is printed regardless */\n }\n}\n\n/** `--ui`: serve the live web runtime + the napplet, watch, and re-run on change. */\nasync function runUiMode(opts: CliOptions): Promise<number> {\n if (opts.url) {\n process.stderr.write('--ui serves a local napplet directory; --url is not supported in UI mode.\\n');\n return 2;\n }\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n const here = dirname(fileURLToPath(import.meta.url));\n const appDir = join(here, 'ui');\n if (!(await isDir(appDir))) {\n process.stderr.write(`Conformance UI assets are missing at ${appDir}. Rebuild @napplet/conformance-cli.\\n`);\n return 2;\n }\n\n const server = await startUiServer({ appDir, nappletDir: resolved.dir, port: opts.port });\n\n const child = opts.exec ? spawn(opts.exec, { cwd: resolve(opts.target), shell: true, stdio: 'inherit' }) : null;\n\n process.stdout.write(\n `\\n napplet conformance UI → ${server.appUrl}\\n` +\n ` watching ${resolved.dir}${opts.exec ? ` (running: ${opts.exec})` : ''}\\n` +\n ` Ctrl-C to stop\\n\\n`,\n );\n if (opts.open) openBrowser(server.appUrl);\n\n await new Promise<void>((resolveWait) => {\n let closing = false;\n const shutdown = (): void => {\n if (closing) return;\n closing = true;\n child?.kill();\n void server.close().finally(resolveWait);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n });\n return 0;\n}\n\nasync function main(argv: string[]): Promise<number> {\n let opts: CliOptions;\n try {\n opts = parseArgs(argv);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n\\n${HELP}`);\n return 2;\n }\n if (opts.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n if (opts.ui) {\n return runUiMode(opts);\n }\n\n // Resolve the subject: local dir or remote URL.\n let nappletDir: string | null = null;\n let manifestHtml: string;\n let forbiddenGlobals: string[] = [];\n let bootUrl: string;\n\n if (opts.url) {\n bootUrl = opts.url;\n manifestHtml = await (await fetch(opts.url)).text();\n } else {\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory or --url.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n nappletDir = resolved.dir;\n manifestHtml = await readFile(resolved.indexHtml, 'utf8');\n forbiddenGlobals = await scanForbiddenGlobals(resolved.dir);\n bootUrl = '/';\n }\n\n const here = dirname(fileURLToPath(import.meta.url));\n const hostBundle = await readFile(join(here, 'host-bundle.global.js'), 'utf8');\n\n const server = await startHarnessServer({\n nappletDir,\n hostBundle,\n bootConfig: {\n url: bootUrl,\n readyTimeoutMs: opts.readyTimeoutMs,\n settleMs: opts.settleMs,\n runDegraded: opts.runDegraded,\n },\n });\n\n let boot: BootCollection;\n try {\n // Generous overall budget: two boots (primary + degraded), each up to\n // readyTimeout + settle, plus browser startup.\n const totalTimeout = (opts.readyTimeoutMs + opts.settleMs) * 2 + 30000;\n boot = await collectBoot(server.hostUrl, opts.allowSameOrigin, totalTimeout);\n } finally {\n await server.close();\n }\n\n const context = buildContext({ manifestHtml, boot, forbiddenGlobals });\n const run = runConformance(context);\n const output = report(run, opts.reporter);\n\n if (opts.out) {\n await writeFile(opts.out, `${output}\\n`, 'utf8');\n process.stdout.write(`Report written to ${opts.out}\\n`);\n } else {\n process.stdout.write(`${output}\\n`);\n }\n\n return run.ok ? 0 : 1;\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((err: unknown) => {\n process.stderr.write(`napplet-conformance: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`);\n process.exitCode = 2;\n });\n","/**\n * @napplet/conformance-cli -- Loopback harness server.\n *\n * Serves the napplet's files plus two reserved harness routes (the host page and\n * the browser bundle). Every response carries `Access-Control-Allow-Origin: *`:\n * the napplet iframe runs at an opaque origin (sandbox without `allow-same-origin`),\n * so its `<script type=\"module\">` subresources are fetched in CORS mode and would\n * otherwise be blocked.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, normalize, extname, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { MIME } from './static.js';\n\n/** The napplet URL + timing knobs the host page passes to `bootAndCollect`. */\nexport interface BootConfig {\n url: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n}\n\n/** Options for {@link startHarnessServer}. */\nexport interface HarnessServerOptions {\n /** Directory of napplet files to serve, or `null` in remote-URL mode. */\n nappletDir: string | null;\n /** JavaScript source of the host browser bundle. */\n hostBundle: string;\n /** Boot configuration injected into the host page. */\n bootConfig: BootConfig;\n}\n\n/** A running harness server. */\nexport interface HarnessServer {\n /** Base origin, e.g. `http://127.0.0.1:54213`. */\n origin: string;\n /** URL of the host harness page. */\n hostUrl: string;\n /** Shut the server down. */\n close(): Promise<void>;\n}\n\nconst HOST_HTML_ROUTE = '/__conformance__/host.html';\nconst HOST_BUNDLE_ROUTE = '/__conformance__/host-bundle.js';\n\nfunction renderHostPage(boot: BootConfig): string {\n // The inline script lives on the harness page (full privilege, our origin), not\n // in the sandboxed napplet — so it is not subject to the napplet CSP.\n const cfg = JSON.stringify(boot);\n return `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>napplet conformance host</title></head>\n<body>\n<script src=\"${HOST_BUNDLE_ROUTE}\"></script>\n<script>\n (function () {\n var cfg = ${cfg};\n window.NappletConformanceHost.run(cfg).then(function (boot) {\n window.__conformanceBoot__ = boot;\n }).catch(function (err) {\n window.__conformanceError__ = String((err && err.stack) || err);\n });\n })();\n</script>\n</body>\n</html>`;\n}\n\n/** Start the loopback harness server on a random port. */\nexport async function startHarnessServer(options: HarnessServerOptions): Promise<HarnessServer> {\n const dir = options.nappletDir ? resolve(options.nappletDir) : null;\n const hostHtml = renderHostPage(options.bootConfig);\n\n const server: Server = createServer((req, res) => {\n void serve(req.url ?? '/', res);\n });\n\n async function serve(rawUrl: string, res: ServerResponse): Promise<void> {\n res.setHeader('Access-Control-Allow-Origin', '*');\n const path = decodeURIComponent(rawUrl.split('?')[0]);\n\n if (path === HOST_HTML_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.html'] });\n res.end(hostHtml);\n return;\n }\n if (path === HOST_BUNDLE_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.js'] });\n res.end(options.hostBundle);\n return;\n }\n\n if (!dir) {\n res.writeHead(404).end('not found');\n return;\n }\n\n // Resolve a safe path under the napplet dir.\n const relative = normalize(path).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n let filePath = join(dir, relative);\n if (!filePath.startsWith(dir)) {\n res.writeHead(403).end('forbidden');\n return;\n }\n try {\n let info = await stat(filePath).catch(() => null);\n if (info?.isDirectory()) {\n filePath = join(filePath, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (path === '/' && !info) {\n filePath = join(dir, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (!info?.isFile()) {\n res.writeHead(404).end('not found');\n return;\n }\n const body = await readFile(filePath);\n res.writeHead(200, { 'Content-Type': MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(body);\n } catch {\n res.writeHead(500).end('server error');\n }\n }\n\n await new Promise<void>((resolveListen) => server.listen(0, '127.0.0.1', resolveListen));\n const address = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${address.port}`;\n\n return {\n origin,\n hostUrl: `${origin}${HOST_HTML_ROUTE}`,\n close: () => new Promise<void>((res, rej) => server.close((err) => (err ? rej(err) : res()))),\n };\n}\n","/**\n * @napplet/conformance-cli -- Shared static-serving helpers used by both the\n * one-shot harness server and the `--ui` watch server.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { extname } from 'node:path';\nimport type { ServerResponse } from 'node:http';\n\n/** Content types by extension. */\nexport const MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.wasm': 'application/wasm',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.map': 'application/json; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n};\n\n/** The napplet iframe runs at an opaque origin; allow its module subresources. */\nexport function setCors(res: ServerResponse): void {\n res.setHeader('Access-Control-Allow-Origin', '*');\n}\n\n/** Send a file if it exists; return false (without writing) when it does not. */\nexport async function sendFile(res: ServerResponse, absPath: string): Promise<boolean> {\n const info = await stat(absPath).catch(() => null);\n if (!info?.isFile()) return false;\n res.writeHead(200, { 'Content-Type': MIME[extname(absPath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(await readFile(absPath));\n return true;\n}\n","/**\n * @napplet/conformance-cli -- The `--ui` watch server.\n *\n * Serves the standalone conformance web app (bundled with the CLI), the napplet\n * under test (same origin, with permissive CORS for the sandboxed iframe), and a\n * Server-Sent-Events stream. It watches the napplet's served directory and pushes\n * a `rerun` event on every change, so the open app re-runs conformance live — the\n * `vitest --ui` model: edit, save, see the verdict update.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { setCors, sendFile } from './static.js';\n\n/** Path the app under `?live=1` is served the napplet from. */\nconst NAPPLET_PREFIX = '/__napplet__/';\n/** SSE endpoint the live app subscribes to. */\nconst EVENTS_ROUTE = '/__conformance__/events';\n\n/** Options for {@link startUiServer}. */\nexport interface UiServerOptions {\n /** Directory of the built conformance web app (served at `/`). */\n appDir: string;\n /** Directory of the built napplet under test (served at `/__napplet__/`). */\n nappletDir: string;\n /** Preferred port (0 = random). */\n port?: number;\n /** Debounce window (ms) for coalescing file-change bursts. Default 120. */\n debounceMs?: number;\n}\n\n/** A running `--ui` server. */\nexport interface UiServer {\n /** Base origin, e.g. `http://127.0.0.1:5219`. */\n origin: string;\n /** The app URL to open (already pointed at the napplet, live mode on). */\n appUrl: string;\n /** Force a `rerun` broadcast (used for the manual/initial trigger). */\n notify(): void;\n /** Shut the server + watcher down. */\n close(): Promise<void>;\n}\n\nfunction safeJoin(dir: string, urlPath: string): string | null {\n const relative = normalize(urlPath).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n const full = join(dir, relative);\n return full.startsWith(dir) ? full : null;\n}\n\n/** Register an SSE subscriber and keep the connection open. */\nfunction handleSse(res: ServerResponse, clients: Set<ServerResponse>): void {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n });\n res.write('retry: 1000\\n\\n');\n clients.add(res);\n res.on('close', () => clients.delete(res));\n}\n\n/** Serve a file under `/__napplet__/`. */\nasync function handleNapplet(res: ServerResponse, nappletDir: string, path: string): Promise<void> {\n const rel = path.slice(NAPPLET_PREFIX.length) || 'index.html';\n const abs = safeJoin(nappletDir, rel);\n if (abs && (await sendFile(res, abs))) return;\n res.writeHead(404).end('napplet asset not found');\n}\n\n/** Serve the conformance web app, with an SPA fallback to its index. */\nasync function handleApp(res: ServerResponse, appDir: string, path: string): Promise<void> {\n const abs = safeJoin(appDir, path === '/' ? 'index.html' : path);\n if (abs && (await sendFile(res, abs))) return;\n if (await sendFile(res, join(appDir, 'index.html'))) return;\n res.writeHead(404).end('not found');\n}\n\n/** Watch a directory (recursively when supported) and call `fire` debounced. */\nfunction startWatcher(dir: string, debounceMs: number, fire: () => void): { close(): void } {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const onEvent = (): void => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(fire, debounceMs);\n };\n let watcher: FSWatcher;\n try {\n watcher = watch(dir, { recursive: true }, onEvent);\n } catch {\n // Recursive watch may be unsupported on some platforms.\n watcher = watch(dir, onEvent);\n }\n return {\n close() {\n if (timer) clearTimeout(timer);\n watcher.close();\n },\n };\n}\n\n/** Start the `--ui` watch server. */\nexport async function startUiServer(options: UiServerOptions): Promise<UiServer> {\n const appDir = resolve(options.appDir);\n const nappletDir = resolve(options.nappletDir);\n const clients = new Set<ServerResponse>();\n const broadcast = (): void => {\n for (const res of clients) res.write(`event: rerun\\ndata: ${Date.now()}\\n\\n`);\n };\n\n const server: Server = createServer((req, res) => {\n setCors(res);\n res.setHeader('Cache-Control', 'no-store'); // never serve stale napplet code\n const path = decodeURIComponent((req.url ?? '/').split('?')[0]);\n if (path === EVENTS_ROUTE) {\n handleSse(res, clients);\n } else if (path === NAPPLET_PREFIX.slice(0, -1) || path.startsWith(NAPPLET_PREFIX)) {\n void handleNapplet(res, nappletDir, path);\n } else {\n void handleApp(res, appDir, path);\n }\n });\n\n await new Promise<void>((r) => server.listen(options.port ?? 0, '127.0.0.1', r));\n const { port } = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${port}`;\n const appUrl = `${origin}/?url=${encodeURIComponent(NAPPLET_PREFIX)}&live=1`;\n const watcher = startWatcher(nappletDir, options.debounceMs ?? 120, broadcast);\n\n return {\n origin,\n appUrl,\n notify: broadcast,\n close: () =>\n new Promise<void>((res, rej) => {\n watcher.close();\n for (const c of clients) c.end();\n clients.clear();\n server.close((err) => (err ? rej(err) : res()));\n }),\n };\n}\n","/**\n * @napplet/conformance-cli -- Static scan for forbidden globals.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe `window.nostr` access at runtime. Instead we scan the served\n * source for references to globals a shell never provides. This is a heuristic, but\n * a true positive is meaningful: shells removed `window.nostr` from napplets in\n * v0.24.0, so any reference is dead or broken code.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\n\n/** Patterns considered forbidden, keyed by the label reported in the verdict. */\nconst FORBIDDEN: Array<{ label: string; re: RegExp }> = [\n { label: 'window.nostr', re: /\\bwindow\\s*\\.\\s*nostr\\b/ },\n { label: 'globalThis.nostr', re: /\\bglobalThis\\s*\\.\\s*nostr\\b/ },\n];\n\nconst SCANNABLE = new Set(['.js', '.mjs', '.cjs', '.html', '.htm']);\nconst SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'coverage']);\n\nasync function collectFiles(dir: string, acc: string[]): Promise<void> {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n await collectFiles(join(dir, entry.name), acc);\n } else if (entry.isFile() && SCANNABLE.has(extname(entry.name).toLowerCase())) {\n acc.push(join(dir, entry.name));\n }\n }\n}\n\n/**\n * Scan a napplet directory for forbidden global references.\n *\n * @param dir - The directory of served napplet files.\n * @returns The distinct forbidden labels found (e.g. `['window.nostr']`).\n */\nexport async function scanForbiddenGlobals(dir: string): Promise<string[]> {\n const info = await stat(dir).catch(() => null);\n if (!info?.isDirectory()) return [];\n const files: string[] = [];\n await collectFiles(dir, files);\n const found = new Set<string>();\n for (const file of files) {\n const text = await readFile(file, 'utf8').catch(() => '');\n for (const { label, re } of FORBIDDEN) {\n if (re.test(text)) found.add(label);\n }\n }\n return [...found];\n}\n"],"mappings":"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","names":["readFile","stat","join","resolve","readFile","stat","extname","stat","readFile","extname","createServer","join","normalize","resolve","normalize","join","resolve","createServer","readFile","stat","join","extname","stat","resolve","join","readFile"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/server.ts","../src/static.ts","../src/ui-server.ts","../src/scan.ts","../src/resolve-napplet.ts"],"sourcesContent":["/**\n * @napplet/conformance-cli -- `napplet-conformance` entry point.\n *\n * Runs the @napplet/conformance engine against a napplet in real headless Chromium:\n *\n * 1. Resolve the napplet's built directory (or a remote `--url`).\n * 2. Serve it on loopback alongside a host harness page + the engine bundle.\n * 3. Drive Playwright Chromium to `bootAndCollect` inside the host page.\n * 4. Assemble a ConformanceContext on the node side (manifest HTML + static scan),\n * run the check catalog, print the chosen report, and set the exit code.\n *\n * Exit codes: `0` conformant, `1` non-conformant, `2` usage/runtime error.\n *\n * @packageDocumentation\n */\n\nimport { readFile, writeFile, stat } from 'node:fs/promises';\nimport { join, dirname, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { spawn } from 'node:child_process';\nimport { chromium } from 'playwright';\nimport {\n buildContext,\n runConformance,\n report,\n type BootCollection,\n type ReporterFormat,\n} from '@napplet/conformance';\nimport { startHarnessServer } from './server.js';\nimport { startUiServer } from './ui-server.js';\nimport { scanForbiddenGlobals } from './scan.js';\nimport { resolveNappletDir } from './resolve-napplet.js';\n\ninterface CliOptions {\n target?: string;\n url?: string;\n reporter: ReporterFormat;\n out?: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n allowSameOrigin: boolean;\n ui: boolean;\n port?: number;\n open: boolean;\n exec?: string;\n help: boolean;\n}\n\nconst HELP = `napplet-conformance — run NAP protocol conformance against a napplet\n\nUsage:\n napplet-conformance [dir] [options] # headless, one-shot, CI exit code\n napplet-conformance --ui [dir] [options] # open the live web runtime (like vitest --ui)\n napplet-conformance --url https://my.napplet/ [options]\n\nArguments:\n dir Built napplet directory (prefers ./dist/index.html, falls back to ./index.html)\n\nOptions:\n --url <url> Test a remotely-served napplet instead of a local dir (headless)\n --reporter <fmt> pretty | json | junit (default: pretty)\n --out <file> Write the report to a file instead of stdout\n --ready-timeout <ms> Boot timeout waiting for shell.ready (default: 5000)\n --settle <ms> Envelope-collection window after boot (default: 600)\n --no-degraded Skip the no-capability graceful-degradation pass\n --allow-same-origin Debug: also grant allow-same-origin (a conformant napplet must not need it)\n\nUI / watch mode (--ui):\n --port <n> Port for the UI server (default: random)\n --no-open Do not open a browser automatically\n --exec <cmd> Run a command (e.g. \"vite build --watch\") so edits rebuild the served dir;\n the watcher then re-runs conformance live on every change\n -h, --help Show this help\n\nExit codes (headless): 0 conformant, 1 non-conformant, 2 usage/runtime error.\n`;\n\nfunction parseArgs(argv: string[]): CliOptions {\n const opts: CliOptions = {\n reporter: 'pretty',\n readyTimeoutMs: 5000,\n settleMs: 600,\n runDegraded: true,\n allowSameOrigin: false,\n ui: false,\n open: true,\n help: false,\n };\n for (let i = 0; i < argv.length; i += 1) {\n const arg = argv[i];\n const value = (): string => {\n const v = argv[i + 1];\n if (v === undefined || v.startsWith('--')) throw new Error(`Missing value for ${arg}`);\n i += 1;\n return v;\n };\n switch (arg) {\n case '-h':\n case '--help':\n opts.help = true;\n break;\n case '--url':\n opts.url = value();\n break;\n case '--reporter': {\n const r = value();\n if (r !== 'pretty' && r !== 'json' && r !== 'junit') throw new Error(`Invalid reporter: ${r}`);\n opts.reporter = r;\n break;\n }\n case '--out':\n opts.out = value();\n break;\n case '--ready-timeout':\n opts.readyTimeoutMs = Number(value());\n break;\n case '--settle':\n opts.settleMs = Number(value());\n break;\n case '--no-degraded':\n opts.runDegraded = false;\n break;\n case '--allow-same-origin':\n opts.allowSameOrigin = true;\n break;\n case '--ui':\n opts.ui = true;\n break;\n case '--port':\n opts.port = Number(value());\n break;\n case '--no-open':\n opts.open = false;\n break;\n case '--exec':\n opts.exec = value();\n break;\n default:\n if (arg.startsWith('--')) throw new Error(`Unknown option: ${arg}`);\n if (opts.target) throw new Error(`Unexpected extra argument: ${arg}`);\n opts.target = arg;\n }\n }\n return opts;\n}\n\nasync function collectBoot(hostUrl: string, allowSameOrigin: boolean, totalTimeoutMs: number): Promise<BootCollection> {\n const browser = await chromium.launch({ args: ['--no-sandbox'] });\n try {\n const page = await browser.newPage();\n page.on('pageerror', (e) => process.stderr.write(`[host page error] ${e.message}\\n`));\n await page.goto(hostUrl, { waitUntil: 'load' });\n await page.waitForFunction(\n () => window.__conformanceBoot__ !== undefined || window.__conformanceError__ !== undefined,\n undefined,\n { timeout: totalTimeoutMs },\n );\n const error = await page.evaluate(() => window.__conformanceError__);\n if (error) throw new Error(`Host harness failed: ${error}`);\n const boot = await page.evaluate(() => window.__conformanceBoot__);\n if (!boot) throw new Error('Host harness produced no result');\n void allowSameOrigin;\n return boot;\n } finally {\n await browser.close();\n }\n}\n\nasync function isDir(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isDirectory() ?? false;\n}\n\n/** Best-effort: open a URL in the default browser. */\nfunction openBrowser(url: string): void {\n try {\n if (process.platform === 'darwin') {\n spawn('open', [url], { stdio: 'ignore', detached: true }).unref();\n } else if (process.platform === 'win32') {\n spawn('cmd', ['/c', 'start', '', url], { stdio: 'ignore', detached: true }).unref();\n } else {\n spawn('xdg-open', [url], { stdio: 'ignore', detached: true }).unref();\n }\n } catch {\n /* best-effort — the URL is printed regardless */\n }\n}\n\n/** `--ui`: serve the live web runtime + the napplet, watch, and re-run on change. */\nasync function runUiMode(opts: CliOptions): Promise<number> {\n if (opts.url) {\n process.stderr.write('--ui serves a local napplet directory; --url is not supported in UI mode.\\n');\n return 2;\n }\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n const here = dirname(fileURLToPath(import.meta.url));\n const appDir = join(here, 'ui');\n if (!(await isDir(appDir))) {\n process.stderr.write(`Conformance UI assets are missing at ${appDir}. Rebuild @napplet/conformance-cli.\\n`);\n return 2;\n }\n\n const server = await startUiServer({ appDir, nappletDir: resolved.dir, port: opts.port });\n\n const child = opts.exec ? spawn(opts.exec, { cwd: resolve(opts.target), shell: true, stdio: 'inherit' }) : null;\n\n process.stdout.write(\n `\\n napplet conformance UI → ${server.appUrl}\\n` +\n ` watching ${resolved.dir}${opts.exec ? ` (running: ${opts.exec})` : ''}\\n` +\n ` Ctrl-C to stop\\n\\n`,\n );\n if (opts.open) openBrowser(server.appUrl);\n\n await new Promise<void>((resolveWait) => {\n let closing = false;\n const shutdown = (): void => {\n if (closing) return;\n closing = true;\n child?.kill();\n void server.close().finally(resolveWait);\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n });\n return 0;\n}\n\nasync function main(argv: string[]): Promise<number> {\n let opts: CliOptions;\n try {\n opts = parseArgs(argv);\n } catch (err) {\n process.stderr.write(`${err instanceof Error ? err.message : String(err)}\\n\\n${HELP}`);\n return 2;\n }\n if (opts.help) {\n process.stdout.write(HELP);\n return 0;\n }\n\n if (opts.ui) {\n return runUiMode(opts);\n }\n\n // Resolve the subject: local dir or remote URL.\n let nappletDir: string | null = null;\n let manifestHtml: string;\n let forbiddenGlobals: string[] = [];\n let bootUrl: string;\n\n if (opts.url) {\n bootUrl = opts.url;\n manifestHtml = await (await fetch(opts.url)).text();\n } else {\n if (!opts.target) {\n process.stderr.write(`Provide a napplet directory or --url.\\n\\n${HELP}`);\n return 2;\n }\n const resolved = await resolveNappletDir(opts.target);\n nappletDir = resolved.dir;\n manifestHtml = await readFile(resolved.indexHtml, 'utf8');\n forbiddenGlobals = await scanForbiddenGlobals(resolved.dir);\n bootUrl = '/';\n }\n\n const here = dirname(fileURLToPath(import.meta.url));\n const hostBundle = await readFile(join(here, 'host-bundle.global.js'), 'utf8');\n\n const server = await startHarnessServer({\n nappletDir,\n hostBundle,\n bootConfig: {\n url: bootUrl,\n readyTimeoutMs: opts.readyTimeoutMs,\n settleMs: opts.settleMs,\n runDegraded: opts.runDegraded,\n },\n });\n\n let boot: BootCollection;\n try {\n // Generous overall budget: two boots (primary + degraded), each up to\n // readyTimeout + settle, plus browser startup.\n const totalTimeout = (opts.readyTimeoutMs + opts.settleMs) * 2 + 30000;\n boot = await collectBoot(server.hostUrl, opts.allowSameOrigin, totalTimeout);\n } finally {\n await server.close();\n }\n\n const context = buildContext({ manifestHtml, boot, forbiddenGlobals });\n const run = runConformance(context);\n const output = report(run, opts.reporter);\n\n if (opts.out) {\n await writeFile(opts.out, `${output}\\n`, 'utf8');\n process.stdout.write(`Report written to ${opts.out}\\n`);\n } else {\n process.stdout.write(`${output}\\n`);\n }\n\n return run.ok ? 0 : 1;\n}\n\nmain(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((err: unknown) => {\n process.stderr.write(`napplet-conformance: ${err instanceof Error ? err.stack ?? err.message : String(err)}\\n`);\n process.exitCode = 2;\n });\n","/**\n * @napplet/conformance-cli -- Loopback harness server.\n *\n * Serves the napplet's files plus two reserved harness routes (the host page and\n * the browser bundle). Every response carries `Access-Control-Allow-Origin: *`:\n * the napplet iframe runs at an opaque origin (sandbox without `allow-same-origin`),\n * so its `<script type=\"module\">` subresources are fetched in CORS mode and would\n * otherwise be blocked.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { readFile, stat } from 'node:fs/promises';\nimport { join, normalize, extname, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { MIME } from './static.js';\n\n/** The napplet URL + timing knobs the host page passes to `bootAndCollect`. */\nexport interface BootConfig {\n url: string;\n readyTimeoutMs: number;\n settleMs: number;\n runDegraded: boolean;\n}\n\n/** Options for {@link startHarnessServer}. */\nexport interface HarnessServerOptions {\n /** Directory of napplet files to serve, or `null` in remote-URL mode. */\n nappletDir: string | null;\n /** JavaScript source of the host browser bundle. */\n hostBundle: string;\n /** Boot configuration injected into the host page. */\n bootConfig: BootConfig;\n}\n\n/** A running harness server. */\nexport interface HarnessServer {\n /** Base origin, e.g. `http://127.0.0.1:54213`. */\n origin: string;\n /** URL of the host harness page. */\n hostUrl: string;\n /** Shut the server down. */\n close(): Promise<void>;\n}\n\nconst HOST_HTML_ROUTE = '/__conformance__/host.html';\nconst HOST_BUNDLE_ROUTE = '/__conformance__/host-bundle.js';\n\nfunction renderHostPage(boot: BootConfig): string {\n // The inline script lives on the harness page (full privilege, our origin), not\n // in the sandboxed napplet — so it is not subject to the napplet CSP.\n const cfg = JSON.stringify(boot);\n return `<!doctype html>\n<html lang=\"en\">\n<head><meta charset=\"utf-8\"><title>napplet conformance host</title></head>\n<body>\n<script src=\"${HOST_BUNDLE_ROUTE}\"></script>\n<script>\n (function () {\n var cfg = ${cfg};\n window.NappletConformanceHost.run(cfg).then(function (boot) {\n window.__conformanceBoot__ = boot;\n }).catch(function (err) {\n window.__conformanceError__ = String((err && err.stack) || err);\n });\n })();\n</script>\n</body>\n</html>`;\n}\n\n/** Start the loopback harness server on a random port. */\nexport async function startHarnessServer(options: HarnessServerOptions): Promise<HarnessServer> {\n const dir = options.nappletDir ? resolve(options.nappletDir) : null;\n const hostHtml = renderHostPage(options.bootConfig);\n\n const server: Server = createServer((req, res) => {\n void serve(req.url ?? '/', res);\n });\n\n async function serve(rawUrl: string, res: ServerResponse): Promise<void> {\n res.setHeader('Access-Control-Allow-Origin', '*');\n const path = decodeURIComponent(rawUrl.split('?')[0]);\n\n if (path === HOST_HTML_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.html'] });\n res.end(hostHtml);\n return;\n }\n if (path === HOST_BUNDLE_ROUTE) {\n res.writeHead(200, { 'Content-Type': MIME['.js'] });\n res.end(options.hostBundle);\n return;\n }\n\n if (!dir) {\n res.writeHead(404).end('not found');\n return;\n }\n\n // Resolve a safe path under the napplet dir.\n const relative = normalize(path).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n let filePath = join(dir, relative);\n if (!filePath.startsWith(dir)) {\n res.writeHead(403).end('forbidden');\n return;\n }\n try {\n let info = await stat(filePath).catch(() => null);\n if (info?.isDirectory()) {\n filePath = join(filePath, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (path === '/' && !info) {\n filePath = join(dir, 'index.html');\n info = await stat(filePath).catch(() => null);\n }\n if (!info?.isFile()) {\n res.writeHead(404).end('not found');\n return;\n }\n const body = await readFile(filePath);\n res.writeHead(200, { 'Content-Type': MIME[extname(filePath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(body);\n } catch {\n res.writeHead(500).end('server error');\n }\n }\n\n await new Promise<void>((resolveListen) => server.listen(0, '127.0.0.1', resolveListen));\n const address = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${address.port}`;\n\n return {\n origin,\n hostUrl: `${origin}${HOST_HTML_ROUTE}`,\n close: () => new Promise<void>((res, rej) => server.close((err) => (err ? rej(err) : res()))),\n };\n}\n","/**\n * @napplet/conformance-cli -- Shared static-serving helpers used by both the\n * one-shot harness server and the `--ui` watch server.\n *\n * @packageDocumentation\n */\n\nimport { readFile, stat } from 'node:fs/promises';\nimport { extname } from 'node:path';\nimport type { ServerResponse } from 'node:http';\n\n/** Content types by extension. */\nexport const MIME: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.webp': 'image/webp',\n '.ico': 'image/x-icon',\n '.wasm': 'application/wasm',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.map': 'application/json; charset=utf-8',\n '.txt': 'text/plain; charset=utf-8',\n};\n\n/** The napplet iframe runs at an opaque origin; allow its module subresources. */\nexport function setCors(res: ServerResponse): void {\n res.setHeader('Access-Control-Allow-Origin', '*');\n}\n\n/** Send a file if it exists; return false (without writing) when it does not. */\nexport async function sendFile(res: ServerResponse, absPath: string): Promise<boolean> {\n const info = await stat(absPath).catch(() => null);\n if (!info?.isFile()) return false;\n res.writeHead(200, { 'Content-Type': MIME[extname(absPath).toLowerCase()] ?? 'application/octet-stream' });\n res.end(await readFile(absPath));\n return true;\n}\n","/**\n * @napplet/conformance-cli -- The `--ui` watch server.\n *\n * Serves the standalone conformance web app (bundled with the CLI), the napplet\n * under test (same origin, with permissive CORS for the sandboxed iframe), and a\n * Server-Sent-Events stream. It watches the napplet's served directory and pushes\n * a `rerun` event on every change, so the open app re-runs conformance live — the\n * `vitest --ui` model: edit, save, see the verdict update.\n *\n * @packageDocumentation\n */\n\nimport { createServer, type Server, type ServerResponse } from 'node:http';\nimport { watch, type FSWatcher } from 'node:fs';\nimport { join, normalize, resolve } from 'node:path';\nimport type { AddressInfo } from 'node:net';\nimport { setCors, sendFile } from './static.js';\n\n/** Path the app under `?live=1` is served the napplet from. */\nconst NAPPLET_PREFIX = '/__napplet__/';\n/** SSE endpoint the live app subscribes to. */\nconst EVENTS_ROUTE = '/__conformance__/events';\n\n/** Options for {@link startUiServer}. */\nexport interface UiServerOptions {\n /** Directory of the built conformance web app (served at `/`). */\n appDir: string;\n /** Directory of the built napplet under test (served at `/__napplet__/`). */\n nappletDir: string;\n /** Preferred port (0 = random). */\n port?: number;\n /** Debounce window (ms) for coalescing file-change bursts. Default 120. */\n debounceMs?: number;\n}\n\n/** A running `--ui` server. */\nexport interface UiServer {\n /** Base origin, e.g. `http://127.0.0.1:5219`. */\n origin: string;\n /** The app URL to open (already pointed at the napplet, live mode on). */\n appUrl: string;\n /** Force a `rerun` broadcast (used for the manual/initial trigger). */\n notify(): void;\n /** Shut the server + watcher down. */\n close(): Promise<void>;\n}\n\nfunction safeJoin(dir: string, urlPath: string): string | null {\n const relative = normalize(urlPath).replace(/^(\\.\\.[/\\\\])+/, '').replace(/^[/\\\\]+/, '');\n const full = join(dir, relative);\n return full.startsWith(dir) ? full : null;\n}\n\n/** Register an SSE subscriber and keep the connection open. */\nfunction handleSse(res: ServerResponse, clients: Set<ServerResponse>): void {\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n Connection: 'keep-alive',\n });\n res.write('retry: 1000\\n\\n');\n clients.add(res);\n res.on('close', () => clients.delete(res));\n}\n\n/** Serve a file under `/__napplet__/`. */\nasync function handleNapplet(res: ServerResponse, nappletDir: string, path: string): Promise<void> {\n const rel = path.slice(NAPPLET_PREFIX.length) || 'index.html';\n const abs = safeJoin(nappletDir, rel);\n if (abs && (await sendFile(res, abs))) return;\n res.writeHead(404).end('napplet asset not found');\n}\n\n/** Serve the conformance web app, with an SPA fallback to its index. */\nasync function handleApp(res: ServerResponse, appDir: string, path: string): Promise<void> {\n const abs = safeJoin(appDir, path === '/' ? 'index.html' : path);\n if (abs && (await sendFile(res, abs))) return;\n if (await sendFile(res, join(appDir, 'index.html'))) return;\n res.writeHead(404).end('not found');\n}\n\n/** Watch a directory (recursively when supported) and call `fire` debounced. */\nfunction startWatcher(dir: string, debounceMs: number, fire: () => void): { close(): void } {\n let timer: ReturnType<typeof setTimeout> | null = null;\n const onEvent = (): void => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(fire, debounceMs);\n };\n let watcher: FSWatcher;\n try {\n watcher = watch(dir, { recursive: true }, onEvent);\n } catch {\n // Recursive watch may be unsupported on some platforms.\n watcher = watch(dir, onEvent);\n }\n return {\n close() {\n if (timer) clearTimeout(timer);\n watcher.close();\n },\n };\n}\n\n/** Start the `--ui` watch server. */\nexport async function startUiServer(options: UiServerOptions): Promise<UiServer> {\n const appDir = resolve(options.appDir);\n const nappletDir = resolve(options.nappletDir);\n const clients = new Set<ServerResponse>();\n const broadcast = (): void => {\n for (const res of clients) res.write(`event: rerun\\ndata: ${Date.now()}\\n\\n`);\n };\n\n const server: Server = createServer((req, res) => {\n setCors(res);\n res.setHeader('Cache-Control', 'no-store'); // never serve stale napplet code\n const path = decodeURIComponent((req.url ?? '/').split('?')[0]);\n if (path === EVENTS_ROUTE) {\n handleSse(res, clients);\n } else if (path === NAPPLET_PREFIX.slice(0, -1) || path.startsWith(NAPPLET_PREFIX)) {\n void handleNapplet(res, nappletDir, path);\n } else {\n void handleApp(res, appDir, path);\n }\n });\n\n await new Promise<void>((r) => server.listen(options.port ?? 0, '127.0.0.1', r));\n const { port } = server.address() as AddressInfo;\n const origin = `http://127.0.0.1:${port}`;\n const appUrl = `${origin}/?url=${encodeURIComponent(NAPPLET_PREFIX)}&live=1`;\n const watcher = startWatcher(nappletDir, options.debounceMs ?? 120, broadcast);\n\n return {\n origin,\n appUrl,\n notify: broadcast,\n close: () =>\n new Promise<void>((res, rej) => {\n watcher.close();\n for (const c of clients) c.end();\n clients.clear();\n server.close((err) => (err ? rej(err) : res()));\n }),\n };\n}\n","/**\n * @napplet/conformance-cli -- Static scan for forbidden globals.\n *\n * A napplet runs in a `sandbox=\"allow-scripts\"` iframe at an opaque origin, so the\n * host cannot observe `window.nostr` access at runtime. Instead we scan the served\n * source for references to globals a shell never provides. This is a heuristic, but\n * a true positive is meaningful: shells removed `window.nostr` from napplets in\n * v0.24.0, so any reference is dead or broken code.\n *\n * @packageDocumentation\n */\n\nimport { readdir, readFile, stat } from 'node:fs/promises';\nimport { join, extname } from 'node:path';\n\n/** Patterns considered forbidden, keyed by the label reported in the verdict. */\nconst FORBIDDEN: Array<{ label: string; re: RegExp }> = [\n { label: 'window.nostr', re: /\\bwindow\\s*\\.\\s*nostr\\b/ },\n { label: 'globalThis.nostr', re: /\\bglobalThis\\s*\\.\\s*nostr\\b/ },\n];\n\nconst SCANNABLE = new Set(['.js', '.mjs', '.cjs', '.html', '.htm']);\nconst SKIP_DIRS = new Set(['node_modules', '.git', '.turbo', 'coverage']);\n\nasync function collectFiles(dir: string, acc: string[]): Promise<void> {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n if (entry.isDirectory()) {\n if (SKIP_DIRS.has(entry.name)) continue;\n await collectFiles(join(dir, entry.name), acc);\n } else if (entry.isFile() && SCANNABLE.has(extname(entry.name).toLowerCase())) {\n acc.push(join(dir, entry.name));\n }\n }\n}\n\n/**\n * Scan a napplet directory for forbidden global references.\n *\n * @param dir - The directory of served napplet files.\n * @returns The distinct forbidden labels found (e.g. `['window.nostr']`).\n */\nexport async function scanForbiddenGlobals(dir: string): Promise<string[]> {\n const info = await stat(dir).catch(() => null);\n if (!info?.isDirectory()) return [];\n const files: string[] = [];\n await collectFiles(dir, files);\n const found = new Set<string>();\n for (const file of files) {\n const text = await readFile(file, 'utf8').catch(() => '');\n for (const { label, re } of FORBIDDEN) {\n if (re.test(text)) found.add(label);\n }\n }\n return [...found];\n}\n","/**\n * @napplet/conformance-cli -- Resolve which local artifact to conformance-test.\n *\n * Kept in its own module (not `cli.ts`, whose top level runs `main()`) so tests\n * can import the resolver without launching the CLI.\n */\n\nimport { stat } from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\n\nasync function isFile(path: string): Promise<boolean> {\n return (await stat(path).catch(() => null))?.isFile() ?? false;\n}\n\n/**\n * Resolve the directory + index.html for a local napplet target.\n *\n * A built napplet is the artifact we test, so `dist/index.html` is preferred over\n * a sibling `index.html`. When `target` is a project root, its source\n * `index.html` (a Vite entry referencing `/src/*.ts` that cannot run in the\n * opaque-origin `srcdoc` sandbox) would otherwise shadow the built single-file\n * output and make conformance test the wrong, never-booting artifact.\n */\nexport async function resolveNappletDir(target: string): Promise<{ dir: string; indexHtml: string }> {\n const base = resolve(target);\n const dist = join(base, 'dist');\n if (await isFile(join(dist, 'index.html'))) return { dir: dist, indexHtml: join(dist, 'index.html') };\n if (await isFile(join(base, 'index.html'))) return { dir: base, indexHtml: join(base, 'index.html') };\n throw new Error(`No index.html found in ${join(dist, 'index.html')} or ${join(base, 'index.html')}. Build the napplet first.`);\n}\n"],"mappings":"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","names":["readFile","stat","join","resolve","readFile","stat","extname","stat","readFile","extname","createServer","join","normalize","resolve","normalize","join","resolve","createServer","readFile","stat","join","extname","stat","join","resolve","stat","join","resolve","readFile"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@napplet/conformance-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "Headless CLI that runs @napplet/conformance against a napplet in real Chromium via Playwright — the `test:conformance` runner",
5
5
  "type": "module",
6
6
  "bin": {