@lupinum/nuxt-pdf 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/API_REPORT.md +262 -0
  2. package/CHANGELOG.md +168 -0
  3. package/CONFORMANCE.md +496 -0
  4. package/LICENSE +21 -0
  5. package/README.md +203 -0
  6. package/THIRD_PARTY_NOTICES.md +75 -0
  7. package/dist/module.d.mts +24 -0
  8. package/dist/module.json +12 -0
  9. package/dist/module.mjs +368 -0
  10. package/dist/runtime/authoring.d.ts +141 -0
  11. package/dist/runtime/authoring.js +25 -0
  12. package/dist/runtime/components/_props.d.ts +196 -0
  13. package/dist/runtime/components/_props.js +63 -0
  14. package/dist/runtime/components/document.d.ts +9 -0
  15. package/dist/runtime/components/document.js +26 -0
  16. package/dist/runtime/components/index.d.ts +4 -0
  17. package/dist/runtime/components/index.js +27 -0
  18. package/dist/runtime/components/svg.d.ts +17 -0
  19. package/dist/runtime/components/svg.js +50 -0
  20. package/dist/runtime/composables/index.d.ts +2 -0
  21. package/dist/runtime/composables/index.js +3 -0
  22. package/dist/runtime/composables/use-pdf-page-numbers.d.ts +39 -0
  23. package/dist/runtime/composables/use-pdf-page-numbers.js +17 -0
  24. package/dist/runtime/define-pdf.d.ts +6 -0
  25. package/dist/runtime/define-pdf.js +5 -0
  26. package/dist/runtime/fonts.d.ts +16 -0
  27. package/dist/runtime/fonts.js +0 -0
  28. package/dist/runtime/renderer/index.d.ts +3 -0
  29. package/dist/runtime/renderer/index.js +1 -0
  30. package/dist/runtime/renderer/node-ops.d.ts +5 -0
  31. package/dist/runtime/renderer/node-ops.js +281 -0
  32. package/dist/runtime/renderer/patch-prop.d.ts +3 -0
  33. package/dist/runtime/renderer/patch-prop.js +223 -0
  34. package/dist/runtime/renderer/render-component.d.ts +14 -0
  35. package/dist/runtime/renderer/render-component.js +201 -0
  36. package/dist/runtime/renderer/types.d.ts +33 -0
  37. package/dist/runtime/renderer/types.js +56 -0
  38. package/dist/runtime/renderer/validate-tree.d.ts +3 -0
  39. package/dist/runtime/renderer/validate-tree.js +428 -0
  40. package/dist/runtime/server/assets/errors.d.ts +12 -0
  41. package/dist/runtime/server/assets/errors.js +15 -0
  42. package/dist/runtime/server/assets/remote.d.ts +48 -0
  43. package/dist/runtime/server/assets/remote.js +234 -0
  44. package/dist/runtime/server/assets/resolve-asset.d.ts +53 -0
  45. package/dist/runtime/server/assets/resolve-asset.js +482 -0
  46. package/dist/runtime/server/engine/CONTRACTS.md +386 -0
  47. package/dist/runtime/server/engine/fonts.d.ts +8 -0
  48. package/dist/runtime/server/engine/fonts.js +19 -0
  49. package/dist/runtime/server/engine/layout-passes.d.ts +42 -0
  50. package/dist/runtime/server/engine/layout-passes.js +58 -0
  51. package/dist/runtime/server/engine/react-pdf-pdfkit.d.ts +7 -0
  52. package/dist/runtime/server/engine/render-document.d.ts +31 -0
  53. package/dist/runtime/server/engine/render-document.js +236 -0
  54. package/dist/runtime/server/index.d.ts +5 -0
  55. package/dist/runtime/server/index.js +9 -0
  56. package/dist/runtime/server/preview.d.ts +17 -0
  57. package/dist/runtime/server/preview.js +332 -0
  58. package/dist/runtime/server/registry.d.ts +41 -0
  59. package/dist/runtime/server/registry.js +270 -0
  60. package/dist/runtime/server/render-limits.d.ts +51 -0
  61. package/dist/runtime/server/render-limits.js +143 -0
  62. package/dist/runtime/server/result.d.ts +6 -0
  63. package/dist/runtime/server/result.js +81 -0
  64. package/dist/runtime/server/tsconfig.json +3 -0
  65. package/dist/runtime/shared/errors.d.ts +22 -0
  66. package/dist/runtime/shared/errors.js +22 -0
  67. package/dist/runtime/shared/index.d.ts +4 -0
  68. package/dist/runtime/shared/index.js +7 -0
  69. package/dist/runtime/shared/template.d.ts +63 -0
  70. package/dist/runtime/shared/template.js +1 -0
  71. package/dist/shared/nuxt-pdf.D2ZziYn4.mjs +1132 -0
  72. package/dist/test.d.mts +198 -0
  73. package/dist/test.mjs +696 -0
  74. package/dist/types.d.mts +13 -0
  75. package/package.json +135 -0
