@maistik/nuxt-pdf 1.0.17
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 +381 -0
- package/dist/module.d.mts +44 -0
- package/dist/module.d.ts +44 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +113 -0
- package/dist/runtime/composables/usePdf.d.ts +22 -0
- package/dist/runtime/composables/usePdf.js +42 -0
- package/dist/runtime/server/api/pdf.post.d.ts +2 -0
- package/dist/runtime/server/api/pdf.post.js +31 -0
- package/dist/runtime/types.d.ts +36 -0
- package/dist/runtime/types.js +0 -0
- package/dist/runtime/utils/compiler.d.ts +5 -0
- package/dist/runtime/utils/compiler.js +185 -0
- package/dist/runtime/utils/providers.d.ts +2 -0
- package/dist/runtime/utils/providers.js +118 -0
- package/dist/types.d.mts +9 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# @maistik/nuxt-pdf
|
|
2
|
+
|
|
3
|
+
A powerful Nuxt 3/4 module for server-side PDF generation using Handlebars templates. Generate beautiful, data-driven PDFs with support for multiple providers (Gotenberg, Browserless, Puppeteer Core) and built-in internationalization.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- 🎨 **Handlebars Templates** - Lightweight templating without Vue overhead
|
|
8
|
+
- 🌍 **Internationalization** - Built-in i18n support with `{{t}}` helper
|
|
9
|
+
- 🔄 **Provider Agnostic** - Support for Gotenberg, Browserless, and Puppeteer Core
|
|
10
|
+
- 🎯 **SSR Safe** - Everything runs server-side in Nitro/Node
|
|
11
|
+
- 🧩 **Composable API** - Easy-to-use `usePdf()` composable
|
|
12
|
+
- 📱 **Responsive Design** - CSS-based layouts with print media queries
|
|
13
|
+
- 🎪 **Playground** - Demo app with sample templates
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @maistik/nuxt-pdf handlebars
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Configuration
|
|
24
|
+
|
|
25
|
+
Add the module to your `nuxt.config.ts`:
|
|
26
|
+
|
|
27
|
+
```typescript
|
|
28
|
+
export default defineNuxtConfig({
|
|
29
|
+
modules: ['@maistik/nuxt-pdf'],
|
|
30
|
+
pdf: {
|
|
31
|
+
provider: 'puppeteer', // or 'gotenberg' or 'browserless'
|
|
32
|
+
components: ['pdf'],
|
|
33
|
+
sharedComponents: ['pdf/partials'],
|
|
34
|
+
enableI18n: true,
|
|
35
|
+
defaultLocale: 'en',
|
|
36
|
+
availableLocales: ['en', 'es', 'fr'],
|
|
37
|
+
i18nMessages: {
|
|
38
|
+
en: {
|
|
39
|
+
invoice: {
|
|
40
|
+
title: 'Invoice',
|
|
41
|
+
total: 'Total'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
es: {
|
|
45
|
+
invoice: {
|
|
46
|
+
title: 'Factura',
|
|
47
|
+
total: 'Total'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
providers: {
|
|
52
|
+
puppeteer: {
|
|
53
|
+
launchOptions: {
|
|
54
|
+
headless: true,
|
|
55
|
+
args: ['--no-sandbox']
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Create a Template
|
|
64
|
+
|
|
65
|
+
Create `pdf/Invoice.hbs` in your project:
|
|
66
|
+
|
|
67
|
+
```handlebars
|
|
68
|
+
<style>
|
|
69
|
+
@page { size: A4; margin: 20mm; }
|
|
70
|
+
body { font-family: Arial, sans-serif; }
|
|
71
|
+
.header { text-align: center; margin-bottom: 30px; }
|
|
72
|
+
.total { font-weight: bold; font-size: 18px; }
|
|
73
|
+
</style>
|
|
74
|
+
|
|
75
|
+
<div class="header">
|
|
76
|
+
<h1>{{t "invoice.title"}}</h1>
|
|
77
|
+
<p>Invoice #{{invoiceNumber}}</p>
|
|
78
|
+
</div>
|
|
79
|
+
|
|
80
|
+
<table>
|
|
81
|
+
{{#each items}}
|
|
82
|
+
<tr>
|
|
83
|
+
<td>{{this.description}}</td>
|
|
84
|
+
<td>{{formatCurrency this.price}}</td>
|
|
85
|
+
</tr>
|
|
86
|
+
{{/each}}
|
|
87
|
+
</table>
|
|
88
|
+
|
|
89
|
+
<div class="total">
|
|
90
|
+
{{t "invoice.total"}}: {{formatCurrency total}}
|
|
91
|
+
</div>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Generate PDFs
|
|
95
|
+
|
|
96
|
+
Use the composable in your Vue components:
|
|
97
|
+
|
|
98
|
+
```vue
|
|
99
|
+
<script setup>
|
|
100
|
+
const { generate, download } = usePdf()
|
|
101
|
+
|
|
102
|
+
const generateInvoice = async () => {
|
|
103
|
+
const data = {
|
|
104
|
+
invoiceNumber: 'INV-001',
|
|
105
|
+
items: [
|
|
106
|
+
{ description: 'Service', price: 100 }
|
|
107
|
+
],
|
|
108
|
+
total: 100
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Generate and preview
|
|
112
|
+
const blob = await generate('Invoice', data, { format: 'A4' }, 'en')
|
|
113
|
+
|
|
114
|
+
// Or download directly
|
|
115
|
+
await download('Invoice', data, { format: 'A4' }, 'invoice.pdf', 'en')
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Providers
|
|
121
|
+
|
|
122
|
+
### Puppeteer Core (Local)
|
|
123
|
+
|
|
124
|
+
Best for development and on-premise deployments:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
pdf: {
|
|
128
|
+
provider: 'puppeteer',
|
|
129
|
+
providers: {
|
|
130
|
+
puppeteer: {
|
|
131
|
+
launchOptions: {
|
|
132
|
+
headless: true,
|
|
133
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox']
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Gotenberg (Docker)
|
|
141
|
+
|
|
142
|
+
Perfect for containerized environments:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
pdf: {
|
|
146
|
+
provider: 'gotenberg',
|
|
147
|
+
providers: {
|
|
148
|
+
gotenberg: {
|
|
149
|
+
url: 'http://gotenberg:3000'
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Browserless (Cloud)
|
|
156
|
+
|
|
157
|
+
Great for serverless deployments:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
pdf: {
|
|
161
|
+
provider: 'browserless',
|
|
162
|
+
providers: {
|
|
163
|
+
browserless: {
|
|
164
|
+
url: 'https://chrome.browserless.io',
|
|
165
|
+
apiKey: process.env.BROWSERLESS_API_KEY
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Built-in Helpers
|
|
172
|
+
|
|
173
|
+
### Internationalization
|
|
174
|
+
```handlebars
|
|
175
|
+
{{t "invoice.title"}} <!-- Outputs localized text -->
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Currency Formatting
|
|
179
|
+
```handlebars
|
|
180
|
+
{{formatCurrency 1234.56}} <!-- $1,234.56 -->
|
|
181
|
+
{{formatCurrency 1234.56 "EUR"}} <!-- €1,234.56 -->
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Date Formatting
|
|
185
|
+
```handlebars
|
|
186
|
+
{{formatDate date}} <!-- 12/25/2024 -->
|
|
187
|
+
{{formatDate date "full"}} <!-- Wednesday, December 25, 2024 -->
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Number Formatting
|
|
191
|
+
```handlebars
|
|
192
|
+
{{formatNumber 1234.56}} <!-- 1,234.56 -->
|
|
193
|
+
{{formatNumber 0.15 style="percent"}} <!-- 15% -->
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### Math Operations
|
|
197
|
+
```handlebars
|
|
198
|
+
{{add 10 5}} <!-- 15 -->
|
|
199
|
+
{{subtract 10 5}} <!-- 5 -->
|
|
200
|
+
{{multiply 10 5}} <!-- 50 -->
|
|
201
|
+
{{divide 10 5}} <!-- 2 -->
|
|
202
|
+
{{percentage 25 100}} <!-- 25% -->
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Custom Helpers
|
|
206
|
+
|
|
207
|
+
You can define your own custom helpers in the configuration:
|
|
208
|
+
|
|
209
|
+
```typescript
|
|
210
|
+
pdf: {
|
|
211
|
+
customHelpers: {
|
|
212
|
+
// Simple value transformation
|
|
213
|
+
customFormat: (value: any) => `[${value}]`,
|
|
214
|
+
|
|
215
|
+
// String manipulation
|
|
216
|
+
repeat: (str: string, times: number) => str.repeat(times || 1),
|
|
217
|
+
|
|
218
|
+
// Block helper with conditional logic
|
|
219
|
+
ifEquals: function(this: any, arg1: any, arg2: any, options: any) {
|
|
220
|
+
return (arg1 === arg2) ? options.fn(this) : options.inverse(this)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Use them in templates:
|
|
227
|
+
```handlebars
|
|
228
|
+
{{customFormat "hello"}} <!-- [hello] -->
|
|
229
|
+
{{repeat "★" 5}} <!-- ★★★★★ -->
|
|
230
|
+
{{#ifEquals status "active"}}Active User{{else}}Inactive User{{/ifEquals}}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### String Helpers
|
|
234
|
+
```handlebars
|
|
235
|
+
{{upper "hello world"}} <!-- HELLO WORLD -->
|
|
236
|
+
{{lower "HELLO WORLD"}} <!-- hello world -->
|
|
237
|
+
{{capitalize "hello world"}} <!-- Hello world -->
|
|
238
|
+
{{truncate "Long text here" 10}} <!-- Long text... -->
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Line Calculations
|
|
242
|
+
```handlebars
|
|
243
|
+
{{lineTotal quantity price}} <!-- quantity * price -->
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Template Features
|
|
247
|
+
|
|
248
|
+
### Automatic Enrichment
|
|
249
|
+
|
|
250
|
+
The module automatically enriches your data based on template names:
|
|
251
|
+
|
|
252
|
+
**Invoice Templates** get:
|
|
253
|
+
- `subtotal` - Sum of all line items
|
|
254
|
+
- `tax` - Calculated tax amount
|
|
255
|
+
- `total` - Subtotal + tax
|
|
256
|
+
- `dueDate` - Calculated from issue date + payment terms
|
|
257
|
+
|
|
258
|
+
**Sales Report Templates** get:
|
|
259
|
+
- `quarters` - Quarterly breakdown of sales data
|
|
260
|
+
- `rating` - Performance rating based on total sales
|
|
261
|
+
|
|
262
|
+
### CSS Styling
|
|
263
|
+
|
|
264
|
+
Use standard CSS with print-specific rules:
|
|
265
|
+
|
|
266
|
+
```css
|
|
267
|
+
@page {
|
|
268
|
+
size: A4;
|
|
269
|
+
margin: 20mm;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.page-break {
|
|
273
|
+
page-break-before: always;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@media print {
|
|
277
|
+
.no-break {
|
|
278
|
+
page-break-inside: avoid;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### Partials
|
|
284
|
+
|
|
285
|
+
Create reusable components in your `sharedComponents` directory:
|
|
286
|
+
|
|
287
|
+
```handlebars
|
|
288
|
+
<!-- pdf/partials/header.hbs -->
|
|
289
|
+
<div class="header">
|
|
290
|
+
<h1>{{title}}</h1>
|
|
291
|
+
<p>{{subtitle}}</p>
|
|
292
|
+
</div>
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Use in templates:
|
|
296
|
+
```handlebars
|
|
297
|
+
{{> header title="My Document" subtitle="Generated Report"}}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## API Reference
|
|
301
|
+
|
|
302
|
+
### `usePdf()`
|
|
303
|
+
|
|
304
|
+
The main composable for PDF operations:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const {
|
|
308
|
+
generate, // (template, data, options?, locale?) => Promise<Blob>
|
|
309
|
+
download, // (template, data, options?, filename?, locale?) => Promise<void>
|
|
310
|
+
getAvailableLocales, // () => string[]
|
|
311
|
+
getDefaultLocale // () => string
|
|
312
|
+
} = usePdf()
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### Options
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
interface PdfOptions {
|
|
319
|
+
format?: 'A4' | 'Letter' | 'Legal'
|
|
320
|
+
margin?: {
|
|
321
|
+
top?: number
|
|
322
|
+
bottom?: number
|
|
323
|
+
left?: number
|
|
324
|
+
right?: number
|
|
325
|
+
}
|
|
326
|
+
landscape?: boolean
|
|
327
|
+
printBackground?: boolean
|
|
328
|
+
pageBreak?: {
|
|
329
|
+
before?: string[]
|
|
330
|
+
after?: string[]
|
|
331
|
+
avoid?: string[]
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Development
|
|
337
|
+
|
|
338
|
+
### Playground
|
|
339
|
+
|
|
340
|
+
The module includes a full playground application:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
npm run dev
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
This starts a demo app with sample Invoice and Sales Report templates in multiple languages.
|
|
347
|
+
|
|
348
|
+
### Building
|
|
349
|
+
|
|
350
|
+
```bash
|
|
351
|
+
npm run build
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Testing
|
|
355
|
+
|
|
356
|
+
```bash
|
|
357
|
+
npm run test
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
## Examples
|
|
361
|
+
|
|
362
|
+
Check out the `playground/` directory for complete examples including:
|
|
363
|
+
|
|
364
|
+
- **Invoice Template** - Complete invoice with line items, taxes, and totals
|
|
365
|
+
- **Sales Report Template** - Comprehensive report with metrics and quarterly breakdown
|
|
366
|
+
- **Multi-language Support** - Templates in English, Spanish, and French
|
|
367
|
+
- **Multiple Providers** - Configuration examples for all supported providers
|
|
368
|
+
|
|
369
|
+
## License
|
|
370
|
+
|
|
371
|
+
MIT
|
|
372
|
+
|
|
373
|
+
## Contributing
|
|
374
|
+
|
|
375
|
+
Contributions are welcome! Please read our contributing guidelines and submit pull requests to our repository.
|
|
376
|
+
|
|
377
|
+
## Support
|
|
378
|
+
|
|
379
|
+
- 📖 [Documentation](https://github.com/Maistik-Studio/nuxt-pdf)
|
|
380
|
+
- 🐛 [Issue Tracker](https://github.com/Maistik-Studio/nuxt-pdf/issues)
|
|
381
|
+
- 💬 [Discussions](https://github.com/Maistik-Studio/nuxt-pdf/discussions)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HelperDelegate } from 'handlebars';
|
|
2
|
+
|
|
3
|
+
interface PdfModuleOptions {
|
|
4
|
+
provider: 'gotenberg' | 'browserless' | 'puppeteer';
|
|
5
|
+
components: string[];
|
|
6
|
+
sharedComponents: string[];
|
|
7
|
+
enableI18n: boolean;
|
|
8
|
+
defaultLocale: string;
|
|
9
|
+
availableLocales: string[];
|
|
10
|
+
i18nMessages: Record<string, Record<string, string>>;
|
|
11
|
+
providers: {
|
|
12
|
+
gotenberg: {
|
|
13
|
+
url: string;
|
|
14
|
+
};
|
|
15
|
+
browserless: {
|
|
16
|
+
url: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
};
|
|
19
|
+
puppeteer: {
|
|
20
|
+
launchOptions: Record<string, any>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
defaultOptions: {
|
|
24
|
+
format: string;
|
|
25
|
+
margin: {
|
|
26
|
+
top: number;
|
|
27
|
+
bottom: number;
|
|
28
|
+
left: number;
|
|
29
|
+
right: number;
|
|
30
|
+
};
|
|
31
|
+
landscape: boolean;
|
|
32
|
+
printBackground: boolean;
|
|
33
|
+
pageBreak: {
|
|
34
|
+
before: string[];
|
|
35
|
+
after: string[];
|
|
36
|
+
avoid: string[];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
customHelpers?: Record<string, HelperDelegate>;
|
|
40
|
+
}
|
|
41
|
+
declare const _default: any;
|
|
42
|
+
|
|
43
|
+
export { _default as default };
|
|
44
|
+
export type { PdfModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { HelperDelegate } from 'handlebars';
|
|
2
|
+
|
|
3
|
+
interface PdfModuleOptions {
|
|
4
|
+
provider: 'gotenberg' | 'browserless' | 'puppeteer';
|
|
5
|
+
components: string[];
|
|
6
|
+
sharedComponents: string[];
|
|
7
|
+
enableI18n: boolean;
|
|
8
|
+
defaultLocale: string;
|
|
9
|
+
availableLocales: string[];
|
|
10
|
+
i18nMessages: Record<string, Record<string, string>>;
|
|
11
|
+
providers: {
|
|
12
|
+
gotenberg: {
|
|
13
|
+
url: string;
|
|
14
|
+
};
|
|
15
|
+
browserless: {
|
|
16
|
+
url: string;
|
|
17
|
+
apiKey: string;
|
|
18
|
+
};
|
|
19
|
+
puppeteer: {
|
|
20
|
+
launchOptions: Record<string, any>;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
defaultOptions: {
|
|
24
|
+
format: string;
|
|
25
|
+
margin: {
|
|
26
|
+
top: number;
|
|
27
|
+
bottom: number;
|
|
28
|
+
left: number;
|
|
29
|
+
right: number;
|
|
30
|
+
};
|
|
31
|
+
landscape: boolean;
|
|
32
|
+
printBackground: boolean;
|
|
33
|
+
pageBreak: {
|
|
34
|
+
before: string[];
|
|
35
|
+
after: string[];
|
|
36
|
+
avoid: string[];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
customHelpers?: Record<string, HelperDelegate>;
|
|
40
|
+
}
|
|
41
|
+
declare const _default: any;
|
|
42
|
+
|
|
43
|
+
export { _default as default };
|
|
44
|
+
export type { PdfModuleOptions };
|
package/dist/module.json
ADDED
package/dist/module.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { defineNuxtModule, createResolver, addServerHandler, addImports } from '@nuxt/kit';
|
|
2
|
+
import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const module = defineNuxtModule({
|
|
6
|
+
meta: {
|
|
7
|
+
name: "@maistik/nuxt-pdf",
|
|
8
|
+
configKey: "pdf",
|
|
9
|
+
compatibility: {
|
|
10
|
+
nuxt: "^3.0.0"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
defaults: {
|
|
14
|
+
provider: "gotenberg",
|
|
15
|
+
components: ["pdf"],
|
|
16
|
+
sharedComponents: ["pdf/partials"],
|
|
17
|
+
enableI18n: true,
|
|
18
|
+
defaultLocale: "en",
|
|
19
|
+
availableLocales: ["en"],
|
|
20
|
+
i18nMessages: {},
|
|
21
|
+
providers: {
|
|
22
|
+
gotenberg: {
|
|
23
|
+
url: "http://localhost:3000"
|
|
24
|
+
},
|
|
25
|
+
browserless: {
|
|
26
|
+
url: "https://chrome.browserless.io",
|
|
27
|
+
apiKey: ""
|
|
28
|
+
},
|
|
29
|
+
puppeteer: {
|
|
30
|
+
launchOptions: {
|
|
31
|
+
headless: true,
|
|
32
|
+
args: ["--no-sandbox", "--disable-setuid-sandbox"]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
customHelpers: {},
|
|
37
|
+
defaultOptions: {
|
|
38
|
+
format: "A4",
|
|
39
|
+
margin: {
|
|
40
|
+
top: 20,
|
|
41
|
+
bottom: 20,
|
|
42
|
+
left: 20,
|
|
43
|
+
right: 20
|
|
44
|
+
},
|
|
45
|
+
landscape: false,
|
|
46
|
+
printBackground: true,
|
|
47
|
+
pageBreak: {
|
|
48
|
+
before: ["always"],
|
|
49
|
+
after: ["always"],
|
|
50
|
+
avoid: [".no-break"]
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
setup(options, nuxt) {
|
|
55
|
+
const resolver = createResolver(import.meta.url);
|
|
56
|
+
const templateSources = {};
|
|
57
|
+
const partialSources = {};
|
|
58
|
+
for (const componentDir of options.components) {
|
|
59
|
+
const fullPath = join(nuxt.options.srcDir, componentDir);
|
|
60
|
+
if (existsSync(fullPath)) {
|
|
61
|
+
scanDirectory(fullPath, templateSources, ".hbs");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const sharedDir of options.sharedComponents) {
|
|
65
|
+
const fullPath = join(nuxt.options.srcDir, sharedDir);
|
|
66
|
+
if (existsSync(fullPath)) {
|
|
67
|
+
scanDirectory(fullPath, partialSources, ".hbs");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
nuxt.options.runtimeConfig.pdf = {
|
|
71
|
+
provider: options.provider,
|
|
72
|
+
enableI18n: options.enableI18n,
|
|
73
|
+
defaultLocale: options.defaultLocale,
|
|
74
|
+
availableLocales: options.availableLocales,
|
|
75
|
+
i18nMessages: options.i18nMessages,
|
|
76
|
+
providers: options.providers,
|
|
77
|
+
defaultOptions: options.defaultOptions,
|
|
78
|
+
templateSources,
|
|
79
|
+
partialSources,
|
|
80
|
+
customHelpers: options.customHelpers || {}
|
|
81
|
+
};
|
|
82
|
+
nuxt.options.runtimeConfig.public.pdf = {
|
|
83
|
+
enableI18n: options.enableI18n,
|
|
84
|
+
defaultLocale: options.defaultLocale,
|
|
85
|
+
availableLocales: options.availableLocales
|
|
86
|
+
};
|
|
87
|
+
addServerHandler({
|
|
88
|
+
route: "/api/pdf",
|
|
89
|
+
handler: resolver.resolve("./runtime/server/api/pdf.post")
|
|
90
|
+
});
|
|
91
|
+
addImports({
|
|
92
|
+
name: "usePdf",
|
|
93
|
+
as: "usePdf",
|
|
94
|
+
from: resolver.resolve("./runtime/composables/usePdf")
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
function scanDirectory(dir, sources, extension) {
|
|
99
|
+
if (!existsSync(dir)) return;
|
|
100
|
+
const files = readdirSync(dir);
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
const filePath = join(dir, file);
|
|
103
|
+
const stat = statSync(filePath);
|
|
104
|
+
if (stat.isDirectory()) {
|
|
105
|
+
scanDirectory(filePath, sources, extension);
|
|
106
|
+
} else if (file.endsWith(extension)) {
|
|
107
|
+
const templateName = file.replace(extension, "");
|
|
108
|
+
sources[templateName] = readFileSync(filePath, "utf-8");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { module as default };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface PdfGenerateOptions {
|
|
2
|
+
format?: string;
|
|
3
|
+
margin?: {
|
|
4
|
+
top?: number;
|
|
5
|
+
bottom?: number;
|
|
6
|
+
left?: number;
|
|
7
|
+
right?: number;
|
|
8
|
+
};
|
|
9
|
+
landscape?: boolean;
|
|
10
|
+
printBackground?: boolean;
|
|
11
|
+
pageBreak?: {
|
|
12
|
+
before?: string[];
|
|
13
|
+
after?: string[];
|
|
14
|
+
avoid?: string[];
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
export declare function usePdf(): {
|
|
18
|
+
generate: (template: string, data: any, options?: PdfGenerateOptions, locale?: string) => Promise<Blob>;
|
|
19
|
+
download: (template: string, data: any, options?: PdfGenerateOptions, filename?: string, locale?: string) => Promise<void>;
|
|
20
|
+
getAvailableLocales: () => string[];
|
|
21
|
+
getDefaultLocale: () => string;
|
|
22
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
export function usePdf() {
|
|
3
|
+
const config = useRuntimeConfig();
|
|
4
|
+
const generate = async (template, data, options = {}, locale) => {
|
|
5
|
+
const response = await $fetch("/api/pdf", {
|
|
6
|
+
method: "POST",
|
|
7
|
+
body: {
|
|
8
|
+
template,
|
|
9
|
+
ctx: {
|
|
10
|
+
data,
|
|
11
|
+
options,
|
|
12
|
+
locale: locale || config.public.pdf.defaultLocale
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
responseType: "arrayBuffer"
|
|
16
|
+
});
|
|
17
|
+
return new Blob([response], { type: "application/pdf" });
|
|
18
|
+
};
|
|
19
|
+
const download = async (template, data, options = {}, filename = "document.pdf", locale) => {
|
|
20
|
+
const blob = await generate(template, data, options, locale);
|
|
21
|
+
const url = URL.createObjectURL(blob);
|
|
22
|
+
const link = document.createElement("a");
|
|
23
|
+
link.href = url;
|
|
24
|
+
link.download = filename;
|
|
25
|
+
document.body.appendChild(link);
|
|
26
|
+
link.click();
|
|
27
|
+
document.body.removeChild(link);
|
|
28
|
+
URL.revokeObjectURL(url);
|
|
29
|
+
};
|
|
30
|
+
const getAvailableLocales = () => {
|
|
31
|
+
return config.public.pdf.availableLocales || ["en"];
|
|
32
|
+
};
|
|
33
|
+
const getDefaultLocale = () => {
|
|
34
|
+
return config.public.pdf.defaultLocale || "en";
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
generate,
|
|
38
|
+
download,
|
|
39
|
+
getAvailableLocales,
|
|
40
|
+
getDefaultLocale
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineEventHandler, readBody, setHeader } from "h3";
|
|
2
|
+
import { useRuntimeConfig } from "#imports";
|
|
3
|
+
import { compilePdfComponent } from "../../utils/compiler.js";
|
|
4
|
+
import { createPdfProvider } from "../../utils/providers.js";
|
|
5
|
+
export default defineEventHandler(async (event) => {
|
|
6
|
+
try {
|
|
7
|
+
const config = useRuntimeConfig();
|
|
8
|
+
const { template, ctx } = await readBody(event);
|
|
9
|
+
if (!template || !ctx) {
|
|
10
|
+
throw new Error("Missing template or context");
|
|
11
|
+
}
|
|
12
|
+
const { data, options = {}, locale } = ctx;
|
|
13
|
+
const mergedOptions = { ...config.pdf.defaultOptions, ...options };
|
|
14
|
+
const messages = config.pdf.enableI18n ? config.pdf.i18nMessages[locale || config.pdf.defaultLocale] || {} : {};
|
|
15
|
+
const html = compilePdfComponent(
|
|
16
|
+
template,
|
|
17
|
+
{ data, options: mergedOptions, locale },
|
|
18
|
+
messages,
|
|
19
|
+
config.pdf.templateSources,
|
|
20
|
+
config.pdf.partialSources
|
|
21
|
+
);
|
|
22
|
+
const provider = createPdfProvider(config.pdf.provider, config.pdf.providers);
|
|
23
|
+
const pdfBuffer = await provider.generatePdf(html, mergedOptions);
|
|
24
|
+
setHeader(event, "Content-Type", "application/pdf");
|
|
25
|
+
setHeader(event, "Content-Length", pdfBuffer.length.toString());
|
|
26
|
+
return pdfBuffer;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error("PDF generation error:", error);
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface PdfContext {
|
|
2
|
+
data: any;
|
|
3
|
+
options: PdfOptions;
|
|
4
|
+
locale?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface PdfOptions {
|
|
7
|
+
format?: 'A4' | 'Letter' | 'Legal';
|
|
8
|
+
margin?: {
|
|
9
|
+
top?: number;
|
|
10
|
+
bottom?: number;
|
|
11
|
+
left?: number;
|
|
12
|
+
right?: number;
|
|
13
|
+
};
|
|
14
|
+
landscape?: boolean;
|
|
15
|
+
printBackground?: boolean;
|
|
16
|
+
pageBreak?: {
|
|
17
|
+
before?: string[];
|
|
18
|
+
after?: string[];
|
|
19
|
+
avoid?: string[];
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export interface PdfProviderConfig {
|
|
23
|
+
gotenberg: {
|
|
24
|
+
url: string;
|
|
25
|
+
};
|
|
26
|
+
browserless: {
|
|
27
|
+
url: string;
|
|
28
|
+
apiKey: string;
|
|
29
|
+
};
|
|
30
|
+
puppeteer: {
|
|
31
|
+
launchOptions: Record<string, any>;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface PdfProvider {
|
|
35
|
+
generatePdf(html: string, options: any): Promise<Buffer>;
|
|
36
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { useRuntimeConfig } from "#imports";
|
|
2
|
+
import Handlebars from "handlebars";
|
|
3
|
+
export function compilePdfComponent(templateName, ctx, messages, templateSources, partialSources) {
|
|
4
|
+
const handlebars = Handlebars.create();
|
|
5
|
+
handlebars.registerHelper("t", function(key) {
|
|
6
|
+
const keys = key.split(".");
|
|
7
|
+
let value = messages;
|
|
8
|
+
for (const k of keys) {
|
|
9
|
+
if (value && typeof value === "object" && k in value) {
|
|
10
|
+
value = value[k];
|
|
11
|
+
} else {
|
|
12
|
+
return key;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return typeof value === "string" ? value : key;
|
|
16
|
+
});
|
|
17
|
+
handlebars.registerHelper("formatCurrency", function(value, currencyOrOptions) {
|
|
18
|
+
if (typeof value !== "number") {
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
let currency = "USD";
|
|
22
|
+
if (typeof currencyOrOptions === "string") {
|
|
23
|
+
currency = currencyOrOptions;
|
|
24
|
+
} else if (currencyOrOptions && typeof currencyOrOptions.hash?.currency === "string") {
|
|
25
|
+
currency = currencyOrOptions.hash.currency;
|
|
26
|
+
}
|
|
27
|
+
if (!/^[A-Z]{3}$/.test(currency)) {
|
|
28
|
+
currency = "USD";
|
|
29
|
+
}
|
|
30
|
+
return new Intl.NumberFormat(ctx.locale || "en-US", {
|
|
31
|
+
style: "currency",
|
|
32
|
+
currency
|
|
33
|
+
}).format(value);
|
|
34
|
+
});
|
|
35
|
+
handlebars.registerHelper("lineTotal", function(qty, price) {
|
|
36
|
+
return (qty * price).toFixed(2);
|
|
37
|
+
});
|
|
38
|
+
handlebars.registerHelper("eq", function(a, b, options) {
|
|
39
|
+
const isEqual = a === b;
|
|
40
|
+
if (options && typeof options.fn === "function") {
|
|
41
|
+
return isEqual ? options.fn(this) : options.inverse(this);
|
|
42
|
+
}
|
|
43
|
+
return isEqual;
|
|
44
|
+
});
|
|
45
|
+
handlebars.registerHelper(
|
|
46
|
+
"ne",
|
|
47
|
+
(a, b, opts) => a !== b ? opts.fn(this) : opts.inverse(this)
|
|
48
|
+
);
|
|
49
|
+
handlebars.registerHelper(
|
|
50
|
+
"gt",
|
|
51
|
+
(a, b, opts) => a > b ? opts.fn(this) : opts.inverse(this)
|
|
52
|
+
);
|
|
53
|
+
handlebars.registerHelper("formatDate", function(date, formatOrOptions) {
|
|
54
|
+
let style = "short";
|
|
55
|
+
if (typeof formatOrOptions === "string") {
|
|
56
|
+
style = formatOrOptions;
|
|
57
|
+
} else if (formatOrOptions && typeof formatOrOptions.hash?.format === "string") {
|
|
58
|
+
style = formatOrOptions.hash.format;
|
|
59
|
+
}
|
|
60
|
+
if (!["full", "long", "medium", "short"].includes(style)) {
|
|
61
|
+
style = "short";
|
|
62
|
+
}
|
|
63
|
+
let dateObj;
|
|
64
|
+
if (typeof date === "string") {
|
|
65
|
+
dateObj = new Date(date);
|
|
66
|
+
} else if (date instanceof Date) {
|
|
67
|
+
dateObj = date;
|
|
68
|
+
} else {
|
|
69
|
+
dateObj = /* @__PURE__ */ new Date();
|
|
70
|
+
}
|
|
71
|
+
if (isNaN(dateObj.getTime())) {
|
|
72
|
+
dateObj = /* @__PURE__ */ new Date();
|
|
73
|
+
}
|
|
74
|
+
return new Intl.DateTimeFormat(ctx.locale || "en-US", {
|
|
75
|
+
dateStyle: style
|
|
76
|
+
}).format(dateObj);
|
|
77
|
+
});
|
|
78
|
+
handlebars.registerHelper("formatNumber", function(value, options = {}) {
|
|
79
|
+
if (typeof value !== "number") return value;
|
|
80
|
+
return new Intl.NumberFormat(ctx.locale || "en-US", options).format(value);
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
const config = useRuntimeConfig();
|
|
84
|
+
const customHelpers = config.pdf.customHelpers;
|
|
85
|
+
if (customHelpers && typeof customHelpers === "object") {
|
|
86
|
+
Object.entries(customHelpers).forEach(([name, helper]) => {
|
|
87
|
+
if (typeof helper === "function") {
|
|
88
|
+
handlebars.registerHelper(name, helper);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn("Failed to load custom helpers:", error);
|
|
94
|
+
}
|
|
95
|
+
handlebars.registerHelper("upper", function(str) {
|
|
96
|
+
return String(str || "").toUpperCase();
|
|
97
|
+
});
|
|
98
|
+
handlebars.registerHelper("lower", function(str) {
|
|
99
|
+
return String(str || "").toLowerCase();
|
|
100
|
+
});
|
|
101
|
+
handlebars.registerHelper("capitalize", function(str) {
|
|
102
|
+
return String(str || "").charAt(0).toUpperCase() + String(str || "").slice(1).toLowerCase();
|
|
103
|
+
});
|
|
104
|
+
handlebars.registerHelper("truncate", function(str, length) {
|
|
105
|
+
const text = String(str || "");
|
|
106
|
+
return text.length > length ? `${text.substring(0, length)}...` : text;
|
|
107
|
+
});
|
|
108
|
+
handlebars.registerHelper("multiply", function(a, b) {
|
|
109
|
+
return (a || 0) * (b || 0);
|
|
110
|
+
});
|
|
111
|
+
handlebars.registerHelper("add", function(a, b) {
|
|
112
|
+
return (a || 0) + (b || 0);
|
|
113
|
+
});
|
|
114
|
+
handlebars.registerHelper("subtract", function(a, b) {
|
|
115
|
+
return (a || 0) - (b || 0);
|
|
116
|
+
});
|
|
117
|
+
handlebars.registerHelper("divide", function(a, b) {
|
|
118
|
+
return b !== 0 ? (a || 0) / b : 0;
|
|
119
|
+
});
|
|
120
|
+
handlebars.registerHelper("percentage", function(value, total) {
|
|
121
|
+
return total !== 0 ? `${((value || 0) / total * 100).toFixed(2)}%` : "0%";
|
|
122
|
+
});
|
|
123
|
+
Object.entries(partialSources).forEach(([name, source]) => {
|
|
124
|
+
handlebars.registerPartial(name, source);
|
|
125
|
+
});
|
|
126
|
+
const templateSource = templateSources[templateName];
|
|
127
|
+
if (!templateSource) {
|
|
128
|
+
throw new Error(`Template "${templateName}" not found`);
|
|
129
|
+
}
|
|
130
|
+
const template = handlebars.compile(templateSource);
|
|
131
|
+
const enrichedData = enrichContextForTemplate(templateName, ctx.data);
|
|
132
|
+
return template({
|
|
133
|
+
...enrichedData,
|
|
134
|
+
options: ctx.options,
|
|
135
|
+
locale: ctx.locale
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
function enrichContextForTemplate(templateName, data) {
|
|
139
|
+
const enriched = { ...data };
|
|
140
|
+
if (templateName.toLowerCase().includes("invoice")) {
|
|
141
|
+
if (enriched.items && Array.isArray(enriched.items)) {
|
|
142
|
+
enriched.subtotal = enriched.items.reduce((sum, item) => {
|
|
143
|
+
return sum + item.quantity * item.price;
|
|
144
|
+
}, 0);
|
|
145
|
+
enriched.tax = enriched.subtotal * (enriched.taxRate || 0.1);
|
|
146
|
+
enriched.total = enriched.subtotal + enriched.tax;
|
|
147
|
+
if (enriched.issueDate && enriched.paymentTerms) {
|
|
148
|
+
const issueDate = new Date(enriched.issueDate);
|
|
149
|
+
const dueDate = new Date(issueDate);
|
|
150
|
+
dueDate.setDate(dueDate.getDate() + enriched.paymentTerms);
|
|
151
|
+
enriched.dueDate = dueDate;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (templateName.toLowerCase().includes("salesreport")) {
|
|
156
|
+
if (enriched.salesData && Array.isArray(enriched.salesData)) {
|
|
157
|
+
enriched.quarters = calculateQuarters(enriched.salesData);
|
|
158
|
+
const totalSales = enriched.salesData.reduce((sum, item) => sum + item.amount, 0);
|
|
159
|
+
enriched.rating = calculatePerformanceRating(totalSales);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return enriched;
|
|
163
|
+
}
|
|
164
|
+
function calculateQuarters(salesData) {
|
|
165
|
+
const quarters = [
|
|
166
|
+
{ name: "Q1", months: [0, 1, 2], total: 0 },
|
|
167
|
+
{ name: "Q2", months: [3, 4, 5], total: 0 },
|
|
168
|
+
{ name: "Q3", months: [6, 7, 8], total: 0 },
|
|
169
|
+
{ name: "Q4", months: [9, 10, 11], total: 0 }
|
|
170
|
+
];
|
|
171
|
+
salesData.forEach((item) => {
|
|
172
|
+
const month = new Date(item.date).getMonth();
|
|
173
|
+
const quarter = quarters.find((q) => q.months.includes(month));
|
|
174
|
+
if (quarter) {
|
|
175
|
+
quarter.total += item.amount;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
return quarters;
|
|
179
|
+
}
|
|
180
|
+
function calculatePerformanceRating(totalSales) {
|
|
181
|
+
if (totalSales >= 1e5) return "Excellent";
|
|
182
|
+
if (totalSales >= 75e3) return "Good";
|
|
183
|
+
if (totalSales >= 5e4) return "Average";
|
|
184
|
+
return "Needs Improvement";
|
|
185
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import FormData from "form-data";
|
|
2
|
+
export function createPdfProvider(providerType, config) {
|
|
3
|
+
switch (providerType) {
|
|
4
|
+
case "gotenberg":
|
|
5
|
+
return new GotenbergProvider(config.gotenberg);
|
|
6
|
+
case "browserless":
|
|
7
|
+
return new BrowserlessProvider(config.browserless);
|
|
8
|
+
case "puppeteer":
|
|
9
|
+
return new PuppeteerProvider(config.puppeteer);
|
|
10
|
+
default:
|
|
11
|
+
throw new Error(`Unknown PDF provider: ${providerType}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
class GotenbergProvider {
|
|
15
|
+
constructor(config) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
async generatePdf(html, options) {
|
|
19
|
+
const form = new FormData();
|
|
20
|
+
const htmlBuffer = Buffer.from(html, "utf8");
|
|
21
|
+
form.append("files", htmlBuffer, {
|
|
22
|
+
filename: "index.html",
|
|
23
|
+
contentType: "text/html",
|
|
24
|
+
knownLength: htmlBuffer.length
|
|
25
|
+
});
|
|
26
|
+
if (options.format) {
|
|
27
|
+
const dimensions = this.getPageDimensions(options.format);
|
|
28
|
+
form.append("paperWidth", dimensions.width);
|
|
29
|
+
form.append("paperHeight", dimensions.height);
|
|
30
|
+
}
|
|
31
|
+
if (options.margin) {
|
|
32
|
+
form.append("marginTop", (options.margin.top / 25.4).toString());
|
|
33
|
+
form.append("marginBottom", (options.margin.bottom / 25.4).toString());
|
|
34
|
+
form.append("marginLeft", (options.margin.left / 25.4).toString());
|
|
35
|
+
form.append("marginRight", (options.margin.right / 25.4).toString());
|
|
36
|
+
}
|
|
37
|
+
if (options.landscape) {
|
|
38
|
+
form.append("landscape", "true");
|
|
39
|
+
}
|
|
40
|
+
if (options.printBackground) {
|
|
41
|
+
form.append("printBackground", "true");
|
|
42
|
+
}
|
|
43
|
+
form.append("waitForNetworkIdle", "true");
|
|
44
|
+
form.append("waitDelay", "1s");
|
|
45
|
+
const bodyBuffer = form.getBuffer();
|
|
46
|
+
const headers = {
|
|
47
|
+
...form.getHeaders(),
|
|
48
|
+
// multipart/form-data; boundary=XXX
|
|
49
|
+
"Content-Length": bodyBuffer.length.toString()
|
|
50
|
+
};
|
|
51
|
+
const response = await fetch(`${this.config.url}/forms/chromium/convert/html`, {
|
|
52
|
+
method: "POST",
|
|
53
|
+
body: bodyBuffer,
|
|
54
|
+
headers
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`Gotenberg error: ${response.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
return Buffer.from(await response.arrayBuffer());
|
|
60
|
+
}
|
|
61
|
+
getPageDimensions(format) {
|
|
62
|
+
const formats = {
|
|
63
|
+
A4: { width: "8.27in", height: "11.69in" },
|
|
64
|
+
Letter: { width: "8.5in", height: "11in" },
|
|
65
|
+
Legal: { width: "8.5in", height: "14in" }
|
|
66
|
+
};
|
|
67
|
+
return formats[format] || formats["A4"];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
class BrowserlessProvider {
|
|
71
|
+
constructor(config) {
|
|
72
|
+
this.config = config;
|
|
73
|
+
}
|
|
74
|
+
async generatePdf(html, options) {
|
|
75
|
+
const response = await fetch(`${this.config.url}/pdf?token=${this.config.apiKey}`, {
|
|
76
|
+
method: "POST",
|
|
77
|
+
headers: {
|
|
78
|
+
"Content-Type": "application/json"
|
|
79
|
+
},
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
html,
|
|
82
|
+
options: {
|
|
83
|
+
format: options.format || "A4",
|
|
84
|
+
margin: options.margin,
|
|
85
|
+
landscape: options.landscape || false,
|
|
86
|
+
printBackground: options.printBackground !== false
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Browserless error: ${response.statusText}`);
|
|
92
|
+
}
|
|
93
|
+
return Buffer.from(await response.arrayBuffer());
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
class PuppeteerProvider {
|
|
97
|
+
constructor(config) {
|
|
98
|
+
this.config = config;
|
|
99
|
+
}
|
|
100
|
+
async generatePdf(html, options) {
|
|
101
|
+
const puppeteer = await import("puppeteer").then((m) => m.default);
|
|
102
|
+
const browser = await puppeteer.launch(this.config.launchOptions);
|
|
103
|
+
try {
|
|
104
|
+
const page = await browser.newPage();
|
|
105
|
+
await page.setContent(html, { waitUntil: "networkidle0" });
|
|
106
|
+
const pdfOptions = {
|
|
107
|
+
format: options.format || "A4",
|
|
108
|
+
margin: options.margin,
|
|
109
|
+
landscape: options.landscape || false,
|
|
110
|
+
printBackground: options.printBackground !== false
|
|
111
|
+
};
|
|
112
|
+
const pdfBuffer = await page.pdf(pdfOptions);
|
|
113
|
+
return Buffer.from(pdfBuffer);
|
|
114
|
+
} finally {
|
|
115
|
+
await browser.close();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NuxtModule } from '@nuxt/schema'
|
|
2
|
+
|
|
3
|
+
import type { default as Module } from './module.mjs'
|
|
4
|
+
|
|
5
|
+
export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
|
|
6
|
+
|
|
7
|
+
export { default } from './module.mjs'
|
|
8
|
+
|
|
9
|
+
export { type PdfModuleOptions } from './module.mjs'
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maistik/nuxt-pdf",
|
|
3
|
+
"version": "1.0.17",
|
|
4
|
+
"description": "A Nuxt 3 module for server-side PDF generation using Handlebars templates",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"main": "./dist/module.mjs",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "nuxt-module-build prepare && nuxt-module-build build",
|
|
13
|
+
"dev": "nuxi dev playground",
|
|
14
|
+
"dev:build": "nuxi build playground",
|
|
15
|
+
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxi prepare playground",
|
|
16
|
+
"release": "npm run lint && npm run test:unit && npm run prepack && changelogen --release && npm publish --access public && git push --follow-tags",
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"lint:fix": "eslint . --fix",
|
|
19
|
+
"test": "vitest run && playwright test",
|
|
20
|
+
"test:watch": "vitest watch",
|
|
21
|
+
"test:unit": "vitest run",
|
|
22
|
+
"test:e2e": "playwright test",
|
|
23
|
+
"test:e2e:ui": "playwright test --ui",
|
|
24
|
+
"prepack": "nuxt-module-build build"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@nuxt/kit": "^3.17.7",
|
|
28
|
+
"handlebars": "^4.7.8",
|
|
29
|
+
"puppeteer": "^24.14.0",
|
|
30
|
+
"form-data": "^4.0.4"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@nuxt/devtools": "latest",
|
|
34
|
+
"@nuxt/eslint-config": "^1.6.0",
|
|
35
|
+
"@nuxt/module-builder": "^1.0.1",
|
|
36
|
+
"@nuxt/schema": "^3.17.7",
|
|
37
|
+
"@nuxt/test-utils": "^3.19.2",
|
|
38
|
+
"@playwright/test": "^1.49.1",
|
|
39
|
+
"changelogen": "^0.6.2",
|
|
40
|
+
"eslint": "^9.31.0",
|
|
41
|
+
"nuxt": "^3.17.7",
|
|
42
|
+
"unbuild": "^3.5.0",
|
|
43
|
+
"vitest": "^3.2.4"
|
|
44
|
+
},
|
|
45
|
+
"keywords": [
|
|
46
|
+
"nuxt",
|
|
47
|
+
"nuxt3",
|
|
48
|
+
"pdf",
|
|
49
|
+
"handlebars",
|
|
50
|
+
"gotenberg",
|
|
51
|
+
"browserless",
|
|
52
|
+
"puppeteer"
|
|
53
|
+
],
|
|
54
|
+
"author": "Maistik Studio",
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/Maistik-Studio/nuxt-pdf"
|
|
59
|
+
},
|
|
60
|
+
"packageManager": "pnpm@9.15.3"
|
|
61
|
+
}
|