@lupinum/nuxt-pdf 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/API_REPORT.md +262 -0
  2. package/CHANGELOG.md +168 -0
  3. package/CONFORMANCE.md +496 -0
  4. package/LICENSE +21 -0
  5. package/README.md +203 -0
  6. package/THIRD_PARTY_NOTICES.md +75 -0
  7. package/dist/module.d.mts +24 -0
  8. package/dist/module.json +12 -0
  9. package/dist/module.mjs +368 -0
  10. package/dist/runtime/authoring.d.ts +141 -0
  11. package/dist/runtime/authoring.js +25 -0
  12. package/dist/runtime/components/_props.d.ts +196 -0
  13. package/dist/runtime/components/_props.js +63 -0
  14. package/dist/runtime/components/document.d.ts +9 -0
  15. package/dist/runtime/components/document.js +26 -0
  16. package/dist/runtime/components/index.d.ts +4 -0
  17. package/dist/runtime/components/index.js +27 -0
  18. package/dist/runtime/components/svg.d.ts +17 -0
  19. package/dist/runtime/components/svg.js +50 -0
  20. package/dist/runtime/composables/index.d.ts +2 -0
  21. package/dist/runtime/composables/index.js +3 -0
  22. package/dist/runtime/composables/use-pdf-page-numbers.d.ts +39 -0
  23. package/dist/runtime/composables/use-pdf-page-numbers.js +17 -0
  24. package/dist/runtime/define-pdf.d.ts +6 -0
  25. package/dist/runtime/define-pdf.js +5 -0
  26. package/dist/runtime/fonts.d.ts +16 -0
  27. package/dist/runtime/fonts.js +0 -0
  28. package/dist/runtime/renderer/index.d.ts +3 -0
  29. package/dist/runtime/renderer/index.js +1 -0
  30. package/dist/runtime/renderer/node-ops.d.ts +5 -0
  31. package/dist/runtime/renderer/node-ops.js +281 -0
  32. package/dist/runtime/renderer/patch-prop.d.ts +3 -0
  33. package/dist/runtime/renderer/patch-prop.js +223 -0
  34. package/dist/runtime/renderer/render-component.d.ts +14 -0
  35. package/dist/runtime/renderer/render-component.js +201 -0
  36. package/dist/runtime/renderer/types.d.ts +33 -0
  37. package/dist/runtime/renderer/types.js +56 -0
  38. package/dist/runtime/renderer/validate-tree.d.ts +3 -0
  39. package/dist/runtime/renderer/validate-tree.js +428 -0
  40. package/dist/runtime/server/assets/errors.d.ts +12 -0
  41. package/dist/runtime/server/assets/errors.js +15 -0
  42. package/dist/runtime/server/assets/remote.d.ts +48 -0
  43. package/dist/runtime/server/assets/remote.js +234 -0
  44. package/dist/runtime/server/assets/resolve-asset.d.ts +53 -0
  45. package/dist/runtime/server/assets/resolve-asset.js +482 -0
  46. package/dist/runtime/server/engine/CONTRACTS.md +386 -0
  47. package/dist/runtime/server/engine/fonts.d.ts +8 -0
  48. package/dist/runtime/server/engine/fonts.js +19 -0
  49. package/dist/runtime/server/engine/layout-passes.d.ts +42 -0
  50. package/dist/runtime/server/engine/layout-passes.js +58 -0
  51. package/dist/runtime/server/engine/react-pdf-pdfkit.d.ts +7 -0
  52. package/dist/runtime/server/engine/render-document.d.ts +31 -0
  53. package/dist/runtime/server/engine/render-document.js +236 -0
  54. package/dist/runtime/server/index.d.ts +5 -0
  55. package/dist/runtime/server/index.js +9 -0
  56. package/dist/runtime/server/preview.d.ts +17 -0
  57. package/dist/runtime/server/preview.js +332 -0
  58. package/dist/runtime/server/registry.d.ts +41 -0
  59. package/dist/runtime/server/registry.js +270 -0
  60. package/dist/runtime/server/render-limits.d.ts +51 -0
  61. package/dist/runtime/server/render-limits.js +143 -0
  62. package/dist/runtime/server/result.d.ts +6 -0
  63. package/dist/runtime/server/result.js +81 -0
  64. package/dist/runtime/server/tsconfig.json +3 -0
  65. package/dist/runtime/shared/errors.d.ts +22 -0
  66. package/dist/runtime/shared/errors.js +22 -0
  67. package/dist/runtime/shared/index.d.ts +4 -0
  68. package/dist/runtime/shared/index.js +7 -0
  69. package/dist/runtime/shared/template.d.ts +63 -0
  70. package/dist/runtime/shared/template.js +1 -0
  71. package/dist/shared/nuxt-pdf.D2ZziYn4.mjs +1132 -0
  72. package/dist/test.d.mts +198 -0
  73. package/dist/test.mjs +696 -0
  74. package/dist/types.d.mts +13 -0
  75. package/package.json +135 -0