@@ -0,0 +1,234 @@
1
+ import { Buffer } from "node:buffer";
2
+ import {
3
+ PDF_ASSET_ERROR_CODES,
4
+ PdfAssetError
5
+ } from "./errors.js";
6
+ export const DEFAULT_REMOTE_TIMEOUT_MS = 1e4;
7
+ export const MAX_REMOTE_REDIRECTS = 3;
8
+ export const redactUrl = (url) => {
9
+ try {
10
+ const parsed = new URL(url);
11
+ return `${parsed.protocol}//${parsed.host}/\u2026`;
12
+ } catch {
13
+ return "<redacted-url>";
14
+ }
15
+ };
16
+ const configError = (message) => {
17
+ throw new TypeError(message);
18
+ };
19
+ const blocked = (message, cause) => {
20
+ throw new PdfAssetError(PDF_ASSET_ERROR_CODES.Blocked, message, { cause });
21
+ };
22
+ const limitExceeded = (message) => {
23
+ throw new PdfAssetError(PDF_ASSET_ERROR_CODES.LimitExceeded, message);
24
+ };
25
+ const positiveSafeInteger = (value, fallback, name) => {
26
+ if (value === void 0) return fallback;
27
+ if (!Number.isSafeInteger(value) || value < 1) {
28
+ return configError(`pdf.remote.${name} must be a positive safe integer.`);
29
+ }
30
+ return value;
31
+ };
32
+ const parseAllowEntry = (entry) => {
33
+ if (typeof entry !== "string" || entry.trim() === "") {
34
+ return configError("pdf.remote.allow entries must be non-empty https://host/path/ prefixes.");
35
+ }
36
+ let parsed;
37
+ try {
38
+ parsed = new URL(entry);
39
+ } catch {
40
+ return configError("A pdf.remote.allow entry is not a valid URL prefix.");
41
+ }
42
+ if (parsed.protocol !== "https:") {
43
+ return configError("pdf.remote.allow entries must use the https:// scheme.");
44
+ }
45
+ if (parsed.username !== "" || parsed.password !== "") {
46
+ return configError("pdf.remote.allow entries must not embed credentials.");
47
+ }
48
+ if (parsed.search !== "" || parsed.hash !== "") {
49
+ return configError("pdf.remote.allow entries must not include a query or fragment.");
50
+ }
51
+ if (parsed.hostname === "" || parsed.hostname.includes("*")) {
52
+ return configError("pdf.remote.allow entries must name one exact host without wildcards.");
53
+ }
54
+ if (!parsed.pathname.endsWith("/")) {
55
+ return configError("pdf.remote.allow entries must end with a path slash (/).");
56
+ }
57
+ return Object.freeze({
58
+ host: parsed.hostname.toLowerCase(),
59
+ port: parsed.port,
60
+ pathPrefix: parsed.pathname
61
+ });
62
+ };
63
+ export const normalizeRemoteAssetPolicy = (options) => {
64
+ if (options === void 0) return void 0;
65
+ if (options === null || typeof options !== "object") {
66
+ return configError("pdf.remote must be an object with an allow list.");
67
+ }
68
+ if (!Array.isArray(options.allow) || options.allow.length === 0) {
69
+ return configError("pdf.remote.allow must list at least one https://host/path/ prefix.");
70
+ }
71
+ const unknownKey = Object.keys(options).find((key) => key !== "allow" && key !== "timeoutMs");
72
+ if (unknownKey) {
73
+ return configError(`pdf.remote.${unknownKey} is not supported.`);
74
+ }
75
+ return Object.freeze({
76
+ allow: Object.freeze(options.allow.map(parseAllowEntry)),
77
+ timeoutMs: positiveSafeInteger(
78
+ options.timeoutMs,
79
+ DEFAULT_REMOTE_TIMEOUT_MS,
80
+ "timeoutMs"
81
+ )
82
+ });
83
+ };
84
+ export const matchesAllowlist = (url, policy) => {
85
+ let parsed;
86
+ try {
87
+ parsed = new URL(url);
88
+ } catch {
89
+ return false;
90
+ }
91
+ if (parsed.protocol !== "https:") return false;
92
+ if (parsed.username !== "" || parsed.password !== "") return false;
93
+ if (parsed.hash !== "") return false;
94
+ const hostname = parsed.hostname.toLowerCase();
95
+ return policy.allow.some(
96
+ (rule) => hostname === rule.host && parsed.port === rule.port && parsed.pathname.startsWith(rule.pathPrefix)
97
+ );
98
+ };
99
+ export const createRemoteRequestState = (limits) => ({ limits, active: 0, requests: 0, waiters: [] });
100
+ const acquireRequestSlot = async (state) => {
101
+ state.limits.deadline.check();
102
+ if (state.limits.abortController.signal.aborted) {
103
+ throw state.limits.abortController.signal.reason;
104
+ }
105
+ state.requests += 1;
106
+ if (state.requests > state.limits.maxRemoteRequests) {
107
+ return limitExceeded(
108
+ `PDF remote requests exceed pdf.limits.maxRemoteRequests (${state.limits.maxRemoteRequests}).`
109
+ );
110
+ }
111
+ if (state.active < state.limits.maxRemoteConcurrency) {
112
+ state.active += 1;
113
+ return;
114
+ }
115
+ await new Promise((resolve, reject) => {
116
+ const signal = state.limits.abortController.signal;
117
+ const waiter = {
118
+ reject,
119
+ resolve: () => {
120
+ signal.removeEventListener("abort", onAbort);
121
+ state.active += 1;
122
+ resolve();
123
+ }
124
+ };
125
+ const onAbort = () => {
126
+ const index = state.waiters.indexOf(waiter);
127
+ if (index >= 0) state.waiters.splice(index, 1);
128
+ reject(signal.reason);
129
+ };
130
+ signal.addEventListener("abort", onAbort, { once: true });
131
+ state.waiters.push(waiter);
132
+ });
133
+ };
134
+ const releaseRequestSlot = (state) => {
135
+ state.active -= 1;
136
+ state.waiters.shift()?.resolve();
137
+ };
138
+ const readCapped = async (response, maxBytes, controller) => {
139
+ const body = response.body;
140
+ if (!body) return Buffer.alloc(0);
141
+ const chunks = [];
142
+ let total = 0;
143
+ for await (const chunk of body) {
144
+ total += chunk.byteLength;
145
+ if (total > maxBytes) {
146
+ controller.abort();
147
+ return limitExceeded(
148
+ `A remote PDF image exceeds pdf.limits.maxImageBytes (${maxBytes}).`
149
+ );
150
+ }
151
+ chunks.push(Buffer.from(chunk));
152
+ }
153
+ return Buffer.concat(chunks, total);
154
+ };
155
+ const fetchOnce = async (url, policy, maxBytes, state) => {
156
+ await acquireRequestSlot(state);
157
+ const controller = new AbortController();
158
+ const renderSignal = state.limits.abortController.signal;
159
+ const abortFromRender = () => controller.abort(renderSignal.reason);
160
+ renderSignal.addEventListener("abort", abortFromRender, { once: true });
161
+ let timedOut = false;
162
+ const remaining = state.limits.deadline.remainingMs();
163
+ const timeoutMs = Math.min(policy.timeoutMs, Math.max(1, Math.ceil(remaining)));
164
+ const timer = setTimeout(() => {
165
+ timedOut = true;
166
+ controller.abort();
167
+ }, timeoutMs);
168
+ try {
169
+ const response = await fetch(url, {
170
+ method: "GET",
171
+ redirect: "manual",
172
+ signal: controller.signal,
173
+ credentials: "omit"
174
+ });
175
+ if (response.status >= 300 && response.status < 400) {
176
+ await response.body?.cancel();
177
+ const location = response.headers.get("location");
178
+ if (!location) {
179
+ return blocked(`The remote PDF image from ${redactUrl(url)} returned a redirect without a Location.`);
180
+ }
181
+ return { redirectTo: new URL(location, url).toString() };
182
+ }
183
+ if (response.status !== 200) {
184
+ await response.body?.cancel();
185
+ return blocked(`The remote PDF image from ${redactUrl(url)} returned HTTP ${response.status}.`);
186
+ }
187
+ const declaredLength = Number(response.headers.get("content-length"));
188
+ if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {
189
+ controller.abort();
190
+ return limitExceeded(
191
+ `A remote PDF image exceeds pdf.limits.maxImageBytes (${maxBytes}).`
192
+ );
193
+ }
194
+ return { buffer: await readCapped(response, maxBytes, controller) };
195
+ } catch (error) {
196
+ if (error instanceof PdfAssetError) throw error;
197
+ if (renderSignal.aborted) throw renderSignal.reason;
198
+ if (timedOut) {
199
+ if (state.limits.deadline.remainingMs() <= 0) {
200
+ state.limits.deadline.check();
201
+ }
202
+ return blocked(`The remote PDF image from ${redactUrl(url)} timed out after ${timeoutMs}ms.`);
203
+ }
204
+ return blocked(`The remote PDF image from ${redactUrl(url)} could not be fetched.`, error);
205
+ } finally {
206
+ clearTimeout(timer);
207
+ renderSignal.removeEventListener("abort", abortFromRender);
208
+ releaseRequestSlot(state);
209
+ }
210
+ };
211
+ const fetchResolved = async (initialUrl, policy, maxBytes, state) => {
212
+ let url = initialUrl;
213
+ for (let redirects = 0; redirects <= MAX_REMOTE_REDIRECTS; redirects += 1) {
214
+ if (!matchesAllowlist(url, policy)) {
215
+ return blocked(
216
+ `The remote PDF image from ${redactUrl(url)} is not permitted by pdf.remote.allow.`
217
+ );
218
+ }
219
+ const outcome = await fetchOnce(url, policy, maxBytes, state);
220
+ if ("buffer" in outcome) return outcome.buffer;
221
+ url = outcome.redirectTo;
222
+ }
223
+ return blocked(
224
+ `The remote PDF image from ${redactUrl(initialUrl)} exceeded the ${MAX_REMOTE_REDIRECTS}-redirect limit.`
225
+ );
226
+ };
227
+ export const fetchRemoteResource = (url, options) => {
228
+ const { policy, maxBytes, state, inflight } = options;
229
+ const existing = inflight?.get(url);
230
+ if (existing) return existing;
231
+ const promise = fetchResolved(url, policy, maxBytes, state);
232
+ inflight?.set(url, promise);
233
+ return promise;
234
+ };
@@ -0,0 +1,53 @@
1
+ import { Buffer } from 'node:buffer';
2
+ import type { PdfDocumentNode } from '../../renderer/index.js';
3
+ import { type RemoteAssetPolicy, type RemoteRequestState } from './remote.js';
4
+ import { type RenderLimits } from '../render-limits.js';
5
+ export { PDF_ASSET_ERROR_CODES, PdfAssetError, type PdfAssetErrorCode, } from './errors.js';
6
+ export type PdfImageFormat = 'jpg' | 'png';
7
+ export type PdfImageAsset = Readonly<{
8
+ data: Uint8Array;
9
+ format: PdfImageFormat;
10
+ }>;
11
+ export type LoadedPdfImageAsset = PdfImageAsset & Readonly<{
12
+ key: string;
13
+ }>;
14
+ export type PdfImageAssetMap = Readonly<Record<string, PdfImageAsset>>;
15
+ export interface LoadPdfImageAssetOptions {
16
+ roots: readonly string[];
17
+ maxBytes?: number;
18
+ maxPixels?: number;
19
+ }
20
+ export interface ResolvePdfImageAssetsOptions {
21
+ assets: PdfImageAssetMap;
22
+ limits?: RenderLimits;
23
+ remote?: RemoteAssetPolicy;
24
+ /**
25
+ * Mutable accounting shared by every image-admission pass in one render.
26
+ * Multi-pass documents re-render their Vue tree between layout passes; this
27
+ * state keeps deduplication and byte/request budgets render-wide.
28
+ */
29
+ state?: PdfImageResolutionState;
30
+ }
31
+ type ImageBudgetState = {
32
+ bytes: number;
33
+ pixels: number;
34
+ };
35
+ type ImageResolutionCache = Map<unknown, Promise<Buffer>>;
36
+ export interface PdfImageResolutionState {
37
+ readonly budget: ImageBudgetState;
38
+ readonly inflight: Map<string, Promise<Buffer>>;
39
+ readonly remote: RemoteRequestState;
40
+ readonly resolved: ImageResolutionCache;
41
+ }
42
+ /**
43
+ * Loads and validates one local image while the Nuxt module still has access to
44
+ * source files. Generated server code should store the returned bytes under
45
+ * `key`; rendering must not retain the absolute source path.
46
+ */
47
+ export declare const loadPdfImageAsset: (relativePath: string, options: LoadPdfImageAssetOptions) => Promise<LoadedPdfImageAsset>;
48
+ export declare const createPdfImageResolutionState: (limits: RenderLimits) => PdfImageResolutionState;
49
+ /**
50
+ * Resolves every image on the canonical renderer tree before layout. The pass
51
+ * is atomic: no image prop changes unless all image sources validate.
52
+ */
53
+ export declare const resolvePdfImageAssets: (document: PdfDocumentNode, options: ResolvePdfImageAssetsOptions) => Promise<PdfDocumentNode>;