Skip to content

Commit a8e5f5d

Browse files
authored
Merge pull request #15 from artiz/claude/nodejs-bun-typescript-bindings-8pk2wk
Add Node.js / Bun bindings via napi-rs
2 parents be7be9c + 174825f commit a8e5f5d

17 files changed

Lines changed: 2244 additions & 0 deletions

File tree

.github/workflows/npm-publish.yml

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# Publish the `fleischwolf` npm package (Node.js / Bun native bindings) for a
2+
# chosen release. The package is a native N-API addon, so it can't be a one-line
3+
# `npm publish`: it's built on a matrix of native runners (one per OS/arch),
4+
# each producing a platform `.node`, then a publish job assembles the main
5+
# package plus per-platform `optionalDependencies` (fleischwolf-<triple>) via
6+
# napi-rs and publishes them all.
7+
#
8+
# Trigger: manual only (workflow_dispatch). Pick the release tag to build — the
9+
# workflow checks out that tag and publishes the npm version derived from it, so
10+
# npm releases stay decoupled from the crates.io release on every master push.
11+
# Run it from the Actions tab (or `gh workflow run npm-publish.yml -f tag=v0.7.0`).
12+
#
13+
# Requires one repository secret: NPM_TOKEN — an npm automation token with
14+
# publish rights to `fleischwolf` and the `fleischwolf-*` platform packages.
15+
16+
name: npm publish
17+
18+
on:
19+
workflow_dispatch:
20+
inputs:
21+
tag:
22+
description: "Release tag to build and publish (e.g. v0.7.0)."
23+
required: true
24+
version:
25+
description: "Override the npm version (e.g. 0.7.0). Blank = derive from the tag."
26+
required: false
27+
default: ""
28+
29+
concurrency:
30+
group: npm-publish-${{ inputs.tag }}
31+
cancel-in-progress: false
32+
33+
defaults:
34+
run:
35+
working-directory: crates/fleischwolf-node
36+
37+
jobs:
38+
build:
39+
name: build ${{ matrix.target }}
40+
runs-on: ${{ matrix.host }}
41+
strategy:
42+
fail-fast: false
43+
matrix:
44+
include:
45+
- target: x86_64-unknown-linux-gnu
46+
host: ubuntu-22.04
47+
# GitHub-hosted ARM64 Linux runner (native — avoids cross-compiling the
48+
# ONNX/ort build). Requires the arm64 runner image to be available.
49+
- target: aarch64-unknown-linux-gnu
50+
host: ubuntu-24.04-arm
51+
- target: x86_64-apple-darwin
52+
host: macos-13
53+
- target: aarch64-apple-darwin
54+
host: macos-14
55+
- target: x86_64-pc-windows-msvc
56+
host: windows-latest
57+
steps:
58+
- uses: actions/checkout@v4
59+
with:
60+
ref: ${{ inputs.tag }}
61+
62+
- uses: dtolnay/rust-toolchain@stable
63+
with:
64+
targets: ${{ matrix.target }}
65+
66+
- uses: Swatinem/rust-cache@v2
67+
with:
68+
# One workspace, key the cache per target so the 5 jobs don't collide.
69+
key: ${{ matrix.target }}
70+
workspaces: "."
71+
72+
- uses: actions/setup-node@v4
73+
with:
74+
node-version: 20
75+
76+
- name: Install napi CLI
77+
run: npm install
78+
79+
# Native addon + the JS loader / d.ts. `--strip` keeps the (ONNX-linked)
80+
# binary as small as possible; strip isn't available under MSVC, so the
81+
# Windows build omits it.
82+
- name: Build (unix)
83+
if: runner.os != 'Windows'
84+
run: npx napi build --platform --release --strip --target ${{ matrix.target }} --js native.js --dts native.d.ts
85+
86+
- name: Build (windows)
87+
if: runner.os == 'Windows'
88+
run: npx napi build --platform --release --target ${{ matrix.target }} --js native.js --dts native.d.ts
89+
90+
- name: Upload prebuilt binary
91+
uses: actions/upload-artifact@v4
92+
with:
93+
name: bindings-${{ matrix.target }}
94+
path: crates/fleischwolf-node/fleischwolf.*.node
95+
if-no-files-found: error
96+
97+
# The JS loader + types are platform-agnostic; upload them once (from the
98+
# linux-x64 build) for the publish job to include in the main package.
99+
- name: Upload JS binding
100+
if: matrix.target == 'x86_64-unknown-linux-gnu'
101+
uses: actions/upload-artifact@v4
102+
with:
103+
name: js-binding
104+
path: |
105+
crates/fleischwolf-node/native.js
106+
crates/fleischwolf-node/native.d.ts
107+
if-no-files-found: error
108+
109+
publish:
110+
name: publish to npm
111+
needs: build
112+
runs-on: ubuntu-latest
113+
steps:
114+
- uses: actions/checkout@v4
115+
with:
116+
ref: ${{ inputs.tag }}
117+
118+
- uses: actions/setup-node@v4
119+
with:
120+
node-version: 20
121+
registry-url: "https://registry.npmjs.org"
122+
123+
- name: Install napi CLI
124+
run: npm install
125+
126+
# Prebuilt binaries → artifacts/<name>/fleischwolf.<triple>.node
127+
- name: Download prebuilt binaries
128+
uses: actions/download-artifact@v4
129+
with:
130+
pattern: bindings-*
131+
path: crates/fleischwolf-node/artifacts
132+
133+
# The JS loader + types → the package root.
134+
- name: Download JS binding
135+
uses: actions/download-artifact@v4
136+
with:
137+
name: js-binding
138+
path: crates/fleischwolf-node
139+
140+
# Version = the explicit override, else the selected tag with its leading `v`.
141+
- name: Resolve version
142+
id: ver
143+
run: |
144+
v="${{ inputs.version }}"
145+
if [ -z "$v" ]; then v="${{ inputs.tag }}"; v="${v#v}"; fi
146+
echo "version=$v" >> "$GITHUB_OUTPUT"
147+
echo "Publishing fleischwolf@$v from tag ${{ inputs.tag }}"
148+
149+
# Skip cleanly if this version is already on npm (idempotent re-runs).
150+
- name: Check if already published
151+
id: check
152+
run: |
153+
v="${{ steps.ver.outputs.version }}"
154+
if npm view "fleischwolf@$v" version >/dev/null 2>&1; then
155+
echo "published=true" >> "$GITHUB_OUTPUT"
156+
echo "fleischwolf@$v is already on npm — skipping."
157+
else
158+
echo "published=false" >> "$GITHUB_OUTPUT"
159+
fi
160+
161+
- name: Set package version
162+
if: steps.check.outputs.published == 'false'
163+
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version
164+
165+
# Create the per-platform package dirs and move each prebuilt .node into
166+
# its dir. `npm publish` then runs `prepublishOnly` (napi prepublish), which
167+
# publishes the fleischwolf-<triple> packages and wires them into the main
168+
# package's optionalDependencies before the main package is published.
169+
- name: Assemble platform packages
170+
if: steps.check.outputs.published == 'false'
171+
run: |
172+
npx napi create-npm-dir -t .
173+
npx napi artifacts --dir artifacts
174+
175+
- name: Publish
176+
if: steps.check.outputs.published == 'false'
177+
run: npm publish --access public
178+
env:
179+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ members = [
1414
"crates/fleischwolf",
1515
"crates/fleischwolf-cli",
1616
"crates/fleischwolf-pdf",
17+
# Node.js / Bun N-API bindings (published to npm, not crates.io).
18+
"crates/fleischwolf-node",
1719
]
1820

