@lupinum/nuxt-pdf 0.3.0 → 0.4.0-beta.1
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/CHANGELOG.md +53 -1
- package/CONFORMANCE.md +16 -11
- package/README.md +80 -23
- package/dist/module.json +4 -3
- package/dist/module.mjs +57 -20
- package/dist/runtime/components/_props.d.ts +7 -13
- package/dist/runtime/components/stubs.d.ts +10 -0
- package/dist/runtime/components/stubs.js +37 -0
- package/dist/runtime/composables/use-pdf-page-numbers.js +1 -1
- package/dist/runtime/renderer/patch-prop.d.ts +3 -1
- package/dist/runtime/renderer/patch-prop.js +1 -3
- package/dist/runtime/renderer/types.js +1 -1
- package/dist/runtime/renderer/validate-tree.js +5 -8
- package/dist/runtime/server/assets/resolve-asset.d.ts +23 -5
- package/dist/runtime/server/assets/resolve-asset.js +86 -13
- package/dist/runtime/server/engine/fonts.d.ts +1 -0
- package/dist/runtime/server/engine/fonts.js +13 -0
- package/dist/runtime/server/preview.js +2 -2
- package/dist/runtime/server/registry.js +2 -2
- package/dist/shared/{nuxt-pdf.D2ZziYn4.mjs → nuxt-pdf._V584vX4.mjs} +1 -1
- package/dist/test.mjs +7 -11
- package/package.json +24 -13
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,59 @@ All notable changes to `@lupinum/nuxt-pdf` are recorded here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project adheres
|
|
5
5
|
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## 0.4.0-beta.1 - 2026-08-22
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Add strict PDF primitive stubs so invalid props fail during authoring instead
|
|
12
|
+
of reaching the renderer.
|
|
13
|
+
- Add safer local and remote image admission, configured-font validation, and
|
|
14
|
+
clearer development preview diagnostics.
|
|
15
|
+
- Add source-derived documentation checks for primitive props, error codes,
|
|
16
|
+
page sizes, render limits, public exports, routes, and complete examples.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Rebuild the documentation around a first-render tutorial, focused task
|
|
21
|
+
guides, complete API reference, production guidance, and compatibility
|
|
22
|
+
evidence.
|
|
23
|
+
- Expand the playground with reusable invoice components, production document
|
|
24
|
+
recipes, and stronger preview scenarios.
|
|
25
|
+
- Refresh the documentation design with stable Ginko packages and the Nuxt
|
|
26
|
+
visual language.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- Keep release workflows tied to the exact verified artifact and independent
|
|
31
|
+
of unnecessary GitHub tokens.
|
|
32
|
+
- Build documentation when no previous Vercel deployment baseline exists.
|
|
33
|
+
- Prepare Nuxt-generated types before clean package and documentation builds.
|
|
34
|
+
|
|
35
|
+
## 0.3.1 - 2026-08-13
|
|
36
|
+
|
|
37
|
+
### Fixed
|
|
38
|
+
|
|
39
|
+
- Prepare the Nuxt type files before clean package and documentation builds.
|
|
40
|
+
- Normalize compiler and package paths on Windows.
|
|
41
|
+
- Preserve literal title markers in public documentation.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- Move the package, repository, documentation, contribution, and support
|
|
46
|
+
surfaces to the shared Lupinum standard.
|
|
47
|
+
- Update the documentation site to Ginko Content `0.4.0-rc.2` and Ginko Docs
|
|
48
|
+
`0.3.0-rc.5`.
|
|
49
|
+
- Add privacy-friendly Plausible analytics and page feedback to the
|
|
50
|
+
documentation site.
|
|
51
|
+
- Stabilize the table-of-contents raster evidence across supported runners.
|
|
52
|
+
|
|
53
|
+
### Security
|
|
54
|
+
|
|
55
|
+
- Replace source publishing with a protected workflow that certifies one
|
|
56
|
+
immutable tarball, publishes it through npm trusted publishing with
|
|
57
|
+
provenance, and creates the matching GitHub release.
|
|
58
|
+
- Add full dependency audits, CodeQL, package previews, and explicit
|
|
59
|
+
repository policy checks.
|
|
8
60
|
|
|
9
61
|
## 0.3.0 - 2026-08-11
|
|
10
62
|
|
package/CONFORMANCE.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# Nuxt PDF 0.
|
|
1
|
+
# Nuxt PDF 0.4.0-beta.1 conformance
|
|
2
2
|
|
|
3
3
|
Nuxt PDF claims behavioral compatibility for a deliberately small, tested
|
|
4
4
|
corpus. It does not claim full React PDF API or test-suite compatibility.
|
|
5
5
|
|
|
6
6
|
## Version boundary
|
|
7
7
|
|
|
8
|
-
| Layer | 0.
|
|
8
|
+
| Layer | 0.4.0-beta.1 boundary |
|
|
9
9
|
|---|---|
|
|
10
10
|
| Node.js | `^22.14.0`, `^24.0.0`, or `^26.0.0` |
|
|
11
11
|
| Nuxt | `^4.4.8` |
|
|
@@ -158,7 +158,9 @@ claimed.
|
|
|
158
158
|
**Images** (`test/corpus/images.test.ts`):
|
|
159
159
|
|
|
160
160
|
- JPEG file paths, base64 `data:` URLs, and `{ data, format }` buffer sources all
|
|
161
|
-
decode
|
|
161
|
+
decode in the lower-engine paired fixture. The public resource boundary
|
|
162
|
+
accepts bundled paths, admitted byte sources, and allowlisted HTTPS images;
|
|
163
|
+
it blocks `data:` URL strings on every public render path;
|
|
162
164
|
- explicit width+height, single-dimension aspect scaling, and percent width
|
|
163
165
|
against the page content box size the laid-out box (reviewed baseline);
|
|
164
166
|
- `objectFit` `contain` letterboxes and `cover` crops while the box stays fixed
|
|
@@ -202,7 +204,7 @@ checking.
|
|
|
202
204
|
|
|
203
205
|
### Vue and Nuxt authoring
|
|
204
206
|
|
|
205
|
-
The 0.
|
|
207
|
+
The 0.4.0-beta.1 tests verify:
|
|
206
208
|
|
|
207
209
|
- `PdfDocument`, `PdfPage`, `PdfView`, `PdfText`, `PdfImage`, `PdfLink`, and
|
|
208
210
|
`PdfNote`;
|
|
@@ -215,8 +217,9 @@ The 0.3.0 tests verify:
|
|
|
215
217
|
- framework-owned `PdfStyle` and exact primitive prop types checked through a
|
|
216
218
|
real Vue SFC, plus closed per-primitive runtime allowlists that reject
|
|
217
219
|
unknown, DOM/event, removed, and wrong-host props without echoing values;
|
|
218
|
-
-
|
|
219
|
-
|
|
220
|
+
- a required `src` for `PdfImage`, a required `href` for `PdfLink` (an `href`
|
|
221
|
+
starting with `#` is an internal destination), and context-specific
|
|
222
|
+
page-flow/SVG `PdfText` invariants;
|
|
220
223
|
- one completed render held behind immutable byte, buffer, and `Response`
|
|
221
224
|
conversions, with exact frozen resolved metadata and one frozen, content-free
|
|
222
225
|
diagnostics object shared by the public result and development preview; the
|
|
@@ -287,7 +290,7 @@ and exposes it through one composable and existing props. The tested boundary:
|
|
|
287
290
|
`PDF_LIMIT_EXCEEDED` `NuxtPdfError` attributed to the template key and file
|
|
288
291
|
through the same boundary as every other render failure.
|
|
289
292
|
- **Named destinations resolve to a section's first page.** A node's `id` becomes
|
|
290
|
-
a named destination; a `PdfLink` `
|
|
293
|
+
a named destination; a `PdfLink` `href="#id"` jumps to it. When the id sits on a
|
|
291
294
|
node that spans a page boundary, both the printed number and the jump target
|
|
292
295
|
resolve to the section's **first** page (a deliberate divergence from React PDF,
|
|
293
296
|
whose last-writer-wins destination table points at the last page). This holds on
|
|
@@ -317,8 +320,10 @@ and exposes it through one composable and existing props. The tested boundary:
|
|
|
317
320
|
|
|
318
321
|
### Local resources
|
|
319
322
|
|
|
320
|
-
The module validates
|
|
321
|
-
|
|
323
|
+
The module validates configured resources during the Nuxt build. Production
|
|
324
|
+
builds embed validated image bytes in the server bundle; development builds
|
|
325
|
+
point at the source files and re-read them per render, so an edited image
|
|
326
|
+
shows up without a restart. The tested boundary includes:
|
|
322
327
|
|
|
323
328
|
- PNG and JPEG extension/signature validation and source byte limits;
|
|
324
329
|
- TTF and OTF signature/extension/SFNT table-directory validation,
|
|
@@ -432,7 +437,7 @@ parser, not two. Claimed:
|
|
|
432
437
|
Verified end-to-end against a real rendered template, including assertion
|
|
433
438
|
failure messages, in `test/test-utils-public.test.ts`.
|
|
434
439
|
|
|
435
|
-
## Explicitly not claimed in 0.
|
|
440
|
+
## Explicitly not claimed in 0.4.0-beta.1
|
|
436
441
|
|
|
437
442
|
- Full React PDF component, hook, browser-helper, or test-suite parity.
|
|
438
443
|
- React runtime compatibility or React-shaped dynamic callback results.
|
|
@@ -446,7 +451,7 @@ failure messages, in `test/test-utils-public.test.ts`.
|
|
|
446
451
|
- Nuxt 3, Node 20, or versions outside the table above.
|
|
447
452
|
- Authenticated remote fetches, request headers/bodies, credentialed requests,
|
|
448
453
|
proxies, or private-IP/DNS-rebinding protection. Opt-in allowlisted remote
|
|
449
|
-
images
|
|
454
|
+
images are claimed above. Remote fonts are not supported.
|
|
450
455
|
- SVG image files (as an image source), SVG `Marker`, alternate gradient
|
|
451
456
|
coordinate systems/transforms/inheritance, and `preserveAspectRatio` modes.
|
|
452
457
|
Radial-gradient inner radius (`fr`) is also absent because the pinned renderer
|
package/README.md
CHANGED
|
@@ -1,28 +1,47 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/public/icon-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/public/icon-light.svg">
|
|
5
|
+
<img src="docs/public/icon-light.svg" width="128" alt="Nuxt PDF icon">
|
|
6
|
+
</picture>
|
|
3
7
|
</p>
|
|
4
8
|
|
|
5
|
-
<h1 align="center">
|
|
9
|
+
<h1 align="center">
|
|
10
|
+
<picture>
|
|
11
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/public/wordmark-dark.svg">
|
|
12
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/public/wordmark-light.svg">
|
|
13
|
+
<img src="docs/public/wordmark-light.svg" width="256" alt="Nuxt PDF">
|
|
14
|
+
</picture>
|
|
15
|
+
</h1>
|
|
6
16
|
|
|
7
17
|
<p align="center">
|
|
8
18
|
Author and render PDFs with Vue components in Nuxt.
|
|
9
19
|
</p>
|
|
10
20
|
|
|
11
21
|
<p align="center">
|
|
12
|
-
<a href="https://www.npmjs.com/package/@lupinum/nuxt-pdf"><img src="https://img.shields.io/npm/v/@lupinum/nuxt-pdf?color=
|
|
22
|
+
<a href="https://www.npmjs.com/package/@lupinum/nuxt-pdf"><img src="https://img.shields.io/npm/v/@lupinum/nuxt-pdf?color=00DC82" alt="npm version"></a>
|
|
13
23
|
<a href="https://github.com/lupinum-dev/nuxt-pdf/actions/workflows/ci.yml"><img src="https://github.com/lupinum-dev/nuxt-pdf/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
|
|
14
|
-
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-
|
|
24
|
+
<a href="./LICENSE"><img src="https://img.shields.io/badge/license-MIT-00DC82" alt="MIT license"></a>
|
|
25
|
+
<a href="https://discord.gg/RPH6SeA36N"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord" alt="Discord"></a>
|
|
26
|
+
<a href="https://deepwiki.com/lupinum-dev/nuxt-pdf"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
|
15
27
|
</p>
|
|
16
28
|
|
|
17
29
|
> [!IMPORTANT]
|
|
18
|
-
> Nuxt PDF is
|
|
30
|
+
> Nuxt PDF is in public beta. Its tested behavior is stable within each
|
|
19
31
|
> release, but a minor release can contain documented breaking changes before
|
|
20
32
|
> version 1.0.
|
|
21
33
|
|
|
34
|
+
## Why use Nuxt PDF?
|
|
35
|
+
|
|
22
36
|
Nuxt PDF discovers Vue Single File Components in `pdfs/`. It renders these
|
|
23
37
|
components on the Node server through a PDF layout engine. The client bundle
|
|
24
38
|
does not contain the renderer or document templates.
|
|
25
39
|
|
|
40
|
+
This approach gives you typed document props, reusable Vue components, stable
|
|
41
|
+
server output, and PDF-specific test helpers in one Nuxt module.
|
|
42
|
+
|
|
43
|
+
## When to use it
|
|
44
|
+
|
|
26
45
|
Use Nuxt PDF for structured documents such as invoices, reports,
|
|
27
46
|
certificates, tickets, and labels.
|
|
28
47
|
|
|
@@ -38,7 +57,7 @@ edit and sign an existing PDF.
|
|
|
38
57
|
Nuxt 3, Node 20, browser rendering, and edge rendering are outside the current
|
|
39
58
|
support boundary.
|
|
40
59
|
|
|
41
|
-
##
|
|
60
|
+
## Installation
|
|
42
61
|
|
|
43
62
|
Install the module in a Nuxt 4 application:
|
|
44
63
|
|
|
@@ -54,11 +73,11 @@ export default defineNuxtConfig({
|
|
|
54
73
|
})
|
|
55
74
|
```
|
|
56
75
|
|
|
57
|
-
##
|
|
76
|
+
## Quick start
|
|
58
77
|
|
|
59
78
|
Create `pdfs/invoice.vue`:
|
|
60
79
|
|
|
61
|
-
```vue
|
|
80
|
+
```vue [pdfs/invoice.vue]
|
|
62
81
|
<script setup lang="ts">
|
|
63
82
|
type InvoiceProps = {
|
|
64
83
|
invoice: {
|
|
@@ -73,6 +92,7 @@ const props = defineProps<InvoiceProps>()
|
|
|
73
92
|
definePdf<InvoiceProps>({
|
|
74
93
|
title: ({ invoice }) => `Invoice ${invoice.number}`,
|
|
75
94
|
filename: ({ invoice }) => `invoice-${invoice.number}.pdf`,
|
|
95
|
+
language: 'en-GB',
|
|
76
96
|
sampleData: {
|
|
77
97
|
invoice: {
|
|
78
98
|
customer: 'Ada Lovelace',
|
|
@@ -116,16 +136,44 @@ export default defineEventHandler(async () => {
|
|
|
116
136
|
})
|
|
117
137
|
```
|
|
118
138
|
|
|
119
|
-
Start Nuxt
|
|
139
|
+
Start Nuxt:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
pnpm dev
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Download the PDF from a second terminal:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
curl -o invoice.pdf http://localhost:3000/api/invoice
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Open these development routes:
|
|
120
152
|
|
|
121
153
|
- `/_pdf` lists the discovered templates.
|
|
122
154
|
- `/_pdf/invoice` shows the document preview.
|
|
123
155
|
- `/api/invoice` returns the production-style PDF response.
|
|
124
156
|
|
|
125
|
-
|
|
126
|
-
|
|
157
|
+
Nuxt PDF regenerates the typed `#pdf` registry whenever templates change. If
|
|
158
|
+
your editor still shows `#pdf` as untyped, run `nuxt prepare` once to refresh
|
|
159
|
+
the generated types.
|
|
160
|
+
|
|
161
|
+
## Discord
|
|
162
|
+
|
|
163
|
+
Join the Lupinum OSS community to discuss Nuxt PDF, ask questions, and share
|
|
164
|
+
what you build.
|
|
165
|
+
|
|
166
|
+
<p align="center">
|
|
167
|
+
<a href="https://discord.gg/RPH6SeA36N">
|
|
168
|
+
<picture>
|
|
169
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/public/discord-dark.svg">
|
|
170
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/public/discord-light.svg">
|
|
171
|
+
<img src="docs/public/discord-light.svg" width="500" alt="Join the Lupinum OSS Discord">
|
|
172
|
+
</picture>
|
|
173
|
+
</a>
|
|
174
|
+
</p>
|
|
127
175
|
|
|
128
|
-
##
|
|
176
|
+
## How it works
|
|
129
177
|
|
|
130
178
|
PDF templates use typed props, interpolation, `v-if`, keyed `v-for`, slots,
|
|
131
179
|
and local Vue components. Nuxt PDF provides document, page, text, image, link,
|
|
@@ -138,11 +186,6 @@ Load request data before the render and pass it through typed props.
|
|
|
138
186
|
Nuxt PDF supports local images and fonts. Remote images are disabled until an
|
|
139
187
|
operator configures an explicit HTTPS allowlist.
|
|
140
188
|
|
|
141
|
-
Read the [documentation](https://nuxt-pdf.lupinum.com) for authoring, assets,
|
|
142
|
-
links, bookmarks, tables of contents, testing, and the complete API reference.
|
|
143
|
-
Read [CONFORMANCE.md](./CONFORMANCE.md) for the exact tested behavior and known
|
|
144
|
-
limitations.
|
|
145
|
-
|
|
146
189
|
## Test a document
|
|
147
190
|
|
|
148
191
|
Install the optional test dependencies:
|
|
@@ -172,16 +215,28 @@ expectPdf(parsed)
|
|
|
172
215
|
The test entry can also inspect links, bookmarks, page geometry, and raster
|
|
173
216
|
baselines.
|
|
174
217
|
|
|
175
|
-
##
|
|
218
|
+
## Documentation
|
|
219
|
+
|
|
220
|
+
Read the [Nuxt PDF documentation](https://nuxt-pdf.lupinum.com) for authoring,
|
|
221
|
+
assets, links, bookmarks, tables of contents, testing, and the complete API
|
|
222
|
+
reference. Read [CONFORMANCE.md](./CONFORMANCE.md) for the exact tested behavior
|
|
223
|
+
and known limitations.
|
|
224
|
+
|
|
225
|
+
Coding agents should start with
|
|
226
|
+
[`/llms.txt`](https://nuxt-pdf.lupinum.com/llms.txt). It links to concise raw
|
|
227
|
+
Markdown for common tasks, reference pages, errors, and compatibility evidence.
|
|
228
|
+
|
|
229
|
+
## Contributing and development
|
|
176
230
|
|
|
177
231
|
```bash
|
|
178
232
|
pnpm install --frozen-lockfile
|
|
179
233
|
pnpm dev
|
|
180
|
-
pnpm
|
|
234
|
+
pnpm verify
|
|
181
235
|
```
|
|
182
236
|
|
|
183
|
-
Use `pnpm docs:dev` to run the documentation site.
|
|
184
|
-
`pnpm release:verify` only for release
|
|
237
|
+
Use `pnpm docs:dev` to run the documentation site. Run `pnpm docs:build` before
|
|
238
|
+
you hand off a documentation change. Use `pnpm release:verify` only for release
|
|
239
|
+
preparation.
|
|
185
240
|
|
|
186
241
|
Read [CONTRIBUTING.md](./CONTRIBUTING.md) before you open a pull request.
|
|
187
242
|
Maintainers use [MAINTAINING.md](./MAINTAINING.md) for release and recovery
|
|
@@ -191,8 +246,10 @@ procedures.
|
|
|
191
246
|
|
|
192
247
|
- Read the [documentation](https://nuxt-pdf.lupinum.com).
|
|
193
248
|
- Join the [Lupinum OSS Discord](https://discord.gg/RPH6SeA36N).
|
|
194
|
-
- Open a [
|
|
195
|
-
reproducible defect.
|
|
249
|
+
- Open a [bug report](https://github.com/lupinum-dev/nuxt-pdf/issues/new?template=bug.md)
|
|
250
|
+
for a reproducible defect.
|
|
251
|
+
- Open a [feature request](https://github.com/lupinum-dev/nuxt-pdf/issues/new?template=proposal.md)
|
|
252
|
+
before you plan a larger change.
|
|
196
253
|
- Follow [SECURITY.md](./SECURITY.md) for a private vulnerability report.
|
|
197
254
|
|
|
198
255
|
## License
|
package/dist/module.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"name": "nuxt-pdf",
|
|
2
|
+
"name": "@lupinum/nuxt-pdf",
|
|
3
|
+
"version": "0.4.0-beta.1",
|
|
3
4
|
"configKey": "pdf",
|
|
5
|
+
"docs": "https://nuxt-pdf.lupinum.com",
|
|
4
6
|
"compatibility": {
|
|
5
|
-
"nuxt": "
|
|
7
|
+
"nuxt": ">=4.4.8"
|
|
6
8
|
},
|
|
7
|
-
"version": "0.3.0",
|
|
8
9
|
"builder": {
|
|
9
10
|
"@nuxt/module-builder": "1.0.2",
|
|
10
11
|
"unbuild": "unknown"
|
package/dist/module.mjs
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { stat } from 'node:fs/promises';
|
|
2
|
-
import { join, isAbsolute, resolve } from 'node:path';
|
|
3
|
-
import { defineNuxtModule, createResolver, getLayerDirectories, createIsIgnored, addServerTemplate, addTemplate, addImports, addTypeTemplate, addServerHandler } from '@nuxt/kit';
|
|
4
|
-
import { d as discoverPdfTemplates, a as discoverPdfComponentFiles, b as discoverPdfImageFiles, c as bundlePdfFonts, e as createPdfSfcPlugin, f as classifyPdfWatchEvent } from './shared/nuxt-pdf.D2ZziYn4.mjs';
|
|
5
2
|
import { Buffer } from 'node:buffer';
|
|
3
|
+
import { join, isAbsolute, resolve, relative } from 'node:path';
|
|
4
|
+
import { defineNuxtModule, createResolver, getLayerDirectories, createIsIgnored, addServerTemplate, addTemplate, addImports, addTypeTemplate, logger, addServerHandler, addComponent } from '@nuxt/kit';
|
|
6
5
|
import { joinURL } from 'ufo';
|
|
6
|
+
import { d as discoverPdfTemplates, a as discoverPdfComponentFiles, b as discoverPdfImageFiles, c as bundlePdfFonts, e as createPdfSfcPlugin, f as classifyPdfWatchEvent } from './shared/nuxt-pdf._V584vX4.mjs';
|
|
7
|
+
import { pdfImageFormatFromKey, loadPdfImageAsset } from '../dist/runtime/server/assets/resolve-asset.js';
|
|
8
|
+
import { PDF_STUB_NAMES } from '../dist/runtime/components/stubs.js';
|
|
7
9
|
import { normalizeRemoteAssetPolicy } from '../dist/runtime/server/assets/remote.js';
|
|
8
|
-
import { loadPdfImageAsset } from '../dist/runtime/server/assets/resolve-asset.js';
|
|
9
10
|
import { normalizePdfLimits, DEFAULT_PDF_RENDER_LIMITS } from '../dist/runtime/server/render-limits.js';
|
|
10
11
|
import 'node:fs';
|
|
11
12
|
import '@vue/compiler-sfc';
|
|
@@ -14,6 +15,8 @@ import 'esbuild';
|
|
|
14
15
|
import 'unimport';
|
|
15
16
|
import '../dist/runtime/shared/template.js';
|
|
16
17
|
|
|
18
|
+
const version = "0.4.0-beta.1";
|
|
19
|
+
|
|
17
20
|
const compareText = (left, right) => {
|
|
18
21
|
if (left < right) return -1;
|
|
19
22
|
if (left > right) return 1;
|
|
@@ -31,14 +34,20 @@ const runtimeOptionsSource = (options) => {
|
|
|
31
34
|
const assets = options.assets ?? [];
|
|
32
35
|
const fonts = options.fonts ?? [];
|
|
33
36
|
const { remote, limits } = options;
|
|
37
|
+
for (const asset of assets) {
|
|
38
|
+
if (asset.root !== void 0 === (asset.dataB64 !== void 0)) {
|
|
39
|
+
throw new TypeError(
|
|
40
|
+
`PDF image asset "${asset.key}" must declare exactly one of root or dataB64.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
34
44
|
if (assets.length === 0 && fonts.length === 0 && !remote && !limits) return [];
|
|
35
45
|
const lines = ["", "const __pdfRuntimeOptions = Object.freeze({"];
|
|
36
46
|
if (assets.length > 0) {
|
|
37
47
|
lines.push(" assets: Object.freeze({");
|
|
38
48
|
for (const asset of assets) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
49
|
+
const entry = asset.root !== void 0 ? `{ format: ${quote$1(asset.format)}, root: ${quote$1(asset.root)} }` : `{ dataB64: ${quote$1(asset.dataB64 ?? "")}, format: ${quote$1(asset.format)} }`;
|
|
50
|
+
lines.push(` ${quote$1(asset.key)}: Object.freeze(${entry}),`);
|
|
42
51
|
}
|
|
43
52
|
lines.push(" }),");
|
|
44
53
|
}
|
|
@@ -86,9 +95,6 @@ const generatePdfRuntimeRegistry = (templates, options) => {
|
|
|
86
95
|
const lines = [
|
|
87
96
|
`import { ${runtimeImports} } from ${quote$1(options.runtimeImport)}`
|
|
88
97
|
];
|
|
89
|
-
if ((options.assets?.length ?? 0) > 0) {
|
|
90
|
-
lines.push("import { Buffer as __pdfBuffer } from 'node:buffer'");
|
|
91
|
-
}
|
|
92
98
|
ordered.forEach((template, index) => {
|
|
93
99
|
lines.push(
|
|
94
100
|
`import __pdfTemplate${index} from ${quote$1(importPath(template.filePath))}`
|
|
@@ -219,10 +225,12 @@ export {}
|
|
|
219
225
|
`;
|
|
220
226
|
const module$1 = defineNuxtModule({
|
|
221
227
|
meta: {
|
|
222
|
-
name: "nuxt-pdf",
|
|
228
|
+
name: "@lupinum/nuxt-pdf",
|
|
229
|
+
version: version,
|
|
223
230
|
configKey: "pdf",
|
|
231
|
+
docs: "https://nuxt-pdf.lupinum.com",
|
|
224
232
|
compatibility: {
|
|
225
|
-
nuxt: "
|
|
233
|
+
nuxt: ">=4.4.8"
|
|
226
234
|
}
|
|
227
235
|
},
|
|
228
236
|
defaults: {
|
|
@@ -248,13 +256,24 @@ const module$1 = defineNuxtModule({
|
|
|
248
256
|
const limits = normalizePdfLimits(options.limits);
|
|
249
257
|
const resolvedLimits = limits ?? DEFAULT_PDF_RENDER_LIMITS;
|
|
250
258
|
const imageFiles = await discoverPdfImageFiles(layers, isIgnored);
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
}
|
|
257
|
-
|
|
259
|
+
const assetEntries = [];
|
|
260
|
+
for (const image of imageFiles) {
|
|
261
|
+
const format = pdfImageFormatFromKey(image.key);
|
|
262
|
+
if (nuxt.options.dev) {
|
|
263
|
+
assetEntries.push({ format, key: image.key, root: image.rootDir });
|
|
264
|
+
} else {
|
|
265
|
+
const loaded = await loadPdfImageAsset(image.key, {
|
|
266
|
+
roots: [image.rootDir],
|
|
267
|
+
maxBytes: resolvedLimits.maxImageBytes,
|
|
268
|
+
maxPixels: resolvedLimits.maxImagePixels
|
|
269
|
+
});
|
|
270
|
+
assetEntries.push({
|
|
271
|
+
dataB64: Buffer.from(loaded.data).toString("base64"),
|
|
272
|
+
format,
|
|
273
|
+
key: image.key
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
}
|
|
258
277
|
const fontRoots = await existingDirectories(
|
|
259
278
|
layers.map((layer) => join(layer.rootDir, "pdfs", "fonts"))
|
|
260
279
|
);
|
|
@@ -272,7 +291,7 @@ const module$1 = defineNuxtModule({
|
|
|
272
291
|
addServerTemplate({
|
|
273
292
|
filename: "#pdf",
|
|
274
293
|
getContents: () => generatePdfRuntimeRegistry(templates, {
|
|
275
|
-
assets,
|
|
294
|
+
assets: assetEntries,
|
|
276
295
|
development: nuxt.options.dev,
|
|
277
296
|
fonts,
|
|
278
297
|
remote,
|
|
@@ -342,6 +361,9 @@ const module$1 = defineNuxtModule({
|
|
|
342
361
|
isIgnored
|
|
343
362
|
);
|
|
344
363
|
if (action === "restart") {
|
|
364
|
+
logger.info(
|
|
365
|
+
`nuxt-pdf: ${relative(nuxt.options.rootDir, absolutePath) || absolutePath} changed; restarting to rebuild the PDF registry\u2026`
|
|
366
|
+
);
|
|
345
367
|
await nuxt.callHook("restart");
|
|
346
368
|
return;
|
|
347
369
|
}
|
|
@@ -361,6 +383,21 @@ const module$1 = defineNuxtModule({
|
|
|
361
383
|
});
|
|
362
384
|
addServerHandler({ route: "/_pdf", handler: previewHandler });
|
|
363
385
|
addServerHandler({ route: "/_pdf/**", handler: previewHandler });
|
|
386
|
+
nuxt.hook("devtools:customTabs", (tabs) => {
|
|
387
|
+
tabs.push({
|
|
388
|
+
name: "nuxt-pdf",
|
|
389
|
+
title: "PDF",
|
|
390
|
+
icon: "i-lucide-file-text",
|
|
391
|
+
view: { type: "iframe", src: joinURL(nuxt.options.app.baseURL || "/", "_pdf") }
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
for (const name of PDF_STUB_NAMES) {
|
|
395
|
+
addComponent({
|
|
396
|
+
name,
|
|
397
|
+
export: name,
|
|
398
|
+
filePath: resolver.resolve("./runtime/components/stubs")
|
|
399
|
+
});
|
|
400
|
+
}
|
|
364
401
|
}
|
|
365
402
|
}
|
|
366
403
|
});
|
|
@@ -71,22 +71,16 @@ export type PdfImageSource = string | Uint8Array | ArrayBuffer | {
|
|
|
71
71
|
uri: string;
|
|
72
72
|
format?: 'png' | 'jpg';
|
|
73
73
|
};
|
|
74
|
-
type
|
|
74
|
+
export type PdfImageProps = PdfBaseProps & PdfBookmarkProp & {
|
|
75
|
+
/** Local bundled path, remote allowlisted URL, or inline bytes. */
|
|
75
76
|
src: PdfImageSource;
|
|
76
|
-
source?: never;
|
|
77
|
-
} | {
|
|
78
|
-
src?: never;
|
|
79
|
-
source: PdfImageSource;
|
|
80
77
|
};
|
|
81
|
-
export type
|
|
82
|
-
|
|
78
|
+
export type PdfLinkProps = PdfBaseProps & {
|
|
79
|
+
/**
|
|
80
|
+
* Link target. A string starting with `#` jumps to the element whose `id`
|
|
81
|
+
* matches the rest of the string; every other value opens as an external URL.
|
|
82
|
+
*/
|
|
83
83
|
href: string;
|
|
84
|
-
src?: never;
|
|
85
|
-
} | {
|
|
86
|
-
href?: never;
|
|
87
|
-
src: string;
|
|
88
|
-
};
|
|
89
|
-
export type PdfLinkProps = PdfBaseProps & PdfLinkTarget & {
|
|
90
84
|
wrap?: boolean;
|
|
91
85
|
hitSlop?: {
|
|
92
86
|
top?: number;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineComponent } from 'vue';
|
|
2
|
+
/**
|
|
3
|
+
* Development-only guards for the global `Pdf*` component names. Types declare
|
|
4
|
+
* these names globally so `pdfs/*.vue` templates typecheck, which means an
|
|
5
|
+
* accidental `<PdfText>` inside an ordinary app component also typechecks —
|
|
6
|
+
* while resolving to nothing at runtime. Registering these stubs turns that
|
|
7
|
+
* silent failure into an immediate, actionable error.
|
|
8
|
+
*/
|
|
9
|
+
export declare const PDF_STUB_NAMES: readonly ["PdfCircle", "PdfClipPath", "PdfDefs", "PdfDocument", "PdfEllipse", "PdfG", "PdfImage", "PdfLine", "PdfLinearGradient", "PdfLink", "PdfNote", "PdfPage", "PdfPath", "PdfPolygon", "PdfPolyline", "PdfRadialGradient", "PdfRect", "PdfStop", "PdfSvg", "PdfText", "PdfTspan", "PdfView"];
|
|
10
|
+
export declare const createPdfStubs: () => Record<string, ReturnType<typeof defineComponent>>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { defineComponent } from "vue";
|
|
2
|
+
import { NuxtPdfError, PDF_ERROR_CODES } from "../shared/errors.js";
|
|
3
|
+
export const PDF_STUB_NAMES = [
|
|
4
|
+
"PdfCircle",
|
|
5
|
+
"PdfClipPath",
|
|
6
|
+
"PdfDefs",
|
|
7
|
+
"PdfDocument",
|
|
8
|
+
"PdfEllipse",
|
|
9
|
+
"PdfG",
|
|
10
|
+
"PdfImage",
|
|
11
|
+
"PdfLine",
|
|
12
|
+
"PdfLinearGradient",
|
|
13
|
+
"PdfLink",
|
|
14
|
+
"PdfNote",
|
|
15
|
+
"PdfPage",
|
|
16
|
+
"PdfPath",
|
|
17
|
+
"PdfPolygon",
|
|
18
|
+
"PdfPolyline",
|
|
19
|
+
"PdfRadialGradient",
|
|
20
|
+
"PdfRect",
|
|
21
|
+
"PdfStop",
|
|
22
|
+
"PdfSvg",
|
|
23
|
+
"PdfText",
|
|
24
|
+
"PdfTspan",
|
|
25
|
+
"PdfView"
|
|
26
|
+
];
|
|
27
|
+
const stubMessage = (name) => `<${name}> only works inside a discovered pdfs/*.vue template rendered by @lupinum/nuxt-pdf. Move this component under pdfs/, or replace it with ordinary Vue/HTML components.`;
|
|
28
|
+
const createPdfStub = (name) => defineComponent({
|
|
29
|
+
name,
|
|
30
|
+
setup() {
|
|
31
|
+
throw new NuxtPdfError(PDF_ERROR_CODES.TemplateInvalid, stubMessage(name));
|
|
32
|
+
},
|
|
33
|
+
render: () => null
|
|
34
|
+
});
|
|
35
|
+
export const createPdfStubs = () => Object.fromEntries(
|
|
36
|
+
PDF_STUB_NAMES.map((name) => [name, createPdfStub(name)])
|
|
37
|
+
);
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
readonly
|
|
4
4
|
} from "vue";
|
|
5
5
|
import { NuxtPdfError, PDF_ERROR_CODES } from "../shared/errors.js";
|
|
6
|
-
export const PDF_PAGE_NUMBERS_KEY = Symbol.for("@lupinum/nuxt-pdf:page-numbers");
|
|
6
|
+
export const PDF_PAGE_NUMBERS_KEY = /* @__PURE__ */ Symbol.for("@lupinum/nuxt-pdf:page-numbers");
|
|
7
7
|
export const usePdfPageNumbers = () => {
|
|
8
8
|
const context = inject(PDF_PAGE_NUMBERS_KEY, null);
|
|
9
9
|
if (!context) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
import type { RendererOptions } from 'vue';
|
|
2
|
-
import { type PdfHostElement, type PdfHostNode } from './types.js';
|
|
2
|
+
import { type PdfElementType, type PdfHostElement, type PdfHostNode } from './types.js';
|
|
3
|
+
/** Closed runtime prop surface; TypeScript alone cannot reject Vue attrs. */
|
|
4
|
+
export declare const PDF_PROP_KEYS: Record<PdfElementType, ReadonlySet<string>>;
|
|
3
5
|
export declare const patchPdfProp: RendererOptions<PdfHostNode, PdfHostElement>['patchProp'];
|
|
@@ -26,7 +26,7 @@ const SVG_PRESENTATION_PROP_NAMES = [
|
|
|
26
26
|
"strokeWidth",
|
|
27
27
|
"transform"
|
|
28
28
|
];
|
|
29
|
-
const PDF_PROP_KEYS = {
|
|
29
|
+
export const PDF_PROP_KEYS = {
|
|
30
30
|
[PDF_PRIMITIVES.Document]: exactPropNames()([
|
|
31
31
|
"author",
|
|
32
32
|
"creationDate",
|
|
@@ -67,14 +67,12 @@ const PDF_PROP_KEYS = {
|
|
|
67
67
|
[PDF_PRIMITIVES.Image]: exactPropNames()([
|
|
68
68
|
...BASE_PROP_NAMES,
|
|
69
69
|
...BOOKMARK_PROP_NAMES,
|
|
70
|
-
"source",
|
|
71
70
|
"src"
|
|
72
71
|
]),
|
|
73
72
|
[PDF_PRIMITIVES.Link]: exactPropNames()([
|
|
74
73
|
...BASE_PROP_NAMES,
|
|
75
74
|
"hitSlop",
|
|
76
75
|
"href",
|
|
77
|
-
"src",
|
|
78
76
|
"wrap"
|
|
79
77
|
]),
|
|
80
78
|
[PDF_PRIMITIVES.Note]: exactPropNames()(BASE_PROP_NAMES),
|
|
@@ -25,7 +25,7 @@ export const PDF_PRIMITIVE_NAMES = {
|
|
|
25
25
|
[PDF_PRIMITIVES.RadialGradient]: "PdfRadialGradient",
|
|
26
26
|
[PDF_PRIMITIVES.Stop]: "PdfStop"
|
|
27
27
|
};
|
|
28
|
-
export const PDF_COMMENT = Symbol("nuxt-pdf-comment");
|
|
28
|
+
export const PDF_COMMENT = /* @__PURE__ */ Symbol("nuxt-pdf-comment");
|
|
29
29
|
const ELEMENT_TYPES = /* @__PURE__ */ new Set([
|
|
30
30
|
PDF_PRIMITIVES.Document,
|
|
31
31
|
PDF_PRIMITIVES.Page,
|
|
@@ -256,18 +256,15 @@ const validatePage = (node) => {
|
|
|
256
256
|
};
|
|
257
257
|
const validateLinkAndImageSources = (node) => {
|
|
258
258
|
if (node.type === PDF_PRIMITIVES.Image) {
|
|
259
|
-
if (hasProp(node, "src")
|
|
260
|
-
treeInvalid('<PdfImage> requires
|
|
259
|
+
if (!hasProp(node, "src")) {
|
|
260
|
+
treeInvalid('<PdfImage> requires a "src" prop.');
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
if (node.type === PDF_PRIMITIVES.Link) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
if (hasHref === hasSrc) {
|
|
267
|
-
treeInvalid('<PdfLink> requires exactly one of "href" or "src".');
|
|
264
|
+
if (!hasProp(node, "href")) {
|
|
265
|
+
treeInvalid('<PdfLink> requires an "href" prop.');
|
|
268
266
|
}
|
|
269
|
-
|
|
270
|
-
if (typeof target !== "string" || target.trim() === "") {
|
|
267
|
+
if (typeof node.props.href !== "string" || node.props.href.trim() === "") {
|
|
271
268
|
treeInvalid("<PdfLink> targets must be non-empty strings.");
|
|
272
269
|
}
|
|
273
270
|
}
|
|
@@ -4,14 +4,23 @@ import { type RemoteAssetPolicy, type RemoteRequestState } from './remote.js';
|
|
|
4
4
|
import { type RenderLimits } from '../render-limits.js';
|
|
5
5
|
export { PDF_ASSET_ERROR_CODES, PdfAssetError, type PdfAssetErrorCode, } from './errors.js';
|
|
6
6
|
export type PdfImageFormat = 'jpg' | 'png';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* One discovered local image. In development the entry points at the source
|
|
9
|
+
* file on disk so edits show up without a restart; in production the validated
|
|
10
|
+
* bytes travel as base64 inside the server bundle so every Nitro preset stays
|
|
11
|
+
* self-contained. Both forms resolve through the same validation boundary.
|
|
12
|
+
*/
|
|
13
|
+
export type PdfImageAssetEntry = Readonly<{
|
|
14
|
+
format: PdfImageFormat;
|
|
15
|
+
root: string;
|
|
16
|
+
} | {
|
|
17
|
+
dataB64: string;
|
|
9
18
|
format: PdfImageFormat;
|
|
10
19
|
}>;
|
|
11
|
-
export type
|
|
20
|
+
export type PdfImageAssetMap = Readonly<Record<string, PdfImageAssetEntry>>;
|
|
21
|
+
export type LoadedPdfImageAsset = Readonly<{
|
|
12
22
|
key: string;
|
|
13
|
-
}
|
|
14
|
-
export type PdfImageAssetMap = Readonly<Record<string, PdfImageAsset>>;
|
|
23
|
+
}> & ResolvedPdfImageAsset;
|
|
15
24
|
export interface LoadPdfImageAssetOptions {
|
|
16
25
|
roots: readonly string[];
|
|
17
26
|
maxBytes?: number;
|
|
@@ -28,6 +37,13 @@ export interface ResolvePdfImageAssetsOptions {
|
|
|
28
37
|
*/
|
|
29
38
|
state?: PdfImageResolutionState;
|
|
30
39
|
}
|
|
40
|
+
type ResolvedPdfImageAsset = Readonly<{
|
|
41
|
+
data: Buffer;
|
|
42
|
+
format: PdfImageFormat;
|
|
43
|
+
height: number;
|
|
44
|
+
pixels: number;
|
|
45
|
+
width: number;
|
|
46
|
+
}>;
|
|
31
47
|
type ImageBudgetState = {
|
|
32
48
|
bytes: number;
|
|
33
49
|
pixels: number;
|
|
@@ -39,6 +55,8 @@ export interface PdfImageResolutionState {
|
|
|
39
55
|
readonly remote: RemoteRequestState;
|
|
40
56
|
readonly resolved: ImageResolutionCache;
|
|
41
57
|
}
|
|
58
|
+
/** Format for a discovered asset key, validated at module setup time. */
|
|
59
|
+
export declare const pdfImageFormatFromKey: (key: string) => PdfImageFormat;
|
|
42
60
|
/**
|
|
43
61
|
* Loads and validates one local image while the Nuxt module still has access to
|
|
44
62
|
* source files. Generated server code should store the returned bytes under
|
|
@@ -72,6 +72,12 @@ const formatFromExtension = (key) => {
|
|
|
72
72
|
if (extension === ".jpg" || extension === ".jpeg") return "jpg";
|
|
73
73
|
return invalid("PDF images must be PNG or JPEG files.");
|
|
74
74
|
};
|
|
75
|
+
export const pdfImageFormatFromKey = (key) => {
|
|
76
|
+
const extension = extname(key).toLowerCase();
|
|
77
|
+
if (extension === ".png") return "png";
|
|
78
|
+
if (extension === ".jpg" || extension === ".jpeg") return "jpg";
|
|
79
|
+
throw new TypeError(`PDF images must be PNG or JPEG files: "${key}".`);
|
|
80
|
+
};
|
|
75
81
|
const claimedFormat = (value) => {
|
|
76
82
|
if (value === void 0) return void 0;
|
|
77
83
|
if (typeof value !== "string") {
|
|
@@ -270,7 +276,30 @@ export const loadPdfImageAsset = async (relativePath, options) => {
|
|
|
270
276
|
"The local PDF image was not found in a configured asset root."
|
|
271
277
|
);
|
|
272
278
|
};
|
|
273
|
-
const
|
|
279
|
+
const RESOLVED_IMAGE_CACHE_BYTES = 32 * 1024 * 1024;
|
|
280
|
+
const resolvedImageCache = /* @__PURE__ */ new Map();
|
|
281
|
+
let resolvedImageCacheBytes = 0;
|
|
282
|
+
const embeddedImageCache = /* @__PURE__ */ new WeakMap();
|
|
283
|
+
const rememberResolvedImage = (cacheKey, image) => {
|
|
284
|
+
if (resolvedImageCache.has(cacheKey)) return;
|
|
285
|
+
resolvedImageCache.set(cacheKey, image);
|
|
286
|
+
resolvedImageCacheBytes += image.data.byteLength;
|
|
287
|
+
while (resolvedImageCacheBytes > RESOLVED_IMAGE_CACHE_BYTES && resolvedImageCache.size > 1) {
|
|
288
|
+
const oldest = resolvedImageCache.keys().next().value;
|
|
289
|
+
if (oldest === void 0) break;
|
|
290
|
+
resolvedImageCacheBytes -= resolvedImageCache.get(oldest).data.byteLength;
|
|
291
|
+
resolvedImageCache.delete(oldest);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
const cachedResolvedImage = (cacheKey) => {
|
|
295
|
+
const cached = resolvedImageCache.get(cacheKey);
|
|
296
|
+
if (!cached) return void 0;
|
|
297
|
+
return {
|
|
298
|
+
...cached,
|
|
299
|
+
data: Buffer.from(cached.data)
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
const resolveLocalImage = async (source, assets, maxBytes, maxPixels, declaredFormat) => {
|
|
274
303
|
const key = canonicalAssetKey(source);
|
|
275
304
|
const pathFormat = formatFromExtension(key);
|
|
276
305
|
const sourceFormat = claimedFormat(declaredFormat);
|
|
@@ -279,20 +308,66 @@ const resolveLocalImage = (source, assets, maxBytes, maxPixels, declaredFormat)
|
|
|
279
308
|
}
|
|
280
309
|
if (!hasOwn(assets, key)) {
|
|
281
310
|
return invalid(
|
|
282
|
-
|
|
311
|
+
`The local PDF image "${key}" was not found under pdfs/assets. Add the file or fix the path.`
|
|
283
312
|
);
|
|
284
313
|
}
|
|
285
|
-
const
|
|
286
|
-
if (!
|
|
314
|
+
const entry = assets[key];
|
|
315
|
+
if (!entry || typeof entry !== "object") {
|
|
287
316
|
return invalid("The generated PDF image asset is invalid.");
|
|
288
317
|
}
|
|
289
|
-
const assetFormat = claimedFormat(
|
|
318
|
+
const assetFormat = claimedFormat(entry.format);
|
|
290
319
|
if (!assetFormat || assetFormat !== pathFormat) {
|
|
291
320
|
return invalid(
|
|
292
321
|
"The generated PDF image format does not match its asset key."
|
|
293
322
|
);
|
|
294
323
|
}
|
|
295
|
-
|
|
324
|
+
if ("dataB64" in entry) {
|
|
325
|
+
const limitsKey = `${maxBytes}\0${maxPixels}`;
|
|
326
|
+
let byLimits = embeddedImageCache.get(entry);
|
|
327
|
+
if (!byLimits) {
|
|
328
|
+
byLimits = /* @__PURE__ */ new Map();
|
|
329
|
+
embeddedImageCache.set(entry, byLimits);
|
|
330
|
+
}
|
|
331
|
+
const cached2 = byLimits.get(limitsKey);
|
|
332
|
+
if (cached2) return { ...cached2, data: Buffer.from(cached2.data) };
|
|
333
|
+
const resolved2 = validateImageBytes(
|
|
334
|
+
Buffer.from(entry.dataB64, "base64"),
|
|
335
|
+
maxBytes,
|
|
336
|
+
assetFormat,
|
|
337
|
+
maxPixels
|
|
338
|
+
);
|
|
339
|
+
byLimits.set(limitsKey, resolved2);
|
|
340
|
+
return { ...resolved2, data: Buffer.from(resolved2.data) };
|
|
341
|
+
}
|
|
342
|
+
let fileStat;
|
|
343
|
+
try {
|
|
344
|
+
fileStat = await stat(resolve(entry.root, ...key.split("/")));
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (isMissingFileError(error)) {
|
|
347
|
+
return invalid(`The local PDF image "${key}" was not found under pdfs/assets.`);
|
|
348
|
+
}
|
|
349
|
+
return invalid("The local PDF image cannot be inspected.", error);
|
|
350
|
+
}
|
|
351
|
+
if (!fileStat.isFile()) {
|
|
352
|
+
return invalid("The local PDF image is not a regular file.");
|
|
353
|
+
}
|
|
354
|
+
const cacheKey = `disk\0${key}\0${fileStat.mtimeMs}\0${fileStat.size}\0${maxBytes}\0${maxPixels}`;
|
|
355
|
+
const cached = cachedResolvedImage(cacheKey);
|
|
356
|
+
if (cached) return cached;
|
|
357
|
+
const loaded = await loadPdfImageAsset(key, {
|
|
358
|
+
roots: [entry.root],
|
|
359
|
+
maxBytes,
|
|
360
|
+
maxPixels
|
|
361
|
+
});
|
|
362
|
+
const resolved = Object.freeze({
|
|
363
|
+
data: loaded.data,
|
|
364
|
+
format: assetFormat,
|
|
365
|
+
height: loaded.height,
|
|
366
|
+
pixels: loaded.pixels,
|
|
367
|
+
width: loaded.width
|
|
368
|
+
});
|
|
369
|
+
rememberResolvedImage(cacheKey, resolved);
|
|
370
|
+
return { ...resolved, data: Buffer.from(resolved.data) };
|
|
296
371
|
};
|
|
297
372
|
const isRemoteCandidate = (source) => /^https?:/i.test(source);
|
|
298
373
|
const imageResolutionCacheKey = (source) => {
|
|
@@ -443,12 +518,10 @@ export const resolvePdfImageAssets = async (document, options) => {
|
|
|
443
518
|
if (hasOwn(node.props, "srcSet") && node.props.srcSet !== void 0) {
|
|
444
519
|
return blocked("PDF image srcSet sources are blocked.");
|
|
445
520
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (hasSrc === hasSource) {
|
|
449
|
-
return invalid("Each PDF image must have exactly one src or source prop.");
|
|
521
|
+
if (!hasOwn(node.props, "src") || node.props.src === void 0) {
|
|
522
|
+
return invalid("Each PDF image must have a src prop.");
|
|
450
523
|
}
|
|
451
|
-
targets.push({ node
|
|
524
|
+
targets.push({ node });
|
|
452
525
|
}
|
|
453
526
|
if (targets.length > limits.maxImages) {
|
|
454
527
|
return imageLimitExceeded(
|
|
@@ -459,7 +532,7 @@ export const resolvePdfImageAssets = async (document, options) => {
|
|
|
459
532
|
try {
|
|
460
533
|
resolved = await Promise.all(targets.map(
|
|
461
534
|
(target) => resolveImageBuffer(
|
|
462
|
-
target.node.props
|
|
535
|
+
target.node.props.src,
|
|
463
536
|
options.assets,
|
|
464
537
|
limits,
|
|
465
538
|
options.remote,
|
|
@@ -475,7 +548,7 @@ export const resolvePdfImageAssets = async (document, options) => {
|
|
|
475
548
|
}
|
|
476
549
|
targets.forEach((target, index) => {
|
|
477
550
|
const data = resolved[index];
|
|
478
|
-
target.node.props
|
|
551
|
+
target.node.props.src = data;
|
|
479
552
|
state.resolved.set(data, Promise.resolve(data));
|
|
480
553
|
});
|
|
481
554
|
return document;
|
|
@@ -5,4 +5,5 @@ export type PdfFontStore = {
|
|
|
5
5
|
readonly [pdfFontStoreBrand]: true;
|
|
6
6
|
};
|
|
7
7
|
export declare const createPdfFontStore: (fonts?: readonly BundledPdfFontDescriptor[]) => PdfFontStore;
|
|
8
|
+
export declare const getSharedPdfFontStore: (fonts?: readonly BundledPdfFontDescriptor[]) => PdfFontStore;
|
|
8
9
|
export {};
|
|
@@ -17,3 +17,16 @@ export const createPdfFontStore = (fonts = []) => {
|
|
|
17
17
|
}
|
|
18
18
|
return fontStore;
|
|
19
19
|
};
|
|
20
|
+
const sameFontList = (left, right) => left.length === right.length && left.every((font, index) => {
|
|
21
|
+
const other = right[index];
|
|
22
|
+
return font.family === other.family && font.src === other.src && font.fontStyle === other.fontStyle && font.fontWeight === other.fontWeight;
|
|
23
|
+
});
|
|
24
|
+
let sharedFontStore;
|
|
25
|
+
export const getSharedPdfFontStore = (fonts = []) => {
|
|
26
|
+
if (sharedFontStore && sameFontList(sharedFontStore.fonts, fonts)) {
|
|
27
|
+
return sharedFontStore.store;
|
|
28
|
+
}
|
|
29
|
+
const store = createPdfFontStore(fonts);
|
|
30
|
+
sharedFontStore = { fonts, store };
|
|
31
|
+
return store;
|
|
32
|
+
};
|
|
@@ -127,7 +127,7 @@ const indexPage = (registry, rootPath) => {
|
|
|
127
127
|
if (templates.length === 0) {
|
|
128
128
|
return htmlResponse(
|
|
129
129
|
"PDF templates",
|
|
130
|
-
"<header><h1>PDF templates</h1></header><p>No templates found. Add <code>pdfs/invoice.vue</code
|
|
130
|
+
"<header><h1>PDF templates</h1></header><p>No templates found. Add <code>pdfs/invoice.vue</code>; Nuxt PDF restarts and registers it automatically.</p>"
|
|
131
131
|
);
|
|
132
132
|
}
|
|
133
133
|
const cards = templates.map((template) => {
|
|
@@ -225,7 +225,7 @@ const viewerPage = async (template, props, rootPath, scenario, hmrClientPath2 =
|
|
|
225
225
|
}
|
|
226
226
|
return htmlResponse(
|
|
227
227
|
title,
|
|
228
|
-
`<header><h1>${escapeHtml(title)}</h1>${actions}</header>${nav}${body}<script type="module">import { createHotContext } from ${JSON.stringify(hmrClientPath2)};createHotContext('/_pdf').on('nuxt-pdf:update',()=>location.reload());<\/script>`
|
|
228
|
+
`<header><h1>${escapeHtml(title)}</h1>${actions}</header>${nav}${body}<script type="module">import { createHotContext } from ${JSON.stringify(hmrClientPath2)};createHotContext('/_pdf').on('nuxt-pdf:update',()=>{const frame=document.querySelector('iframe');if(frame&&frame.contentWindow){frame.contentWindow.location.reload();}else{location.reload();}});<\/script>`
|
|
229
229
|
);
|
|
230
230
|
};
|
|
231
231
|
export const renderPdfPreview = async (registry, request = {}) => {
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
enforceTreeLimits,
|
|
18
18
|
resolvePdfRenderLimits
|
|
19
19
|
} from "./render-limits.js";
|
|
20
|
-
import {
|
|
20
|
+
import { getSharedPdfFontStore } from "./engine/fonts.js";
|
|
21
21
|
import { createPdfRenderResult } from "./result.js";
|
|
22
22
|
const EMPTY_SCENARIOS = Object.freeze({});
|
|
23
23
|
const EMPTY_ASSETS = Object.freeze({});
|
|
@@ -123,7 +123,7 @@ const renderTemplate = async (component, props, metadata, options, maxPasses, li
|
|
|
123
123
|
component,
|
|
124
124
|
props
|
|
125
125
|
);
|
|
126
|
-
const fontStore =
|
|
126
|
+
const fontStore = getSharedPdfFontStore(options.fonts);
|
|
127
127
|
if (mounted.usesPageNumbers) {
|
|
128
128
|
const live = mounted;
|
|
129
129
|
const result2 = await renderDocumentMultiPass(
|
|
@@ -22,7 +22,7 @@ const classifyPdfWatchEvent = (event, absolutePath, layers, isIgnored) => {
|
|
|
22
22
|
if (PDF_STRUCTURE_EVENTS.has(event)) return "restart";
|
|
23
23
|
if (event !== "change") return "ignore";
|
|
24
24
|
const [rootDirectory] = pathWithinPdfs.split(/[\\/]/);
|
|
25
|
-
return rootDirectory === "
|
|
25
|
+
return rootDirectory === "fonts" ? "restart" : "refresh";
|
|
26
26
|
}
|
|
27
27
|
return "ignore";
|
|
28
28
|
};
|
package/dist/test.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { defineComponent, h } from 'vue';
|
|
3
3
|
import { normalizeRemoteAssetPolicy } from '../dist/runtime/server/assets/remote.js';
|
|
4
4
|
import { createPdfTemplate } from '../dist/runtime/server/registry.js';
|
|
5
|
-
import { normalizePdfLimits
|
|
5
|
+
import { normalizePdfLimits } from '../dist/runtime/server/render-limits.js';
|
|
6
6
|
import { PDF_DEFINITION_PROPERTY } from '../dist/runtime/shared/template.js';
|
|
7
7
|
import { Buffer } from 'node:buffer';
|
|
8
8
|
import { existsSync } from 'node:fs';
|
|
@@ -10,8 +10,8 @@ import { mkdir, writeFile, rm, readFile, readdir } from 'node:fs/promises';
|
|
|
10
10
|
import { resolve, dirname, join, relative, basename } from 'node:path';
|
|
11
11
|
import { pathToFileURL, fileURLToPath } from 'node:url';
|
|
12
12
|
import { build } from 'esbuild';
|
|
13
|
-
import { t as templateKeyFromRelativePath, b as discoverPdfImageFiles, c as bundlePdfFonts, g as compilePdfSfc } from './shared/nuxt-pdf.
|
|
14
|
-
import {
|
|
13
|
+
import { t as templateKeyFromRelativePath, b as discoverPdfImageFiles, c as bundlePdfFonts, g as compilePdfSfc } from './shared/nuxt-pdf._V584vX4.mjs';
|
|
14
|
+
import { pdfImageFormatFromKey } from '../dist/runtime/server/assets/resolve-asset.js';
|
|
15
15
|
import '@vue/compiler-sfc';
|
|
16
16
|
import '@jridgewell/remapping';
|
|
17
17
|
import 'unimport';
|
|
@@ -268,7 +268,6 @@ async function withPdfDocument(data, read) {
|
|
|
268
268
|
const loadingTask = pdfJs.getDocument({
|
|
269
269
|
CanvasFactory: NapiCanvasFactory,
|
|
270
270
|
data: copyPdfData(data),
|
|
271
|
-
isEvalSupported: false,
|
|
272
271
|
stopAtErrors: true,
|
|
273
272
|
useWorkerFetch: false,
|
|
274
273
|
verbosity: 0
|
|
@@ -572,18 +571,15 @@ async function renderPdfSfc(filename, props, options = {}) {
|
|
|
572
571
|
throw new Error(`PDF SFC ${JSON.stringify(entry)} must be a template directly inside pdfs/ or one of its feature directories.`);
|
|
573
572
|
}
|
|
574
573
|
const normalizedLimits = normalizePdfLimits(options.limits);
|
|
575
|
-
const limits = resolvePdfRenderLimits(normalizedLimits);
|
|
576
574
|
const [component, imageFiles, fonts] = await Promise.all([
|
|
577
575
|
loadPdfSfc(entry),
|
|
578
576
|
discoverPdfImageFiles([{ name: "test", rootDir }]),
|
|
579
577
|
bundlePdfFonts(options.fonts ?? [], { fontRoots: [join(rootDir, "pdfs", "fonts")] })
|
|
580
578
|
]);
|
|
581
|
-
const
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
})));
|
|
586
|
-
const assets = Object.fromEntries(loadedAssets.map((asset) => [asset.key, asset]));
|
|
579
|
+
const assets = Object.fromEntries(imageFiles.map((image) => [
|
|
580
|
+
image.key,
|
|
581
|
+
Object.freeze({ format: pdfImageFormatFromKey(image.key), root: image.rootDir })
|
|
582
|
+
]));
|
|
587
583
|
return renderPreparedPdfTemplate(component, props, {
|
|
588
584
|
assets,
|
|
589
585
|
file: `pdfs/${relativePath}`,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lupinum/nuxt-pdf",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0-beta.1",
|
|
4
4
|
"description": "Author and render PDFs with Vue components in Nuxt",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nuxt",
|
|
@@ -55,18 +55,23 @@
|
|
|
55
55
|
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
|
|
56
56
|
"build": "nuxt prepare && nuxt-module-build build",
|
|
57
57
|
"api:write": "node scripts/check-api-report.mjs --write",
|
|
58
|
-
"
|
|
58
|
+
"changelog": "changelogen --no-output",
|
|
59
|
+
"docs:build": "pnpm docs:theme && pnpm build && pnpm --dir docs build",
|
|
60
|
+
"docs:build:vercel": "node scripts/build-docs-vercel.mjs",
|
|
59
61
|
"docs:dev": "pnpm --dir docs dev",
|
|
62
|
+
"docs:theme": "node scripts/check-docs-theme.mjs",
|
|
60
63
|
"format": "eslint . --fix",
|
|
61
64
|
"format:check": "eslint .",
|
|
62
65
|
"lint": "eslint .",
|
|
63
66
|
"test": "pnpm test:prepare && vitest run",
|
|
64
67
|
"test:api": "node scripts/check-api-report.mjs",
|
|
68
|
+
"release:notes": "node scripts/write-release-notes.mjs",
|
|
65
69
|
"release:pack": "pnpm build && node scripts/check-release-artifact.mjs release-artifacts",
|
|
66
|
-
"release:
|
|
70
|
+
"release:prepare": "changelogen --bump --clean",
|
|
71
|
+
"release:verify": "pnpm verify && pnpm release:pack",
|
|
67
72
|
"test:artifact": "node scripts/check-release-artifact.mjs",
|
|
68
73
|
"test:dependencies": "node scripts/check-production-dependencies.mjs",
|
|
69
|
-
"test:docs": "node scripts/check-documentation.mjs && pnpm --dir docs check",
|
|
74
|
+
"test:docs": "node scripts/check-documentation.mjs && pnpm docs:theme && pnpm --dir docs check",
|
|
70
75
|
"test:package": "node scripts/check-package.mjs",
|
|
71
76
|
"test:package-metadata": "node scripts/check-api-report.mjs",
|
|
72
77
|
"test:performance": "node --expose-gc ./node_modules/vitest/vitest.mjs run --config vitest.performance.config.ts",
|
|
@@ -75,14 +80,18 @@
|
|
|
75
80
|
"test:quickstart": "node scripts/test-package-quickstart.mjs",
|
|
76
81
|
"test:raster": "vitest run --config vitest.raster.config.ts",
|
|
77
82
|
"test:version-headings": "node scripts/check-version-headings.mjs",
|
|
83
|
+
"test:workflows": "node scripts/check-workflow-policy.mjs",
|
|
84
|
+
"check:vercel": "node scripts/check-vercel-config.mjs",
|
|
78
85
|
"test:serverless": "vitest run --config vitest.serverless.config.ts",
|
|
79
86
|
"test:watch": "vitest watch",
|
|
80
87
|
"typecheck": "nuxt prepare && vue-tsc --noEmit && nuxt prepare test/fixtures/basic && vue-tsc --noEmit -p test/fixtures/basic/tsconfig.json && pnpm test:prepare && pnpm --dir playground exec vue-tsc --noEmit",
|
|
81
|
-
"
|
|
88
|
+
"audit:all": "pnpm audit",
|
|
89
|
+
"verify": "pnpm audit:all && pnpm check",
|
|
90
|
+
"check": "pnpm format:check && pnpm check:vercel && pnpm typecheck && pnpm test:docs && pnpm test:version-headings && pnpm test:workflows && pnpm test:dependencies && pnpm test && pnpm test:production && pnpm test:serverless && pnpm build && pnpm test:package-metadata && pnpm dev:build && pnpm test:artifact"
|
|
82
91
|
},
|
|
83
92
|
"dependencies": {
|
|
84
93
|
"@jridgewell/remapping": "2.3.5",
|
|
85
|
-
"@nuxt/kit": "^4.
|
|
94
|
+
"@nuxt/kit": "^4.5.2",
|
|
86
95
|
"@react-pdf/font": "4.0.8",
|
|
87
96
|
"@react-pdf/layout": "4.6.1",
|
|
88
97
|
"@react-pdf/pdfkit": "5.1.1",
|
|
@@ -97,7 +106,7 @@
|
|
|
97
106
|
"peerDependencies": {
|
|
98
107
|
"@napi-rs/canvas": "^0.1.97",
|
|
99
108
|
"nuxt": "^4.4.8",
|
|
100
|
-
"pdfjs-dist": "^5.6.205",
|
|
109
|
+
"pdfjs-dist": "^5.6.205 || ^6.2.108",
|
|
101
110
|
"vue": "^3.5.0"
|
|
102
111
|
},
|
|
103
112
|
"peerDependenciesMeta": {
|
|
@@ -112,24 +121,26 @@
|
|
|
112
121
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
113
122
|
"@jridgewell/trace-mapping": "0.3.31",
|
|
114
123
|
"@napi-rs/canvas": "0.1.97",
|
|
115
|
-
"@nuxt/devtools": "^3.
|
|
124
|
+
"@nuxt/devtools": "^3.3.1",
|
|
116
125
|
"@nuxt/eslint-config": "^1.15.2",
|
|
117
126
|
"@nuxt/module-builder": "^1.0.2",
|
|
118
|
-
"@nuxt/schema": "^4.
|
|
127
|
+
"@nuxt/schema": "^4.5.2",
|
|
119
128
|
"@nuxt/test-utils": "^4.0.3",
|
|
120
129
|
"@react-pdf/renderer": "4.5.1",
|
|
121
130
|
"@types/node": "26.1.1",
|
|
122
131
|
"@types/react": "19.2.14",
|
|
123
|
-
"changelogen": "
|
|
132
|
+
"changelogen": "0.6.2",
|
|
124
133
|
"eslint": "^10.4.1",
|
|
125
|
-
"nuxt": "^4.
|
|
126
|
-
"pdfjs-dist": "
|
|
134
|
+
"nuxt": "^4.5.2",
|
|
135
|
+
"pdfjs-dist": "6.2.108",
|
|
136
|
+
"pkg-pr-new": "0.0.87",
|
|
127
137
|
"publint": "^0.3.21",
|
|
128
138
|
"react": "19.2.0",
|
|
129
139
|
"source-code-pro": "2.42.0",
|
|
130
140
|
"typescript": "~5.9.3",
|
|
131
141
|
"vitest": "^4.1.8",
|
|
132
142
|
"vue": "3.5.40",
|
|
133
|
-
"vue-tsc": "^3.3.3"
|
|
143
|
+
"vue-tsc": "^3.3.3",
|
|
144
|
+
"yaml": "2.9.0"
|
|
134
145
|
}
|
|
135
146
|
}
|