@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 +123 -0
- package/dist/module.d.mts +12 -0
- package/dist/module.json +9 -0
- package/dist/module.mjs +83 -0
- package/dist/runtime/composables/usePdf.d.ts +24 -0
- package/dist/runtime/composables/usePdf.js +55 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/jasy-pdf.d.ts +24 -0
- package/dist/runtime/server/utils/jasy-pdf.js +35 -0
- package/dist/types.d.mts +3 -0
- package/package.json +54 -0
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 };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -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,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
|
+
}
|
package/dist/types.d.mts
ADDED
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
|
+
}
|