Something went wrong. Try again.
A serverless AT Protocol lexicon authority
atproto lexicon
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133/** * `corpora serve` — run a built `dist/` over HTTP. * * This is the local development story: the same handler the Worker runs, in front of the * same bytes, so what verifies here verifies in production. * * @see [INTERFACES](../../docs/INTERFACES.md#cli) */
import { existsSync } from 'node:fs'import { resolve } from 'node:path'import { pathToFileURL } from 'node:url'import { parseArgs } from 'node:util'import { serve } from './index.js'
const USAGE = `corpora serve — serve a built dist/ over HTTP
Usage: corpora serve --dist <dir> [--port 2583] [--host 127.0.0.1]
Options: --dist <dir> directory produced by \`corpora build\` (must contain data.js) --port <n> port to bind, 0 for an ephemeral one (default 2583) --host <addr> address to bind (default 127.0.0.1) --quiet print nothing but errors --help show this message`
/** * @param {string[]} argv * @returns {Promise<number>} */export async function run(argv) { const { values } = parseArgs({ args: argv, options: { dist: { type: 'string' }, port: { type: 'string', default: '2583' }, host: { type: 'string', default: '127.0.0.1' }, quiet: { type: 'boolean', default: false }, help: { type: 'boolean', default: false }, }, allowPositionals: false, })
if (values.help) { process.stdout.write(USAGE) return 0 } if (!values.dist) { process.stderr.write(`corpora serve: --dist is required\n\n${USAGE}`) return 1 }
const port = Number(values.port) if (!Number.isInteger(port) || port < 0 || port > 65535) { process.stderr.write(`corpora serve: --port must be 0-65535, got '${values.port}'\n`) return 1 }
const dataPath = resolve(values.dist, 'data.js') if (!existsSync(dataPath)) { process.stderr.write( `corpora serve: no data.js in ${resolve(values.dist)}\n` + ` run \`corpora build --lexicons <dir> --out ${values.dist} --did <did>\` first\n`, ) return 1 }
/** @type {{ default: import('../serve/types.js').CorporaData }} */ const mod = await import(pathToFileURL(dataPath).href) const data = mod.default const { url, close } = await serve(data, { port, host: values.host })
if (!values.quiet) process.stdout.write(banner(url, data))
await stopSignal() if (!values.quiet) process.stdout.write('\ncorpora serve: stopping\n') await close() return 0}
/** * Copy-pasteable requests against the data actually loaded, not against placeholders. * * Query strings are built with `URLSearchParams`, never by interpolation. A `did:web` * carrying a port is spelled `did:web:localhost%3A2583`, so the DID already contains a * percent-escape that must itself be escaped to survive a query string. Interpolating it * yields `%3A`, which decodes back to a colon and produces a `RepoNotFound` naming a DID * the user never typed — and a port-bearing `did:web` is exactly the local-development * shape, so that would be the first command anyone copies. * * @param {string} url * @param {import('../serve/types.js').CorporaData} data * @returns {string} */function banner(url, data) { const nsids = Object.keys(data.records) const rkey = nsids[0] ?? 'com.example.thing' const { did, collection } = data const xrpc = (/** @type {string} */ nsid, /** @type {Record<string, string>} */ params) => `${url}/xrpc/${nsid}?${new URLSearchParams(params)}`
return [ `corpora serve: ${did} — ${nsids.length} lexicon${nsids.length === 1 ? '' : 's'} at rev ${data.rev}`, ` ${url}`, '', ` curl -s ${url}/.well-known/did.json`, ` curl -s '${xrpc('com.atproto.repo.getRecord', { repo: did, collection, rkey })}'`, ` curl -sD- -o proof.car '${xrpc('com.atproto.sync.getRecord', { did, collection, rkey })}'`, '', ' Ctrl-C to stop.', '', ].join('\n')}
/** * Resolves on the first Ctrl-C or SIGTERM. `run` must not return before then: the CLI * dispatcher exits the process on the value it resolves with. * @returns {Promise<void>} */function stopSignal() { return new Promise((done) => { const stop = () => { process.off('SIGINT', stop) process.off('SIGTERM', stop) done() } process.once('SIGINT', stop) process.once('SIGTERM', stop) })}