1921
[workspace.package]

README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,35 @@ buffered `export_to_markdown_with_images` path. Use
152152
The CLI streams Markdown by default (`--no-stream` opts back into buffering;
153153
`--to json` and `--images referenced` always buffer).
154154

155+
## Node.js / Bun bindings
156+
157+
Native TypeScript bindings live in
158+
[`crates/fleischwolf-node`](./crates/fleischwolf-node) (built with
159+
[napi-rs](https://napi.rs)). They ship a real `.node` addon that loads in both
160+
Node.js and Bun (Bun implements N-API — same binary, no rebuild), exposing the
161+
converter with the same knobs as the Rust API: Markdown / docling JSON output,
162+
`strict` mode, image modes, allowed-format restriction, `fetchImages`, sync +
163+
async (`Promise`) calls, and a `streamFileMarkdown` async generator.
164+
165+
```bash
166+
cd crates/fleischwolf-node && npm install && npm run build
167+
```
168+
169+
```ts
170+
import { convertFile, convertFileAsync, streamFileMarkdown } from 'fleischwolf'
171+
172+
const { content } = convertFile('report.docx') // Markdown
173+
const json = await convertFileAsync('paper.pdf', { to: 'json' })
174+
for await (const chunk of streamFileMarkdown('paper.pdf')) process.stdout.write(chunk)
175+
```
176+
177+
Declarative formats work out of the box. The PDF/image pipeline needs pdfium +
178+
the ONNX models (not bundled), so it throws until you call `installDependencies()`
179+
— which auto-downloads pdfium/OCR and fetches the layout/TableFormer ONNX from a
180+
`modelsUrl` you host. A reusable `Pipeline` keeps those models warm across many
181+
PDFs. See [`crates/fleischwolf-node/README.md`](./crates/fleischwolf-node/README.md)
182+
for the full API and runnable Node + Bun examples.
183+
155184
## Testing
156185

157186
All commands run from the `fleischwolf/` workspace root.
@@ -286,7 +315,9 @@ fleischwolf — bigger means Rust wins by more.
286315
|---|---|---|
287316
| `fleischwolf-core` | `DoclingDocument` model + serializers | `docling-core` |
288317
| `fleischwolf` | `DocumentConverter`, source loading, backends | `docling` |
318+
| `fleischwolf-pdf` | PDF/image ML pipeline (pdfium + ONNX layout/table/OCR) | `docling` PDF pipeline |
289319
| `fleischwolf-cli` | command-line interface | `docling.cli` |
320+
| `fleischwolf-node` | Node.js / Bun N-API bindings (npm package) ||
290321

291322
## License
292323

crates/fleischwolf-node/.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# npm
2+
node_modules/
3+
package-lock.json
4+
5+
# Build artifacts generated by `napi build` (regenerate with `npm run build`):
6+
# - the native addon (large; carries the linked ONNX runtime)
7+
# - the platform loader + its types
8+
*.node
9+
native.js
10+
native.d.ts
11+
12+
# Example scratch output
13+
examples/artifacts/
14+
15+
# Cross-platform publish scaffolding, generated in CI by `napi create-npm-dir`
16+
# and `napi artifacts` (per-platform packages + downloaded prebuilt binaries)
17+
npm/
18+
artifacts/

crates/fleischwolf-node/Cargo.toml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Node.js / Bun bindings for Fleischwolf, built with napi-rs.
2+
#
3+
# Produces a native N-API addon (`.node`) that loads in both Node.js and Bun
4+
# (Bun implements N-API). The TypeScript surface mirrors the Rust
5+
# `DocumentConverter`: convert a file or in-memory bytes to Markdown or docling
6+
# JSON, with the same knobs (strict Markdown, image modes, allowed formats,
7+
# external image fetching) plus incremental Markdown streaming.
8+
#
9+
# This crate is intentionally kept OUT of the default workspace members' publish
10+
# flow (it ships to npm, not crates.io). It is a `cdylib` — the only artifact is
11+
# the addon, there is no Rust library API here.
12+
[package]
13+
name = "fleischwolf-node"
14+
description = "Node.js / Bun bindings for Fleischwolf (a Rust port of docling)."
15+
version.workspace = true
16+
edition.workspace = true
17+
license.workspace = true
18+
repository.workspace = true
19+
homepage.workspace = true
20+
readme = "README.md"
21+
# ort (via fleischwolf-pdf) needs a newer compiler than the std-only crates.
22+
rust-version = "1.82"
23+
# Not a crates.io library — the deliverable is the npm package.
24+
publish = false
25+
26+
[lib]
27+
# napi addons are C-ABI dynamic libraries loaded by the JS runtime.
28+
crate-type = ["cdylib"]
29+
30+
[dependencies]
31+
fleischwolf = { path = "../fleischwolf", version = "0.6.1" }
32+
# Node-API bindings. `napi4` enables threadsafe functions, used for streaming.
33+
napi = { version = "2", default-features = false, features = ["napi4"] }
34+
napi-derive = "2"
35+
36+
[build-dependencies]
37+
napi-build = "2"

0 commit comments

Comments
 (0)