From dc3f0a2a49ff447ea6a4abbfc099bc1c10c41612 Mon Sep 17 00:00:00 2001 From: krflol Date: Mon, 5 Oct 2026 20:12:23 -0500 Subject: [PATCH] Document Node process isolation and update server starters --- docs/assets/nextjs/project.zip | Bin 104399 -> 105014 bytes docs/assets/nextjs/source.json | 6 +- docs/assets/nextjs/verification.json | 119 ++++++++++++++++++++------- docs/assets/node/project.json | 18 ++-- docs/assets/node/project.zip | Bin 5450 -> 5446 bytes docs/getting-started/node.md | 60 +++++++++++--- docs/guides/nextjs-pdf.md | 34 +++++--- examples/node/README.md | 4 +- examples/node/package-lock.json | 8 +- examples/node/package.json | 2 +- examples/node/source.json | 10 +-- tools/verify_nextjs_starter.py | 5 +- tools/verify_node_examples.py | 16 +++- 13 files changed, 199 insertions(+), 83 deletions(-) diff --git a/docs/assets/nextjs/project.zip b/docs/assets/nextjs/project.zip index d8d00adac9a96e349d21e585a2db8994fa1b01cf..afb5ddbc84ee7563edcfb13052cb0e9282a73846 100644 GIT binary patch delta 15094 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 diff --git a/docs/assets/nextjs/source.json b/docs/assets/nextjs/source.json index fa166c4..45b3753 100644 --- a/docs/assets/nextjs/source.json +++ b/docs/assets/nextjs/source.json @@ -1,12 +1,12 @@ { "sourceRepository": "https://github.com/fullbleed-engine/fullbleed-node", - "sourceCommit": "fe0fa10499428841ba3c0eac8571b5fc31b00fdc", + "sourceCommit": "4f574b5e11d3a680a50c561bf9ea071de29d0cb8", "files": 18, "assets": [ { "file": "project.zip", - "sha256": "9a211e5553f81508c54c87ab52d02f498f17f0e2703eff088cfd440867969564", - "bytes": 104399 + "sha256": "d6586c7b3d5f465fa08ac292b02c634b2ad649f0722616c47b5bc26cf751e157", + "bytes": 105014 }, { "file": "invoice.pdf", diff --git a/docs/assets/nextjs/verification.json b/docs/assets/nextjs/verification.json index f6538d6..d060af0 100644 --- a/docs/assets/nextjs/verification.json +++ b/docs/assets/nextjs/verification.json @@ -1,10 +1,9 @@ { - "checkedAt": "2026-10-05T23:55:42.874759+00:00", + "checkedAt": "2026-10-06T01:06:26.050552+00:00", "sourceRepository": "https://github.com/fullbleed-engine/fullbleed-node", - "sourceCommit": "fe0fa10499428841ba3c0eac8571b5fc31b00fdc", - "mergedCommit": "39062ed78bde8cca2bb73842e6c04b0dc6531a71", - "pullRequest": "https://github.com/fullbleed-engine/fullbleed-node/pull/15", - "ciRun": "https://github.com/fullbleed-engine/fullbleed-node/actions/runs/37279997939", + "sourceCommit": "4f574b5e11d3a680a50c561bf9ea071de29d0cb8", + "pullRequest": "https://github.com/fullbleed-engine/fullbleed-node/pull/17", + "ciRun": "https://github.com/fullbleed-engine/fullbleed-node/actions/runs/37397228563", "standaloneChecks": [ { "name": "standalone artifact is isolated from the source application", @@ -62,6 +61,14 @@ "name": "a valid document renders after the page limit rejects a job", "passed": true }, + { + "name": "a failed render child returns an HTTP error while the production server stays alive", + "passed": true + }, + { + "name": "the production server returns a valid PDF after a render-process failure", + "passed": true + }, { "name": "client JavaScript excludes the server PDF engine and worker", "passed": true @@ -69,14 +76,14 @@ ], "ciResults": [ { - "artifact": "nextjs-ubuntu-latest-22", - "checkedAt": "2026-10-05T07:50:52.448Z", + "checkedAt": "2026-10-06T01:04:06.668Z", "node": "v22.23.3", "platform": "linux", "next": "16.3.8", - "nodePackage": "0.1.5", + "nodePackage": "0.2.0", "engineVersion": "2.5.8", - "packageLockSha256": "eb0b1ff6cf424cebf8464f44b38192a175465bcb77626a7645a18c85f3f0ce40", + "isolation": "process", + "packageLockSha256": "ceda9f7f9099fa91ab5a98d6ce8fd0ba4d2db761b46a19b412688f8db045c179", "pages": 1, "pdfSha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", "burst": [ @@ -93,17 +100,18 @@ 503, 503 ], - "checksPassed": 15 + "artifact": "nextjs-ubuntu-latest-22", + "checksPassed": 17 }, { - "artifact": "nextjs-ubuntu-latest-24", - "checkedAt": "2026-10-05T07:50:44.440Z", + "checkedAt": "2026-10-06T01:04:06.836Z", "node": "v24.21.0", "platform": "linux", "next": "16.3.8", - "nodePackage": "0.1.5", + "nodePackage": "0.2.0", "engineVersion": "2.5.8", - "packageLockSha256": "eb0b1ff6cf424cebf8464f44b38192a175465bcb77626a7645a18c85f3f0ce40", + "isolation": "process", + "packageLockSha256": "ceda9f7f9099fa91ab5a98d6ce8fd0ba4d2db761b46a19b412688f8db045c179", "pages": 1, "pdfSha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", "burst": [ @@ -120,17 +128,18 @@ 503, 503 ], - "checksPassed": 15 + "artifact": "nextjs-ubuntu-latest-24", + "checksPassed": 17 }, { - "artifact": "nextjs-windows-latest-24", - "checkedAt": "2026-10-05T07:51:41.715Z", + "checkedAt": "2026-10-06T01:04:28.012Z", "node": "v24.21.0", "platform": "win32", "next": "16.3.8", - "nodePackage": "0.1.5", + "nodePackage": "0.2.0", "engineVersion": "2.5.8", - "packageLockSha256": "eb0b1ff6cf424cebf8464f44b38192a175465bcb77626a7645a18c85f3f0ce40", + "isolation": "process", + "packageLockSha256": "ceda9f7f9099fa91ab5a98d6ce8fd0ba4d2db761b46a19b412688f8db045c179", "pages": 1, "pdfSha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", "burst": [ @@ -147,25 +156,22 @@ 503, 503 ], - "checksPassed": 15 + "artifact": "nextjs-windows-latest-24", + "checksPassed": 17 } ], - "packageChecks": { - "run": "https://github.com/fullbleed-engine/fullbleed-node/actions/runs/37279997930", - "jobsPassed": 10, - "conclusion": "success" - }, - "downloadVerification": { + "downloadedStarter": { "ok": true, - "checkedAt": "2026-10-05T07:51:53.410802+00:00", - "sourceCommit": "fe0fa10499428841ba3c0eac8571b5fc31b00fdc", + "checkedAt": "2026-10-06T01:05:03.951160+00:00", + "sourceCommit": "4f574b5e11d3a680a50c561bf9ea071de29d0cb8", "downloadFiles": 18, - "projectZipSha256": "9a211e5553f81508c54c87ab52d02f498f17f0e2703eff088cfd440867969564", + "projectZipSha256": "d6586c7b3d5f465fa08ac292b02c634b2ad649f0722616c47b5bc26cf751e157", "node": "v22.23.3", "platform": "win32", "next": "16.3.8", - "nodePackage": "0.1.5", + "nodePackage": "0.2.0", "engineVersion": "2.5.8", + "isolation": "process", "commands": [ { "name": "install", @@ -243,6 +249,14 @@ "name": "a valid document renders after the page limit rejects a job", "passed": true }, + { + "name": "a failed render child returns an HTTP error while the production server stays alive", + "passed": true + }, + { + "name": "the production server returns a valid PDF after a render-process failure", + "passed": true + }, { "name": "client JavaScript excludes the server PDF engine and worker", "passed": true @@ -250,5 +264,48 @@ ], "scope": "Fresh public ZIP installation, production build, isolated standalone HTTP server, and exact sample-asset comparison. No application authentication or hosted-provider claim." }, - "scope": "Published npm 0.1.5 in the Next.js standalone starter. Three CI environments and a fresh downloadable ZIP install pass 15 HTTP/worker/error-recovery checks. The retained invoice PDF and preview match. No application authorization, hosting-provider, speed, or standards certification claim." + "browser": { + "ok": true, + "checked_at": "2026-10-06T01:05:29.710577+00:00", + "browser": "154.0.8037.98", + "isolation": "process", + "checks": [ + { + "width": 1440, + "keyboard": false, + "layout": { + "width": 1440, + "documentWidth": 1440, + "bodyWidth": 1440 + }, + "pdf_sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", + "page_errors": [] + }, + { + "width": 390, + "keyboard": true, + "layout": { + "width": 390, + "documentWidth": 390, + "bodyWidth": 390 + }, + "pdf_sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", + "page_errors": [] + }, + { + "width": 320, + "keyboard": false, + "layout": { + "width": 320, + "documentWidth": 320, + "bodyWidth": 320 + }, + "pdf_sha256": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", + "page_errors": [] + } + ], + "scope": "Independent headless Chrome and isolated Next standalone artifact; real PDF downloads, keyboard activation, narrow layouts. Existing authenticated browser sessions were not used." + }, + "scope": "Published npm 0.2.0 with process isolation in the Next.js standalone starter. Three CI environments and a fresh downloadable ZIP install pass 17 HTTP/process/error-recovery checks. Real browser downloads match the retained invoice. No application authorization, hosted-provider, speed, or standards certification claim.", + "mergedCommit": "36a5b1d382c27d5a0f3414c732a88529e0bf5c7b" } diff --git a/docs/assets/node/project.json b/docs/assets/node/project.json index 34ce19d..580f5d0 100644 --- a/docs/assets/node/project.json +++ b/docs/assets/node/project.json @@ -1,10 +1,10 @@ { - "package_version": "0.1.5", + "package_version": "0.2.0", "engine_version": "2.5.8", - "source_commit": "847b943934ee050c735c712ffcaaff1d3d4c277e", - "package_url": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.5/fullbleed-0.1.5.tgz", - "package_integrity": "sha512-3AvPt+n2j/0KMoUk1rq/cu4dzcsnyUNKJNOwpUJkW48vkndFcroFtXgMx4CXJX46O2/gDSli67tuhUUiVsu5OQ==", - "package_sha256": "817e9ba5a4741b6626b62c5e829fdd921c0b69d1e165fb4905483d56887237dc", + "source_commit": "969f23c479cd02f366cc1d2e099b6bda19dfaac9", + "package_url": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.2.0/fullbleed-0.2.0.tgz", + "package_integrity": "sha512-7khp008/oaJxMbroCQuv82ICOnJVQ1RUgYsOZWAqcW92qjKEAVzRK80UZDIJkNWcaMh1yDdTR8BJ5PAFc3QbnA==", + "package_sha256": "90bd0bbf4ae542b6be1deb3103ba492b9d8675e47b71f895060b40419d705f4a", "rendered": { "invoice.pdf": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", "invoice.png": "b76f6db748fb47b928c93c12fe788de94a0f5d8da4bb350f644a58705c163cf7" @@ -27,7 +27,7 @@ }, "README.md": { "bytes": 902, - "sha256": "bc0a1e02e586dceb49ce48c5b08cbae46f840474a865b177ba0cc926ada55088" + "sha256": "98501bbbfe6844436d01f3d53cf7ea33c58910333734a981ff4ec599cc26f8ba" }, "invoice.css": { "bytes": 3546, @@ -39,16 +39,16 @@ }, "package-lock.json": { "bytes": 1075, - "sha256": "2432927cb75a10910107f41ff9e372dde8cbcf170588b07bd4948a066c71a24c" + "sha256": "9041f8e21fcde488123d7178b0d0f00b98b6e856141e2c0465ff914daf59d549" }, "package.json": { "bytes": 241, - "sha256": "07cdcec79b81d4f5dc9df229c0a97523ef1af7c32576adc6cd16c7673e1fce64" + "sha256": "6f638329f16f5dd22b767ce3784ce01ef219fcf7bb371261d0a28e0a2b3b5184" }, "render.mjs": { "bytes": 1064, "sha256": "dd63e83658800df2672c8c5ca1b176dbe49fc0d5c49b8c2b79af91515a68e776" } }, - "zip_sha256": "8b49592ad3a49d52ea27fc8eff14e013f9b63da201ca118e2138849f1fc5bcbb" + "zip_sha256": "3cdf9f3fb454dd269f4b2430eec7bd7ff468ce65d24e30f949c48ff4e39022a3" } diff --git a/docs/assets/node/project.zip b/docs/assets/node/project.zip index a9e5575c3d24f5ae61fd80c737b932c733808723..56c4f5b8ca6d0d8d025ded5a02e14e7747cb98e3 100644 GIT binary patch delta 1000 zcmV6M^N6t#uKIm+;3rr$#!sWb9CywnErf^2bIMG7Um6}vV`xUfUk=#=&)Kr zNL)H$T7wVpc%~uvTKiTqF*wU`PE}#R8tW_?DMZ`0yn7Hnj-V|pmuyc+xXe1)NO`KK z;vu0o8<=Xl5MLvDXhWe`sJU2j{gu&l?F9XQLu6}~lh^~(GHuf^OqShMvNU*XdL**G zU)FDe&@`UMn%ANldH1-15bGiGgjm-eYm}{?Qw$UZjXTBEemkmT(q%(BHc;qWdW{N5*2S#e+F5WvU^#k$j2bqvUo*O@yq>HUpYSEwv zdh0npIvO)iccwFO&I9-Zz$1ikGja^eQqO~8v9XoM9o7kOh(8}baf)flfzj_@x<>qr zgjI4*t!H7je-sSvhyaQ8Ij(BjyxnyT)ust<)Ud9~HM=;js`YQJdSQ?%iY_N${Bz0` zA1?AjSFPcUjO|5dx$-w`=B$l_xBjdi_LB$0J})P_vT{b&IDA@rU>bBzBlx`32jlMC z_~vN~mwcerEf)XCdhb5_Z_BsJ@3r*{P)h>@6aWAKe+U2o076}h(?g<>0002-0000C z0001RVPk7yXJsyGb8l`DZM50blyMm=%Qer?qxw#LScq=+ zU%5;fF;guqUR&9M4CEj)#+&3`>hLP6^|$T-J(1wxFv-U4YV99$kXWV(kL>Rg7L2y- zUN*B}5s3l>XUf%wlkF3x1p0002-0000C00000000000001_ W0i=`d6gdH3lP47&2A~rF0001cdByes delta 1028 zcmX@6bxLc)dL|a_?gEL)o0(KuUb2aKO}@jFR9~T-|KzRH9JbZ$79#mF<(C`k)e=aP`vTUFYU!IZ*l(m-*)1f&g2`+myLEWOPryzdv!|Y zhZ|o!Qfl|FxzDmN)282ay{qY`cZJM~*PNelZd_-3O(WWNx!!Am$W!{8@3LIxtdD>C zu<05j1A{RO1A`y~14BV#a&}^Rs%}nxa<*Ppaem&?pxb$~9RzB>hbzQI>#@nWPmH-* zIIBrE{*uU}4@r!>+e|ljmx-^CeR`i;<@i7o){DB!lJr1>QScu?%mb5 z4_D3o|0*$h>!&vrg}!U|S1;}F7X9C_b?%~b&yMu{{u0Pp#4N+q_WfIuS**_KGxLsp zKUOK2Tp$}zboTngKkbH*{@0J)3<^H9dN<2l-b%TfWArW* zqE(;QS~~cvSXlUMD%{=oSl;+SMonthly statement', + isolation: 'process', + timeoutMs: 10_000, + maxPages: 20, +}); +``` + +The caller does not compile WebAssembly or create render workers in this mode. +The promise waits for process exit and IPC disconnection before settling, even +after success. A failed child exit rejects with `PROCESS_FAILED`; available +`exitCode` and `signal` fields describe the exit. Timeouts and aborts terminate +the child before rejecting. A PDF received before a failed shutdown is not +returned as a successful result. + +Process startup adds latency and memory use. There is no process pool or +automatic retry; keep concurrency bounded. Application startup flags and +`NODE_OPTIONS` are not replayed in the child. This option requires a normal +Node executable and is not an operating-system security sandbox. See the +[process isolation guide](https://github.com/fullbleed-engine/fullbleed-node/blob/v0.2.0/docs/process-isolation.md) +for deployment details and the tested failure paths. The Next.js starter uses +this mode for its PDF route. + An intermittent process crash on Linux with Node 24.21.0 was observed with `fullbleed@0.1.2`; [issue #7](https://github.com/fullbleed-engine/fullbleed-node/issues/7) remains open. Version 0.1.3 fixes worker shutdown timing, but has not established that this native crash is resolved. See the [runtime investigation](https://github.com/fullbleed-engine/fullbleed-node/blob/main/docs/runtime-diagnostics.md) for the observed scope and synthetic diagnostic. A process crash can occur -before JavaScript can return an error. +before JavaScript can return an error in worker mode. Process isolation contains +tested child failures; it does not establish a fix for that native crash. This first Node API covers ordinary PDF rendering and previews. Use the [Python API](../engine/pdf-engine.md) or [Rust crate](rust.md) for profiles, @@ -144,15 +178,17 @@ TypeScript. Its release evidence compares retained native and WebAssembly fixtures and installs the same tarball across Node 22, 24, and 26 on Windows, Linux, and macOS. These are scoped engineering checks, not PDF standards certification. -The [npm publication record](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.5/npm-publication-0.1.5.json) +The [npm publication record](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.2.0/npm-publication-0.2.0.json) confirms the registry tarball matches that release. A fresh registry install on Windows / Node 22 reproduces its PDF/PNG hashes, including ESM/CommonJS and -recovery after a cold timeout. Seven font fixtures also pass independent font, -text, and page-pixel checks against the registry-installed package. Ten custom-family -cases also match explicit-face controls; the same checker rejects the incorrect -0.1.4 result. [Inspect the family-selection evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.5/font-family-verification.json). +recovery after a cold timeout. The process consumer also forbids parent +WebAssembly and render workers, checks that application preloads stay out of +children, forces a child termination, and renders again. The release's CI +checks retain seven embedded-font fixtures and ten custom-family cases with +independent font, text, and page-pixel checks. +[Inspect the family-selection evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.2.0/font-family-verification.json). [Package source and API](https://github.com/fullbleed-engine/fullbleed-node) · [npm package](https://www.npmjs.com/package/fullbleed) · -[Versioned release and evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.1.5) · +[Versioned release and evidence](https://github.com/fullbleed-engine/fullbleed-node/releases/tag/v0.2.0) · [CSS coverage](../css-coverage.md) · [Examples](../examples.md) diff --git a/docs/guides/nextjs-pdf.md b/docs/guides/nextjs-pdf.md index 714698d..8b2656d 100644 --- a/docs/guides/nextjs-pdf.md +++ b/docs/guides/nextjs-pdf.md @@ -34,17 +34,18 @@ Open `http://127.0.0.1:3000` and select **Download sample PDF**. The route `/api/invoices/NS-1042` returns the invoice as an attachment. The browser stays on the starter page. Use `npm run dev` while editing the app. -The lockfile pins Next.js 16.3.8, React 19.3.0 and Fullbleed Node 0.1.5, powered +The lockfile pins Next.js 16.3.8, React 19.3.0 and Fullbleed Node 0.2.0, powered by engine 2.5.8. Fullbleed is installed from npm. The registry tarball is byte-identical to the verified GitHub release. For an existing Node application, start with the [Node.js quickstart](../getting-started/node.md). ## Keep Fullbleed on the Node server -The route declares `runtime = 'nodejs'`. Fullbleed uses worker threads and an -in-memory WebAssembly engine, and reads bundled assets from its installed -package. The app keeps it outside Next's server bundling and explicitly includes -those files in the standalone output: +The route declares `runtime = 'nodejs'` and calls Fullbleed with +`isolation: 'process'`. Each request runs its worker and WebAssembly engine in +a fresh Node process, using bundled assets from the installed package. The app +keeps Fullbleed outside Next's server bundling and explicitly includes those +runtime files in the standalone output: ```javascript // next.config.mjs — the project also sets its tracing/build root. @@ -69,14 +70,14 @@ browser assets automatically. The PDF engine is imported only by the server route. The browser receives the page, a static PNG preview and the requested PDF; it does not load Fullbleed's -worker or WASM. This integration needs a Node server, not an Edge runtime or -a static site export. +worker or WASM. Use a Node server that permits child processes, workers, and +WebAssembly. Edge runtimes and static site exports cannot run this route. ## Return a private PDF response The [complete route](https://github.com/fullbleed-engine/fullbleed-node/blob/main/examples/nextjs/app/api/invoices/%5Bid%5D/route.js) looks up the fictional invoice, loads its template, then calls `renderPdf` with -a five-page cap, a 15-second deadline and the request's abort signal. +a five-page cap, a 15-second deadline, process isolation and the request's abort signal. | Result | HTTP behavior | | --- | --- | @@ -84,6 +85,7 @@ a five-page cap, a 15-second deadline and the request's abort signal. | Unknown fixture ID | `404` with a small JSON error | | Another render is active | `503 BUSY` with `Retry-After: 2`; no waiting queue | | Renderer rejects the document | Generic `500` error; no partial PDF or document input in the response | +| Render child exits unsuccessfully | Generic `500` error; the server can handle the next request | | Render deadline expires | `504` error | One render is allowed per server process. Use shared rate and usage controls @@ -91,10 +93,14 @@ across replicas, and measure memory before raising concurrency. A worker can grow to the Node package's 512 MiB WASM ceiling. Hosting-provider limits and serverless deployments require their own validation. -Fullbleed waits for a rendering worker to exit before its promise settles, -including after failure, timeout, or cancellation. The route keeps its capacity -slot until that worker stops. A timeout starts termination; settlement includes -the time needed to stop the worker. +Fullbleed waits for process exit and IPC disconnection before its promise +settles, including after failure, timeout, or cancellation. The route keeps +its capacity slot until cleanup finishes. Process startup adds latency and +memory overhead; there is no process pool or automatic retry. Timeouts start +termination, and settlement includes the time needed to stop the child. + +This contains tested child failures. It does not establish a fix for the native +crash tracked in [issue #7](https://github.com/fullbleed-engine/fullbleed-node/issues/7). ## Use your design and authorized data @@ -122,9 +128,11 @@ After building, run `npm run verify`. It copies the standalone app into a fresh temporary directory outside the source project, starts it on loopback, and checks real HTTP PDFs against direct engine output. It also exercises unknown IDs, a concurrent burst, missing-glyph and page-limit failures, and recovery. +It then deliberately fails the isolated artifact's render child, verifies the +HTTP error and live server, restores the child entrypoint, and downloads a valid PDF. The server closes when the checks finish; reports and a PDF remain in `output`. -The [retained verification](../assets/nextjs/verification.json) records 15 passing +The [retained verification](../assets/nextjs/verification.json) records 17 passing standalone checks on Linux with Node 22/24 and Windows with Node 24. All three runs produced the same sample PDF and PNG bytes. Local Chrome checks also cover the real download, keyboard activation and a narrow viewport. Documentation CI diff --git a/examples/node/README.md b/examples/node/README.md index 7142e32..a7ca99d 100644 --- a/examples/node/README.md +++ b/examples/node/README.md @@ -1,6 +1,6 @@ # A designed invoice from Node.js -This fictional Northstar Studio invoice uses Fullbleed's Node package 0.1.5, +This fictional Northstar Studio invoice uses Fullbleed's Node package 0.2.0, which bundles the Fullbleed 2.5.8 engine and Inter, DM Serif Display, and Bebas Neue fonts. You need Node.js 22 or newer; no Python or Rust installation is needed. @@ -13,7 +13,7 @@ Open `output/invoice/invoice.pdf` and `output/invoice/page-1.png`. Change the `customer` value in `render.mjs`; the script escapes it before insertion into HTML. Edit `invoice.html` and `invoice.css` for your own layout and data. -The lockfile pins Fullbleed 0.1.5 from npm and its integrity hash. +The lockfile pins Fullbleed 0.2.0 from npm and its integrity hash. The package renders locally; it does not upload documents to a service. The sample and engine are MIT licensed. The package retains the bundled font diff --git a/examples/node/package-lock.json b/examples/node/package-lock.json index d050eca..5ea1372 100644 --- a/examples/node/package-lock.json +++ b/examples/node/package-lock.json @@ -8,7 +8,7 @@ "name": "fullbleed-node-invoice", "version": "1.0.0", "dependencies": { - "fullbleed": "0.1.5" + "fullbleed": "0.2.0" }, "engines": { "node": ">=22" @@ -21,9 +21,9 @@ "license": "MIT OR Apache-2.0" }, "node_modules/fullbleed": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.1.5.tgz", - "integrity": "sha512-3AvPt+n2j/0KMoUk1rq/cu4dzcsnyUNKJNOwpUJkW48vkndFcroFtXgMx4CXJX46O2/gDSli67tuhUUiVsu5OQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/fullbleed/-/fullbleed-0.2.0.tgz", + "integrity": "sha512-7khp008/oaJxMbroCQuv82ICOnJVQ1RUgYsOZWAqcW92qjKEAVzRK80UZDIJkNWcaMh1yDdTR8BJ5PAFc3QbnA==", "license": "MIT", "dependencies": { "@bjorn3/browser_wasi_shim": "0.4.2" diff --git a/examples/node/package.json b/examples/node/package.json index f4868e9..e301e82 100644 --- a/examples/node/package.json +++ b/examples/node/package.json @@ -10,6 +10,6 @@ "node": ">=22" }, "dependencies": { - "fullbleed": "0.1.5" + "fullbleed": "0.2.0" } } diff --git a/examples/node/source.json b/examples/node/source.json index b82f718..60c83fc 100644 --- a/examples/node/source.json +++ b/examples/node/source.json @@ -1,10 +1,10 @@ { - "package_version": "0.1.5", + "package_version": "0.2.0", "engine_version": "2.5.8", - "source_commit": "847b943934ee050c735c712ffcaaff1d3d4c277e", - "package_url": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.1.5/fullbleed-0.1.5.tgz", - "package_integrity": "sha512-3AvPt+n2j/0KMoUk1rq/cu4dzcsnyUNKJNOwpUJkW48vkndFcroFtXgMx4CXJX46O2/gDSli67tuhUUiVsu5OQ==", - "package_sha256": "817e9ba5a4741b6626b62c5e829fdd921c0b69d1e165fb4905483d56887237dc", + "source_commit": "969f23c479cd02f366cc1d2e099b6bda19dfaac9", + "package_url": "https://github.com/fullbleed-engine/fullbleed-node/releases/download/v0.2.0/fullbleed-0.2.0.tgz", + "package_integrity": "sha512-7khp008/oaJxMbroCQuv82ICOnJVQ1RUgYsOZWAqcW92qjKEAVzRK80UZDIJkNWcaMh1yDdTR8BJ5PAFc3QbnA==", + "package_sha256": "90bd0bbf4ae542b6be1deb3103ba492b9d8675e47b71f895060b40419d705f4a", "rendered": { "invoice.pdf": "9d93174ed961bb2ef577f5f38d2836763b414edc53365f1a9c61e2b50d40f054", "invoice.png": "b76f6db748fb47b928c93c12fe788de94a0f5d8da4bb350f644a58705c163cf7" diff --git a/tools/verify_nextjs_starter.py b/tools/verify_nextjs_starter.py index f316062..e3362bc 100644 --- a/tools/verify_nextjs_starter.py +++ b/tools/verify_nextjs_starter.py @@ -75,7 +75,8 @@ def run(label, arguments): preview = json.loads((project / "output/preview.json").read_text(encoding="utf8")) verification = json.loads((project / "output/verification.json").read_text(encoding="utf8")) assert preview["pages"] == 1 and preview["missingGlyphs"] == 0 -assert len(verification["checks"]) == 15 and all(item["passed"] for item in verification["checks"]) +assert len(verification["checks"]) == 17 and all(item["passed"] for item in verification["checks"]) +assert verification["isolation"] == "process" for actual, expected in [("output/invoice.pdf", "invoice.pdf"), ("output/route.pdf", "invoice.pdf"), ("public/invoice.png", "invoice.png")]: @@ -89,7 +90,7 @@ def run(label, arguments): "projectZipSha256": expected_assets["project.zip"]["sha256"], "node": verification["node"], "platform": verification["platform"], "next": verification["next"], "nodePackage": verification["nodePackage"], - "engineVersion": verification["engineVersion"], "commands": commands, + "engineVersion": verification["engineVersion"], "isolation": verification["isolation"], "commands": commands, "pdfSha256": digest((out / "route.pdf").read_bytes()), "previewSha256": digest((out / "invoice.png").read_bytes()), "checks": verification["checks"], diff --git a/tools/verify_node_examples.py b/tools/verify_node_examples.py index 205c17d..4294dd2 100644 --- a/tools/verify_node_examples.py +++ b/tools/verify_node_examples.py @@ -69,7 +69,7 @@ def run(label, command, cwd=workspace): source = (ROOT / "docs/getting-started/node.md").read_text(encoding="utf8") blocks = re.findall(r"```javascript\n(.*?)```", source, re.S) -assert len(blocks) == 1 +assert len(blocks) == 2 home = (ROOT / 'docs/index.md').read_text(encoding='utf8') home_code = re.findall(r'```javascript\n(.*?)```', home, re.S) assert len(home_code) == 1 and dedent(home_code[0]).strip() == blocks[0].strip(), 'Homepage Node example differs from the executed guide.' @@ -81,12 +81,26 @@ def run(label, command, cwd=workspace): for name in ["invoice.pdf", "invoice.png"]: data = (snippet / name).read_bytes() assert digest(data) == manifest["quickstart"][name], name +process_script = """import assert from 'node:assert/strict'; +import { writeFile } from 'node:fs/promises'; +import { renderPdf } from 'fullbleed'; +""" + blocks[1] + """ +assert.equal(result.pages, 1); +assert.equal(result.missingGlyphs, 0); +assert.equal(result.pdf.subarray(0, 5).toString(), '%PDF-'); +await writeFile('process-statement.pdf', result.pdf); +""" +(snippet / "process-snippet.mjs").write_text(process_script, encoding="utf8", newline="\n") +run("process-snippet", ["process-snippet.mjs"], snippet) +process_pdf = (snippet / "process-statement.pdf").read_bytes() +(out / "process-statement.pdf").write_bytes(process_pdf) report = {"ok": True, "node": subprocess.check_output([str(node), "--version"], text=True).strip(), "platform": os.name, "package_version": installed["version"], "engine_version": installed["fullbleed"]["engineVersion"], "installed_package_matches_manifest": True, "source_commit": manifest["source_commit"], "commands": commands, "download_files": len(manifest["files"]), "project_zip_sha256": manifest["zip_sha256"], "outputs": outputs, "quickstart": manifest["quickstart"], "homepage_snippet_matches_executed_guide": True, + "process_snippet": {"ok": True, "pdf_sha256": digest(process_pdf)}, "workspace": workspace.name, "scope": "Actual public package installation, downloadable project, and documentation snippet; reviewed output hashes."} (out / "verification.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf8") print(json.dumps({"ok": True, "node": report["node"], "files": report["download_files"], "commands": len(commands)}))