From 66f241d141734ee8e1aee6a86a2477634055c4ce Mon Sep 17 00:00:00 2001 From: pkurcx Date: Thu, 3 Sep 2026 17:54:07 +0200 Subject: [PATCH] feat: WebAssembly build and browser entry Ship the generator as a wasm32-wasip1-threads build published as @avsystem/openapi-ng-wasm32-wasi, and make browser.js a working entry backed by it. - The native export returns a { result, error } outcome instead of throwing, so typed GenerateErrors are identical on native and WASI; the JS wrappers construct the error. - wasm32-wasip1-threads joins the napi targets; emnapi pinned via the workspace overrides; CI builds it, runs the suite forced to WASI plus a native run with a WASI cross-check, and gates publish on it. - browser.js re-exports lib/browser.js (inputContents only, lazy import of the wasm package, E_UNSUPPORTED_RUNTIME on load failure); a typed ./browser subpath exposes createGenerate. - Under the Node WASI fallback, relative paths and the banner resolve against the host cwd via PWD. - napi, napi-derive and napi-build are pinned to the last releases that link against the emnapi 1.x archive: napi-build 2.4 exports the emnapi v2 entry points, and rustc 1.98's linker rejects the missing symbols instead of ignoring them. - Runtime and Node API docs describe the browser path. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011t8uBVY6h4kfN5cXHXuCpR --- .github/workflows/CI.yml | 63 ++++- .gitignore | 6 + Cargo.lock | 17 +- Cargo.toml | 11 +- __test__/browser.spec.ts | 117 +++++++-- __test__/generate.spec.ts | 16 +- __test__/package.spec.ts | 83 ++++-- browser.d.ts | 36 +++ browser.js | 32 +-- build.rs | 42 --- index.d.ts | 32 ++- index.d.ts.in | 21 +- lib/browser.js | 113 ++++++++ lib/index.js | 14 +- lib/wrapper-core.js | 36 ++- native.js | 112 ++++---- package.json | 20 +- pnpm-lock.yaml | 247 ++++++------------ pnpm-workspace.yaml | 3 + scripts/patch-types.mjs | 100 +++---- src/bindings.rs | 148 +++++------ src/emit/mod.rs | 5 +- src/io/host_cwd.rs | 46 ++++ src/io/mod.rs | 1 + src/io/writer.rs | 5 +- src/lib.rs | 33 ++- src/parse/input.rs | 5 +- .../src/content/docs/reference/node-api.md | 8 + website/src/content/docs/reference/runtime.md | 63 +++-- 29 files changed, 863 insertions(+), 572 deletions(-) create mode 100644 browser.d.ts create mode 100644 lib/browser.js create mode 100644 src/io/host_cwd.rs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 02f7852..17778b5 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -105,6 +105,9 @@ jobs: - host: windows-latest target: aarch64-pc-windows-msvc build: pnpm build --target aarch64-pc-windows-msvc + - host: ubuntu-latest + target: wasm32-wasip1-threads + build: pnpm build --target wasm32-wasip1-threads name: stable - ${{ matrix.settings.target }} runs-on: ${{ matrix.settings.host }} steps: @@ -156,7 +159,15 @@ jobs: uses: actions/upload-artifact@v7 with: name: bindings-${{ matrix.settings.target }} - path: '*.node' + path: | + *.node + *.wasm + !*.debug.wasm + *.wasi.cjs + *.wasi-browser.js + *.wasi.d.cts + wasi-worker.mjs + wasi-worker-browser.mjs if-no-files-found: error test-macOS-windows-binding: name: Test bindings on ${{ matrix.settings.target }} - node@${{ matrix.node }} @@ -273,6 +284,55 @@ jobs: name: test-binding options: -v ${{ steps.docker.outputs.PNPM_STORE_PATH }}:${{ steps.docker.outputs.PNPM_STORE_PATH }} -v ${{ github.workspace }}:${{ github.workspace }} -w ${{ github.workspace }} --platform ${{ steps.docker.outputs.PLATFORM }} args: npm run test + test-wasm-binding: + name: Test wasm32-wasi binding - node@${{ matrix.node }} + needs: + - build + strategy: + fail-fast: false + matrix: + node: + - '22' + - '26' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: setup pnpm + uses: pnpm/action-setup@v6 + - name: Setup node + uses: actions/setup-node@v7 + with: + node-version: ${{ matrix.node }} + cache: pnpm + - name: Install dependencies + run: pnpm install + - name: Download artifacts + uses: actions/download-artifact@v8 + with: + name: bindings-wasm32-wasip1-threads + path: . + - name: Download native artifacts + uses: actions/download-artifact@v8 + with: + name: bindings-x86_64-unknown-linux-gnu + path: . + - name: List packages + run: ls -R . + shell: bash + - name: Test bindings (WASI forced) + run: pnpm test + env: + # The generated loader only honours the literal strings 'true' and + # 'error'; 'error' would also force @oxc-node/core (ava's TS loader) + # onto a WASI build it does not ship. + NAPI_RS_FORCE_WASI: 'true' + # Node prints "ExperimentalWarning: WASI" on stderr, which CLI tests compare. + NODE_OPTIONS: --no-warnings + - name: Test bindings (native, WASI cross-check) + run: pnpm test + env: + OPENAPI_NG_EXPECT_WASI_BINDING: '1' + NODE_OPTIONS: --no-warnings publish: name: Publish runs-on: ubuntu-latest @@ -281,6 +341,7 @@ jobs: - test-rust-cross-os - test-macOS-windows-binding - test-linux-binding + - test-wasm-binding steps: - uses: actions/checkout@v7 - name: setup pnpm diff --git a/.gitignore b/.gitignore index c896fcc..50ca5a6 100644 --- a/.gitignore +++ b/.gitignore @@ -153,6 +153,12 @@ dist .pnp.* *.node +*.wasm +*.wasi.cjs +*.wasi-browser.js +*.wasi.d.cts +wasi-worker.mjs +wasi-worker-browser.mjs ### Node Patch ### # Serverless Webpack directories diff --git a/Cargo.lock b/Cargo.lock index 0f637c0..6cc823f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -214,14 +214,13 @@ checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" [[package]] name = "napi" -version = "3.12.1" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459197f1592f4c3dbbf9c1b13f5a4599a343e4ef66b96bc340e2a518b36a6662" +checksum = "de33522036981030a75c231829566bc63414e08101a6f5ff4ac6cef19c8e0941" dependencies = [ "bitflags", "ctor", "futures", - "libc", "napi-build", "napi-sys", "nohash-hasher", @@ -230,15 +229,15 @@ dependencies = [ [[package]] name = "napi-build" -version = "2.4.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60fdf9b392c50e7c4170fa633bd909490ed7835cea4c046776d1a4dd8d2ae0ab" +checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" [[package]] name = "napi-derive" -version = "3.6.3" +version = "3.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa55ea69990c90b888e9e77044410e304ce7f35de599dc6d0b5c1923d2e59af" +checksum = "b0fe526e81c105d3640516fcde83909dd1afe757c0d7a15af58830b5bc0fb9a1" dependencies = [ "convert_case", "ctor", @@ -250,9 +249,9 @@ dependencies = [ [[package]] name = "napi-derive-backend" -version = "6.1.2" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df4056ac7c18e4438ccf0edaed4340ca0d269278c8ec19284f7b23cb039fd0ae" +checksum = "514281397bcddd9ea9a876c7a21a57bff2374237a000ca9a64ea0211ec1993e2" dependencies = [ "convert_case", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index a858cff..693515f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,8 +8,12 @@ crate-type = ["cdylib", "rlib"] [dependencies] indexmap = { version = "2", features = ["serde"] } -napi = "3.0.0" -napi-derive = "3.0.0" +# Pinned together with napi-build below: napi >= 3.12 requires napi-build 2.4, +# which links for the emnapi v2 archive (`--export=emnapi_create_env`) that +# emnapi 1.x does not define; rustc >= 1.98 (lld) rejects the missing export. +# napi-derive 3.6 generates code only napi 3.12 provides. +napi = "=3.11.0" +napi-derive = "=3.5.10" regex = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" @@ -19,7 +23,8 @@ serde_json = "1.0" serde_yml = "=0.0.12" [build-dependencies] -napi-build = "2" +# See the napi pin above. +napi-build = "=2.3.2" [dev-dependencies] proptest = { version = "1.7", default-features = false, features = ["std"] } diff --git a/__test__/browser.spec.ts b/__test__/browser.spec.ts index 40905cd..8d22bbc 100644 --- a/__test__/browser.spec.ts +++ b/__test__/browser.spec.ts @@ -1,35 +1,118 @@ import test from 'ava'; +import fs from 'node:fs'; import { createRequire } from 'node:module'; import path from 'node:path'; -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import { generate as nativeGenerate } from '../lib/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.join(__dirname, '..'); const require = createRequire(import.meta.url); -const browserEntry = require(path.join(repoRoot, 'browser.js')) as { - generate: (options?: unknown) => Promise; - GenerateError: { - isGenerateError: (value: unknown) => boolean; - new (payload?: unknown): Error & { code?: string; message: string }; - }; +type GenerateFn = (options: unknown) => Promise<{ + artifacts: Array<{ path: string; contents: string }>; + diagnostics: unknown[]; +}>; +type BrowserEntry = { + generate: GenerateFn; + createGenerate: (load: () => Promise) => GenerateFn; + GenerateError: { isGenerateError: (value: unknown) => boolean }; EmitTarget: { Models: string; Angular: string }; }; +type TypedError = { code?: string; subcode?: string | null; message: string }; -test('browser generate throws a GenerateError, not a plain Error', async t => { - const { generate, GenerateError } = browserEntry; - const err = await t.throwsAsync(async () => { - await generate({ inputPath: 'x', emit: ['models'] }); +const browserEntry = require(path.join(repoRoot, 'browser.js')) as BrowserEntry; +const wasiCjsPath = path.join(repoRoot, 'openapi-ng.wasi.cjs'); +// `import()` needs a URL, not a bare Windows path. +const wasiCjs = pathToFileURL(wasiCjsPath).href; +const hasWasi = fs.existsSync(wasiCjsPath); +// CI's cross-check job downloads both bindings, so a skipped WASI test there +// means the artifact went missing rather than that the run is a local one. +const expectWasi = process.env.OPENAPI_NG_EXPECT_WASI_BINDING === '1'; +if (expectWasi && !hasWasi) { + test('WASI binding is present when OPENAPI_NG_EXPECT_WASI_BINDING is set', t => { + t.fail(`expected ${wasiCjsPath} to exist`); }); - t.true(GenerateError.isGenerateError(err)); - t.is((err as { code?: string } | undefined)?.code, 'E_UNSUPPORTED_RUNTIME'); - // Be lenient on the message — the test originally expected /browser/i but - // the new wrapper says "browser/runtime context". - t.regex(err?.message ?? '', /browser|runtime/i); +} +const wasiTest = hasWasi ? test : test.skip; +const petstore = fs.readFileSync( + path.join(repoRoot, 'test', 'fixtures', 'petstore-minimal.openapi.yaml'), + 'utf8', +); +const petstoreOptions = { + inputContents: petstore, + displayPath: 'petstore-minimal.openapi.yaml', + emit: ['models', 'angular'], +}; + +wasiTest( + 'browser generate through the WASI binding matches the native output', + async t => { + const generate = browserEntry.createGenerate(() => import(wasiCjs)); + const [fromWasi, fromNative] = await Promise.all([ + generate(petstoreOptions), + nativeGenerate(petstoreOptions), + ]); + t.deepEqual( + fromWasi.artifacts.map(a => [a.path, a.contents]), + fromNative.artifacts.map(a => [a.path, a.contents]), + ); + t.deepEqual(fromWasi.diagnostics, fromNative.diagnostics); + }, +); + +wasiTest('browser generate surfaces typed fatal diagnostics', async t => { + const generate = browserEntry.createGenerate(() => import(wasiCjs)); + const err = (await t.throwsAsync(() => + generate({ + inputContents: + 'openapi: 3.0.3\ninfo: {title: x, version: "1"}\npaths: {/a: {get: {responses: {"200": {description: ok}}}}}', + displayPath: 'x.yaml', + emit: ['models', 'angular'], + }), + )) as TypedError; + t.true(browserEntry.GenerateError.isGenerateError(err)); + t.is(err.code, 'E_POLICY_VIOLATION'); + t.is(err.subcode, 'missing-operation-id'); +}); + +test('browser generate rejects inputPath with E_INVALID_OPTION', async t => { + const err = (await t.throwsAsync(() => + browserEntry.generate({ inputPath: 'spec.yaml', emit: ['models'] }), + )) as TypedError; + t.is(err.code, 'E_INVALID_OPTION'); + t.is(err.subcode, 'shape'); + t.regex(err.message, /inputContents/); +}); + +test('browser generate rejects outputPath with E_INVALID_OPTION', async t => { + const err = (await t.throwsAsync(() => + browserEntry.generate({ ...petstoreOptions, outputPath: 'out' }), + )) as TypedError; + t.is(err.code, 'E_INVALID_OPTION'); + t.is(err.subcode, 'shape'); + t.regex(err.message, /outputPath/); +}); + +test('browser generate maps a failed binding load to E_UNSUPPORTED_RUNTIME', async t => { + const generate = browserEntry.createGenerate(() => + Promise.reject(new Error('no wasm here')), + ); + const err = (await t.throwsAsync(() => generate(petstoreOptions))) as TypedError; + t.is(err.code, 'E_UNSUPPORTED_RUNTIME'); + t.regex(err.message, /openapi-ng-wasm32-wasi/); + t.regex(err.message, /no wasm here/); +}); + +test('browser generate maps a binding without generateNative to E_UNSUPPORTED_RUNTIME', async t => { + const generate = browserEntry.createGenerate(() => Promise.resolve({})); + const err = (await t.throwsAsync(() => generate(petstoreOptions))) as TypedError; + t.is(err.code, 'E_UNSUPPORTED_RUNTIME'); + t.regex(err.message, /generateNative/); }); test('browser entry exports EmitTarget mirror', t => { - t.truthy(browserEntry.EmitTarget); t.is(browserEntry.EmitTarget.Models, 'models'); t.is(browserEntry.EmitTarget.Angular, 'angular'); }); diff --git a/__test__/generate.spec.ts b/__test__/generate.spec.ts index e5c12d6..759d0b8 100644 --- a/__test__/generate.spec.ts +++ b/__test__/generate.spec.ts @@ -1444,7 +1444,11 @@ test('generate wraps Rust panic into E_UNEXPECTED GenerateError', async t => { { instanceOf: GenerateError }, ); t.is(err?.code, 'E_UNEXPECTED'); - t.regex(err?.message ?? '', /unexpected.*panic/i); + // wasm cannot unwind, so a panic traps as `unreachable` and the payload is + // lost; only the native binding carries the panic text through. + if (process.env.NAPI_RS_FORCE_WASI !== 'true') { + t.regex(err?.message ?? '', /unexpected.*panic/i); + } }); test('GenerateError is a real class so consumers can guard with instanceof', async t => { @@ -1508,11 +1512,11 @@ test('GenerateError.isGenerateError detects upgraded errors via the cross-realm t.false(GenerateError.isGenerateError({ code: 'E_INVALID_OPTION' })); }); -test('GenerateError marker constant matches the value Rust embeds via build.rs', async t => { - // `lib/error-marker.json` is the single source of truth shared with - // the Rust binding through `env!("OPENAPI_NG_ERROR_MARKER")` (see - // build.rs). The constant inside the published JSON file must remain - // a non-empty string so the cross-realm marker survives the boundary. +test('GenerateError marker constant is stamped by the constructor alone', async t => { + // `lib/error-marker.json` is the only source of the marker; the Rust side + // neither embeds nor reads it, `GenerateError`'s constructor stamps it. + // The constant must stay a non-empty string so the cross-realm marker + // survives the boundary. const marker = JSON.parse( fs.readFileSync(path.join(__dirname, '..', 'lib', 'error-marker.json'), 'utf8'), ); diff --git a/__test__/package.spec.ts b/__test__/package.spec.ts index 1686aeb..40d2fe8 100644 --- a/__test__/package.spec.ts +++ b/__test__/package.spec.ts @@ -11,41 +11,55 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const repoRoot = path.join(__dirname, '..'); const require = createRequire(import.meta.url); -test('browser entry exists and fails with an explicit unsupported-runtime error', async t => { - const browserEntry = require(path.join(repoRoot, 'browser.js')) as { - generate: (options?: unknown) => Promise; - }; - - t.is(typeof browserEntry.generate, 'function'); - - // The browser entry is a hard-error stub — browser/edge runtimes are - // unsupported, so generate() rejects with E_UNSUPPORTED_RUNTIME at call - // time. The module itself must stay importable so bundlers don't choke. - const generateError = (await t.throwsAsync(async () => { - await browserEntry.generate(); - })) as GenerateError | undefined; - t.is(generateError?.code, 'E_UNSUPPORTED_RUNTIME'); - t.regex(generateError?.message ?? '', /browser|runtime/i); +test('browser entry re-exports lib/browser.js and ships in the files allow-list', t => { + const browserSource = fs.readFileSync(path.join(repoRoot, 'browser.js'), 'utf8'); + t.regex(browserSource, /require\('\.\/lib\/browser\.js'\)/); + const packageJson = JSON.parse( + fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'), + ) as { files?: string[] }; + t.true(packageJson.files?.includes('lib/browser.js') ?? false); }); test('package.json exports map covers node, browser, default, and types conditions', t => { const packageJson = JSON.parse( fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'), ) as { - exports?: Record | string>; + exports?: Record; }; const root = packageJson.exports?.['.']; t.truthy(root, 'package.json exports map must include "."'); t.is(typeof root, 'object'); - const conditions = root as Record; + const conditions = root as Record; t.is(conditions.types, './index.d.ts'); - t.is(conditions.browser, './browser.js'); + t.deepEqual(conditions.browser, { + types: './browser.d.ts', + default: './browser.js', + }); // Node entry is the wrapper at lib/index.js that upgrades thrown // errors into `GenerateError` instances. The raw napi-rs binding at // ./index.js is internal. t.is(conditions.node, './lib/index.js'); t.is(conditions.default, './lib/index.js'); + + t.deepEqual(packageJson.exports?.['./browser'], { + types: './browser.d.ts', + default: './browser.js', + }); +}); + +test('createGenerate is declared on the browser surface only', t => { + const dts = fs.readFileSync(path.join(repoRoot, 'index.d.ts'), 'utf8'); + const browserDts = fs.readFileSync(path.join(repoRoot, 'browser.d.ts'), 'utf8'); + // The Node entry does not export it, so declaring it in index.d.ts would + // typecheck green and fail at runtime. + t.false(dts.includes('createGenerate'), 'index.d.ts must not declare createGenerate'); + t.true(browserDts.includes('createGenerate')); + + const packageJson = JSON.parse( + fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'), + ) as { files?: string[] }; + t.true(packageJson.files?.includes('browser.d.ts') ?? false); }); test('package metadata keeps the node-only packaging contract explicit', t => { @@ -67,14 +81,16 @@ test('package metadata keeps the node-only packaging contract explicit', t => { t.notRegex(packageJson.description ?? '', /Template project/i); }); -test('napi.targets is non-empty and lists only native triples (no wasm)', t => { +test('napi.targets is non-empty and the only wasm target is the wasip1-threads fallback', t => { const packageJson = JSON.parse( fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'), ) as { napi?: { targets?: string[] } }; const targets = packageJson.napi?.targets ?? []; t.true(targets.length > 0, 'napi.targets must be non-empty'); for (const triple of targets) { - t.false(triple.startsWith('wasm32-'), `unexpected wasm32 target: ${triple}`); + if (triple.startsWith('wasm32-')) { + t.is(triple, 'wasm32-wasip1-threads', `unexpected wasm32 target: ${triple}`); + } } }); @@ -163,14 +179,16 @@ test('patch-types narrows GeneratorDiagnostic and GenerateErrorPayload bodies', t.false(dts.includes(' severity: string')); }); -test('runtime docs document the current node-only boundary and browser stub', t => { +test('runtime docs describe the WebAssembly browser path', t => { const runtimeDoc = fs.readFileSync( path.join(repoRoot, 'website', 'src', 'content', 'docs', 'reference', 'runtime.md'), 'utf8', ); - t.regex(runtimeDoc, /does not support browser runtimes/i); + t.regex(runtimeDoc, /@avsystem\/openapi-ng-wasm32-wasi/); + t.regex(runtimeDoc, /Cross-Origin-Embedder-Policy/); t.regex(runtimeDoc, /E_UNSUPPORTED_RUNTIME/); + t.notRegex(runtimeDoc, /does not support browser runtimes/i); }); test('native.js includes a friendly unsupported-platform error with supported list', t => { @@ -197,6 +215,27 @@ test('native.js includes a friendly unsupported-platform error with supported li } }); +// `NAPI_RS_FORCE_WASI=true` keeps native as a lazy fallback, so a missing +// wasm artifact would otherwise let the WASI job pass on a host binary. +test('NAPI_RS_FORCE_WASI=true loads the WASI binding, not a native one', t => { + if (process.env.NAPI_RS_FORCE_WASI !== 'true') { + t.pass('not a forced-WASI run'); + return; + } + require(path.join(repoRoot, 'lib', 'index.js')); + const loaded = Object.keys(require.cache).map(p => path.basename(p)); + t.true( + loaded.includes('openapi-ng.wasi.cjs'), + `loaded modules: ${loaded.filter(n => /\.node$|wasi/.test(n)).join(', ')}`, + ); + // Scoped to our binary: ava's TypeScript loader (@oxc-node/core) always + // brings its own .node addon into the cache. + t.false( + loaded.some(n => /^openapi-ng\..+\.node$/.test(n)), + 'a native openapi-ng binding was loaded alongside WASI', + ); +}); + test('lib/config.js is shipped in package.json files allow-list', t => { const packageJson = JSON.parse( fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'), diff --git a/browser.d.ts b/browser.d.ts new file mode 100644 index 0000000..0fd11b0 --- /dev/null +++ b/browser.d.ts @@ -0,0 +1,36 @@ +// Types for the browser entry (`browser.js` / `./browser` subpath). +import type { GenerateOptions, GenerateResult } from './index'; + +export type { + DiagnosticCode, + DiagnosticSubcode, + GenerateErrorPayload, + GeneratedArtifact, + GenerateOptions, + GenerateResult, + GenerateSummary, + GeneratorDiagnostic, + InputFormat, + MappedType, + NamingConfig, + ResponseTypeMapping, +} from './index'; +export { EmitTarget, GenerateError } from './index'; + +/** + * Generate in the browser. Same contract as the Node `generate`, except + * `inputPath` and `outputPath` are rejected with `E_INVALID_OPTION`; pass + * `inputContents` with `displayPath` and read `result.artifacts`. + */ +export declare function generate(options: GenerateOptions): Promise; + +/** + * Build a `generate` bound to a custom loader for the WebAssembly binding, + * for hosts that serve the binding themselves instead of resolving + * `@avsystem/openapi-ng-wasm32-wasi`. The loader resolves to the WASI + * module namespace or its CommonJS `module.exports`; both expose + * `generateNative`. + */ +export declare function createGenerate( + loadBinding: () => Promise, +): (options: GenerateOptions) => Promise; diff --git a/browser.js b/browser.js index 6ee3699..6a3385f 100644 --- a/browser.js +++ b/browser.js @@ -1,33 +1,3 @@ 'use strict'; -// Browser/edge entry point. openapi-ng requires the native binding to -// generate code, so any browser or edge runtime (Vite/Webpack/esbuild -// resolving the `browser` field, Cloudflare Workers, Vercel Edge, etc.) -// gets a stub that throws `E_UNSUPPORTED_RUNTIME` at call time. The -// module itself stays importable so bundlers don't choke at build time. - -const { GenerateError } = require('./lib/generate-error.js'); - -async function generate() { - throw new GenerateError({ - code: 'E_UNSUPPORTED_RUNTIME', - message: - 'openapi-ng does not support browser or edge runtimes. ' + - 'Run the generator from Node, or remove openapi-ng from your browser bundle.', - warnings: [], - }); -} - -// Frozen runtime shape for `EmitTarget`. Mirrors the ambient const -// declared in `index.d.ts` and matches the `lib/index.js` entry, so the -// surface a consumer destructures is identical across both runtimes. -const EmitTarget = Object.freeze({ - Models: 'models', - Angular: 'angular', -}); - -module.exports = { - generate, - GenerateError, - EmitTarget, -}; +module.exports = require('./lib/browser.js'); diff --git a/build.rs b/build.rs index 43918af..1f866b6 100644 --- a/build.rs +++ b/build.rs @@ -1,47 +1,5 @@ extern crate napi_build; -use std::fs; -use std::path::Path; - fn main() { napi_build::setup(); - propagate_error_marker(); -} - -/// Read the shared `lib/error-marker.json` file at compile time and -/// expose the marker string to the Rust source via the -/// `OPENAPI_NG_ERROR_MARKER` env var (consumed via `env!()` in -/// `src/bindings.rs`). Keeping the marker in one file consumed by both -/// the Rust binding and `lib/index.js` makes drift impossible — the two -/// sides can never disagree on the sentinel that identifies a thrown -/// GenerateError across realms. -fn propagate_error_marker() { - let manifest_dir = - std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"); - let marker_path = Path::new(&manifest_dir) - .join("lib") - .join("error-marker.json"); - println!("cargo:rerun-if-changed={}", marker_path.display()); - - let raw = fs::read_to_string(&marker_path) - .unwrap_or_else(|err| panic!("failed to read {}: {err}", marker_path.display())); - let marker = extract_marker(&raw).unwrap_or_else(|| { - panic!( - "missing or invalid `marker` field in {}", - marker_path.display() - ) - }); - println!("cargo:rustc-env=OPENAPI_NG_ERROR_MARKER={marker}"); -} - -/// Tiny JSON walker — we only need the value of the top-level -/// `"marker"` string, and pulling in a JSON crate at build time is more -/// machinery than the one-field file warrants. -fn extract_marker(raw: &str) -> Option { - let after_key = raw.split_once("\"marker\"")?.1; - let after_colon = after_key.split_once(':')?.1; - let trimmed = after_colon.trim_start(); - let after_open_quote = trimmed.strip_prefix('"')?; - let (value, _rest) = after_open_quote.split_once('"')?; - Some(value.to_string()) } diff --git a/index.d.ts b/index.d.ts index 0500ca3..2936e30 100644 --- a/index.d.ts +++ b/index.d.ts @@ -10,8 +10,6 @@ export declare const EmitTarget: { readonly Angular: 'angular'; }; -export declare function generate(options: GenerateOptions): Promise - /** * A single generated artifact. `contents` always carries the emitted * source; callers that only need on-disk output can pass `outputPath` @@ -23,11 +21,10 @@ export interface GeneratedArtifact { } /** - * Payload attached to every fatal native throw. The JS wrapper in - * `lib/index.js` upgrades the thrown plain Error into a `GenerateError` - * (a real JS class that extends Error), copying these own-properties - * across so consumers can `instanceof GenerateError` and still read - * `code/subcode/message/path/warnings`. + * Payload returned inside `GenerateOutcome.error`. The JS wrapper + * constructs a `GenerateError` (a real JS class that extends Error) + * from these fields, so consumers can `instanceof GenerateError` and + * read `code/subcode/message/path/warnings`. * * The fatal sits at the top level (`code/subcode/message/path`); pre-fatal * warnings ride in `warnings`. `subcode` is set for `PolicyViolation` @@ -218,16 +215,16 @@ export interface ResponseTypeMapping { // Hand-authored tail concatenated onto the napi-rs-generated index.d.ts // by scripts/patch-types.mjs. Contains TS surface that napi-rs cannot // emit on its own (named string-literal unions, the GenerateError class -// defined in lib/index.js). +// defined in lib/generate-error.js). /** * Diagnostic code taxonomy. Each value maps 1:1 to a Rust DiagnosticCode, * except `E_UNEXPECTED` and `E_UNSUPPORTED_RUNTIME`, which are reserved - * for the JS wrapper in `lib/index.js`: + * for the JS wrappers: * - `E_UNEXPECTED` is a defensive fallback when a `GenerateError` is * constructed without an explicit `code`. - * - `E_UNSUPPORTED_RUNTIME` is thrown by `browser.js` when `generate()` - * is called in a browser or edge runtime (no native binding available). + * - `E_UNSUPPORTED_RUNTIME` is thrown by the browser entry when the + * WebAssembly binding cannot be loaded. * * `E_INVALID_OPTION` covers both wrapper-side shape rejections (a JS * caller passed an option whose type/shape is wrong — surfaced with @@ -282,9 +279,9 @@ export type DiagnosticSubcode = * that extends `Error`, so consumers can write `err instanceof * GenerateError`. * - * Defined in `lib/index.js` (not by napi-rs) — the native binding - * throws a plain Error carrying these own-properties and a marker that - * the JS wrapper uses to upgrade it. + * Defined in `lib/generate-error.js`. The native binding returns the + * payload as data; the JS wrappers (`lib/index.js`, `lib/browser.js`) + * construct and throw this class. */ export declare class GenerateError extends Error { constructor(payload: GenerateErrorPayload); @@ -350,6 +347,13 @@ export interface Config { naming?: NamingConfig; } +/** + * Generate TypeScript artifacts from an OpenAPI 3.x document. Resolves + * with the summary, warnings and artifacts; rejects with `GenerateError` + * on any fatal diagnostic. + */ +export declare function generate(options: GenerateOptions): Promise; + /** * Identity helper that anchors TypeScript inference for JS/TS configs: * import { defineConfig } from '@avsystem/openapi-ng/config'; diff --git a/index.d.ts.in b/index.d.ts.in index 0a6b7b8..170cef6 100644 --- a/index.d.ts.in +++ b/index.d.ts.in @@ -1,16 +1,16 @@ // Hand-authored tail concatenated onto the napi-rs-generated index.d.ts // by scripts/patch-types.mjs. Contains TS surface that napi-rs cannot // emit on its own (named string-literal unions, the GenerateError class -// defined in lib/index.js). +// defined in lib/generate-error.js). /** * Diagnostic code taxonomy. Each value maps 1:1 to a Rust DiagnosticCode, * except `E_UNEXPECTED` and `E_UNSUPPORTED_RUNTIME`, which are reserved - * for the JS wrapper in `lib/index.js`: + * for the JS wrappers: * - `E_UNEXPECTED` is a defensive fallback when a `GenerateError` is * constructed without an explicit `code`. - * - `E_UNSUPPORTED_RUNTIME` is thrown by `browser.js` when `generate()` - * is called in a browser or edge runtime (no native binding available). + * - `E_UNSUPPORTED_RUNTIME` is thrown by the browser entry when the + * WebAssembly binding cannot be loaded. * * `E_INVALID_OPTION` covers both wrapper-side shape rejections (a JS * caller passed an option whose type/shape is wrong — surfaced with @@ -65,9 +65,9 @@ export type DiagnosticSubcode = * that extends `Error`, so consumers can write `err instanceof * GenerateError`. * - * Defined in `lib/index.js` (not by napi-rs) — the native binding - * throws a plain Error carrying these own-properties and a marker that - * the JS wrapper uses to upgrade it. + * Defined in `lib/generate-error.js`. The native binding returns the + * payload as data; the JS wrappers (`lib/index.js`, `lib/browser.js`) + * construct and throw this class. */ export declare class GenerateError extends Error { constructor(payload: GenerateErrorPayload); @@ -133,6 +133,13 @@ export interface Config { naming?: NamingConfig; } +/** + * Generate TypeScript artifacts from an OpenAPI 3.x document. Resolves + * with the summary, warnings and artifacts; rejects with `GenerateError` + * on any fatal diagnostic. + */ +export declare function generate(options: GenerateOptions): Promise; + /** * Identity helper that anchors TypeScript inference for JS/TS configs: * import { defineConfig } from '@avsystem/openapi-ng/config'; diff --git a/lib/browser.js b/lib/browser.js new file mode 100644 index 0000000..b8b97eb --- /dev/null +++ b/lib/browser.js @@ -0,0 +1,113 @@ +'use strict'; + +// Browser entry. Options go through the same normalisation as the Node +// entry; generation runs in the WebAssembly binding published as +// `@avsystem/openapi-ng-wasm32-wasi`, whose `browser` field points at +// napi-rs's WASI browser loader. The page must be cross-origin isolated +// (COOP `same-origin`, COEP `require-corp`) because the binding uses +// shared memory. + +const { GenerateError } = require('./generate-error.js'); +const { prepareOptions, unwrapOutcome, upgradeError } = require('./wrapper-core.js'); + +const WASI_PACKAGE = '@avsystem/openapi-ng-wasm32-wasi'; + +function invalidOption(message) { + return new GenerateError({ + code: 'E_INVALID_OPTION', + subcode: 'shape', + message, + warnings: [], + }); +} + +// No filesystem and no node:net in the browser: the spec arrives as +// `inputContents`, artifacts leave as `result.artifacts`. +function rejectPathOptions(options) { + if (options === null || typeof options !== 'object') return; + if (options.inputPath !== undefined) { + throw invalidOption( + 'inputPath is not available in the browser entry; read the spec yourself and pass inputContents with displayPath.', + ); + } + if (options.outputPath !== undefined) { + throw invalidOption( + 'outputPath is not available in the browser entry; write result.artifacts yourself.', + ); + } +} + +function unreachableFetch() { + throw new Error( + 'openapi-ng: URL inputs are rejected before fetch in the browser entry', + ); +} + +function unsupportedRuntime(cause) { + const err = new GenerateError({ + code: 'E_UNSUPPORTED_RUNTIME', + message: + `openapi-ng could not load its WebAssembly binding (${WASI_PACKAGE}). ` + + `Install that package next to @avsystem/openapi-ng and serve the page with ` + + `Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. ` + + `Cause: ${cause && cause.message ? cause.message : String(cause)}`, + warnings: [], + }); + err.cause = cause; + return err; +} + +// `loadBinding` resolves to the WASI module namespace (ESM loader) or a +// CommonJS `module.exports` under `default`; both expose `generateNative`. +function createGenerate(loadBinding) { + let bindingPromise; + const load = () => { + if (!bindingPromise) { + bindingPromise = loadBinding() + .then(mod => { + const binding = + mod && typeof mod.generateNative === 'function' ? mod : mod && mod.default; + if (!binding || typeof binding.generateNative !== 'function') { + // Plain Error: the catch below turns it into E_UNSUPPORTED_RUNTIME + // and clears the cached promise, so a bad shape and a failed load + // surface identically. + throw new Error('module does not export generateNative'); + } + return binding; + }) + .catch(cause => { + bindingPromise = undefined; + throw unsupportedRuntime(cause); + }); + } + return bindingPromise; + }; + + return async function generate(options) { + rejectPathOptions(options); + const prepared = await prepareOptions(options, unreachableFetch); + const binding = await load(); + let outcome; + try { + outcome = binding.generateNative(prepared); + } catch (err) { + throw upgradeError(err); + } + return unwrapOutcome(outcome); + }; +} + +const generate = createGenerate(() => import(WASI_PACKAGE)); + +// Mirrors the frozen const in `lib/index.js` and `index.d.ts`. +const EmitTarget = Object.freeze({ + Models: 'models', + Angular: 'angular', +}); + +module.exports = { + generate, + createGenerate, + GenerateError, + EmitTarget, +}; diff --git a/lib/index.js b/lib/index.js index 39daa75..f7fdfe0 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,12 +7,8 @@ if (process.env.OPENAPI_NG_DISABLE_NATIVE_FOR_TEST === '1') { } // Node entry point. The native binding sits at ../native.js (auto-generated -// by napi-rs); this wrapper upgrades thrown errors into a real -// `GenerateError` JS class that extends `Error`, so consumers can write -// `err instanceof GenerateError`. The native binding itself only attaches -// own-properties to a plain Error — making the class subclass `Error` on -// the JS side is the simplest path that survives napi-rs's class -// registration not setting the Error prototype. +// by napi-rs). The wrapper throws `GenerateError` built from the native +// `{ result, error }` union; the native binding never throws on its own. // // The native binding is NOT a published entry point — `package.json#main` // only exposes `lib/index.js`, so consumers can never bypass this wrapper. @@ -25,15 +21,17 @@ if (process.env.OPENAPI_NG_DISABLE_NATIVE_FOR_TEST === '1') { const native = require('../native.js'); const { GenerateError } = require('./generate-error.js'); const { fetchInput } = require('./fetch-input.js'); -const { prepareOptions, upgradeError } = require('./wrapper-core.js'); +const { prepareOptions, unwrapOutcome, upgradeError } = require('./wrapper-core.js'); async function generate(options) { const prepared = await prepareOptions(options, fetchInput); + let outcome; try { - return native.generate(prepared); + outcome = native.generateNative(prepared); } catch (err) { throw upgradeError(err); } + return unwrapOutcome(outcome); } // Frozen runtime shape for `EmitTarget`. Mirrors the ambient const diff --git a/lib/wrapper-core.js b/lib/wrapper-core.js index 5cd1315..cfa1e4a 100644 --- a/lib/wrapper-core.js +++ b/lib/wrapper-core.js @@ -117,19 +117,32 @@ function normalizeNamingValue(value, key) { return { rule: entry.rule }; } -function upgradeError(err) { - if (GenerateError.isGenerateError(err)) { - const upgraded = new GenerateError({ - code: err.code, - subcode: err.subcode, - message: err.message, - path: err.path, - warnings: err.warnings, +// Turn the native `{ result, error }` union into return-or-throw. +function unwrapOutcome(outcome) { + if (outcome && outcome.error) { + throw new GenerateError(outcome.error); + } + if (!outcome || !outcome.result) { + throw new GenerateError({ + code: 'E_UNEXPECTED', + message: 'openapi-ng: native binding returned neither a result nor an error.', + warnings: [], }); - if (typeof err.stack === 'string') upgraded.stack = err.stack; - return upgraded; } - return err; + return outcome.result; +} + +// Catch-all for anything still thrown across the binding boundary +// (loader failures, argument marshalling). Typed errors pass through. +function upgradeError(err) { + if (GenerateError.isGenerateError(err)) return err; + const upgraded = new GenerateError({ + code: 'E_UNEXPECTED', + message: err && err.message ? err.message : String(err), + warnings: [], + }); + upgraded.cause = err; + return upgraded; } function validateGenerateOptions(options) { @@ -335,6 +348,7 @@ module.exports = { normalizeNamingEntry, normalizeNamingValue, validateGenerateOptions, + unwrapOutcome, upgradeError, prepareOptions, }; diff --git a/native.js b/native.js index 6b8d65a..4371127 100644 --- a/native.js +++ b/native.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-android-arm64') const bindingPackageVersion = require('@avsystem/openapi-ng-android-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-android-arm-eabi') const bindingPackageVersion = require('@avsystem/openapi-ng-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-win32-x64-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-win32-x64-msvc') const bindingPackageVersion = require('@avsystem/openapi-ng-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-win32-ia32-msvc') const bindingPackageVersion = require('@avsystem/openapi-ng-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-win32-arm64-msvc') const bindingPackageVersion = require('@avsystem/openapi-ng-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-darwin-universal') const bindingPackageVersion = require('@avsystem/openapi-ng-darwin-universal/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-darwin-x64') const bindingPackageVersion = require('@avsystem/openapi-ng-darwin-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-darwin-arm64') const bindingPackageVersion = require('@avsystem/openapi-ng-darwin-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-freebsd-x64') const bindingPackageVersion = require('@avsystem/openapi-ng-freebsd-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-freebsd-arm64') const bindingPackageVersion = require('@avsystem/openapi-ng-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-x64-musl') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-x64-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-arm64-musl') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-arm64-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-arm-musleabihf') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-arm-gnueabihf') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-loong64-musl') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-loong64-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-riscv64-musl') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-riscv64-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-ppc64-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-linux-s390x-gnu') const bindingPackageVersion = require('@avsystem/openapi-ng-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-openharmony-arm64') const bindingPackageVersion = require('@avsystem/openapi-ng-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-openharmony-x64') const bindingPackageVersion = require('@avsystem/openapi-ng-openharmony-x64/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@avsystem/openapi-ng-openharmony-arm') const bindingPackageVersion = require('@avsystem/openapi-ng-openharmony-arm/package.json').version - if (bindingPackageVersion !== '0.2.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -648,8 +648,8 @@ if (!nativeBinding || forceWasi) { if (!candidateFailed) { if (process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { const bindingPackageVersion = require('@avsystem/openapi-ng-wasm32-wasi/package.json').version - if (bindingPackageVersion !== '0.2.0') { - throw new Error(`WASI binding package version mismatch, expected 0.2.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '0.4.0') { + throw new Error(`WASI binding package version mismatch, expected 0.4.0 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } } wasiBinding = require('@avsystem/openapi-ng-wasm32-wasi') @@ -694,7 +694,7 @@ if (!nativeBinding && !__OPENAPI_NG_SUPPORTED__.has(__OPENAPI_NG_PLATFORM_KEY__) throw new Error( 'openapi-ng does not ship a native binary for ' + __OPENAPI_NG_PLATFORM_KEY__ + '. ' + 'Supported platforms: ' + [...__OPENAPI_NG_SUPPORTED__].sort().join(', ') + '. ' + - 'If you need this platform, please file an issue.', + 'If you need this platform, please file an issue, or install @avsystem/openapi-ng-wasm32-wasi for a WebAssembly fallback.', ); } @@ -715,6 +715,6 @@ if (!nativeBinding) { module.exports = nativeBinding module.exports.EmitTarget = nativeBinding.EmitTarget -module.exports.generate = nativeBinding.generate +module.exports.generateNative = nativeBinding.generateNative module.exports.InputFormat = nativeBinding.InputFormat module.exports.ResponseType = nativeBinding.ResponseType diff --git a/package.json b/package.json index 0d6b51f..3c74b71 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "bin/openapi-ng.js", "bin/lib/parse.js", "lib/index.js", + "lib/browser.js", "lib/config.js", "lib/fetch-input.js", "lib/generate-error.js", @@ -28,7 +29,8 @@ "lib/error-marker.json", "index.d.ts", "native.js", - "browser.js" + "browser.js", + "browser.d.ts" ], "main": "lib/index.js", "browser": "browser.js", @@ -36,10 +38,17 @@ "exports": { ".": { "types": "./index.d.ts", - "browser": "./browser.js", + "browser": { + "types": "./browser.d.ts", + "default": "./browser.js" + }, "node": "./lib/index.js", "default": "./lib/index.js" }, + "./browser": { + "types": "./browser.d.ts", + "default": "./browser.js" + }, "./config": { "types": "./index.d.ts", "default": "./lib/config.js" @@ -73,11 +82,15 @@ "@angular/common": "22.1.3", "@angular/core": "22.1.3", "@angular/forms": "22.1.3", + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", "@napi-rs/cli": "3.8.6", + "@napi-rs/wasm-runtime": "1.2.3", "@oxc-node/core": "0.1.0", "@taplo/cli": "0.7.0", "@types/node": "26.2.0", "ava": "8.0.1", + "emnapi": "1.11.3", "husky": "9.1.7", "lint-staged": "17.3.0", "npm-run-all2": "9.0.3", @@ -128,7 +141,8 @@ "aarch64-unknown-linux-gnu", "aarch64-pc-windows-msvc", "x86_64-unknown-linux-musl", - "aarch64-unknown-linux-musl" + "aarch64-unknown-linux-musl", + "wasm32-wasip1-threads" ] }, "ava": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a0bcdda..516cadc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,9 @@ settings: overrides: tar@<7.5.15: 7.5.15 glob@<13: 13.0.6 + emnapi: 1.11.3 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 importers: @@ -25,9 +28,18 @@ importers: '@angular/forms': specifier: 22.1.3 version: 22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(rxjs@7.8.2))(@angular/platform-browser@22.1.3(@angular/common@22.1.3(@angular/core@22.1.3(rxjs@7.8.2))(rxjs@7.8.2))(@angular/core@22.1.3(rxjs@7.8.2)))(rxjs@7.8.2) + '@emnapi/core': + specifier: 1.11.3 + version: 1.11.3 + '@emnapi/runtime': + specifier: 1.11.3 + version: 1.11.3 '@napi-rs/cli': specifier: 3.8.6 - version: 3.8.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(emnapi@1.9.2) + version: 3.8.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(emnapi@1.11.3) + '@napi-rs/wasm-runtime': + specifier: 1.2.3 + version: 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@oxc-node/core': specifier: 0.1.0 version: 0.1.0 @@ -40,6 +52,9 @@ importers: ava: specifier: 8.0.1 version: 8.0.1(rollup@4.60.4) + emnapi: + specifier: 1.11.3 + version: 1.11.3 husky: specifier: 9.1.7 version: 9.1.7 @@ -84,10 +99,10 @@ importers: dependencies: '@astrojs/starlight': specifier: 0.41.7 - version: 0.41.7(@astrojs/markdown-remark@7.2.4)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0))(typescript@6.0.3) + version: 0.41.7(@astrojs/markdown-remark@7.2.4)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0))(typescript@6.0.3) astro: specifier: 7.2.4 - version: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) + version: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) sharp: specifier: 0.35.3 version: 0.35.3(@types/node@26.2.0) @@ -424,35 +439,14 @@ packages: resolution: {integrity: sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==} engines: {node: '>=14'} - '@emnapi/core@1.11.1': - resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} - - '@emnapi/core@1.9.1': - resolution: {integrity: sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==} - - '@emnapi/core@1.9.2': - resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} - - '@emnapi/runtime@1.11.1': - resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/core@1.11.3': + resolution: {integrity: sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==} '@emnapi/runtime@1.11.3': resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - - '@emnapi/runtime@1.9.2': - resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} - - '@emnapi/wasi-threads@1.2.0': - resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} - - '@emnapi/wasi-threads@1.2.1': - resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - - '@emnapi/wasi-threads@1.2.2': - resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@emnapi/wasi-threads@1.2.3': + resolution: {integrity: sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==} '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} @@ -1259,9 +1253,9 @@ packages: engines: {node: ^20.17.0 || ^22.13.0 || >= 23.5.0} hasBin: true peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 - emnapi: ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + emnapi: 1.11.3 peerDependenciesMeta: '@emnapi/core': optional: true @@ -1522,18 +1516,12 @@ packages: resolution: {integrity: sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==} engines: {node: '>= 10'} - '@napi-rs/wasm-runtime@1.1.2': - resolution: {integrity: sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==} - peerDependencies: - '@emnapi/core': ^1.7.1 - '@emnapi/runtime': ^1.7.1 - '@napi-rs/wasm-runtime@1.2.3': resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: - '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 - '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 '@napi-rs/wasm-tools-android-arm-eabi@1.1.0': resolution: {integrity: sha512-p6J8PB59I8d/XItXB/go5JH6nKW+xIbpzaL43EBTV0hi7mrS/Z4gs+MsB04ZrlqZN29BdZV8fChRyasuXLhRaA==} @@ -2370,9 +2358,6 @@ packages: resolution: {integrity: sha512-Ck3zFhQhIhi02Hl6T4ZmJsXdnJE+wXcJz5f8klxd4keRYgenMnip3JDPMGDRLbnC/2iGd8P0sBIQqI3KxfVjBg==} hasBin: true - '@tybys/wasm-util@0.10.1': - resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} - '@tybys/wasm-util@0.10.3': resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} @@ -2814,8 +2799,8 @@ packages: resolution: {integrity: sha512-FLtgn/CGBXiX3ZtPAm5q4LWWepHChOt55J9u01WFu3dyap2U7IwptlrqoE1COR/kxwdy/DOxIBALSxIW449I1g==} engines: {node: '>=22'} - emnapi@1.9.2: - resolution: {integrity: sha512-OdUoQe/8so7FvubnE/DNV9sNNSFwDYQiK4ZCAz4agMnD1s6faLuDn2gzxfJrmMoKfxZhhsckqGNwqPnS5K140A==} + emnapi@1.11.3: + resolution: {integrity: sha512-+/ZS90YK/rYfVOHtGLHkGffVsnmD/MAKaBHio+Y4XAtg75RLr4cveV/w0jTkUdLM1CcAlaRgG76mpIemWAlk0A==} peerDependencies: node-addon-api: '>= 6.1.0' peerDependenciesMeta: @@ -4459,9 +4444,9 @@ snapshots: '@astrojs/compiler-binding-linux-x64-musl@0.3.2': optional: true - '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@astrojs/compiler-binding-wasm32-wasi@0.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -4473,7 +4458,7 @@ snapshots: '@astrojs/compiler-binding-win32-x64-msvc@0.3.2': optional: true - '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@astrojs/compiler-binding@0.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': optionalDependencies: '@astrojs/compiler-binding-darwin-arm64': 0.3.2 '@astrojs/compiler-binding-darwin-x64': 0.3.2 @@ -4481,16 +4466,16 @@ snapshots: '@astrojs/compiler-binding-linux-arm64-musl': 0.3.2 '@astrojs/compiler-binding-linux-x64-gnu': 0.3.2 '@astrojs/compiler-binding-linux-x64-musl': 0.3.2 - '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-binding-wasm32-wasi': 0.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@astrojs/compiler-binding-win32-arm64-msvc': 0.3.2 '@astrojs/compiler-binding-win32-x64-msvc': 0.3.2 transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' - '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@astrojs/compiler-rs@0.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-binding': 0.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -4535,13 +4520,13 @@ snapshots: github-slugger: 2.0.0 satteri: 0.10.5 - '@astrojs/mdx@7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0))': + '@astrojs/mdx@7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0))': dependencies: '@astrojs/internal-helpers': 0.10.4 '@astrojs/markdown-remark': 7.2.4 '@mdx-js/mdx': 3.1.1 acorn: 8.16.0 - astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) + astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) es-module-lexer: 2.1.0 estree-util-visit: 2.0.0 hast-util-to-html: 9.0.5 @@ -4567,17 +4552,17 @@ snapshots: stream-replace-string: 2.0.0 zod: 4.4.3 - '@astrojs/starlight@0.41.7(@astrojs/markdown-remark@7.2.4)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0))(typescript@6.0.3)': + '@astrojs/starlight@0.41.7(@astrojs/markdown-remark@7.2.4)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0))(typescript@6.0.3)': dependencies: '@astrojs/markdown-satteri': 0.3.7 - '@astrojs/mdx': 7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0)) + '@astrojs/mdx': 7.0.7(@astrojs/markdown-satteri@0.3.7)(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0)) '@astrojs/sitemap': 3.7.3 '@pagefind/default-ui': 1.5.2 '@types/hast': 3.0.4 '@types/js-yaml': 4.0.9 '@types/mdast': 4.0.4 - astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) - astro-expressive-code: 0.44.1(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0)) + astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) + astro-expressive-code: 0.44.1(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0)) bcp-47: 2.1.0 hast-util-from-html: 2.0.3 hast-util-select: 6.0.4 @@ -4663,16 +4648,16 @@ snapshots: '@bruits/satteri-wasm32-wasi@0.10.5': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true '@bruits/satteri-wasm32-wasi@0.9.5': dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true '@bruits/satteri-win32-arm64-msvc@0.10.5': @@ -4734,58 +4719,18 @@ snapshots: '@ctrl/tinycolor@4.2.0': {} - '@emnapi/core@1.11.1': - dependencies: - '@emnapi/wasi-threads': 1.2.2 - tslib: 2.6.3 - optional: true - - '@emnapi/core@1.9.1': - dependencies: - '@emnapi/wasi-threads': 1.2.0 - tslib: 2.6.3 - optional: true - - '@emnapi/core@1.9.2': - dependencies: - '@emnapi/wasi-threads': 1.2.1 - tslib: 2.6.3 - optional: true - - '@emnapi/runtime@1.11.1': + '@emnapi/core@1.11.3': dependencies: + '@emnapi/wasi-threads': 1.2.3 tslib: 2.6.3 - optional: true '@emnapi/runtime@1.11.3': dependencies: tslib: 2.6.3 - optional: true - - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.6.3 - optional: true - - '@emnapi/runtime@1.9.2': - dependencies: - tslib: 2.6.3 - optional: true - - '@emnapi/wasi-threads@1.2.0': - dependencies: - tslib: 2.6.3 - optional: true - - '@emnapi/wasi-threads@1.2.1': - dependencies: - tslib: 2.6.3 - optional: true - '@emnapi/wasi-threads@1.2.2': + '@emnapi/wasi-threads@1.2.3': dependencies: tslib: 2.6.3 - optional: true '@esbuild/aix-ppc64@0.28.1': optional: true @@ -5353,10 +5298,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@napi-rs/cli@3.8.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(emnapi@1.9.2)': + '@napi-rs/cli@3.8.6(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(emnapi@1.11.3)': dependencies: '@inquirer/prompts': 8.6.0(@types/node@26.2.0) - '@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@napi-rs/wasm-tools': 1.1.0 '@octokit/rest': 22.0.1 clipanion: 4.0.0-rc.4(typanion@3.14.0) @@ -5368,9 +5313,9 @@ snapshots: typanion: 3.14.0 typescript: 6.0.3 optionalDependencies: - '@emnapi/core': 1.11.1 + '@emnapi/core': 1.11.3 '@emnapi/runtime': 1.11.3 - emnapi: 1.9.2 + emnapi: 1.11.3 transitivePeerDependencies: - '@napi-rs/cross-toolchain-arm64-target-aarch64' - '@napi-rs/cross-toolchain-arm64-target-armv7' @@ -5385,10 +5330,10 @@ snapshots: - '@types/node' - supports-color - '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/lzma': 1.4.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) - '@napi-rs/tar': 1.1.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/lzma': 1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) + '@napi-rs/tar': 1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) debug: 4.4.3 transitivePeerDependencies: - '@emnapi/core' @@ -5434,9 +5379,9 @@ snapshots: '@napi-rs/lzma-linux-x64-musl@1.4.5': optional: true - '@napi-rs/lzma-wasm32-wasi@1.4.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@napi-rs/lzma-wasm32-wasi@1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -5451,7 +5396,7 @@ snapshots: '@napi-rs/lzma-win32-x64-msvc@1.4.5': optional: true - '@napi-rs/lzma@1.4.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@napi-rs/lzma@1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': optionalDependencies: '@napi-rs/lzma-android-arm-eabi': 1.4.5 '@napi-rs/lzma-android-arm64': 1.4.5 @@ -5466,7 +5411,7 @@ snapshots: '@napi-rs/lzma-linux-s390x-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-gnu': 1.4.5 '@napi-rs/lzma-linux-x64-musl': 1.4.5 - '@napi-rs/lzma-wasm32-wasi': 1.4.5(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/lzma-wasm32-wasi': 1.4.5(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@napi-rs/lzma-win32-arm64-msvc': 1.4.5 '@napi-rs/lzma-win32-ia32-msvc': 1.4.5 '@napi-rs/lzma-win32-x64-msvc': 1.4.5 @@ -5510,9 +5455,9 @@ snapshots: '@napi-rs/tar-linux-x64-musl@1.1.0': optional: true - '@napi-rs/tar-wasm32-wasi@1.1.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@napi-rs/tar-wasm32-wasi@1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -5527,7 +5472,7 @@ snapshots: '@napi-rs/tar-win32-x64-msvc@1.1.0': optional: true - '@napi-rs/tar@1.1.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@napi-rs/tar@1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': optionalDependencies: '@napi-rs/tar-android-arm-eabi': 1.1.0 '@napi-rs/tar-android-arm64': 1.1.0 @@ -5541,7 +5486,7 @@ snapshots: '@napi-rs/tar-linux-s390x-gnu': 1.1.0 '@napi-rs/tar-linux-x64-gnu': 1.1.0 '@napi-rs/tar-linux-x64-musl': 1.1.0 - '@napi-rs/tar-wasm32-wasi': 1.1.0(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@napi-rs/tar-wasm32-wasi': 1.1.0(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@napi-rs/tar-win32-arm64-msvc': 1.1.0 '@napi-rs/tar-win32-ia32-msvc': 1.1.0 '@napi-rs/tar-win32-x64-msvc': 1.1.0 @@ -5549,40 +5494,11 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.3 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@napi-rs/wasm-runtime@1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1)': - dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@tybys/wasm-util': 0.10.1 - optional: true - - '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': - dependencies: - '@emnapi/core': 1.11.1 - '@emnapi/runtime': 1.11.1 - '@tybys/wasm-util': 0.10.3 - optional: true - - '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)': + '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)': dependencies: - '@emnapi/core': 1.11.1 + '@emnapi/core': 1.11.3 '@emnapi/runtime': 1.11.3 '@tybys/wasm-util': 0.10.3 - optional: true - - '@napi-rs/wasm-runtime@1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': - dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@tybys/wasm-util': 0.10.3 - optional: true '@napi-rs/wasm-tools-android-arm-eabi@1.1.0': optional: true @@ -5613,9 +5529,9 @@ snapshots: '@napi-rs/wasm-tools-wasm32-wasi@1.1.0': dependencies: - '@emnapi/core': 1.9.2 - '@emnapi/runtime': 1.9.2 - '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true '@napi-rs/wasm-tools-win32-arm64-msvc@1.1.0': @@ -5761,9 +5677,9 @@ snapshots: '@oxc-node/core-wasm32-wasi@0.1.0': dependencies: - '@emnapi/core': 1.9.1 - '@emnapi/runtime': 1.9.1 - '@napi-rs/wasm-runtime': 1.1.2(@emnapi/core@1.9.1)(@emnapi/runtime@1.9.1) + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 + '@napi-rs/wasm-runtime': 1.2.3(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) optional: true '@oxc-node/core-win32-arm64-msvc@0.1.0': @@ -6128,15 +6044,9 @@ snapshots: '@taplo/cli@0.7.0': {} - '@tybys/wasm-util@0.10.1': - dependencies: - tslib: 2.6.3 - optional: true - '@tybys/wasm-util@0.10.3': dependencies: tslib: 2.6.3 - optional: true '@types/debug@4.1.13': dependencies: @@ -6264,15 +6174,15 @@ snapshots: astring@1.9.0: {} - astro-expressive-code@0.44.1(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0)): + astro-expressive-code@0.44.1(astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0)): dependencies: - astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) + astro: 7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0) rehype-expressive-code: 0.44.1 url-extras: 0.1.0 - astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0): + astro@7.2.4(@astrojs/markdown-remark@7.2.4)(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3)(@types/node@26.2.0)(yaml@2.9.0): dependencies: - '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.3) + '@astrojs/compiler-rs': 0.3.2(@emnapi/core@1.11.3)(@emnapi/runtime@1.11.3) '@astrojs/internal-helpers': 0.10.4 '@astrojs/markdown-satteri': 0.3.7 '@astrojs/telemetry': 3.3.3 @@ -6640,8 +6550,7 @@ snapshots: emittery@2.0.0: {} - emnapi@1.9.2: - optional: true + emnapi@1.11.3: {} emoji-regex@10.3.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d478d86..33305a1 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -10,3 +10,6 @@ allowBuilds: overrides: 'tar@<7.5.15': '7.5.15' 'glob@<13': '13.0.6' + emnapi: 1.11.3 + '@emnapi/core': 1.11.3 + '@emnapi/runtime': 1.11.3 diff --git a/scripts/patch-types.mjs b/scripts/patch-types.mjs index dffef24..541eb3b 100644 --- a/scripts/patch-types.mjs +++ b/scripts/patch-types.mjs @@ -27,8 +27,7 @@ const tailPath = path.join(repoRoot, 'index.d.ts.in'); let content = fs.readFileSync(dtsPath, 'utf8'); const tail = fs.readFileSync(tailPath, 'utf8'); -// Strip any prior tail concatenation so reruns stay idempotent. The -// tail begins with the BEGIN marker comment. +// Strip any prior tail concatenation so reruns stay idempotent. const TAIL_BEGIN = '\n// Hand-authored tail'; const beginIdx = content.indexOf(TAIL_BEGIN); if (beginIdx !== -1) { @@ -124,26 +123,29 @@ if (content.includes('naming?: NamingOptions')) { throw new Error('patch-types: expected `naming?: NamingOptions` in index.d.ts'); } -// `generate` becomes async on the JS wrapper boundary: it always -// returns Promise. napi-rs emits a synchronous -// signature because the Rust generate fn itself is sync; the wrapper -// in lib/index.js adds the async semantics around URL fetching, so -// the published surface must reflect that. +// The native export and its union are wrapper-internal. Remove them (and +// their leading doc comment) from the public surface; the hand-authored +// tail declares `generate`. +// `[^*]|\*(?!/)` (not `[\s\S]*?`) so the lazy match can't skip past this +// declaration's own closing `*/` and swallow a later, unrelated block. +const LEADING_DOC = '(?:^/\\*\\*(?:[^*]|\\*(?!/))*\\*/\\n)?'; +const NATIVE_FN_RE = new RegExp( + `${LEADING_DOC}^export declare function generateNative\\([^\\n]*\\n`, + 'm', +); +const OUTCOME_RE = new RegExp( + `${LEADING_DOC}^export interface GenerateOutcome \\{[\\s\\S]*?^\\}\\n`, + 'm', +); +content = content.replace(NATIVE_FN_RE, '').replace(OUTCOME_RE, ''); +// Scoped to the declaration forms because GenerateErrorPayload's doc comment +// legitimately mentions `GenerateOutcome.error` in prose. if ( - content.includes( - 'export declare function generate(options: GenerateOptions): GenerateResult', - ) + /^export (?:declare function generateNative|interface GenerateOutcome)\b/m.test(content) ) { - content = content.replace( - 'export declare function generate(options: GenerateOptions): GenerateResult', - 'export declare function generate(options: GenerateOptions): Promise', + throw new Error( + 'patch-types: generateNative/GenerateOutcome declaration survived stripping; update the patterns', ); -} else if ( - !content.includes( - 'export declare function generate(options: GenerateOptions): Promise', - ) -) { - throw new Error('patch-types: expected `generate(...): GenerateResult` in index.d.ts'); } // `inputPath` becomes optional on the published surface because the @@ -160,6 +162,8 @@ if (content.includes('inputPath: string')) { } content = content.trimEnd() + '\n\n' + tail.trimEnd() + '\n'; +// Stripped declarations leave runs of blank lines behind. +content = content.replace(/\n{3,}/g, '\n\n'); fs.writeFileSync(dtsPath, content); console.log('patch-types: narrowed code/severity and appended tail to index.d.ts'); @@ -195,7 +199,7 @@ if (!nativeContent.includes(INJECTION_GUARD)) { ' throw new Error(\n' + " 'openapi-ng does not ship a native binary for ' + __OPENAPI_NG_PLATFORM_KEY__ + '. ' +\n" + " 'Supported platforms: ' + [...__OPENAPI_NG_SUPPORTED__].sort().join(', ') + '. ' +\n" + - " 'If you need this platform, please file an issue.',\n" + + " 'If you need this platform, please file an issue, or install @avsystem/openapi-ng-wasm32-wasi for a WebAssembly fallback.',\n" + ' );\n' + '}\n\n'; @@ -209,48 +213,12 @@ if (!nativeContent.includes(INJECTION_GUARD)) { } // --------------------------------------------------------------------------- -// Re-author browser.js. `napi build` writes a single-line `export *` stub -// that defeats our hand-authored entry. We overwrite unconditionally so -// the post-build state is canonical regardless of what napi-rs emitted. -// The browser entry is a hard-error stub — browser/edge runtimes are not -// supported, so `generate()` throws `E_UNSUPPORTED_RUNTIME` at call time. +// Re-author browser.js. `napi build` writes an `export *` stub over it, so +// the post-build step restores the real entry, which lives in +// lib/browser.js where napi never touches it. // --------------------------------------------------------------------------- const browserPath = path.join(repoRoot, 'browser.js'); - -const browserContent = `'use strict'; - -// Browser/edge entry point. openapi-ng requires the native binding to -// generate code, so any browser or edge runtime (Vite/Webpack/esbuild -// resolving the \`browser\` field, Cloudflare Workers, Vercel Edge, etc.) -// gets a stub that throws \`E_UNSUPPORTED_RUNTIME\` at call time. The -// module itself stays importable so bundlers don't choke at build time. - -const { GenerateError } = require('./lib/generate-error.js'); - -async function generate() { - throw new GenerateError({ - code: 'E_UNSUPPORTED_RUNTIME', - message: - 'openapi-ng does not support browser or edge runtimes. ' + - 'Run the generator from Node, or remove openapi-ng from your browser bundle.', - warnings: [], - }); -} - -// Frozen runtime shape for \`EmitTarget\`. Mirrors the ambient const -// declared in \`index.d.ts\` and matches the \`lib/index.js\` entry, so the -// surface a consumer destructures is identical across both runtimes. -const EmitTarget = Object.freeze({ - Models: 'models', - Angular: 'angular', -}); - -module.exports = { - generate, - GenerateError, - EmitTarget, -}; -`; +const browserContent = `'use strict';\n\nmodule.exports = require('./lib/browser.js');\n`; const existingBrowser = fs.existsSync(browserPath) ? fs.readFileSync(browserPath, 'utf8') @@ -259,5 +227,15 @@ if (existingBrowser === browserContent) { console.log('patch-types: browser.js already canonical (idempotent skip)'); } else { fs.writeFileSync(browserPath, browserContent); - console.log('patch-types: re-authored browser.js with hard-error stub'); + console.log('patch-types: re-authored browser.js as a lib/browser.js re-export'); +} + +// `browser.d.ts` types the `./browser` subpath. napi never writes a file with +// this name, so it is hand-authored and committed; fail loud if it goes +// missing rather than publishing an untyped browser entry. +const browserDtsPath = path.join(repoRoot, 'browser.d.ts'); +if (!fs.existsSync(browserDtsPath)) { + throw new Error( + 'patch-types: browser.d.ts is missing — the ./browser subpath would publish untyped', + ); } diff --git a/src/bindings.rs b/src/bindings.rs index 12887af..1822a11 100644 --- a/src/bindings.rs +++ b/src/bindings.rs @@ -1,5 +1,3 @@ -use napi::bindgen_prelude::{Function, JsObjectValue, Object, Unknown}; -use napi::{Env, Error, Status}; use napi_derive::napi; use crate::{ @@ -114,11 +112,10 @@ pub struct GenerateResult { pub artifacts: Vec, } -/// Payload attached to every fatal native throw. The JS wrapper in -/// `lib/index.js` upgrades the thrown plain Error into a `GenerateError` -/// (a real JS class that extends Error), copying these own-properties -/// across so consumers can `instanceof GenerateError` and still read -/// `code/subcode/message/path/warnings`. +/// Payload returned inside `GenerateOutcome.error`. The JS wrapper +/// constructs a `GenerateError` (a real JS class that extends Error) +/// from these fields, so consumers can `instanceof GenerateError` and +/// read `code/subcode/message/path/warnings`. /// /// The fatal sits at the top level (`code/subcode/message/path`); pre-fatal /// warnings ride in `warnings`. `subcode` is set for `PolicyViolation` @@ -132,26 +129,19 @@ pub struct GenerateErrorPayload { pub warnings: Vec, } -/// Sentinel set on every thrown error so the JS wrapper can identify -/// them without leaking the marker into application code (consumers -/// guard with `err instanceof GenerateError`, not by inspecting this). -/// -/// The value is read from `lib/error-marker.json` at compile time by -/// `build.rs`, the same file `lib/index.js` reads at module load. Single -/// source of truth — the two sides cannot drift. -const GENERATE_ERROR_MARKER: &str = env!("OPENAPI_NG_ERROR_MARKER"); +/// Return shape of the native export. Exactly one field is set. The JS +/// wrapper turns `error` into a thrown `GenerateError`; returning data +/// instead of throwing keeps native and WASI runtimes identical. +#[napi(object)] +pub struct GenerateOutcome { + pub result: Option, + pub error: Option, +} -/// Project a `catch_unwind` payload into the same `GenerateError` shape -/// that fatal diagnostics produce. The two common payload types are -/// `&'static str` (from `panic!("literal")`) and `String` (from -/// `panic!("{}", ...)` / `panic!(format!(...))`); everything else -/// collapses to a generic fallback message so the surface stays bounded. -/// -/// The result is a `napi::Error` indistinguishable from the one a typed -/// fatal would produce, so the JS wrapper upgrades it to a real -/// `GenerateError` via the same path and consumers can write -/// `err.code === 'E_UNEXPECTED'`. -pub(crate) fn map_panic(panic: Box, env: Env) -> Error { +/// Project a `catch_unwind` payload into the same payload shape a typed +/// fatal produces. `&'static str` and `String` are the two common panic +/// payload types; anything else collapses to a generic message. +pub(crate) fn map_panic(panic: Box) -> GenerateErrorPayload { let message = panic .downcast_ref::<&'static str>() .map(|s| (*s).to_string()) @@ -163,63 +153,22 @@ pub(crate) fn map_panic(panic: Box, env: Env) -> Error message: format!("unexpected panic in native binding: {message}"), path: std::rc::Rc::from(""), }; - map_failure( - GenerateFailure { - warnings: Vec::new(), - fatal, - }, - env, - ) + map_failure(GenerateFailure { + warnings: Vec::new(), + fatal, + }) } -pub(crate) fn map_failure(failure: GenerateFailure, env: Env) -> Error { +pub(crate) fn map_failure(failure: GenerateFailure) -> GenerateErrorPayload { let GenerateFailure { warnings, fatal } = failure; let fatal = fatal.to_napi_error(); - let warnings: Vec = - warnings.iter().map(Diagnostic::to_napi_warning).collect(); - try_enrich_error(&fatal, &warnings, env).unwrap_or_else(|_| { - // Boundary-side decoration failed (typically OOM during JS Object - // construction): embed the diagnostic code and warning count into the - // message so consumers branching on `err.code` still get a usable - // signal instead of a bare Error. - let dropped_suffix = if warnings.is_empty() { - String::new() - } else { - format!(" ({} warning(s) dropped)", warnings.len()) - }; - Error::new( - Status::GenericFailure, - format!("[{}] {}{}", fatal.code, fatal.message, dropped_suffix), - ) - }) -} - -fn try_enrich_error( - fatal: &GeneratorDiagnostic, - warnings: &[GeneratorDiagnostic], - env: Env, -) -> napi::Result { - // Build a plain JS Error and decorate it with the public own-properties. - // The JS wrapper in `lib/index.js` then re-throws as a GenerateError - // class instance so `err instanceof GenerateError` works while keeping - // the native binding free of subclass-of-Error gymnastics that - // `napi_is_error` doesn't honor for `#[napi]` classes. - let global = env.get_global()?; - let error_ctor: Function = global.get_named_property("Error")?; - let unknown = error_ctor.new_instance(fatal.message.clone())?; - // SAFETY: `unknown` was just constructed on the previous line via - // `error_ctor.new_instance(...)` against the global `Error` constructor, - // which always returns a JS `Object`. Downcasting back to `Object` therefore - // cannot violate the napi-rs type invariant on `Unknown::cast`. - let mut js_error: Object = unsafe { unknown.cast()? }; - js_error.set_named_property("code", fatal.code.clone())?; - if let Some(subcode) = fatal.subcode.clone() { - js_error.set_named_property("subcode", subcode)?; + GenerateErrorPayload { + code: fatal.code, + subcode: fatal.subcode, + message: fatal.message, + path: fatal.path, + warnings: warnings.iter().map(Diagnostic::to_napi_warning).collect(), } - js_error.set_named_property("path", fatal.path.clone())?; - js_error.set_named_property("warnings", warnings.to_vec())?; - js_error.set_named_property(GENERATE_ERROR_MARKER, true)?; - Ok(Error::from(unknown)) } /// Boundary projection: take the wire-shaped `GenerateOptions` from the @@ -263,7 +212,7 @@ mod tests { bindings::{EmitTarget, GenerateOptions}, error::{Diagnostic, DiagnosticCode}, options::GenerateConfig, - pipeline::GenerateResult as ApplicationGenerateResult, + pipeline::{GenerateFailure, GenerateResult as ApplicationGenerateResult}, result::{GenerateSummary, GeneratedArtifact}, }; @@ -333,4 +282,45 @@ mod tests { assert_eq!(result.diagnostics.len(), 1); assert_eq!(result.diagnostics[0].code, "E_UNSUPPORTED_SEMANTIC"); } + + #[test] + fn map_failure_projects_fatal_and_warnings_into_payload() { + let failure = GenerateFailure { + warnings: vec![Diagnostic::new( + DiagnosticCode::UnsupportedSemantic, + "warned", + std::rc::Rc::from("spec.yaml"), + )], + fatal: Diagnostic { + code: DiagnosticCode::PolicyViolation, + subcode: Some("missing-operation-id"), + message: "no operationId".to_string(), + path: std::rc::Rc::from("spec.yaml"), + }, + }; + + let payload = super::map_failure(failure); + + assert_eq!(payload.code, "E_POLICY_VIOLATION"); + assert_eq!(payload.subcode.as_deref(), Some("missing-operation-id")); + assert_eq!(payload.message, "no operationId"); + assert_eq!(payload.path, "spec.yaml"); + assert_eq!(payload.warnings.len(), 1); + assert_eq!(payload.warnings[0].code, "E_UNSUPPORTED_SEMANTIC"); + assert_eq!(payload.warnings[0].severity, "warning"); + } + + #[test] + fn map_panic_projects_string_payloads_into_e_unexpected() { + let payload = super::map_panic(Box::new("boom")); + assert_eq!(payload.code, "E_UNEXPECTED"); + assert!(payload.message.contains("boom")); + assert!(payload.warnings.is_empty()); + + let payload = super::map_panic(Box::new(String::from("owned boom"))); + assert!(payload.message.contains("owned boom")); + + let payload = super::map_panic(Box::new(42_u8)); + assert!(payload.message.contains("unexpected panic")); + } } diff --git a/src/emit/mod.rs b/src/emit/mod.rs index aa81754..e1a6695 100644 --- a/src/emit/mod.rs +++ b/src/emit/mod.rs @@ -45,19 +45,20 @@ pub(crate) fn render_generated_banner(source_path: &str) -> String { /// Relativise an input path against the current working directory. /// Returns the input unchanged when: /// - the input is not absolute (already relative — caller's call), or -/// - `current_dir()` is unavailable (e.g. CWD was deleted), or +/// - the host cwd is unknown (e.g. CWD was deleted), or /// - `strip_prefix` fails (path lives outside CWD). /// /// Uses forward slashes on the result so banner format stays /// platform-independent and matches `pipeline::generate`'s display-path /// normalization. fn relativise_against_cwd(source_path: &str) -> String { + use crate::io::host_cwd::host_cwd; use std::path::Path; let input = Path::new(source_path); if !input.is_absolute() { return source_path.to_owned(); } - let Ok(cwd) = std::env::current_dir() else { + let Some(cwd) = host_cwd() else { return source_path.to_owned(); }; input.strip_prefix(&cwd).map_or_else( diff --git a/src/io/host_cwd.rs b/src/io/host_cwd.rs new file mode 100644 index 0000000..481f583 --- /dev/null +++ b/src/io/host_cwd.rs @@ -0,0 +1,46 @@ +use std::path::{Path, PathBuf}; + +/// Working directory of the host process that invoked the generator. +/// Native builds ask the OS. WASI has no host cwd, but the Node loader +/// passes `process.env` through, so `PWD` carries it there. +#[cfg(not(target_os = "wasi"))] +pub(crate) fn host_cwd() -> Option { + std::env::current_dir().ok() +} + +#[cfg(target_os = "wasi")] +pub(crate) fn host_cwd() -> Option { + std::env::var_os("PWD").map(PathBuf::from) +} + +/// Join a relative path onto the host cwd; absolute paths pass through. +/// Without a known cwd the path is returned unchanged. +pub(crate) fn resolve_against_host_cwd(path: &Path) -> PathBuf { + if path.is_absolute() { + return path.to_path_buf(); + } + host_cwd().map_or_else(|| path.to_path_buf(), |cwd| cwd.join(path)) +} + +#[cfg(test)] +mod tests { + use super::{host_cwd, resolve_against_host_cwd}; + use std::path::{Path, PathBuf}; + + #[test] + fn absolute_paths_pass_through_unchanged() { + let absolute = if cfg!(windows) { + PathBuf::from(r"C:\specs\petstore.yaml") + } else { + PathBuf::from("/specs/petstore.yaml") + }; + assert_eq!(resolve_against_host_cwd(&absolute), absolute); + } + + #[test] + fn relative_paths_join_the_host_cwd() { + let cwd = host_cwd().expect("native builds always have a cwd"); + let relative = Path::new("test/fixtures/petstore.yaml"); + assert_eq!(resolve_against_host_cwd(relative), cwd.join(relative)); + } +} diff --git a/src/io/mod.rs b/src/io/mod.rs index 376a24f..8746b1c 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -1 +1,2 @@ +pub(crate) mod host_cwd; pub(crate) mod writer; diff --git a/src/io/writer.rs b/src/io/writer.rs index db6b8d0..f339529 100644 --- a/src/io/writer.rs +++ b/src/io/writer.rs @@ -1,7 +1,8 @@ -use std::{fs, path::PathBuf}; +use std::fs; use crate::{ error::{Diagnostic, DiagnosticCode, Reporter}, + io::host_cwd::resolve_against_host_cwd, result::GeneratedArtifact, }; @@ -51,7 +52,7 @@ fn write_artifact( )); } - let output_dir = PathBuf::from(output_path); + let output_dir = resolve_against_host_cwd(std::path::Path::new(output_path)); fs::create_dir_all(&output_dir).map_err(|error| { reporter.error( DiagnosticCode::WriteFailed, diff --git a/src/lib.rs b/src/lib.rs index 232a945..82cc288 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,27 +13,36 @@ mod result; #[cfg(test)] mod test_support; -use napi::Env; use napi_derive::napi; -pub use crate::bindings::{EmitTarget, GenerateErrorPayload, GenerateOptions, GenerateResult}; +pub use crate::bindings::{ + EmitTarget, GenerateErrorPayload, GenerateOptions, GenerateOutcome, GenerateResult, +}; use crate::bindings::{map_failure, map_generate_result, map_panic}; pub use crate::options::{GenerateConfig, MappedType}; pub use crate::pipeline::execute_generate; pub use crate::plan::naming::NamingConfig; -#[napi(js_name = "generate")] -pub fn generate(env: Env, options: GenerateOptions) -> napi::Result { +/// Native export consumed only by `lib/index.js` and `lib/browser.js`. +/// Never throws: fatals and panics travel as `GenerateOutcome.error`. +#[napi(js_name = "generateNative")] +pub fn generate(options: GenerateOptions) -> GenerateOutcome { let config = GenerateConfig::from(options); - // `catch_unwind` ensures a Rust panic inside the pipeline becomes a - // typed `E_UNEXPECTED` GenerateError rather than aborting the host - // Node process. `AssertUnwindSafe` is sound here because `config` - // is consumed by value and nothing the closure touches is observed - // after the unwind path. + // `AssertUnwindSafe` is sound: `config` is consumed by value and nothing + // the closure touches is observed after an unwind. let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| execute_generate(config))); match outcome { - Ok(Ok(result)) => Ok(map_generate_result(result)), - Ok(Err(failure)) => Err(map_failure(failure, env)), - Err(panic_payload) => Err(map_panic(panic_payload, env)), + Ok(Ok(result)) => GenerateOutcome { + result: Some(map_generate_result(result)), + error: None, + }, + Ok(Err(failure)) => GenerateOutcome { + result: None, + error: Some(map_failure(failure)), + }, + Err(panic_payload) => GenerateOutcome { + result: None, + error: Some(map_panic(panic_payload)), + }, } } diff --git a/src/parse/input.rs b/src/parse/input.rs index 03a2d98..db3c095 100644 --- a/src/parse/input.rs +++ b/src/parse/input.rs @@ -1,8 +1,9 @@ -use std::{fs, path::Path, path::PathBuf, rc::Rc, sync::OnceLock}; +use std::{fs, path::Path, rc::Rc, sync::OnceLock}; use crate::{ bindings::InputFormat, error::{Diagnostic, DiagnosticCode}, + io::host_cwd::resolve_against_host_cwd, parse::openapi_model::OpenApiDocument, }; @@ -101,7 +102,7 @@ pub(crate) fn read_and_decode( input_path: &str, display_path: &Rc, ) -> Result { - let path = PathBuf::from(input_path); + let path = resolve_against_host_cwd(Path::new(input_path)); let metadata = fs::metadata(&path).map_err(|error| { Diagnostic::new( diff --git a/website/src/content/docs/reference/node-api.md b/website/src/content/docs/reference/node-api.md index 227f640..7ab595a 100644 --- a/website/src/content/docs/reference/node-api.md +++ b/website/src/content/docs/reference/node-api.md @@ -68,6 +68,14 @@ await generate({ | `responseTypeMapping` | Per-content-type override for the response decoding kind (`json` / `blob` / `text` / `arrayBuffer`). Matched case-insensitively against the spec's media types. Wires the right `requestFactory.*` variant. | | `naming` | Customise emitted method and group names. See the [Configuration guide](/guides/configuration/). | +### Browser usage + +The same `generate` function is exported from the package's `browser` +entry. Only two options differ: `inputPath` and `outputPath` are rejected +with `E_INVALID_OPTION`; pass `inputContents` and `displayPath` and read +`result.artifacts`. See [Runtime & platforms](/reference/runtime/#browsers) +for installation and the required response headers. + ### `MappedType` ```ts diff --git a/website/src/content/docs/reference/runtime.md b/website/src/content/docs/reference/runtime.md index 6ed1118..3a14f99 100644 --- a/website/src/content/docs/reference/runtime.md +++ b/website/src/content/docs/reference/runtime.md @@ -1,6 +1,6 @@ --- title: Runtime & platforms -description: Supported runtimes and platforms — Node, Bun, Deno via N-API. Browser and edge runtimes are not supported. +description: Supported runtimes and platforms — Node, Bun, Deno via N-API; browsers via WebAssembly. --- This page covers the runtimes that can host the **`openapi-ng` @@ -31,20 +31,53 @@ Pre-built native binaries are published for: The correct glibc or musl artifact is selected at load time, so Alpine and distroless images need no extra configuration. -On any other platform (FreeBSD, 32-bit, etc.), `require` of the package -throws an explicit unsupported-platform error listing the supported set -— open an issue if you need an additional target. +On any other platform (FreeBSD, 32-bit, etc.) the loader falls back to +the WebAssembly build if `@avsystem/openapi-ng-wasm32-wasi` is installed +next to `@avsystem/openapi-ng`; otherwise `require` throws an explicit +unsupported-platform error listing the supported set. -## Browser and edge runtimes +## Browsers -`openapi-ng` does not support browser runtimes, Cloudflare Workers, -Vercel Edge, Deno Deploy, or any other host that cannot load a native -N-API binary. The package ships a `browser.js` stub that any -browser-aware bundler (Vite, Webpack, esbuild) will resolve in those -contexts; calling `generate()` from it throws a `GenerateError` with -code `E_UNSUPPORTED_RUNTIME`. +The generator also ships as WebAssembly in the platform package +`@avsystem/openapi-ng-wasm32-wasi`. The `browser` entry of +`@avsystem/openapi-ng` loads it on demand, so bundlers that honour the +`browser` field (Vite, webpack, esbuild) pick the WebAssembly path +automatically. -Code generation is a build-time activity — run it from a Node script, -not from a browser bundle. See -[`E_UNSUPPORTED_RUNTIME`](/reference/diagnostics/#diagnostic-codes) -for the error shape. +Two requirements: + +1. Add the platform package yourself. napi-rs does not list it among the + root package's optional dependencies, so it is not installed + transitively: + + ```bash + pnpm add -D @avsystem/openapi-ng @avsystem/openapi-ng-wasm32-wasi + ``` + + Import `createGenerate` from `@avsystem/openapi-ng/browser` when you + serve the WebAssembly files yourself. + +2. Serve the page cross-origin isolated. The binding uses shared memory, + which browsers only enable under these response headers: + + ``` + Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp + ``` + +If the package is missing, bundlers may warn about an unresolvable +dynamic import and `generate()` rejects with `E_UNSUPPORTED_RUNTIME` at +call time. + +In the browser, `generate()` accepts `inputContents` plus `displayPath` +and returns the artifacts in memory. `inputPath` and `outputPath` are +rejected with `E_INVALID_OPTION` because there is no filesystem. If the +WebAssembly binding cannot be loaded, `generate()` rejects with +`E_UNSUPPORTED_RUNTIME` and a message naming the missing package or +header. + +## Edge runtimes + +Cloudflare Workers, Vercel Edge and Deno Deploy are not verified. The +WebAssembly build may run there; open an issue with your findings if you +try it.