diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc35b2c..50e0656 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,6 +32,7 @@ on: - .github/workflows/release.yml - package.json - npm/** + - tools/reference.mjs - Cargo.toml - Cargo.lock - rust-toolchain.toml @@ -218,6 +219,36 @@ jobs: path: npm/ if-no-files-found: error + # The reference, generated from the declarations this release + # publishes. It is a job here rather than only a test in CI because a + # reference is something a release hands over, and the version it is + # about is the version being released. + # + # It needs a binary for the machine it runs on, because the last thing + # the tool does is require the package and hold the names it exports + # against the names it documented. That check is the point: the + # declarations and the addon are generated from one Rust crate and + # published as two files, and the failure worth catching is the one + # where they stop agreeing. + reference: + needs: binary + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - run: npm ci + - uses: actions/download-artifact@v4 + with: + name: binary-x86_64-unknown-linux-gnu + - run: npm run reference + - uses: actions/upload-artifact@v4 + with: + name: reference + path: reference/ + if-no-files-found: error + # Publishing is the one step that cannot be taken back, so it happens # on a tag and nowhere else. The platform packages go first: the root # package is what a user installs, and it is worthless until every diff --git a/.gitignore b/.gitignore index d123f53..cdd7fc8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,11 @@ dist/ # compared against the one in etc/ that is committed. temp/ +# The generated API reference. Built from the declarations rather than +# kept beside them, so a copy in the tree is a copy that is out of date +# the moment a signature moves. +reference/ + # The built addon, which napi drops into the source tree so that the # tests load the same file a published package would. *.node diff --git a/README.md b/README.md index e3d2461..43c3e75 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,12 @@ Both formats reach one loaded addon, so a `ZuDate` made through `import` is an i The declarations are separate files rather than one shared `.d.ts`, since a resolver reads `.d.cts` for `require` and `.d.mts` for `import`, and `types` is the first condition in each entry: conditions match in the order they are written, so `types` after `default` is a `types` nothing reaches, and a package that compiles here would be `any` everywhere else. `npm run check:types` compiles a program in each format against the published shape, and `npm run check:package` runs [`attw`](https://github.com/arethetypeswrong/arethetypeswrong.github.io) over a real `npm pack` for node10, node16 CJS, node16 ESM and bundler resolution. +## Reference + +Every exported name, its signature and what its doc comment says, generated from the declarations this package publishes rather than written by hand beside them. The release builds it from the version it is about to publish, and `npm run reference` builds the same pages here. + +typedoc rather than api-documenter, which would have been the obvious pick since api-extractor already runs here for the stability report. api-extractor's doc model does not carry `conn.stream(...)` or `await using`, because both reach the type of a connection through the `declare module` in `zudb.d.cts` and it does not follow one. A reference missing the streaming entry point is a reference that sends a reader to the cursor. So the generator that reads the declarations with the TypeScript compiler is the one used, and the build fails if it stops carrying them, along with any exported name whose types have gone missing. + ## Installing, once there is something to install `npm i zudb`, and that is the whole of it. The install downloads one file, runs nothing, and needs no compiler: the root package carries the loader and no binary, each platform has its own package holding exactly one addon, and npm picks the one for the machine out of `optionalDependencies` by its `os`, `cpu` and `libc`. There is no `postinstall`, no `node-gyp`, no `node-pre-gyp` and no fetch from anywhere but the registry, which is what makes the package installable behind a proxy, inside a locked-down CI image, and on a machine with no toolchain on it. diff --git a/package-lock.json b/package-lock.json index 0d63c9b..de799ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@microsoft/api-extractor": "^7.58.12", "@napi-rs/cli": "^3.8.6", "@types/node": "^26.2.0", + "typedoc": "^0.28.20", "typescript": "^5.9.3" }, "engines": { @@ -134,6 +135,20 @@ "tslib": "^2.4.0" } }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", + "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.23.0", + "@shikijs/langs": "^3.23.0", + "@shikijs/themes": "^3.23.0", + "@shikijs/types": "^3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@inquirer/ansi": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", @@ -2003,6 +2018,55 @@ "sprintf-js": "~1.0.2" } }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -2034,6 +2098,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/node": { "version": "26.2.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", @@ -2044,6 +2118,13 @@ "undici-types": "~8.3.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -2393,6 +2474,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/environment": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", @@ -2682,6 +2776,26 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/lru-cache": { "version": "11.5.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", @@ -2692,6 +2806,41 @@ "node": "20 || >=22" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/markdown-it": { + "version": "14.3.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.0.tgz", + "integrity": "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, "node_modules/marked": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", @@ -2740,6 +2889,13 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/minimatch": { "version": "10.2.3", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.3.tgz", @@ -2856,6 +3012,16 @@ "dev": true, "license": "MIT" }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -3093,6 +3259,46 @@ "website" ] }, + "node_modules/typedoc": { + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.23.0", + "lunr": "^2.3.9", + "markdown-it": "^14.3.0", + "minimatch": "^10.2.5", + "yaml": "^2.9.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -3107,6 +3313,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "8.3.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", @@ -3179,6 +3392,22 @@ "node": ">=10" } }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "16.2.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", diff --git a/package.json b/package.json index 105ccb1..9bd046a 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "check:package": "attw --pack .", "check:api": "api-extractor run --verbose", "check:api:update": "api-extractor run --local", + "reference": "node tools/reference.mjs reference", "bench": "node bench/query.mjs", "bench:temporal": "node --harmony-temporal bench/query.mjs" }, @@ -77,6 +78,7 @@ "@microsoft/api-extractor": "^7.58.12", "@napi-rs/cli": "^3.8.6", "@types/node": "^26.2.0", + "typedoc": "^0.28.20", "typescript": "^5.9.3" } } diff --git a/test/reference.test.mjs b/test/reference.test.mjs new file mode 100644 index 0000000..13715f1 --- /dev/null +++ b/test/reference.test.mjs @@ -0,0 +1,71 @@ +// The generated reference, and the two things about it worth asserting. +// +// Most of what a documentation generator does is not this suite's +// business: typedoc's theme is typedoc's, and a test that counted +// headings would fail on the week it renders one differently. What is +// this package's business is that the reference covers what the package +// publishes, and this package is assembled from two languages, so the +// interesting failure is the quiet one. +// +// `conn.stream(...)` and `await using` are put on the native class from +// JavaScript, because the class is registered by the addon and there is +// nowhere in Rust to write a generator or a method whose name is a +// symbol. They reach the types through the `declare module` in +// zudb.d.cts, which is the one construct a generator can read the whole +// file and still miss: api-extractor does, which is why the reference +// is not built from its doc model. A reference missing them builds, +// looks finished, and sends a reader to the cursor. + +import assert from 'node:assert/strict' +import { execFile } from 'node:child_process' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import test from 'node:test' +import { fileURLToPath } from 'node:url' +import { promisify } from 'node:util' + +const run = promisify(execFile) +const root = fileURLToPath(new URL('../', import.meta.url)) +const tool = fileURLToPath(new URL('../tools/reference.mjs', import.meta.url)) + +// Bun and Deno resolve a package by rules of their own, and typedoc is +// the TypeScript compiler with a theme on it either way. What is being +// asked about here is the declarations rather than which runtime is +// reading them. +const NODE = !process.versions.bun && !process.versions.deno + +async function built(t) { + const into = await mkdtemp(join(tmpdir(), 'zu-node-reference-')) + t.after(() => rm(into, { recursive: true, force: true })) + const { stdout } = await run(process.execPath, [tool, into], { cwd: root }) + return { into, stdout } +} + +test('the reference covers every name the package exports', { skip: !NODE }, async (t) => { + const { stdout } = await built(t) + assert.match(stdout, /0 complaints$/m) + + // The count in the line, against the package's own idea of how many + // names it has. The tool checks the values; this checks that it was + // looking at the whole surface and not at an entry point that + // resolved to nothing. + const { default: zudb } = await import(new URL('../zudb.cjs', import.meta.url).href) + const names = Number(stdout.match(/(\d+) names/)[1]) + assert.ok(names >= Object.keys(zudb).length, `${names} documented, ${Object.keys(zudb).length} exported`) +}) + +test('the reference carries the half written in JavaScript', { skip: !NODE }, async (t) => { + const { into } = await built(t) + const page = await readFile(join(into, 'classes', 'Connection.html'), 'utf8') + + // The two members that reach the types by augmentation, and the + // reason this package's reference is not built from api-extractor's + // doc model. + assert.match(page, /\bstream\b/) + assert.match(page, /asyncDispose/) + + // And the ones that come from the Rust, so a page holding only the + // augmentation would fail here rather than pass the check above. + for (const name of ['query', 'exec', 'cursor', 'close']) assert.match(page, new RegExp(`>${name}<`)) +}) diff --git a/tools/reference.mjs b/tools/reference.mjs new file mode 100644 index 0000000..1626eff --- /dev/null +++ b/tools/reference.mjs @@ -0,0 +1,132 @@ +// The API reference, generated from the types this package publishes. +// +// A reference written by hand beside the code is wrong by the second +// release, and wrong in the way that costs the most: it looks +// maintained. So this one is generated, and the release generates it +// from the same declarations the published package carries rather than +// from a page somebody remembered to edit. +// +// node tools/reference.mjs +// +// typedoc rather than api-documenter, which is the other half of the +// api-extractor toolchain and would have been the obvious pick since +// api-extractor already runs here for the stability report. It reads +// the doc model api-extractor builds, and that model does not carry +// `conn.stream(...)` or `await using`: both arrive on `Connection` +// through the `declare module` in zudb.d.cts, which api-extractor does +// not follow. A reference missing the streaming entry point is a +// reference that sends a reader to the cursor, so the generator that +// reads the declarations with the TypeScript compiler is the one to +// use, and the check below is what says it still does. +// +// The entry point is zudb.d.cts and not zudb.d.mts, for the reason +// api-extractor.json gives: the CommonJS declarations are the ones +// written, the ESM file re-exports them, and a public API in two copies +// is two copies that drift. + +import { readFile } from 'node:fs/promises' +import { argv, exit } from 'node:process' +import { fileURLToPath } from 'node:url' + +import { Application, ReflectionKind, TSConfigReader } from 'typedoc' + +const root = new URL('../', import.meta.url) + +// An entry point is a glob, and in a glob a backslash escapes whatever +// comes after it. So on Windows the path this file computes for its own +// sibling is read as `zudb.d.cts` with three escapes in it, matches +// nothing, and typedoc says so and carries on to generate an empty +// reference. Separators forward, which is what typedoc asks for and what +// every path option here takes on either platform. +const posix = (url) => fileURLToPath(url).replaceAll('\\', '/') + +// The members `zudb.cjs` puts on the native class from JavaScript, +// which is where anything with a generator or a symbol for a name has +// to live: the class is registered by the addon and there is nowhere in +// Rust to write either. They are the reason this file uses typedoc, so +// a reference that lost them is the failure worth naming, and naming +// them here means a new one shows up as a mismatch rather than as a +// page nobody notices is thin. +const AUGMENTED = ['stream', '[asyncDispose]'] + +async function build(into) { + const app = await Application.bootstrapWithPlugins( + { + entryPoints: [posix(new URL('zudb.d.cts', root))], + tsconfig: posix(new URL('tools/api-extractor.tsconfig.json', root)), + // The declarations are read rather than checked. Checking them is + // what `npm run check:types` is for, against the tsconfig a user's + // own compiler would use, and a generator that also type-checks is + // a second opinion nobody asked for on the week the two disagree. + skipErrorChecking: true, + name: 'zudb', + readme: 'none', + githubPages: false, + }, + [new TSConfigReader()], + ) + const project = await app.convert() + if (!project) throw new Error('typedoc read the declarations and made nothing of them') + // A file it could not find is an error it prints and then carries on + // from, with a project that has nothing in it. The complaints below + // would report that as thirteen missing pages, which is a true answer + // to the wrong question, so it is said here in one line instead. + if (app.logger.hasErrors()) throw new Error('typedoc reported an error, so what is above it is not a reference') + await app.generateDocs(project, into) + return project +} + +// What the reference has to cover, and the two ways it could quietly +// stop covering it. +function complaints(project, exported) { + const wrong = [] + const documented = new Map((project.children ?? []).map((each) => [each.name, each])) + + // A name a program can `require` and the reference does not carry is + // a name whose types went missing, which is a worse failure than a + // thin page: it compiles nowhere. + for (const name of exported) { + const found = documented.get(name) + if (!found) wrong.push(`${name} is exported and the reference has no page for it`) + else if (!found.kindOf([ReflectionKind.Class, ReflectionKind.Function, ReflectionKind.Variable])) + wrong.push( + `${name} is exported as a value and documented as ${ReflectionKind.singularString(found.kind)}`, + ) + } + + // And the other direction, on the one class that is assembled from + // two languages. Every method a connection answers to has to be on + // the page, including the ones that are there by augmentation, which + // is the thing this generator was chosen for. + const connection = documented.get('Connection') + const members = new Set((connection?.children ?? []).map((each) => each.name)) + for (const name of AUGMENTED) { + if (!members.has(name)) { + wrong.push( + `${name} is on Connection at runtime and not in the reference, ` + + 'so the generator has stopped following the augmentation in zudb.d.cts', + ) + } + } + + return wrong +} + +const into = argv[2] +if (!into) { + console.error('usage: node tools/reference.mjs ') + exit(2) +} + +const project = await build(into) +// Read after the build rather than imported at the top, because the +// loader dlopens the addon for this machine and a machine with no +// binary built should be told that by the require and not by a stack +// trace out of typedoc. +const { default: zudb } = await import(new URL('zudb.cjs', root).href) +const wrong = complaints(project, Object.keys(zudb)) + +for (const each of wrong) console.error(each) +const version = JSON.parse(await readFile(new URL('package.json', root), 'utf8')).version +console.log(`zudb ${version}: ${project.children?.length ?? 0} names in ${into}, ${wrong.length} complaints`) +exit(wrong.length ? 1 : 0)