@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.
- package/API_REPORT.md +262 -0
- package/CHANGELOG.md +168 -0
- package/CONFORMANCE.md +496 -0
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/THIRD_PARTY_NOTICES.md +75 -0
- package/dist/module.d.mts +24 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +368 -0
- package/dist/runtime/authoring.d.ts +141 -0
- package/dist/runtime/authoring.js +25 -0
- package/dist/runtime/components/_props.d.ts +196 -0
- package/dist/runtime/components/_props.js +63 -0
- package/dist/runtime/components/document.d.ts +9 -0
- package/dist/runtime/components/document.js +26 -0
- package/dist/runtime/components/index.d.ts +4 -0
- package/dist/runtime/components/index.js +27 -0
- package/dist/runtime/components/svg.d.ts +17 -0
- package/dist/runtime/components/svg.js +50 -0
- package/dist/runtime/composables/index.d.ts +2 -0
- package/dist/runtime/composables/index.js +3 -0
- package/dist/runtime/composables/use-pdf-page-numbers.d.ts +39 -0
- package/dist/runtime/composables/use-pdf-page-numbers.js +17 -0
- package/dist/runtime/define-pdf.d.ts +6 -0
- package/dist/runtime/define-pdf.js +5 -0
- package/dist/runtime/fonts.d.ts +16 -0
- package/dist/runtime/fonts.js +0 -0
- package/dist/runtime/renderer/index.d.ts +3 -0
- package/dist/runtime/renderer/index.js +1 -0
- package/dist/runtime/renderer/node-ops.d.ts +5 -0
- package/dist/runtime/renderer/node-ops.js +281 -0
- package/dist/runtime/renderer/patch-prop.d.ts +3 -0
- package/dist/runtime/renderer/patch-prop.js +223 -0
- package/dist/runtime/renderer/render-component.d.ts +14 -0
- package/dist/runtime/renderer/render-component.js +201 -0
- package/dist/runtime/renderer/types.d.ts +33 -0
- package/dist/runtime/renderer/types.js +56 -0
- package/dist/runtime/renderer/validate-tree.d.ts +3 -0
- package/dist/runtime/renderer/validate-tree.js +428 -0
- package/dist/runtime/server/assets/errors.d.ts +12 -0
- package/dist/runtime/server/assets/errors.js +15 -0
- package/dist/runtime/server/assets/remote.d.ts +48 -0
- package/dist/runtime/server/assets/remote.js +234 -0
- package/dist/runtime/server/assets/resolve-asset.d.ts +53 -0
- package/dist/runtime/server/assets/resolve-asset.js +482 -0
- package/dist/runtime/server/engine/CONTRACTS.md +386 -0
- package/dist/runtime/server/engine/fonts.d.ts +8 -0
- package/dist/runtime/server/engine/fonts.js +19 -0
- package/dist/runtime/server/engine/layout-passes.d.ts +42 -0
- package/dist/runtime/server/engine/layout-passes.js +58 -0
- package/dist/runtime/server/engine/react-pdf-pdfkit.d.ts +7 -0
- package/dist/runtime/server/engine/render-document.d.ts +31 -0
- package/dist/runtime/server/engine/render-document.js +236 -0
- package/dist/runtime/server/index.d.ts +5 -0
- package/dist/runtime/server/index.js +9 -0
- package/dist/runtime/server/preview.d.ts +17 -0
- package/dist/runtime/server/preview.js +332 -0
- package/dist/runtime/server/registry.d.ts +41 -0
- package/dist/runtime/server/registry.js +270 -0
- package/dist/runtime/server/render-limits.d.ts +51 -0
- package/dist/runtime/server/render-limits.js +143 -0
- package/dist/runtime/server/result.d.ts +6 -0
- package/dist/runtime/server/result.js +81 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/shared/errors.d.ts +22 -0
- package/dist/runtime/shared/errors.js +22 -0
- package/dist/runtime/shared/index.d.ts +4 -0
- package/dist/runtime/shared/index.js +7 -0
- package/dist/runtime/shared/template.d.ts +63 -0
- package/dist/runtime/shared/template.js +1 -0
- package/dist/shared/nuxt-pdf.D2ZziYn4.mjs +1132 -0
- package/dist/test.d.mts +198 -0
- package/dist/test.mjs +696 -0
- package/dist/types.d.mts +13 -0
- 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.
|