package/CONFORMANCE.md ADDED
@@ -0,0 +1,496 @@
1
+ # Nuxt PDF 0.3.0 conformance
2
+
3
+ Nuxt PDF claims behavioral compatibility for a deliberately small, tested
4
+ corpus. It does not claim full React PDF API or test-suite compatibility.
5
+
6
+ ## Version boundary
7
+
8
+ | Layer | 0.3.0 boundary |
9
+ |---|---|
10
+ | Node.js | `^22.14.0`, `^24.0.0`, or `^26.0.0` |
11
+ | Nuxt | `^4.4.8` |
12
+ | Vue | `^3.5.0` |
13
+ | React PDF reference | commit `d41a8207fb06a56e60fcb53ac0e18ce27e7d32d6` |
14
+ | `@react-pdf/font` | `4.0.8` |
15
+ | `@react-pdf/layout` | `4.6.1` |
16
+ | `@react-pdf/pdfkit` | `5.1.1` |
17
+ | `@react-pdf/primitives` | `4.3.0` |
18
+ | `@react-pdf/render` | `4.5.1` |
19
+
20
+ The engine packages are exact pins. Compatibility must be re-evaluated when
21
+ any of them changes.
22
+
23
+ ## Verified corpus
24
+
25
+ ### Compatibility kernel
26
+
27
+ The paired React/Vue fixture verifies:
28
+
29
+ - the Vue renderer produces the node contract consumed directly by layout;
30
+ - ordinary Vue components, props, slots, conditionals, and keyed lists;
31
+ - A4 layout, normal text flow, explicit page breaking, and two output pages;
32
+ - fixed content repeated across pages;
33
+ - synchronous dynamic text returning scalar page-number and page-total text;
34
+ - a local Roboto TTF and local PNG;
35
+ - an external link annotation; and
36
+ - equivalent extracted text, link annotations, page count, and a thresholded
37
+ page-by-page raster comparison for this fixture.
38
+
39
+ Renderer tests separately cover insertion, removal, keyed movement, prop and
40
+ text updates, primitive resolution, rejection of invalid roots, and fail-closed
41
+ rejection of non-whitespace orphan text without echoing its content.
42
+
43
+ Dynamic text (page-number footers) renders at correct page-bottom geometry even
44
+ when a `lineHeight` reaches it from `PdfPage` or a nested `PdfView`
45
+ chains, given as a percentage, or set on the dynamic node itself. This is a
46
+ deliberate divergence from upstream React PDF, which drops such footers, so it
47
+ is verified by Vue-only engine tests (`test/engine.test.ts`) against the
48
+ equivalent static-text geometry, not by the paired React fixture.
49
+
50
+ ### SVG drawing primitives
51
+
52
+ A second paired React/Vue fixture (`test/svg-conformance.test.ts`) draws through
53
+ the same layout and render engine and verifies:
54
+
55
+ - `PdfSvg`, `PdfG`, `PdfPath`, `PdfRect`, `PdfCircle`, `PdfEllipse`, `PdfLine`,
56
+ `PdfPolyline`, `PdfPolygon`, `PdfDefs`, `PdfClipPath`, `PdfLinearGradient`,
57
+ `PdfRadialGradient`, `PdfStop`, and `PdfTspan`;
58
+ - `PdfSvg` as a flex leaf in normal page flow, measured from its `viewBox`
59
+ aspect ratio, alongside paragraph text;
60
+ - basic shapes with numeric coercion and the `rx`/`ry` mutual default on rects;
61
+ - a `PdfG` `transform` (translate + rotate) with presentation inheritance
62
+ (`fill` set on the group cascading to child shapes);
63
+ - a `PdfDefs` `PdfLinearGradient` referenced by `fill="url(#id)"`, proving def
64
+ indexing and `url()` substitution;
65
+ - a `PdfDefs` `PdfClipPath` referenced by `clipPath="url(#id)"`;
66
+ - SVG text: a `PdfSvg` `PdfText` with `x`/`y` and two `PdfTspan` children,
67
+ proving tspan joining and x-chaining, with its content preserved as extracted
68
+ page text; direct SVG `fill` is raster-proven independently rather than
69
+ relying on page-flow `style.color`; and
70
+ - equivalent extracted page text plus a thresholded page raster comparison
71
+ against React output and a reviewed committed baseline.
72
+
73
+ Renderer tests separately cover the SVG nesting rules: which primitives each
74
+ container accepts, `PdfSvg` as a valid child of `PdfPage` and `PdfView`, the
75
+ rejection of `PdfSvg` directly inside `PdfText`, leaf shapes staying childless,
76
+ closed per-primitive prop allowlists, required props (with numeric zero treated
77
+ as present), numeric/range/viewBox/transform validation, page-flow versus SVG
78
+ text props, `PdfTspan`'s `x`/`y`/`fill`-only boundary, and coercion of kebab-case
79
+ SVG attributes (`stroke-width`) to the camelCase prop names (`strokeWidth`) the
80
+ engine reads.
81
+
82
+ SVG presentation uses direct camelCase props; `PdfG` and shapes do not expose a
83
+ generic style prop. `PdfSvg.style` is page-flow sizing/positioning, SVG
84
+ `PdfText.style` is for text metrics, and direct `fill` controls SVG text paint.
85
+ `transform` is a direct prop on groups/shapes (unlike `PdfView`, where it is a
86
+ style key) and is limited to one to three unitless translate/rotate operations.
87
+ A `url(#id)` resolves only against the single `PdfDefs` in the same `PdfSvg`;
88
+ definition ids are safe, unique within that SVG, reusable in other SVGs, and
89
+ separate from destination ids. Missing, malformed, or incompatible fill/clip
90
+ references fail with `PDF_TREE_INVALID`.
91
+
92
+ Vue-only raster regressions additionally prove the intentional zero-value
93
+ repair at the serialization boundary: `fillOpacity: 0` is transparent,
94
+ `strokeWidth: 0` paints no PDF hairline, linear `x2: 0` stays zero, and radial
95
+ `cx`/`cy`/`fx`/`fy`/`r` zeroes stay zero after layout resolves definitions.
96
+ `strokeOpacity` paint is independently raster-proven. Not exposed within SVG:
97
+ `Marker` (`markerStart`/`markerMid`/`markerEnd`), alternate gradient coordinate
98
+ systems/transforms/inheritance, and `preserveAspectRatio` modes. SVG image files
99
+ are not supported as an image source.
100
+
101
+ ### Behavioural conformance corpus
102
+
103
+ A themed corpus under `test/corpus/` renders each fixture through both React PDF
104
+ (`renderToBuffer`) and the Vue renderer from one shared, renderer-agnostic data
105
+ module, so any divergence is the renderer boundary. Claims are proven by
106
+ extracted text, per-page marker positions, laid-out box geometry, annotation and
107
+ catalog reads and, for visual claims, React/Vue raster parity plus
108
+ reviewed baselines.
109
+
110
+ **Pagination** (`test/corpus/pagination.test.ts`):
111
+
112
+ - `wrap={false}` does not paginate: the single page grows taller than A4 to hold
113
+ all overflow (nothing clipped or dropped), and Vue's grown MediaBox equals
114
+ React's exactly (reviewed raster baseline);
115
+ - `break` on nested wrapped Views lands each block on its own page;
116
+ - `minPresenceAhead` pushes a bottom-of-page heading onto the next page to rejoin
117
+ its block, versus the control that strands it;
118
+ - `orphans`/`widows` split a wrapped paragraph at the same line boundary as React;
119
+ - a `fixed` header and footer repeat on every page of a multi-page flow while the
120
+ body advances; and
121
+ - dynamic page-number footers (`{ pageNumber, totalPages }`) are correct on every
122
+ page of a wrapped flow. A page-count-derived computed oracle proves this claim
123
+ because React PDF diverges on dynamic-text line spacing (see the kernel note).
124
+
125
+ **Text** (`test/corpus/text.test.ts`):
126
+
127
+ - a custom `hyphenationCallback` introduces break opportunities so a long token
128
+ wraps with trailing hyphen glyphs, versus a disabled callback that leaves it
129
+ unbroken and overflowing;
130
+ - `letterSpacing` widens glyph advances and changes wrapping;
131
+ - `textAlign` left/center/right shifts line origins and `justify` fills interior
132
+ lines while the last line keeps its natural advance (raster parity);
133
+ - nested style inheritance and inline `fontFamily` switching resolve the correct
134
+ embedded font per run and inherit color across wraps (raster parity);
135
+ - German umlauts, eszett, and Latin-extended diacritics round-trip through text
136
+ extraction; and
137
+ - `maxLines` with `textOverflow: 'ellipsis'` clamps to the line count and appends
138
+ the ellipsis (U+2026).
139
+
140
+ **International typography** (`test/international-text.test.ts`):
141
+
142
+ - Latin Extended, Greek, and Cyrillic render and extract exactly with an
143
+ explicitly registered covering font;
144
+ - representative Chinese/Japanese text renders, extracts, and matches a
145
+ reviewed raster with a test-only Noto subset (experimental broader CJK claim);
146
+ - representative Arabic shapes visually, reports an RTL text run, and exposes
147
+ the expected bidi extraction reorder (experimental);
148
+ - combining marks render correctly but extraction can detach mark association
149
+ (experimental);
150
+ - a variable Source Code TTF renders and extracts at its default instance
151
+ (experimental; no axis-selection API); and
152
+ - face emoji fail faithful serialization and are explicitly unsupported.
153
+
154
+ `PdfRenderDiagnostics.registeredFontFaces` reports only configured family,
155
+ weight, and style. Missing-glyph detection and family fallback chains are not
156
+ claimed.
157
+
158
+ **Images** (`test/corpus/images.test.ts`):
159
+
160
+ - JPEG file paths, base64 `data:` URLs, and `{ data, format }` buffer sources all
161
+ decode and render;
162
+ - explicit width+height, single-dimension aspect scaling, and percent width
163
+ against the page content box size the laid-out box (reviewed baseline);
164
+ - `objectFit` `contain` letterboxes and `cover` crops while the box stays fixed
165
+ (reviewed baseline); and
166
+ - an image in a `fixed` header repeats once per page.
167
+
168
+ **Styles and layout** (`test/corpus/styles.test.ts`):
169
+
170
+ `PdfStyle` is a framework-owned contract, not a re-export of the wider upstream
171
+ stylesheet types. Real TypeScript and Vue SFC negative fixtures reject unknown
172
+ keys, unsupported units/values, and invalid style-array entries; runtime props
173
+ remain independently closed because Vue fallthrough attrs can escape static
174
+ checking.
175
+
176
+ - flexbox with `row` or `column` direction, `flexGrow`, `flexBasis` and `flexShrink`,
177
+ `justifyContent: space-between`, `alignItems: center`, and `gap`;
178
+ - percent width/height against page and nested parent boxes;
179
+ - resolved margin/padding/border on every edge and border-box offset;
180
+ - style-array flattening with `false`/`null`/`undefined` entries filtered, equal to
181
+ the merged object form;
182
+ - `fontFamily`/`fontSize`/`color` cascading through nested Views into Text with
183
+ own-value override; and
184
+ - `backgroundColor`, `opacity` alpha-blending, a stroked `border`, and a
185
+ `rotate`/`scale`/`translate` `transform` that paints transformed while the layout
186
+ box stays invariant (raster parity for the painted claims). React's own resolved
187
+ layout tree (`onRender`) is the geometry oracle; every scenario asserts identical
188
+ ordered boxes and an independent numeric oracle on both sides.
189
+
190
+ **Annotations, metadata, and page setup** (`test/corpus/annotations.test.ts`):
191
+
192
+ - external `https` and `mailto` `PdfLink` annotations round-trip their `url` and
193
+ `unsafeUrl` verbatim;
194
+ - `PdfNote` renders as a `Text` sticky-note annotation carrying its contents;
195
+ - document metadata (title, author, subject, keywords, creator, producer,
196
+ creation date, language, PDF version) round-trips through the info dictionary;
197
+ - `pdfVersion` and `pageLayout` flow into the catalog (`TwoColumnLeft`, format
198
+ version `1.5`); and
199
+ - A4, Letter, custom `[w, h]` and `{ width, height }`, landscape, and
200
+ `px`-with-`dpi` page sizes resolve to the same MediaBox as React and as a
201
+ hand-computed oracle.
202
+
203
+ ### Vue and Nuxt authoring
204
+
205
+ The 0.3.0 tests verify:
206
+
207
+ - `PdfDocument`, `PdfPage`, `PdfView`, `PdfText`, `PdfImage`, `PdfLink`, and
208
+ `PdfNote`;
209
+ - one compile-time `definePdf` definition per PDF template;
210
+ - template components with typed props and slots;
211
+ - deterministic `pdfs/**/*.vue` discovery, reserved-directory exclusion,
212
+ nested keys, collisions, and project-over-layer precedence;
213
+ - generated typed `#pdf` access, including negative type fixtures for missing
214
+ props, extra props, invalid props, and invalid template keys;
215
+ - framework-owned `PdfStyle` and exact primitive prop types checked through a
216
+ real Vue SFC, plus closed per-primitive runtime allowlists that reject
217
+ unknown, DOM/event, removed, and wrong-host props without echoing values;
218
+ - exactly one `src`/`source` for `PdfImage`, exactly one `href`/`src` for
219
+ `PdfLink`, and context-specific page-flow/SVG `PdfText` invariants;
220
+ - one completed render held behind immutable byte, buffer, and `Response`
221
+ conversions, with exact frozen resolved metadata and one frozen, content-free
222
+ diagnostics object shared by the public result and development preview; the
223
+ preview calls the exact public `render(props)` path once and parks that
224
+ completed result rather than re-evaluating metadata or using a second preview
225
+ renderer;
226
+ - a closed public `PdfTemplate` handle containing exactly `key`,
227
+ `resolveMetadata(props)`, and `render(props)`. It never contains the compile-time
228
+ definition, sample data, scenarios, source path, or a preview render method;
229
+ - `definePdf` title/language precedence over `PdfDocument` fallback props,
230
+ including reapplication after every page-number feed in a multi-pass render;
231
+ completed `result.metadata` reports the title/language actually written into
232
+ the PDF after that precedence, plus the resolved download filename (the
233
+ filename is response metadata, not a PDF Info field);
234
+ - template attribution on failure: every error surfaced from a template's
235
+ `render()` is a `NuxtPdfError` carrying `templateKey`; development renders
236
+ also carry `templateFile` and a message prefixed with the source file
237
+ (under `pdfs/`), while production omits that preview-only path. Invalid
238
+ nesting fails with `PDF_TREE_INVALID`; it never returns a partial document or
239
+ downgrades the failure to a warning. Font-resolution failures surface as a single
240
+ `PDF_LAYOUT_ERROR` (font resolution is a layout sub-stage) whose message
241
+ preserves React PDF's exact "Font family not registered" text rather than a
242
+ separate font-error code;
243
+ - safe PDF response headers: bounded Unicode-safe filename sanitization, a
244
+ default `document.pdf` attachment name, exact `content-length`, and forced PDF
245
+ content type;
246
+ - development preview index, viewer, raw PDF, and named scenarios, backed by a
247
+ separate internal development sidecar;
248
+ - a production Nitro route rendering through the generated registry;
249
+ - structural removal of `sampleData` and `scenarios` during production SFC
250
+ compilation, with unique fixture canaries and preview-only API tokens rejected
251
+ by a recursive scan of the emitted Nitro server artifact; metadata follows
252
+ module-scope macro rules, so imports are supported while setup-local bindings
253
+ fail compilation before they can leak or throw at runtime;
254
+ - absence of development preview behavior in production; and
255
+ - absence of React PDF engine code from the Nuxt client bundle and React
256
+ renderer runtimes from production dependencies.
257
+
258
+ ### Table of contents, internal links, and bookmarks
259
+
260
+ Nuxt PDF resolves table-of-contents page numbers with a multi-pass layout loop
261
+ and exposes it through one composable and existing props. The tested boundary:
262
+
263
+ - **`usePdfPageNumbers()`** is an auto-imported composable that returns a readonly,
264
+ reactive `Record<string, number | undefined>` mapping each destination `id` to
265
+ the 1-based page it finally lands on. On the first pass every entry is
266
+ `undefined`, so templates must tolerate a missing number. The composable is
267
+ auto-injected into a PDF SFC that uses it (verified by compiling the real
268
+ `playground/pdfs/report.vue` and by inject/skip unit tests).
269
+ - **Activation gate.** The multi-pass loop runs only when a template calls
270
+ `usePdfPageNumbers()` during mount. It is the only feature that consumes resolved
271
+ page numbers. Internal `#id` links do **not** activate it: a named destination
272
+ resolves by name in a single pass. Every other document, including a document with links,
273
+ renders through the single-pass path at no added cost, verified by a spy
274
+ asserting the multi-pass entry point is not called for a plain document nor for
275
+ a link-only document whose destination still resolves. Calling
276
+ `usePdfPageNumbers()` outside a PDF render throws instead of returning a map
277
+ that could be mistaken for first-pass state.
278
+ - **Convergence.** The loop is a fixed point: it re-lays-out the same mounted
279
+ tree, feeding each pass's destination-page map back through the composable, until the
280
+ map it produces equals the map it was laid out with. After every feed the live
281
+ tree is re-admitted. Tree limits, metadata reapplication, and image/resource
282
+ policy run again against that exact tree. Therefore, conditional content introduced by
283
+ page numbers cannot bypass admission. An ordinary document converges in two
284
+ passes. A document whose layout depends on the numbers it prints (a TOC entry
285
+ whose height changes with its page number) never converges and, after
286
+ `maxPasses` (a validated positive integer on `definePdf`, default 5), raises a
287
+ `PDF_LIMIT_EXCEEDED` `NuxtPdfError` attributed to the template key and file
288
+ through the same boundary as every other render failure.
289
+ - **Named destinations resolve to a section's first page.** A node's `id` becomes
290
+ a named destination; a `PdfLink` `src="#id"` jumps to it. When the id sits on a
291
+ node that spans a page boundary, both the printed number and the jump target
292
+ resolve to the section's **first** page (a deliberate divergence from React PDF,
293
+ whose last-writer-wins destination table points at the last page). This holds on
294
+ both render paths. Page-spanning regression fixtures verify the
295
+ printed number and the pdfjs destination independently, through the multi-pass
296
+ loop and through the single-pass path. The anchoring is copy-on-write, so a
297
+ `fixed` node repeated on every page (which pagination represents as one shared
298
+ node object) keeps its destination, anchored at its first page.
299
+ - **Internal links** are verified paired against React PDF on non-splitting
300
+ targets (where first- and last-page resolution agree): matching `Link`
301
+ annotations and matching named-destination pages. An internal `#id` that does
302
+ not match any destination in the mounted document fails closed with
303
+ `PDF_TREE_INVALID` before layout. A `PdfDocument` without at least one
304
+ `PdfPage` likewise fails closed.
305
+ - **Bookmarks (outline).** The upstream `bookmark` prop (a string or
306
+ `{ title, expanded }`) on `PdfPage`, `PdfView`, `PdfText`, or `PdfImage` builds a
307
+ nested PDF outline. Verified paired against React PDF via pdfjs `getOutline`
308
+ (React PDF is the oracle for the bookmark-to-outline mechanics), and combined with
309
+ the multi-pass loop: two independent renders produce an identical outline, and
310
+ the loop resets each pass's authored `bookmark` so the in-place resolution
311
+ `resolveBookmarks` performs cannot accumulate a stale hierarchy. A fixture
312
+ whose bookmark ancestry shifts across passes fails without the reset, and the
313
+ snapshot is merged before every pass, so a bookmark that first appears mid-loop
314
+ (behind a resolved page number) is captured with its authored value too.
315
+ - A **reviewed raster baseline** of a realistic report's TOC page, following the
316
+ same `UPDATE_PDF_BASELINES` policy and thresholds as the other paired fixtures.
317
+
318
+ ### Local resources
319
+
320
+ The module validates and embeds configured resources during the Nuxt build.
321
+ The tested boundary includes:
322
+
323
+ - PNG and JPEG extension/signature validation and source byte limits;
324
+ - TTF and OTF signature/extension/SFNT table-directory validation,
325
+ registration validation, source byte limits, and source-removal rendering;
326
+ - explicit local `pdfs/assets` and `pdfs/fonts` roots;
327
+ - rejection of absolute paths, traversal, missing assets, ambiguous sources,
328
+ unsupported URLs, and symlink escapes; and
329
+ - font rendering after its source file is removed, and image resolution from
330
+ embedded bytes without a runtime filesystem fallback.
331
+
332
+ This is a fail-closed local-resource boundary, not a general remote-fetching
333
+ or filesystem sandbox claim.
334
+
335
+ ### Opt-in remote images
336
+
337
+ Remote fetching is off by default: with `pdf.remote` absent the module performs
338
+ zero network I/O and every URL image source fails closed. Remote fonts are
339
+ unconditionally rejected. When an operator configures `pdf.remote.allow`, the
340
+ module fetches allowlisted images and converts them
341
+ to bytes before layout. The tested boundary guarantees:
342
+
343
+ - Allowlist entries are exact `https://host/path/` prefixes. `http://`, wildcard
344
+ hosts, embedded credentials, fragments, missing trailing slashes, and
345
+ non-matching hosts, ports, or paths are blocked. Runtime errors expose only
346
+ scheme/host and redact the path, query, and fragment.
347
+ - Redirects are followed manually and the allowlist is re-checked on every hop
348
+ (bounded to three), so an allowlisted host cannot redirect out of the allowlist.
349
+ - `pdf.limits` is the only source for per-image and aggregate byte/pixel caps,
350
+ request count, concurrency, output size, and the whole-render deadline. Source
351
+ byte caps are enforced from `Content-Length` and while streaming; fatal
352
+ failures abort sibling requests.
353
+ - The byte signature is authoritative: a deceptive `Content-Type` cannot make
354
+ non-image bytes validate. PNG/JPEG structure and dimensions are inspected
355
+ before decode or engine admission; SVG stays rejected.
356
+ - Fetches are `GET` only, send no request headers, and carry no credentials; a
357
+ per-hop timeout (default 10s) covers the body read.
358
+ - Remote images resolve at render time with per-render deduplication (a repeated
359
+ URL is fetched once, with no cross-render cache).
360
+
361
+ These are intentional Nuxt-PDF-only guarantees. React PDF has no allowlist,
362
+ timeout, byte cap, or per-hop redirect policy, so the blocked, oversized,
363
+ redirect, timeout, and unconfigured behaviors have no React oracle; once the
364
+ policy admits bytes they take the same embedded-bytes path the local-image
365
+ conformance fixture already rasters against React. Not claimed: authenticated
366
+ fetches, request headers or bodies, cookies/credentials, remote fonts,
367
+ private-IP or DNS rebinding exceptions, and any cross-render caching.
368
+
369
+ ### Render limits
370
+
371
+ Every render is bounded by the operator-overridable `pdf.limits` fields below,
372
+ enforced identically on the single-pass and multi-pass paths through one shared
373
+ pipeline (`test/limits.test.ts`). Defaults apply when a field is omitted; every
374
+ field is a positive safe integer validated at module setup.
375
+
376
+ | Field | Default | When checked |
377
+ | --- | --- | --- |
378
+ | `timeoutMs` | `30_000` | Whole public render deadline, started before metadata evaluation |
379
+ | `maxPages` | `2_000` | After each layout, before serialization |
380
+ | `maxNodes` | `50_000` | Post-mount tree walk (and after every multi-pass feed) |
381
+ | `maxTreeDepth` | `128` | Post-mount tree walk (and after every multi-pass feed) |
382
+ | `maxTextCharacters` | `2_000_000` | Post-mount tree walk (and after every multi-pass feed) |
383
+ | `maxImages` | `256` | Post-mount tree walk (and after every multi-pass feed) |
384
+ | `maxImageBytes` | `10_485_760` | Per unique image before engine admission |
385
+ | `maxTotalImageBytes` | `33_554_432` | Aggregate unique images for the render |
386
+ | `maxImagePixels` | `25_000_000` | Per unique image before engine admission |
387
+ | `maxTotalImagePixels` | `100_000_000` | Aggregate unique images for the render |
388
+ | `maxRemoteRequests` | `32` | Remote fetch accounting, including redirect hops |
389
+ | `maxRemoteConcurrency` | `4` | Simultaneous remote HTTP requests |
390
+ | `maxOutputBytes` | `67_108_864` | Completed PDF byte buffer before result handoff |
391
+
392
+ Exceeding a limit fails closed with `PDF_LIMIT_EXCEEDED`. Tree/node/text/image-count
393
+ checks are **post-mount rejection**: Vue has already mounted the tree, so they do
394
+ not create hard memory isolation before mount. Image byte/pixel and remote
395
+ accounting share one render-wide state across multi-pass feeds. `timeoutMs` is a
396
+ checked deadline polled between engine stages, not mid-step hard cancellation.
397
+
398
+ ### Serverless build
399
+
400
+ The module builds under a serverless Nitro preset (`test/serverless-build.test.ts`
401
+ builds the basic fixture with `NITRO_PRESET=vercel`): the build succeeds, the
402
+ React PDF engine lands in the server function bundle, the `.vercel/output`
403
+ serverless structure is emitted, and no React renderer runtime leaks into the
404
+ bundle. This claims exactly that it **builds** under the vercel preset;
405
+ execution is verified on node-server (`test/production.test.ts`). The Vercel
406
+ runtime is not executed locally.
407
+
408
+ ### Testing utilities
409
+
410
+ The verification helpers this suite runs on ship as `@lupinum/nuxt-pdf/test`.
411
+ They live in `src/test/`; repository tests import that canonical implementation
412
+ directly, and the package bundles it as the public test entry. There is one
413
+ parser, not two. Claimed:
414
+
415
+ - `parsePdf` accepts PDF bytes or a `PdfRenderResult` and returns page text,
416
+ page count, flattened link annotations (named destination or external URL),
417
+ and the outline via pdfjs;
418
+ - `expectPdf` runner-agnostic assertions (`toHavePageCount`, `toContainText`,
419
+ `toHaveLink`, `toHaveOutline`) that throw a `PdfAssertionError` without a Vitest or
420
+ jest dependency;
421
+ - `renderPdfTemplate`, which renders a Vue PDF component through the real
422
+ registry pipeline (assets, fonts, single- or multi-pass layout) without Nuxt,
423
+ with or without `definePdf` metadata;
424
+ - `renderPdfSfc`, which uses the production SFC compiler and resource admission
425
+ path for a real nested `pdfs/*.vue` graph;
426
+ - `rasterizePdf` and `comparePdfSnapshot`, the reviewed per-page PNG baseline
427
+ flow with an `UPDATE_PDF_BASELINES` update mode; and
428
+ - `pdfjs-dist` and `@napi-rs/canvas` as optional peer dependencies, loaded
429
+ lazily with an actionable install error and absent from the module's
430
+ production dependency graph.
431
+
432
+ Verified end-to-end against a real rendered template, including assertion
433
+ failure messages, in `test/test-utils-public.test.ts`.
434
+
435
+ ## Explicitly not claimed in 0.3.0
436
+
437
+ - Full React PDF component, hook, browser-helper, or test-suite parity.
438
+ - React runtime compatibility or React-shaped dynamic callback results.
439
+ - Asynchronous dynamic text; callbacks return only scalar text or numbers.
440
+ - A `lineHeight` multiplier applied to dynamic text. Dynamic text renders with
441
+ font-default line spacing regardless of an inherited or explicit `lineHeight`;
442
+ the multiplier is intentionally not applied, because the upstream engine
443
+ re-resolves dynamic-node styles during pagination and cannot carry an absolute
444
+ `lineHeight` through. Static `PdfText` honors `lineHeight` normally.
445
+ - Browser-side or edge-runtime rendering. The engine is Node server-only.
446
+ - Nuxt 3, Node 20, or versions outside the table above.
447
+ - Authenticated remote fetches, request headers/bodies, credentialed requests,
448
+ proxies, or private-IP/DNS-rebinding protection. Opt-in allowlisted remote
449
+ images and fonts are claimed above under "Opt-in remote resources".
450
+ - SVG image files (as an image source), SVG `Marker`, alternate gradient
451
+ coordinate systems/transforms/inheritance, and `preserveAspectRatio` modes.
452
+ Radial-gradient inner radius (`fr`) is also absent because the pinned renderer
453
+ hardcodes it to zero. SVG drawing primitives are otherwise claimed above.
454
+ - `wordSpacing` authoring. The pinned `@react-pdf/layout` + textkit pipeline does
455
+ not apply it to wrapping or glyph advances, so Nuxt PDF leaves the no-op
456
+ property out of `PdfStyle`. Use the verified `letterSpacing` property instead.
457
+ - Browser CSS, HTML printing, a PDF stylesheet compiler, or paged-media CSS.
458
+ - A first-class table layout engine, charts, forms, signing, editing, or PDF
459
+ merging. A table of contents is authored from ordinary components; there is no
460
+ TOC component or automatic heading collection.
461
+ - Bookmark destination geometry (`top`/`left`/`zoom`/`fit`) and outline click
462
+ actions are not exposed. Bookmark titles, expanded state, and parent/child
463
+ nesting are verified; the title hierarchy is inspected via pdfjs
464
+ `getOutline`.
465
+ - Multi-pass resolution of anything other than destination page numbers, and
466
+ convergence for documents whose geometry depends on the numbers they print
467
+ (these fail closed with `PDF_LIMIT_EXCEEDED`, they are not made to converge).
468
+ - Tagged PDF, PDF/UA, archival, or other accessibility/compliance profiles.
469
+ - PDF encryption. Password and permission props are not exposed without a
470
+ conformance fixture proving their behavior.
471
+ - Deterministic PDF bytes across operating systems or PDF viewers.
472
+ - Hard render cancellation. The `pdf.limits.timeoutMs` budget is polled between
473
+ engine stages and passes. Upstream layout is not abortable during a step, so a
474
+ single engine stage can overshoot it; it is a checked deadline, not mid-step
475
+ cancellation. The thirteen `pdf.limits` fields themselves are claimed above
476
+ under "Render limits".
477
+ - Pre-mount memory isolation. `maxNodes`, `maxTreeDepth`, `maxTextCharacters`,
478
+ and `maxImages` reject after Vue has mounted the tree; they are not a sandbox
479
+ that prevents allocation during mount.
480
+ - Worker isolation or cross-render concurrency guarantees.
481
+ - Visual equivalence beyond the paired fixture, pinned inputs, and tested
482
+ raster environment.
483
+
484
+ ## Verification
485
+
486
+ Run the complete repository gate with:
487
+
488
+ ```bash
489
+ pnpm check
490
+ ```
491
+
492
+ The most direct compatibility evidence is in `test/conformance.test.ts`.
493
+ Renderer behavior, generated types, Nuxt development and production builds,
494
+ resource policy, and dependency boundaries are protected by the remaining
495
+ tests and verification scripts. A new public compatibility claim belongs in
496
+ this document only after an executable fixture protects it.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present Lupinum OG and contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.