Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions .github/workflows/npm-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Publish the `fleischwolf` npm package (Node.js / Bun native bindings) for a
# chosen release. The package is a native N-API addon, so it can't be a one-line
# `npm publish`: it's built on a matrix of native runners (one per OS/arch),
# each producing a platform `.node`, then a publish job assembles the main
# package plus per-platform `optionalDependencies` (fleischwolf-<triple>) via
# napi-rs and publishes them all.
#
# Trigger: manual only (workflow_dispatch). Pick the release tag to build — the
# workflow checks out that tag and publishes the npm version derived from it, so
# npm releases stay decoupled from the crates.io release on every master push.
# Run it from the Actions tab (or `gh workflow run npm-publish.yml -f tag=v0.7.0`).
#
# Requires one repository secret: NPM_TOKEN — an npm automation token with
# publish rights to `fleischwolf` and the `fleischwolf-*` platform packages.

name: npm publish

on:
workflow_dispatch:
inputs:
tag:
description: "Release tag to build and publish (e.g. v0.7.0)."
required: true
version:
description: "Override the npm version (e.g. 0.7.0). Blank = derive from the tag."
required: false
default: ""

concurrency:
group: npm-publish-${{ inputs.tag }}
cancel-in-progress: false

defaults:
run:
working-directory: crates/fleischwolf-node

jobs:
build:
name: build ${{ matrix.target }}
runs-on: ${{ matrix.host }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
host: ubuntu-22.04
# GitHub-hosted ARM64 Linux runner (native — avoids cross-compiling the
# ONNX/ort build). Requires the arm64 runner image to be available.
- target: aarch64-unknown-linux-gnu
host: ubuntu-24.04-arm
- target: x86_64-apple-darwin
host: macos-13
- target: aarch64-apple-darwin
host: macos-14
- target: x86_64-pc-windows-msvc
host: windows-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}

- uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}

- uses: Swatinem/rust-cache@v2
with:
# One workspace, key the cache per target so the 5 jobs don't collide.
key: ${{ matrix.target }}
workspaces: "."

- uses: actions/setup-node@v4
with:
node-version: 20

- name: Install napi CLI
run: npm install

# Native addon + the JS loader / d.ts. `--strip` keeps the (ONNX-linked)
# binary as small as possible; strip isn't available under MSVC, so the
# Windows build omits it.
- name: Build (unix)
if: runner.os != 'Windows'
run: npx napi build --platform --release --strip --target ${{ matrix.target }} --js native.js --dts native.d.ts

- name: Build (windows)
if: runner.os == 'Windows'
run: npx napi build --platform --release --target ${{ matrix.target }} --js native.js --dts native.d.ts

- name: Upload prebuilt binary
uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.target }}
path: crates/fleischwolf-node/fleischwolf.*.node
if-no-files-found: error

# The JS loader + types are platform-agnostic; upload them once (from the
# linux-x64 build) for the publish job to include in the main package.
- name: Upload JS binding
if: matrix.target == 'x86_64-unknown-linux-gnu'
uses: actions/upload-artifact@v4
with:
name: js-binding
path: |
crates/fleischwolf-node/native.js
crates/fleischwolf-node/native.d.ts
if-no-files-found: error

publish:
name: publish to npm
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.tag }}

- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"

- name: Install napi CLI
run: npm install

# Prebuilt binaries → artifacts/<name>/fleischwolf.<triple>.node
- name: Download prebuilt binaries
uses: actions/download-artifact@v4
with:
pattern: bindings-*
path: crates/fleischwolf-node/artifacts

# The JS loader + types → the package root.
- name: Download JS binding
uses: actions/download-artifact@v4
with:
name: js-binding
path: crates/fleischwolf-node

# Version = the explicit override, else the selected tag with its leading `v`.
- name: Resolve version
id: ver
run: |
v="${{ inputs.version }}"
if [ -z "$v" ]; then v="${{ inputs.tag }}"; v="${v#v}"; fi
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "Publishing fleischwolf@$v from tag ${{ inputs.tag }}"

# Skip cleanly if this version is already on npm (idempotent re-runs).
- name: Check if already published
id: check
run: |
v="${{ steps.ver.outputs.version }}"
if npm view "fleischwolf@$v" version >/dev/null 2>&1; then
echo "published=true" >> "$GITHUB_OUTPUT"
echo "fleischwolf@$v is already on npm — skipping."
else
echo "published=false" >> "$GITHUB_OUTPUT"
fi

