From 24ce74e3bdedbc06ae846b04c6b0bbfcfddde494 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 18 Aug 2026 20:12:55 +0700 Subject: [PATCH] dual ESM and CJS, with types first in every export condition The binding napi generates is now binding.cjs and binding.d.cts, and the package is what sits over it: zudb.cjs requires the binding and names every export one at a time, zudb.mjs re-exports those same objects through createRequire, and the two declaration files say the same shape to a resolver reading .d.cts for require and .d.mts for import. One load, so a ZuDate from either format is an instance of the other's class. No default export in either, because a default beside the named exports is a second spelling of every name whose meaning depends on the caller's bundler. types is the first key in both conditions, since conditions match in the order they are written and a types after default is a types nothing reaches. isZuError joins the surface, because a catch clause holds an unknown and there was no way to ask whether it was a zu failure without reading the name off it. npm run check:types compiles a program in each format against the published shape, npm run check:package runs attw over a real npm pack, and both are a CI job. The install round trip now requires and imports the packed package rather than only importing it. --- .github/workflows/ci.yml | 20 + .github/workflows/release.yml | 9 +- README.md | 17 +- bench/query.mjs | 2 +- index.js => binding.cjs | 0 index.d.ts => binding.d.cts | 0 package-lock.json | 707 +++++++++++++++++++++++++++++++++- package.json | 37 +- test/connect.test.mjs | 2 +- test/exports.test.mjs | 91 +++++ test/helper.mjs | 8 +- test/packaging.test.mjs | 13 +- test/query.test.mjs | 2 +- test/types/cjs.cts | 19 + test/types/esm.mts | 38 ++ test/types/tsconfig.json | 24 ++ test/values.test.mjs | 2 +- tools/install.mjs | 25 +- zudb.cjs | 48 +++ zudb.d.cts | 29 ++ zudb.d.mts | 10 + zudb.mjs | 29 ++ 22 files changed, 1105 insertions(+), 27 deletions(-) rename index.js => binding.cjs (100%) rename index.d.ts => binding.d.cts (100%) create mode 100644 test/exports.test.mjs create mode 100644 test/types/cjs.cts create mode 100644 test/types/esm.mts create mode 100644 test/types/tsconfig.json create mode 100644 zudb.cjs create mode 100644 zudb.d.cts create mode 100644 zudb.d.mts create mode 100644 zudb.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 907690b..e09e104 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,26 @@ jobs: # are JavaScript, and the job below is what runs them. - run: cargo clippy --all-features -- -D warnings + # The package as a resolver sees it, which no amount of running it + # here can answer: the tests import the checkout, where every path + # exists and every condition is beside the last. What breaks a + # published package is the resolution instead, and only tsc and attw + # read a package that way. + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - uses: Swatinem/rust-cache@v2 + - run: npm ci + # The declarations are generated by the build, so there is nothing + # to type-check until it has run. + - run: npm run build:debug + - run: npm run check:types + - run: npm run check:package + test: strategy: fail-fast: false diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1320b51..cc35b2c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -131,13 +131,18 @@ jobs: with: key: ${{ matrix.target }} + # napi rather than npm run build, because only this workflow builds + # for a target that is not the machine's own. The two file names + # are the ones package.json ships, and a build that leaves them + # under napi's defaults is a published package whose loader is not + # the loader that was built. - name: Build if: matrix.alpine == '' shell: bash run: | set -eu npm ci - npx napi build --platform --release --target ${{ matrix.target }} + npx napi build --platform --release --target ${{ matrix.target }} --js binding.cjs --dts binding.d.cts # Alpine builds the row's own architecture natively, so there is # no target flag: inside the image the default target is the musl @@ -166,7 +171,7 @@ jobs: rustup-init -y --no-modify-path --profile minimal export PATH="$HOME/.cargo/bin:$PATH" npm ci - npx napi build --platform --release + npx napi build --platform --release --js binding.cjs --dts binding.d.cts ' # One file, named after the platform it runs on, which is what the diff --git a/README.md b/README.md index 2332034..315b463 100644 --- a/README.md +++ b/README.md @@ -30,12 +30,12 @@ The rows are an array, so iterating them is `for (const row of rows)` and nothin - **INT64 is `bigint`.** Always, by default. A JavaScript number stops being exact at 2^53 and zu's integers go to 2^63, so a count that came back as a number would be a count you cannot trust. `{ bigIntMode: "number" }` is planned, will be documented with its precision hazard, and will never be the default. - **Nothing blocks the event loop.** Every native call runs on libuv's threadpool and hands back a promise before the statement has started. There is no synchronous variant, and the ones that arrive later will say in their own documentation that they belong in scripts, not servers. - **`await using` is the intended scoping.** A connection is `Symbol.asyncDispose`, and `close()` stays public for callers who cannot use the syntax. -- **A failure is an ordinary `Error`.** Every `catch`, logger and rejection handler already knows what to do with one. What makes it a zu error is the fields, and none of them has to be parsed back out of the message: `code` is the GQLSTATUS and picks the branch, `condition` is the standard's own words for it, `line` and `column` and `excerpt` underline the token, and `retryable` decides whether a retry loop goes round again. A mistake this client caught before the engine saw it carries no `code` and is named `ZuUsageError`, so a caller mapping codes to branches can tell a missing code from one it does not recognize. +- **A failure is an ordinary `Error`.** Every `catch`, logger and rejection handler already knows what to do with one. What makes it a zu error is the fields, and none of them has to be parsed back out of the message: `code` is the GQLSTATUS and picks the branch, `condition` is the standard's own words for it, `line` and `column` and `excerpt` underline the token, and `retryable` decides whether a retry loop goes round again. A mistake this client caught before the engine saw it carries no `code` and is named `ZuUsageError`, so a caller mapping codes to branches can tell a missing code from one it does not recognize. `isZuError(caught)` is the exported guard for the `catch` clause, where the value is `unknown` and could be anything at all, and in TypeScript it narrows to the full shape. - **A refusal is a rejection.** A closed connection and a parameter of a type nothing can bind are refused inside the promise rather than thrown out of the call, so one `await` catches everything one statement can do. ## What works today -`connect`, `query`, `exec`, `close`, `dispose` and `await using`. Named parameters both ways, including lists, records and nesting. Every scalar the engine has, plus nodes, edges and paths with their tables named rather than numbered, and `ZuDate`, `ZuTime`, `ZuTimestamp` and `ZuDuration`. Read-only connections, memory and thread limits. An `AbortSignal` on any statement. The full error surface above. +`connect`, `query`, `exec`, `close`, `dispose` and `await using`. Named parameters both ways, including lists, records and nesting. Every scalar the engine has, plus nodes, edges and paths with their tables named rather than numbered, and `ZuDate`, `ZuTime`, `ZuTimestamp` and `ZuDuration`. Read-only connections, memory and thread limits. An `AbortSignal` on any statement. The full error surface above, and `isZuError` to recognize it. Both module formats, typed separately. Build it with `npm run build`, and run the suite with `npm test`. Nothing is published yet, so `npm i zudb` is not a thing you can type at anybody's terminal, but everything it will do is built and installed on every run of the release workflow. @@ -51,6 +51,17 @@ It is the signal JavaScript already has, so a timeout written like the one above What the promise rejects with is the signal's own reason, which is what `fetch` does: `AbortSignal.timeout(50)` rejects with the runtime's `TimeoutError`, `controller.abort(new RequestGone())` rejects with the `RequestGone` you made, and a bare `controller.abort()` rejects with the runtime's `AbortError`. A signal that has already fired stops the statement before the engine sees it at all. A signal that never fires costs one listener, taken off again when the statement ends, whether it answered, failed or was stopped. +## Importing it, either way + +```ts +import { connect } from "zudb"; // ESM, and TypeScript resolving through the import condition +const { connect } = require("zudb"); // CommonJS, the same names and the same objects +``` + +Both formats reach one loaded addon, so a `ZuDate` made through `import` is an instance of the `ZuDate` reached through `require`, and a program that mixes the two, which is most programs with a dependency tree, is not quietly holding two of everything. There is no default export in either format, because a default alongside the named exports is a second spelling of every name whose meaning depends on the caller's bundler and their `esModuleInterop`. + +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. + ## 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. @@ -72,7 +83,7 @@ Anything outside that table has no binary and no source build to fall back on, s ## Still to come -`AsyncIterable` and Web Streams over a result. `bigIntMode`. `toTemporal()` and `{ temporal: true }`, for the runtimes where Temporal is unflagged: it reached Stage 4 in March 2026 and is unflagged in Node 26, but Node 24 is still the active LTS and Safari is still behind a flag, which is why the stable types are the four classes above. Dual ESM and CJS, with types first in every export condition. Bun and Deno in CI, and the WASM build for the browser. +`AsyncIterable` and Web Streams over a result. `bigIntMode`. `toTemporal()` and `{ temporal: true }`, for the runtimes where Temporal is unflagged: it reached Stage 4 in March 2026 and is unflagged in Node 26, but Node 24 is still the active LTS and Safari is still behind a flag, which is why the stable types are the four classes above. Bun and Deno in CI, and the WASM build for the browser. ## Runtimes diff --git a/bench/query.mjs b/bench/query.mjs index 60ae8e9..8c09355 100644 --- a/bench/query.mjs +++ b/bench/query.mjs @@ -17,7 +17,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { connect } from '../index.js' +import { connect } from 'zudb' const ROWS = Number(process.env.ZU_BENCH_ROWS ?? 50_000) const REPEATS = Number(process.env.ZU_BENCH_REPEATS ?? 9) diff --git a/index.js b/binding.cjs similarity index 100% rename from index.js rename to binding.cjs diff --git a/index.d.ts b/binding.d.cts similarity index 100% rename from index.d.ts rename to binding.d.cts diff --git a/package-lock.json b/package-lock.json index 6a34abb..3448996 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,12 +9,96 @@ "version": "0.0.1", "license": "Apache-2.0", "devDependencies": { - "@napi-rs/cli": "^3.8.6" + "@arethetypeswrong/cli": "^0.18.2", + "@napi-rs/cli": "^3.8.6", + "@types/node": "^26.2.0", + "typescript": "^5.9.3" }, "engines": { "node": ">= 24" } }, + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.4.tgz", + "integrity": "sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@arethetypeswrong/cli": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.18.5.tgz", + "integrity": "sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@arethetypeswrong/core": "0.18.5", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^7.1.0", + "semver": "^7.5.4" + }, + "bin": { + "attw": "dist/index.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/core": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.5.tgz", + "integrity": "sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", + "cjs-module-lexer": "^1.2.3", + "fflate": "^0.8.3", + "lru-cache": "^11.0.1", + "semver": "^7.5.4", + "typescript": "5.6.1-rc", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", + "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@braidai/lang": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@emnapi/core": { "version": "1.11.2", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", @@ -393,6 +477,16 @@ } } }, + "node_modules/@loaderkit/resolve": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.6.tgz", + "integrity": "sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.0" + } + }, "node_modules/@napi-rs/cli": { "version": "3.8.6", "resolved": "https://registry.npmjs.org/@napi-rs/cli/-/cli-3.8.6.tgz", @@ -441,6 +535,20 @@ } } }, + "node_modules/@napi-rs/cli/node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@napi-rs/cross-toolchain": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@napi-rs/cross-toolchain/-/cross-toolchain-1.0.3.tgz", @@ -1684,6 +1792,19 @@ "@octokit/openapi-types": "^28.0.0" } }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, "node_modules/@tybys/wasm-util": { "version": "0.10.3", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", @@ -1695,6 +1816,68 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -1709,6 +1892,33 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/chardet": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", @@ -1716,6 +1926,51 @@ "dev": true, "license": "MIT" }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.0" + } + }, "node_modules/cli-width": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", @@ -1742,6 +1997,38 @@ "typanion": "*" } }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -1749,6 +2036,16 @@ "dev": true, "license": "MIT" }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/content-type": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", @@ -1781,6 +2078,33 @@ } } }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-toolkit": { "version": "1.51.0", "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", @@ -1794,6 +2118,16 @@ "tests/browser-compat" ] }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-string-truncated-width": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", @@ -1821,6 +2155,43 @@ "fast-string-width": "^3.0.2" } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/iconv-lite": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", @@ -1838,6 +2209,16 @@ "url": "https://opencollective.com/express" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/js-yaml": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", @@ -1868,6 +2249,64 @@ "dev": true, "license": "MIT" }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1885,6 +2324,44 @@ "node": "^20.17.0 || >=22.9.0" } }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/obug": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", @@ -1899,6 +2376,40 @@ "node": ">=12.20.0" } }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1932,6 +2443,110 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -1951,9 +2566,9 @@ ] }, "node_modules/typescript": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", - "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1964,12 +2579,96 @@ "node": ">=14.17" } }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/universal-user-agent": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", "dev": true, "license": "ISC" + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } } } } diff --git a/package.json b/package.json index f1051db..81e4883 100644 --- a/package.json +++ b/package.json @@ -18,11 +18,29 @@ "engines": { "node": ">= 24" }, - "main": "index.js", - "types": "index.d.ts", + "type": "commonjs", + "main": "./zudb.cjs", + "types": "./zudb.d.cts", + "exports": { + ".": { + "import": { + "types": "./zudb.d.mts", + "default": "./zudb.mjs" + }, + "require": { + "types": "./zudb.d.cts", + "default": "./zudb.cjs" + } + }, + "./package.json": "./package.json" + }, "files": [ - "index.js", - "index.d.ts", + "binding.cjs", + "binding.d.cts", + "zudb.cjs", + "zudb.mjs", + "zudb.d.cts", + "zudb.d.mts", "README.md", "LICENSE" ], @@ -41,12 +59,17 @@ ] }, "scripts": { - "build": "napi build --platform --release", - "build:debug": "napi build --platform", + "build": "napi build --platform --release --js binding.cjs --dts binding.d.cts", + "build:debug": "napi build --platform --js binding.cjs --dts binding.d.cts", "test": "node --test \"test/*.test.mjs\"", + "check:types": "tsc --noEmit --project test/types/tsconfig.json", + "check:package": "attw --pack .", "bench": "node bench/query.mjs" }, "devDependencies": { - "@napi-rs/cli": "^3.8.6" + "@arethetypeswrong/cli": "^0.18.2", + "@napi-rs/cli": "^3.8.6", + "@types/node": "^26.2.0", + "typescript": "^5.9.3" } } diff --git a/test/connect.test.mjs b/test/connect.test.mjs index 52f7804..0491550 100644 --- a/test/connect.test.mjs +++ b/test/connect.test.mjs @@ -4,7 +4,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import test from 'node:test' -import { abiVersion, connect, version } from '../index.js' +import { abiVersion, connect, version } from 'zudb' import { fresh, twoPeople } from './helper.mjs' test('the client says which version it is and which ABI it implements', () => { diff --git a/test/exports.test.mjs b/test/exports.test.mjs new file mode 100644 index 0000000..e082156 --- /dev/null +++ b/test/exports.test.mjs @@ -0,0 +1,91 @@ +// Two module formats, one package, and the same names out of both. + +import assert from 'node:assert/strict' +import { readFile } from 'node:fs/promises' +import { createRequire } from 'node:module' +import test from 'node:test' + +import * as esm from 'zudb' + +import { fresh } from './helper.mjs' + +const root = new URL('../', import.meta.url) +const require = createRequire(import.meta.url) +const cjs = require('../zudb.cjs') + +// Everything the package promises, written out rather than derived from +// either format, because a name that goes missing from both at once is +// exactly what a test derived from one of them cannot see. +const SURFACE = [ + 'connect', + 'version', + 'abiVersion', + 'isZuError', + 'Connection', + 'ZuDate', + 'ZuTime', + 'ZuTimestamp', + 'ZuDuration', + 'ZuNode', + 'ZuRel', +] + +test('the same names come out of require and out of import', () => { + assert.deepEqual(Object.keys(cjs).sort(), [...SURFACE].sort()) + // A namespace object also carries the module's own machinery, so the + // comparison is of what the package exports rather than of every key. + const named = Object.keys(esm).filter((name) => name !== '__esModule') + assert.deepEqual(named.sort(), [...SURFACE].sort()) + + // The same objects, not two copies of them, which is what makes + // `instanceof` work for a program that mixes the two formats. A + // separate load per format would give a ZuDate from one that fails + // `instanceof ZuDate` from the other. + for (const name of SURFACE) assert.equal(esm[name], cjs[name], `${name} is loaded twice`) +}) + +test('there is no default export in either format', () => { + // A default export of the namespace is a second spelling of every + // name, and which of the two a caller gets then depends on their + // bundler and their `esModuleInterop`. + assert.equal(esm.default, undefined) + assert.equal(cjs.default, undefined) +}) + +test('every export condition names its types first', async () => { + const pkg = JSON.parse(await readFile(new URL('package.json', root), 'utf8')) + + for (const [condition, entry] of Object.entries(pkg.exports['.'])) { + // Conditions are matched in the order they are written, and a + // resolver takes the first one that matches. `types` after + // `default` is a `types` nothing ever reaches, which is a package + // that compiles here and is `any` everywhere else. + assert.equal(Object.keys(entry)[0], 'types', `${condition} does not name its types first`) + } + + // The two formats are told apart by extension rather than by a + // directory, so a resolver knows what it has before it reads it. + assert.equal(pkg.exports['.'].import.default, './zudb.mjs') + assert.equal(pkg.exports['.'].require.default, './zudb.cjs') + assert.equal(pkg.exports['.'].import.types, './zudb.d.mts') + assert.equal(pkg.exports['.'].require.types, './zudb.d.cts') + + // The old fields, for a resolver that reads no conditions at all. + assert.equal(pkg.main, './zudb.cjs') + assert.equal(pkg.types, './zudb.d.cts') +}) + +test('the guard narrows a caught value to a zu failure', async (t) => { + assert.equal(cjs.isZuError(new Error('plain')), false) + assert.equal(cjs.isZuError('a string'), false) + assert.equal(cjs.isZuError(null), false) + // A signal's abort is a failure the caller caused, and a caller + // separating their own cancellation from the database's conditions + // needs it to answer no. + assert.equal(cjs.isZuError(new DOMException('stopped', 'AbortError')), false) + + const { conn } = await fresh(t) + const caught = await conn.query('MATCH (').then(() => null, (err) => err) + assert.equal(cjs.isZuError(caught), true) + assert.equal(caught.name, 'ZuSyntaxError') +}) diff --git a/test/helper.mjs b/test/helper.mjs index 8df30c4..0643920 100644 --- a/test/helper.mjs +++ b/test/helper.mjs @@ -2,7 +2,7 @@ import { mkdtemp, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' -import { connect } from '../index.js' +import { connect, isZuError as guard } from 'zudb' // A database of its own per test, in a directory of its own, removed // when the test ends. Sharing one would make the order the tests run in @@ -27,7 +27,9 @@ export async function twoPeople(t) { } // What a test asserts about an error it caught, in one place, since -// every one of them wants the same first two things. +// every one of them wants the same first two things. The guard the +// package exports is what says it is a zu failure at all, so the tests +// exercise the same predicate a caller would write. export function isZuError(err, name) { - return err instanceof Error && err.name === name && typeof err.retryable === 'boolean' + return guard(err) && err.name === name } diff --git a/test/packaging.test.mjs b/test/packaging.test.mjs index 7782956..7511ea2 100644 --- a/test/packaging.test.mjs +++ b/test/packaging.test.mjs @@ -38,7 +38,16 @@ test('installing builds nothing, so no compiler is needed and no script runs', a // The root package carries no binary. Every one of them is its own // package, so npm downloads exactly the one the machine can run // rather than all eight. - assert.deepEqual(pkg.files, ['index.js', 'index.d.ts', 'README.md', 'LICENSE']) + assert.deepEqual(pkg.files, [ + 'binding.cjs', + 'binding.d.cts', + 'zudb.cjs', + 'zudb.mjs', + 'zudb.d.cts', + 'zudb.d.mts', + 'README.md', + 'LICENSE', + ]) }) test('every target is built, published and asked for, or none of the three', async () => { @@ -96,7 +105,7 @@ test('a platform package says which machine it is for and holds one file', async }) test('the loader asks for the packages that are published', async () => { - const loader = await readFile(new URL('index.js', root), 'utf8') + const loader = await readFile(new URL('binding.cjs', root), 'utf8') // The loader is generated and the packages are generated, from the // same list, but not by the same command and not at the same time. diff --git a/test/query.test.mjs b/test/query.test.mjs index 8f885e9..f398646 100644 --- a/test/query.test.mjs +++ b/test/query.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { ZuNode } from '../index.js' +import { ZuNode } from 'zudb' import { twoPeople } from './helper.mjs' test('the rows come back as an array of objects keyed by column', async (t) => { diff --git a/test/types/cjs.cts b/test/types/cjs.cts new file mode 100644 index 0000000..f60ac8b --- /dev/null +++ b/test/types/cjs.cts @@ -0,0 +1,19 @@ +// The same package as a CommonJS user sees it, which is a different +// resolution through a different condition to a different file, and so +// is worth compiling separately rather than assuming. + +import { connect, isZuError, type ZuParam } from 'zudb' + +export async function insert(path: string, values: Record): Promise { + const conn = await connect(path) + try { + // A parameter object is wider than a result: `undefined` binds as + // null, and a whole `number` binds as INT64. + await conn.exec('INSERT (p:person {id: $id, name: $name})', values) + } catch (caught) { + if (isZuError(caught) && caught.code === '42001') return + throw caught + } finally { + conn.close() + } +} diff --git a/test/types/esm.mts b/test/types/esm.mts new file mode 100644 index 0000000..955ab67 --- /dev/null +++ b/test/types/esm.mts @@ -0,0 +1,38 @@ +// The package as an ES module user sees it. Nothing here runs: what is +// being asserted is that it compiles, which is the half of an API that +// a test suite in JavaScript cannot reach. + +import { connect, isZuError, ZuDate, type ZuError, type ZuRows } from 'zudb' + +export async function people(path: string, name: string): Promise { + // `await using` is the intended scoping, and it needs the connection + // to carry `Symbol.asyncDispose` in its type as well as at runtime. + await using conn = await connect(path, { readOnly: true }) + + const rows: ZuRows<{ id: bigint; name: string }> = await conn.query( + 'MATCH (p:person) WHERE p.name = $name RETURN p.id AS id, p.name AS name', + { name }, + { signal: AbortSignal.timeout(50) }, + ) + + // The projection in the order it was written, beside the rows rather + // than among them. + const columns: string[] = rows.columns + if (columns.length !== 2) throw new Error('the projection changed shape') + + // An INT64 is a bigint, which is the one rule worth learning first: a + // `number` here would not compile, and that is the whole point of it. + return rows.map((row) => `${row.name} ${row.id.toString()}`) +} + +export function retryable(caught: unknown): boolean { + // `catch` gives `unknown`, and the guard is what narrows it. Reading + // `caught.retryable` without it does not compile. + if (!isZuError(caught)) return false + const err: ZuError = caught + return err.retryable +} + +export function epoch(): ZuDate { + return new ZuDate(0) +} diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 0000000..da59a5f --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,24 @@ +{ + "//": [ + "What a TypeScript user's compiler does with this package.", + "node16 for both module and moduleResolution, because that is the", + "one setting that reads the `exports` map the way Node does: a", + "resolver on the old `node` algorithm never sees a condition and so", + "never catches a package that named the wrong file in one of them.", + "The two fixtures import by package name rather than by path, which", + "resolves through `exports` exactly as an installed copy would." + ], + "compilerOptions": { + "module": "node16", + "moduleResolution": "node16", + "target": "es2023", + "lib": ["ES2023", "ESNext.Disposable"], + "types": ["node"], + "strict": true, + "noEmit": true, + "skipLibCheck": false, + "exactOptionalPropertyTypes": true, + "noUncheckedIndexedAccess": true + }, + "files": ["esm.mts", "cjs.cts"] +} diff --git a/test/values.test.mjs b/test/values.test.mjs index 968c66e..99febe4 100644 --- a/test/values.test.mjs +++ b/test/values.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { ZuDate, ZuDuration, ZuTime, ZuTimestamp } from '../index.js' +import { ZuDate, ZuDuration, ZuTime, ZuTimestamp } from 'zudb' import { fresh, twoPeople } from './helper.mjs' // What a parameter binds as is what comes back, so one statement that diff --git a/tools/install.mjs b/tools/install.mjs index 8c041bd..d31adce 100644 --- a/tools/install.mjs +++ b/tools/install.mjs @@ -57,18 +57,39 @@ try { join(app, 'run.mjs'), [ "import assert from 'node:assert/strict'", - "import { connect } from 'zudb'", + "import { connect, isZuError } from 'zudb'", "const conn = await connect(':memory:')", 'await conn.exec("INSERT (:person {id: 1, name: \'ada\'})")', "const rows = await conn.query('MATCH (p:person) RETURN p.name AS name')", "assert.deepEqual(rows.columns, ['name'])", "assert.equal(rows[0].name, 'ada')", + "assert.equal(isZuError(await conn.query('MATCH (').catch((err) => err)), true)", 'await conn.close()', ].join('\n'), ) execFileSync(process.execPath, [join(app, 'run.mjs')], { cwd: app, stdio: 'inherit' }) - console.log(`installed zudb and zudb-${platform.dir} into an empty project, and it ran a statement`) + // The other half of the package, resolved through the other condition. + // The two formats reach the same loaded module, so a value made by one + // is an instance of the class the other exports, and a program that + // mixes them is not quietly holding two of everything. + await writeFile( + join(app, 'run.cjs'), + [ + "const assert = require('node:assert/strict')", + "const required = require('zudb')", + "import('zudb').then(async (imported) => {", + ' assert.equal(required.ZuDate, imported.ZuDate)', + " const conn = await required.connect(':memory:')", + " const rows = await conn.query('RETURN 1 AS n')", + ' assert.equal(rows[0].n, 1n)', + ' await conn.close()', + '})', + ].join('\n'), + ) + execFileSync(process.execPath, [join(app, 'run.cjs')], { cwd: app, stdio: 'inherit' }) + + console.log(`installed zudb and zudb-${platform.dir} into an empty project, and it ran a statement from both formats`) } finally { await rm(where, { recursive: true, force: true }) } diff --git a/zudb.cjs b/zudb.cjs new file mode 100644 index 0000000..f4ea505 --- /dev/null +++ b/zudb.cjs @@ -0,0 +1,48 @@ +// The package, as CommonJS. +// +// The file the binding generator writes is a loader: it works out which +// platform package holds the addon for this machine, dlopens it, and +// re-exports whatever the addon registered. This file is the package's +// own surface over it, which is where anything written in JavaScript +// rather than in Rust belongs, and it is what both `require('zudb')` and +// `import 'zudb'` end at. +// +// Every name is listed one at a time rather than spread across, because +// the list is the API. A spread would publish whatever the addon happens +// to register, which is how a helper meant for a test ends up as +// something somebody depends on. + +const binding = require('./binding.cjs') + +/** + * Whether a caught value is a failure from this client. + * + * A `catch` in TypeScript gives you `unknown`, and this is the guard + * that narrows it. It tests the two things every zu failure has and + * nothing else does: a name in the family, and a `retryable` that says + * whether running the same statement again could work. An `AbortError` + * from a signal is not one of these, which is the point, since it came + * from the caller rather than from the database. + */ +function isZuError(value) { + return ( + value instanceof Error && + typeof value.name === 'string' && + value.name.startsWith('Zu') && + typeof value.retryable === 'boolean' + ) +} + +module.exports = { + connect: binding.connect, + version: binding.version, + abiVersion: binding.abiVersion, + isZuError, + Connection: binding.Connection, + ZuDate: binding.ZuDate, + ZuTime: binding.ZuTime, + ZuTimestamp: binding.ZuTimestamp, + ZuDuration: binding.ZuDuration, + ZuNode: binding.ZuNode, + ZuRel: binding.ZuRel, +} diff --git a/zudb.d.cts b/zudb.d.cts new file mode 100644 index 0000000..34267c4 --- /dev/null +++ b/zudb.d.cts @@ -0,0 +1,29 @@ +/* The types for `require('zudb')`. */ + +import type { ZuError } from './binding.cjs' + +export * from './binding.cjs' + +/** + * `await using` on a connection, in the types as well as at runtime. + * + * The disposal is put on every connection as it is made, under the key + * `await using` looks up. It cannot be declared where the rest of the + * class is, because the generator writes that file from the Rust and a + * method's name there is a string while this key is a symbol. + */ +declare module './binding.cjs' { + interface Connection extends AsyncDisposable {} +} + +/** + * Whether a caught value is a failure from this client. + * + * A `catch` gives you `unknown`, and this is the guard that narrows it. + * It tests the two things every zu failure has and nothing else does: a + * name in the family, and a `retryable` that says whether running the + * same statement again could work. An `AbortError` from a signal is not + * one of these, which is the point, since it came from the caller rather + * than from the database. + */ +export declare function isZuError(value: unknown): value is ZuError diff --git a/zudb.d.mts b/zudb.d.mts new file mode 100644 index 0000000..28df51a --- /dev/null +++ b/zudb.d.mts @@ -0,0 +1,10 @@ +/* The types for `import ... from "zudb"`. + * + * The same declarations as the CommonJS ones, taken from them rather + * than written twice, because two copies of a public API are two things + * that drift. A resolver following the `import` condition lands here, + * sees an ES module, and reads the types out of the file the `require` + * condition lands on. + */ + +export * from './zudb.cjs' diff --git a/zudb.mjs b/zudb.mjs new file mode 100644 index 0000000..c5fc0fa --- /dev/null +++ b/zudb.mjs @@ -0,0 +1,29 @@ +// The same package, as an ES module. +// +// A native addon is a CommonJS thing: what loads it is `process.dlopen` +// through `require`, and there is no import that does it. So this file +// is what an ES module import of the package reaches, and it takes the +// same surface out of the CommonJS one rather than repeating it. There +// is one implementation of everything and two ways of naming it. +// +// Named exports only, and no default. A default export of a namespace is +// a second spelling of every name in the package, and the two spellings +// then differ by bundler, by `esModuleInterop`, and by whether the file +// doing the importing is itself a module. + +import { createRequire } from 'node:module' + +const require = createRequire(import.meta.url) +const zudb = require('./zudb.cjs') + +export const connect = zudb.connect +export const version = zudb.version +export const abiVersion = zudb.abiVersion +export const isZuError = zudb.isZuError +export const Connection = zudb.Connection +export const ZuDate = zudb.ZuDate +export const ZuTime = zudb.ZuTime +export const ZuTimestamp = zudb.ZuTimestamp +export const ZuDuration = zudb.ZuDuration +export const ZuNode = zudb.ZuNode +export const ZuRel = zudb.ZuRel