@jasy/nuxt 1.0.0-alpha.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/README.md ADDED
@@ -0,0 +1,123 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/jasy-pdf/jasy/main/docs/jasy-nuxt.png" width="120" alt="jasy nuxt">
3
+ </p>
4
+
5
+ <h1 align="center">@jasy/nuxt</h1>
6
+
7
+ <p align="center">
8
+ <b>Author PDFs as Vue components in Nuxt - render them in the browser or on a server route, zero config.</b>
9
+ </p>
10
+
11
+ The Nuxt module for [`@jasy/vue`](https://npmx.dev/@jasy/vue) and [`@jasy/pdf`](https://npmx.dev/@jasy/pdf).
12
+ Add it to `modules` and the components and render helpers are just there - no imports, no wiring. Render a
13
+ PDF client-side with `usePdf`, or stream one from a Nitro route with `definePdfHandler`. No headless
14
+ browser, no Java.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npx nuxi module add @jasy/nuxt
20
+ ```
21
+
22
+ That installs the package and adds it to `modules` for you. Or wire it up by hand:
23
+
24
+ ```bash
25
+ pnpm add @jasy/nuxt
26
+ ```
27
+
28
+ ```ts
29
+ // nuxt.config.ts
30
+ export default defineNuxtConfig({
31
+ modules: ["@jasy/nuxt"],
32
+ });
33
+ ```
34
+
35
+ ## Client: `usePdf`
36
+
37
+ Author a PDF as a Vue component - the jasy components are auto-registered, so no imports:
38
+
39
+ ```vue
40
+ <!-- Invoice.vue -->
41
+ <template>
42
+ <Document :size="11">
43
+ <Page :size="'A4'" :margin="48">
44
+ <Text :size="24" bold color="#0a2348">Invoice #{{ id }}</Text>
45
+ </Page>
46
+ </Document>
47
+ </template>
48
+ ```
49
+
50
+ ```vue
51
+ <script setup lang="ts">
52
+ import Invoice from "./Invoice.vue";
53
+
54
+ const { open, download, pending } = usePdf(Invoice, { props: { id: 42 } });
55
+ </script>
56
+
57
+ <template>
58
+ <button :disabled="pending" @click="open">View PDF</button>
59
+ </template>
60
+ ```
61
+
62
+ `open()` and `download()` render on demand and reuse the result, so one click is one render. Pass
63
+ `{ immediate: true }` to pre-render on mount.
64
+
65
+ ## Server: `definePdfHandler`
66
+
67
+ The `@jasy/pdf` tree API is auto-imported in `server/`. No Vue, no browser - build the document and stream
68
+ it:
69
+
70
+ ```ts
71
+ // server/api/invoice/[id].get.ts
72
+ export default definePdfHandler((event) =>
73
+ Document([
74
+ Page({ size: "A4", margin: 48 }, [
75
+ Text(`Invoice #${getRouterParam(event, "id")}`, { size: 24, bold: true }),
76
+ ]),
77
+ ]),
78
+ );
79
+ ```
80
+
81
+ `GET /api/invoice/42` streams `application/pdf`. Need auth or a data fetch first? Use
82
+ `sendPdf(event, doc, opts)` inside your own handler, or `renderToBytes(doc)` to just get the bytes (save
83
+ them, attach to an email, anything).
84
+
85
+ ### Caching
86
+
87
+ Wrap a route in Nitro's cache - keyed by path + query, so it caches per request out of the box:
88
+
89
+ ```ts
90
+ export default definePdfHandler(build, { cache: { maxAge: 3600 } });
91
+ ```
92
+
93
+ Expired entries re-render fresh (`swr` is off by default) - a stale invoice is never served.
94
+
95
+ ## What the module sets up
96
+
97
+ - **Components** for templates: `Document` · `Page` · `Column` · `Row` · `Box` · `Padding` · `Text` ·
98
+ `Image` · `Table` and the rest. Set a `prefix` to dodge name clashes (`prefix: "Pdf"` makes it
99
+ `<PdfDocument>`).
100
+ - **Auto-imports**: `usePdf` and `renderToPdf` on the client; the `@jasy/pdf` tree API, `definePdfHandler`
101
+ and `sendPdf` on the server.
102
+ - **Bundle-safety**: keeps jimp (the server-side image decoder) out of the client bundle - the browser
103
+ decodes images via canvas instead.
104
+
105
+ ## Options
106
+
107
+ ```ts
108
+ export default defineNuxtConfig({
109
+ modules: ["@jasy/nuxt"],
110
+ jasy: {
111
+ autoImport: true, // auto-register components + the tree API (default true)
112
+ prefix: "Pdf", // <PdfDocument> in templates, PdfDocument(...) in server/ (default none)
113
+ },
114
+ });
115
+ ```
116
+
117
+ ## Links
118
+
119
+ - [`@jasy/pdf`](https://npmx.dev/@jasy/pdf) - the pure-TypeScript PDF engine
120
+ - [`@jasy/vue`](https://npmx.dev/@jasy/vue) - author PDFs as Vue components
121
+ - [jasy.dev](https://jasy.dev) - documentation
122
+
123
+ MIT, by Florian Heuberger
@@ -0,0 +1,12 @@
1
+ import * as _nuxt_schema from '@nuxt/schema';
2
+
3
+ interface ModuleOptions {
4
+ /** Auto-register the jasy components (client) + the @jasy/pdf tree API (server) so they need no import. Default true. */
5
+ autoImport?: boolean;
6
+ /** Component name prefix, e.g. "Pdf" -> <PdfDocument>, <PdfText>. Default none. */
7
+ prefix?: string;
8
+ }
9
+ declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
10
+
11
+ export { _default as default };
12
+ export type { ModuleOptions };
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "@jasy/nuxt",
3
+ "configKey": "jasy",
4
+ "version": "1.0.0-alpha.1",
5
+ "builder": {
6
+ "@nuxt/module-builder": "1.0.2",
7
+ "unbuild": "unknown"
8
+ }
9
+ }
@@ -0,0 +1,83 @@
1
+ import { join, dirname } from 'node:path';
2
+ import { defineNuxtModule, createResolver, addComponent, addServerImports, addImports, addImportsDir, addServerImportsDir, resolvePath, extendViteConfig } from '@nuxt/kit';
3
+
4
+ const COMPONENTS = [
5
+ "Document",
6
+ "Page",
7
+ "Column",
8
+ "Row",
9
+ "Box",
10
+ "Padding",
11
+ "Expanded",
12
+ "Spacer",
13
+ "Divider",
14
+ "Image",
15
+ "Text",
16
+ "Paragraph",
17
+ "Span",
18
+ "Table",
19
+ "TableRow",
20
+ "TableCell",
21
+ "Positioned",
22
+ "DefaultTextStyle"
23
+ ];
24
+ const SERVER_FACTORIES = [
25
+ "Document",
26
+ "Page",
27
+ "Column",
28
+ "Row",
29
+ "Box",
30
+ "Padding",
31
+ "Expanded",
32
+ "Spacer",
33
+ "Divider",
34
+ "Image",
35
+ "Text",
36
+ "Paragraph",
37
+ "span",
38
+ "Table",
39
+ "Positioned",
40
+ "DefaultTextStyle"
41
+ ];
42
+ const SERVER_UTILS = ["renderToBytes", "renderPdf", "mm"];
43
+ const module$1 = defineNuxtModule({
44
+ meta: {
45
+ name: "@jasy/nuxt",
46
+ configKey: "jasy"
47
+ },
48
+ defaults: {
49
+ autoImport: true
50
+ },
51
+ async setup(options) {
52
+ const resolver = createResolver(import.meta.url);
53
+ if (options.autoImport) {
54
+ const prefix = options.prefix ?? "";
55
+ for (const name of COMPONENTS) {
56
+ addComponent({ name: `${prefix}${name}`, filePath: "@jasy/vue", export: name });
57
+ }
58
+ addServerImports([
59
+ ...SERVER_FACTORIES.map((name) => ({ name, as: `${prefix}${name}`, from: "@jasy/pdf" })),
60
+ ...SERVER_UTILS.map((name) => ({ name, from: "@jasy/pdf" }))
61
+ ]);
62
+ addImports([
63
+ { name: "renderToPdf", from: "@jasy/vue" },
64
+ { name: "renderToPdfString", from: "@jasy/vue" }
65
+ ]);
66
+ }
67
+ addImportsDir(resolver.resolve("./runtime/composables"));
68
+ addServerImportsDir(resolver.resolve("./runtime/server/utils"));
69
+ const browserImage = join(dirname(await resolvePath("@jasy/pdf")), "platform/browser-image.js");
70
+ extendViteConfig((config) => {
71
+ config.resolve ||= {};
72
+ const existing = config.resolve.alias;
73
+ const alias = Array.isArray(existing) ? existing : Object.entries(existing ?? {}).map(([find, replacement]) => ({ find, replacement }));
74
+ alias.push({ find: /^.*platform[\\/]node-image\.js$/, replacement: browserImage });
75
+ config.resolve.alias = alias;
76
+ config.optimizeDeps ||= {};
77
+ config.optimizeDeps.include = [...config.optimizeDeps.include ?? [], "fflate"];
78
+ config.optimizeDeps.exclude = [...config.optimizeDeps.exclude ?? [], "jimp"];
79
+ });
80
+ }
81
+ });
82
+
83
+ export { module$1 as default };
@@ -0,0 +1,24 @@
1
+ import type { Component } from "vue";
2
+ import type { RenderOptions } from "@jasy/pdf";
3
+ export interface UsePdfOptions {
4
+ /** Props passed to the rendered component. A function is re-read on each render (reactive). */
5
+ props?: Record<string, any> | (() => Record<string, any>);
6
+ /** Render immediately on mount (client only). Default false. */
7
+ immediate?: boolean;
8
+ /** Engine options forwarded to renderToPdf (e.g. onOverflow). */
9
+ renderOptions?: RenderOptions;
10
+ }
11
+ /**
12
+ * Render a PDF component to bytes in the browser. `open()` / `download()` render on demand and reuse the
13
+ * result, so one click is one render whether or not `immediate` pre-rendered. `render()` forces a fresh
14
+ * one (e.g. after the data changed). The object URL is revoked on re-render and scope dispose.
15
+ */
16
+ export declare function usePdf(component: Component, options?: UsePdfOptions): {
17
+ bytes: import("vue").ShallowRef<Uint8Array<ArrayBufferLike> | undefined, Uint8Array<ArrayBufferLike> | undefined>;
18
+ url: import("vue").Ref<string | undefined, string | undefined>;
19
+ pending: import("vue").Ref<boolean, boolean>;
20
+ error: import("vue").Ref<unknown, unknown>;
21
+ render: () => Promise<void>;
22
+ download: (filename?: string) => Promise<void>;
23
+ open: () => Promise<void>;
24
+ };
@@ -0,0 +1,55 @@
1
+ import { ref, shallowRef, onMounted, onScopeDispose } from "vue";
2
+ import { renderToPdf } from "@jasy/vue";
3
+ export function usePdf(component, options = {}) {
4
+ const bytes = shallowRef();
5
+ const url = ref();
6
+ const pending = ref(false);
7
+ const error = ref();
8
+ let inflight = null;
9
+ function revoke() {
10
+ if (url.value) {
11
+ URL.revokeObjectURL(url.value);
12
+ url.value = void 0;
13
+ }
14
+ }
15
+ async function run() {
16
+ if (typeof document === "undefined") return;
17
+ pending.value = true;
18
+ error.value = void 0;
19
+ try {
20
+ const props = typeof options.props === "function" ? options.props() : options.props;
21
+ const out = await renderToPdf(component, props, options.renderOptions);
22
+ bytes.value = out;
23
+ revoke();
24
+ url.value = URL.createObjectURL(new Blob([out], { type: "application/pdf" }));
25
+ } catch (e) {
26
+ error.value = e;
27
+ } finally {
28
+ pending.value = false;
29
+ }
30
+ }
31
+ function render() {
32
+ inflight ??= run().finally(() => {
33
+ inflight = null;
34
+ });
35
+ return inflight;
36
+ }
37
+ async function ensure() {
38
+ if (!url.value) await render();
39
+ }
40
+ async function open() {
41
+ await ensure();
42
+ if (url.value) window.open(url.value, "_blank");
43
+ }
44
+ async function download(filename = "document.pdf") {
45
+ await ensure();
46
+ if (!url.value) return;
47
+ const a = document.createElement("a");
48
+ a.href = url.value;
49
+ a.download = filename;
50
+ a.click();
51
+ }
52
+ onScopeDispose(revoke);
53
+ if (options.immediate) onMounted(render);
54
+ return { bytes, url, pending, error, render, download, open };
55
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "extends": "../../../.nuxt/tsconfig.server.json"
3
+ }
@@ -0,0 +1,24 @@
1
+ import { type H3Event } from "h3";
2
+ import { renderToBytes, type RenderOptions } from "@jasy/pdf";
3
+ type PdfDoc = Parameters<typeof renderToBytes>[0];
4
+ export interface SendPdfOptions {
5
+ /** Suggested file name. Default "document.pdf". */
6
+ filename?: string;
7
+ /** Send as a download (attachment) instead of inline. Default false. */
8
+ download?: boolean;
9
+ /** Engine options forwarded to renderToBytes (e.g. onOverflow). */
10
+ renderOptions?: RenderOptions;
11
+ }
12
+ export interface PdfHandlerOptions extends SendPdfOptions {
13
+ /**
14
+ * Cache the rendered PDF with Nitro. `true` for defaults, or pass its cache options (`maxAge`, `name`,
15
+ * `getKey`, `swr`, ...). Default key is path + query (so `/x?id=1` caches per id); `swr` defaults to
16
+ * false. Node runtime.
17
+ */
18
+ cache?: boolean | Record<string, any>;
19
+ }
20
+ /** Render a @jasy/pdf document and write it as the response (sets the PDF headers), returns the bytes. */
21
+ export declare function sendPdf(event: H3Event, doc: PdfDoc, options?: SendPdfOptions): Promise<Uint8Array>;
22
+ /** A PDF endpoint in one line: build a document from the request, get a streaming application/pdf route. */
23
+ export declare function definePdfHandler(build: (event: H3Event) => PdfDoc | Promise<PdfDoc>, options?: PdfHandlerOptions): import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<Uint8Array<ArrayBufferLike>>>;
24
+ export {};
@@ -0,0 +1,35 @@
1
+ import { defineEventHandler, setResponseHeader } from "h3";
2
+ import { defineCachedFunction } from "#imports";
3
+ import { renderToBytes } from "@jasy/pdf";
4
+ function setPdfHeaders(event, options) {
5
+ setResponseHeader(event, "content-type", "application/pdf");
6
+ setResponseHeader(
7
+ event,
8
+ "content-disposition",
9
+ `${options.download ? "attachment" : "inline"}; filename="${options.filename ?? "document.pdf"}"`
10
+ );
11
+ }
12
+ export async function sendPdf(event, doc, options = {}) {
13
+ const bytes = await renderToBytes(doc, options.renderOptions);
14
+ setPdfHeaders(event, options);
15
+ return bytes;
16
+ }
17
+ export function definePdfHandler(build, options = {}) {
18
+ const { cache, ...send } = options;
19
+ if (!cache) {
20
+ return defineEventHandler(async (event) => sendPdf(event, await build(event), send));
21
+ }
22
+ const renderCached = defineCachedFunction(
23
+ async (event) => {
24
+ const bytes = await renderToBytes(await build(event), send.renderOptions);
25
+ return Buffer.from(bytes).toString("base64");
26
+ },
27
+ // swr:false - never serve a stale (e.g. hour-old) invoice; re-render once the entry expires.
28
+ { swr: false, getKey: (event) => event.path, ...cache === true ? {} : cache }
29
+ );
30
+ return defineEventHandler(async (event) => {
31
+ const bytes = new Uint8Array(Buffer.from(await renderCached(event), "base64"));
32
+ setPdfHeaders(event, send);
33
+ return bytes;
34
+ });
35
+ }
@@ -0,0 +1,3 @@
1
+ export { default } from './module.mjs'
2
+
3
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@jasy/nuxt",
3
+ "version": "1.0.0-alpha.1",
4
+ "description": "Author PDFs as Vue components in Nuxt - client or server, zero config.",
5
+ "license": "MIT",
6
+ "author": "Florian Heuberger",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "jasy-pdf/jasy"
10
+ },
11
+ "files": [
12
+ "dist"
13
+ ],
14
+ "type": "module",
15
+ "main": "./dist/module.mjs",
16
+ "typesVersions": {
17
+ "*": {
18
+ ".": [
19
+ "./dist/types.d.mts"
20
+ ]
21
+ }
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/types.d.mts",
26
+ "import": "./dist/module.mjs"
27
+ }
28
+ },
29
+ "dependencies": {
30
+ "@nuxt/kit": "^4.4.8",
31
+ "@jasy/pdf": "1.0.0-alpha.2",
32
+ "@jasy/vue": "1.0.0-alpha.2"
33
+ },
34
+ "devDependencies": {
35
+ "@nuxt/devtools": "^3.2.4",
36
+ "@nuxt/module-builder": "^1.0.2",
37
+ "@nuxt/schema": "^4.4.8",
38
+ "@nuxt/test-utils": "^4.0.3",
39
+ "@types/node": "latest",
40
+ "nuxt": "^4.4.8",
41
+ "typescript": "~6.0.3",
42
+ "vitest": "^4.1.8",
43
+ "vue-tsc": "^3.3.3"
44
+ },
45
+ "scripts": {
46
+ "dev": "npm run dev:prepare && nuxt dev playground",
47
+ "dev:build": "nuxt build playground",
48
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
49
+ "lint": "oxlint",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest watch",
52
+ "test:types": "vue-tsc --noEmit && cd playground && vue-tsc --noEmit"
53
+ }
54
+ }