- name: Set package version
if: steps.check.outputs.published == 'false'
run: npm version "${{ steps.ver.outputs.version }}" --no-git-tag-version --allow-same-version

# Create the per-platform package dirs and move each prebuilt .node into
# its dir. `npm publish` then runs `prepublishOnly` (napi prepublish), which
# publishes the fleischwolf-<triple> packages and wires them into the main
# package's optionalDependencies before the main package is published.
- name: Assemble platform packages
if: steps.check.outputs.published == 'false'
run: |
npx napi create-npm-dir -t .
npx napi artifacts --dir artifacts

- name: Publish
if: steps.check.outputs.published == 'false'
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ members = [
"crates/fleischwolf",
"crates/fleischwolf-cli",
"crates/fleischwolf-pdf",
# Node.js / Bun N-API bindings (published to npm, not crates.io).
"crates/fleischwolf-node",
]

[workspace.package]
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,35 @@ buffered `export_to_markdown_with_images` path. Use
The CLI streams Markdown by default (`--no-stream` opts back into buffering;
`--to json` and `--images referenced` always buffer).

## Node.js / Bun bindings

Native TypeScript bindings live in
[`crates/fleischwolf-node`](./crates/fleischwolf-node) (built with
[napi-rs](https://napi.rs)). They ship a real `.node` addon that loads in both
Node.js and Bun (Bun implements N-API — same binary, no rebuild), exposing the
converter with the same knobs as the Rust API: Markdown / docling JSON output,
`strict` mode, image modes, allowed-format restriction, `fetchImages`, sync +
async (`Promise`) calls, and a `streamFileMarkdown` async generator.

```bash
cd crates/fleischwolf-node && npm install && npm run build
```

```ts
import { convertFile, convertFileAsync, streamFileMarkdown } from 'fleischwolf'

const { content } = convertFile('report.docx') // Markdown
const json = await convertFileAsync('paper.pdf', { to: 'json' })
for await (const chunk of streamFileMarkdown('paper.pdf')) process.stdout.write(chunk)
```

Declarative formats work out of the box. The PDF/image pipeline needs pdfium +
the ONNX models (not bundled), so it throws until you call `installDependencies()`
— which auto-downloads pdfium/OCR and fetches the layout/TableFormer ONNX from a
`modelsUrl` you host. A reusable `Pipeline` keeps those models warm across many
PDFs. See [`crates/fleischwolf-node/README.md`](./crates/fleischwolf-node/README.md)
for the full API and runnable Node + Bun examples.

## Testing

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

## License

Expand Down
18 changes: 18 additions & 0 deletions crates/fleischwolf-node/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# npm
node_modules/
package-lock.json

# Build artifacts generated by `napi build` (regenerate with `npm run build`):
# - the native addon (large; carries the linked ONNX runtime)
# - the platform loader + its types
*.node
native.js
native.d.ts

# Example scratch output
examples/artifacts/

# Cross-platform publish scaffolding, generated in CI by `napi create-npm-dir`
# and `napi artifacts` (per-platform packages + downloaded prebuilt binaries)
npm/
artifacts/
37 changes: 37 additions & 0 deletions crates/fleischwolf-node/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Node.js / Bun bindings for Fleischwolf, built with napi-rs.
#
# Produces a native N-API addon (`.node`) that loads in both Node.js and Bun
# (Bun implements N-API). The TypeScript surface mirrors the Rust
# `DocumentConverter`: convert a file or in-memory bytes to Markdown or docling
# JSON, with the same knobs (strict Markdown, image modes, allowed formats,
# external image fetching) plus incremental Markdown streaming.
#
# This crate is intentionally kept OUT of the default workspace members' publish
# flow (it ships to npm, not crates.io). It is a `cdylib` — the only artifact is
# the addon, there is no Rust library API here.
[package]
name = "fleischwolf-node"
description = "Node.js / Bun bindings for Fleischwolf (a Rust port of docling)."
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
readme = "README.md"
# ort (via fleischwolf-pdf) needs a newer compiler than the std-only crates.
rust-version = "1.82"
# Not a crates.io library — the deliverable is the npm package.
publish = false

[lib]
# napi addons are C-ABI dynamic libraries loaded by the JS runtime.
crate-type = ["cdylib"]

[dependencies]
fleischwolf = { path = "../fleischwolf", version = "0.6.1" }
# Node-API bindings. `napi4` enables threadsafe functions, used for streaming.
napi = { version = "2", default-features = false, features = ["napi4"] }
napi-derive = "2"

[build-dependencies]
napi-build = "2"
Loading
Loading