nuxt-generation-emails 0.1.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.
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.mjs +172 -0
- package/dist/module.d.mts +11 -0
- package/dist/module.d.ts +11 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +402 -0
- package/dist/runtime/components/ApiTester.d.vue.ts +6 -0
- package/dist/runtime/components/ApiTester.vue +124 -0
- package/dist/runtime/components/ApiTester.vue.d.ts +6 -0
- package/dist/runtime/components/EmailTemplateSelector.d.vue.ts +6 -0
- package/dist/runtime/components/EmailTemplateSelector.vue +161 -0
- package/dist/runtime/components/EmailTemplateSelector.vue.d.ts +6 -0
- package/dist/runtime/components/NgeTreeNode.d.vue.ts +20 -0
- package/dist/runtime/components/NgeTreeNode.vue +64 -0
- package/dist/runtime/components/NgeTreeNode.vue.d.ts +20 -0
- package/dist/runtime/pages/__emails.d.vue.ts +25 -0
- package/dist/runtime/pages/__emails.vue +151 -0
- package/dist/runtime/pages/__emails.vue.d.ts +25 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/server/utils/send-gen-emails.d.ts +8 -0
- package/dist/runtime/server/utils/send-gen-emails.js +8 -0
- package/dist/runtime/utils/url-params.d.ts +8 -0
- package/dist/runtime/utils/url-params.js +18 -0
- package/dist/types.d.mts +3 -0
- package/package.json +90 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 treygrr
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
# nuxt-generation-emails
|
|
2
|
+
|
|
3
|
+
[![License][license-src]][license-href]
|
|
4
|
+
[![Nuxt][nuxt-src]][nuxt-href]
|
|
5
|
+
|
|
6
|
+
A Nuxt module for authoring, previewing, and sending transactional email templates — all from inside your Nuxt app.
|
|
7
|
+
|
|
8
|
+
## ✨ Features
|
|
9
|
+
|
|
10
|
+
- 📧 **Vue-powered email templates** — Build emails with `@vue-email/components` and Tailwind CSS
|
|
11
|
+
- 🔌 **Auto-generated API routes** — Every template gets a `POST /api/emails/...` endpoint automatically
|
|
12
|
+
- 🖥️ **Live preview UI** — Browse and tweak templates at `/__emails/` with a built-in props editor
|
|
13
|
+
- 🛠️ **CLI scaffolding** — `nuxt-gen-emails add` creates templates with the right structure instantly
|
|
14
|
+
- 🔄 **Hot reload** — New templates are detected automatically during dev (server restarts to register routes)
|
|
15
|
+
- 📋 **OpenAPI docs** — Generated routes include full OpenAPI metadata out of the box
|
|
16
|
+
- 🔗 **Shareable URLs** — Share template previews with pre-filled prop values via URL params
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 📦 1. Installation
|
|
21
|
+
|
|
22
|
+
Install the module in your Nuxt 4+ project:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install nuxt-generation-emails
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then add it to your `nuxt.config.ts`:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export default defineNuxtConfig({
|
|
32
|
+
modules: ['nuxt-generation-emails'],
|
|
33
|
+
|
|
34
|
+
nuxtGenerationEmails: {
|
|
35
|
+
// Directory for email templates (relative to your app's srcDir)
|
|
36
|
+
emailDir: 'emails', // default
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
> **Tip:** Enable Nitro's OpenAPI support to get auto-generated API docs for every email endpoint:
|
|
42
|
+
>
|
|
43
|
+
> ```ts
|
|
44
|
+
> nitro: {
|
|
45
|
+
> experimental: {
|
|
46
|
+
> openAPI: true,
|
|
47
|
+
> },
|
|
48
|
+
> },
|
|
49
|
+
> ```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 📁 2. Folder Structure
|
|
54
|
+
|
|
55
|
+
Templates live inside your app's source directory under the configured `emailDir` (default: `emails/`). You can nest them in subdirectories to organize by version, category, or however you like.
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
app/
|
|
59
|
+
emails/
|
|
60
|
+
v1/
|
|
61
|
+
order-confirmation.vue
|
|
62
|
+
welcome.vue
|
|
63
|
+
v2/
|
|
64
|
+
order-confirmation.vue
|
|
65
|
+
marketing/
|
|
66
|
+
promo.vue
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The directory structure maps directly to routes:
|
|
70
|
+
|
|
71
|
+
| Template file | Preview URL | API endpoint |
|
|
72
|
+
|----------------------------------------|-------------------------------------------|------------------------------------------|
|
|
73
|
+
| `emails/v1/order-confirmation.vue` | `/__emails/v1/order-confirmation` | `POST /api/emails/v1/order-confirmation` |
|
|
74
|
+
| `emails/v1/welcome.vue` | `/__emails/v1/welcome` | `POST /api/emails/v1/welcome` |
|
|
75
|
+
| `emails/v2/order-confirmation.vue` | `/__emails/v2/order-confirmation` | `POST /api/emails/v2/order-confirmation` |
|
|
76
|
+
| `emails/marketing/promo.vue` | `/__emails/marketing/promo` | `POST /api/emails/marketing/promo` |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## 🛠️ 3. Adding Templates with the CLI
|
|
81
|
+
|
|
82
|
+
The fastest way to create a new email template:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npx nuxt-gen-emails add <name>
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Basic usage
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
# Creates emails/welcome.vue
|
|
92
|
+
npx nuxt-gen-emails add welcome
|
|
93
|
+
|
|
94
|
+
# Creates emails/v1/order-confirmation.vue (creates v1/ if it doesn't exist)
|
|
95
|
+
npx nuxt-gen-emails add v1/order-confirmation
|
|
96
|
+
|
|
97
|
+
# Creates emails/marketing/campaigns/summer-sale.vue (deeply nested)
|
|
98
|
+
npx nuxt-gen-emails add marketing/campaigns/summer-sale
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Interactive directory selection
|
|
102
|
+
|
|
103
|
+
If you run the command without a path prefix, and directories already exist, the CLI will ask if you want to place the template in an existing directory:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx nuxt-gen-emails add reset-password
|
|
107
|
+
|
|
108
|
+
# ? Would you like to select an existing directory? (y/N)
|
|
109
|
+
# ? Select a directory:
|
|
110
|
+
# > emails/ (root)
|
|
111
|
+
# emails/v1/
|
|
112
|
+
# emails/v2/
|
|
113
|
+
# emails/marketing/
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### What gets generated
|
|
117
|
+
|
|
118
|
+
Every `add` command creates a single `.vue` file with a ready-to-customize starter template:
|
|
119
|
+
|
|
120
|
+
```vue
|
|
121
|
+
<script setup lang="ts">
|
|
122
|
+
import { Body, Button, Font, Head, Hr, Html, Text, Tailwind } from '@vue-email/components'
|
|
123
|
+
|
|
124
|
+
defineOptions({ name: 'WelcomeNge' })
|
|
125
|
+
|
|
126
|
+
const props = withDefaults(defineProps<{
|
|
127
|
+
title?: string
|
|
128
|
+
message?: string
|
|
129
|
+
}>(), {
|
|
130
|
+
title: 'Welcome!',
|
|
131
|
+
message: 'This is the welcome email template.',
|
|
132
|
+
})
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<template>
|
|
136
|
+
<Tailwind>
|
|
137
|
+
<Html lang="en">
|
|
138
|
+
<Head />
|
|
139
|
+
<Font
|
|
140
|
+
font-family="DM Sans"
|
|
141
|
+
:fallback-font-family="['Arial', 'Helvetica', 'sans-serif']"
|
|
142
|
+
:web-font="{ url: 'https://fonts.gstatic.com/s/dmsans/v15/rP2Hp2ywxg089UriCZOIHTWEBlw.woff2', format: 'woff2' }"
|
|
143
|
+
/>
|
|
144
|
+
<Body style="font-family: 'DM Sans', Arial, Helvetica, sans-serif;">
|
|
145
|
+
<Text>{{ props.title }}</Text>
|
|
146
|
+
<p>{{ props.message }}</p>
|
|
147
|
+
<Hr />
|
|
148
|
+
<Button href="https://example.com">
|
|
149
|
+
Click me
|
|
150
|
+
</Button>
|
|
151
|
+
</Body>
|
|
152
|
+
</Html>
|
|
153
|
+
</Tailwind>
|
|
154
|
+
</template>
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
If the dev server is running, it will automatically detect the new file and restart to register the new routes — no manual restart needed.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## ✍️ 4. Writing Email Templates
|
|
162
|
+
|
|
163
|
+
Templates are standard Vue SFCs using [`@vue-email/components`](https://vuemail.net/). Define your template's dynamic data using `defineProps` with `withDefaults`:
|
|
164
|
+
|
|
165
|
+
```vue
|
|
166
|
+
<script setup lang="ts">
|
|
167
|
+
import { Body, Button, Container, Head, Heading, Html, Text, Tailwind } from '@vue-email/components'
|
|
168
|
+
|
|
169
|
+
defineOptions({ name: 'OrderConfirmationNge' })
|
|
170
|
+
|
|
171
|
+
const props = withDefaults(defineProps<{
|
|
172
|
+
customerName?: string
|
|
173
|
+
orderNumber?: string
|
|
174
|
+
orderDate?: string
|
|
175
|
+
totalAmount?: number
|
|
176
|
+
}>(), {
|
|
177
|
+
customerName: 'Customer',
|
|
178
|
+
orderNumber: 'ORD-000000',
|
|
179
|
+
orderDate: 'January 1, 2026',
|
|
180
|
+
totalAmount: 0,
|
|
181
|
+
})
|
|
182
|
+
</script>
|
|
183
|
+
|
|
184
|
+
<template>
|
|
185
|
+
<Tailwind>
|
|
186
|
+
<Html lang="en">
|
|
187
|
+
<Head />
|
|
188
|
+
<Body>
|
|
189
|
+
<Container>
|
|
190
|
+
<Heading as="h1">Order Confirmed!</Heading>
|
|
191
|
+
<Text>Hi {{ props.customerName }},</Text>
|
|
192
|
+
<Text>Your order #{{ props.orderNumber }} placed on {{ props.orderDate }} is confirmed.</Text>
|
|
193
|
+
<Text>Total: ${{ props.totalAmount?.toFixed(2) }}</Text>
|
|
194
|
+
<Button href="https://example.com/orders">View Order</Button>
|
|
195
|
+
</Container>
|
|
196
|
+
</Body>
|
|
197
|
+
</Html>
|
|
198
|
+
</Tailwind>
|
|
199
|
+
</template>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Key rules
|
|
203
|
+
|
|
204
|
+
1. **Use `withDefaults(defineProps<{...}>())`** — Props are extracted at build time to populate the preview UI and API example payloads
|
|
205
|
+
2. **Make all props optional** (`?:`) — The defaults provide sensible preview values
|
|
206
|
+
3. **Supported prop types** — `string`, `number`, `boolean` appear as editable fields in the preview UI. Complex types (objects, arrays) work fine but are only editable via the JSON editor
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
## 🖥️ 5. Using the Preview UI
|
|
211
|
+
|
|
212
|
+
Navigate to `/__emails/` in your browser during development to access the preview interface.
|
|
213
|
+
|
|
214
|
+
### What you'll see
|
|
215
|
+
|
|
216
|
+
- **Template selector** — A dropdown at the top of the page lists all templates, organized by directory. Click a folder to expand/collapse it, click a template name to load it.
|
|
217
|
+
- **Props sidebar** — String and number props appear as editable input fields on the left. Changes update the preview in real time.
|
|
218
|
+
- **Live preview** — The rendered email is displayed on the right, exactly as it will look when sent.
|
|
219
|
+
- **Share URL button** — Copies a URL with the current prop values encoded as query parameters, useful for sharing specific test states with teammates.
|
|
220
|
+
- **API tester** — At the bottom of the sidebar, a built-in API tester lets you fire a real `POST` request to the template's endpoint. It shows the full JSON request body (editable) and the response including the rendered HTML.
|
|
221
|
+
|
|
222
|
+
### Navigation
|
|
223
|
+
|
|
224
|
+
Every template is accessible at a direct URL matching its file path:
|
|
225
|
+
|
|
226
|
+
```
|
|
227
|
+
http://localhost:3000/__emails/v1/order-confirmation
|
|
228
|
+
http://localhost:3000/__emails/v2/welcome
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 📨 6. Wiring Up Email Sending (SendGrid Example)
|
|
234
|
+
|
|
235
|
+
The module generates `POST` endpoints for every template that render the email HTML. To actually **send** emails, provide a `sendGenEmails` function in your module config.
|
|
236
|
+
|
|
237
|
+
### With SendGrid
|
|
238
|
+
|
|
239
|
+
Install the SendGrid SDK:
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
npm install @sendgrid/mail
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Configure the handler in `nuxt.config.ts`:
|
|
246
|
+
|
|
247
|
+
```ts
|
|
248
|
+
import sgMail from '@sendgrid/mail'
|
|
249
|
+
|
|
250
|
+
sgMail.setApiKey(process.env.SENDGRID_API_KEY!)
|
|
251
|
+
|
|
252
|
+
export default defineNuxtConfig({
|
|
253
|
+
modules: ['nuxt-generation-emails'],
|
|
254
|
+
|
|
255
|
+
nuxtGenerationEmails: {
|
|
256
|
+
emailDir: 'emails',
|
|
257
|
+
|
|
258
|
+
sendGenEmails: async (html, data) => {
|
|
259
|
+
await sgMail.send({
|
|
260
|
+
to: data.to as string,
|
|
261
|
+
from: 'noreply@yourdomain.com',
|
|
262
|
+
subject: data.subject as string || 'No Subject',
|
|
263
|
+
html,
|
|
264
|
+
})
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### How it works
|
|
271
|
+
|
|
272
|
+
When a `POST` request hits an email endpoint (e.g., `/api/emails/v1/order-confirmation`):
|
|
273
|
+
|
|
274
|
+
1. The request body is read and passed as props to the Vue email template
|
|
275
|
+
2. `@vue-email/render` renders the template to an HTML string
|
|
276
|
+
3. If `sendGenEmails` is configured, it's called with the rendered `html` and the original request `data`
|
|
277
|
+
4. The response always returns `{ success: true, html: "..." }` so you can inspect the rendered output
|
|
278
|
+
|
|
279
|
+
### Example API call
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
curl -X POST http://localhost:3000/api/emails/v1/order-confirmation \
|
|
283
|
+
-H "Content-Type: application/json" \
|
|
284
|
+
-d '{
|
|
285
|
+
"customerName": "Jane Doe",
|
|
286
|
+
"orderNumber": "ORD-123456",
|
|
287
|
+
"orderDate": "February 17, 2026",
|
|
288
|
+
"totalAmount": 89.99,
|
|
289
|
+
"to": "jane@example.com",
|
|
290
|
+
"subject": "Your Order is Confirmed!"
|
|
291
|
+
}'
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Using the Nitro hook alternative
|
|
295
|
+
|
|
296
|
+
If you prefer not to configure the handler in `nuxt.config.ts`, you can listen for the `nuxt-gen-emails:send` hook in a Nitro plugin:
|
|
297
|
+
|
|
298
|
+
```ts
|
|
299
|
+
// server/plugins/email-sender.ts
|
|
300
|
+
export default defineNitroPlugin((nitro) => {
|
|
301
|
+
nitro.hooks.hook('nuxt-gen-emails:send', async ({ html, data }) => {
|
|
302
|
+
// Your sending logic here
|
|
303
|
+
console.log('Sending email with data:', data)
|
|
304
|
+
})
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## 🔧 7. Module Options
|
|
311
|
+
|
|
312
|
+
| Option | Type | Default | Description |
|
|
313
|
+
|------------------|------------|------------|--------------------------------------------------------------------|
|
|
314
|
+
| `emailDir` | `string` | `'emails'` | Directory containing email templates (relative to `srcDir`) |
|
|
315
|
+
| `sendGenEmails` | `function` | `undefined`| Async function called with `(html, data)` when an email is sent |
|
|
316
|
+
|
|
317
|
+
Full config key: `nuxtGenerationEmails`
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## 🧩 Auto-Imports
|
|
322
|
+
|
|
323
|
+
The module auto-imports these utilities for convenience:
|
|
324
|
+
|
|
325
|
+
### Client-side
|
|
326
|
+
|
|
327
|
+
- `encodeStoreToUrlParams(store)` — Encode a props object into URL search parameters
|
|
328
|
+
- `generateShareableUrl(store)` — Generate a full shareable URL for the current template with encoded props
|
|
329
|
+
|
|
330
|
+
### Server-side
|
|
331
|
+
|
|
332
|
+
- `getSendGenEmailsHandler()` — Retrieve the configured `sendGenEmails` function (used internally by generated routes)
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 🏗️ Development
|
|
337
|
+
|
|
338
|
+
<details>
|
|
339
|
+
<summary>Local development</summary>
|
|
340
|
+
|
|
341
|
+
```bash
|
|
342
|
+
# Install dependencies
|
|
343
|
+
npm install
|
|
344
|
+
|
|
345
|
+
# Generate type stubs
|
|
346
|
+
npm run dev:prepare
|
|
347
|
+
|
|
348
|
+
# Develop with the playground
|
|
349
|
+
npm run dev
|
|
350
|
+
|
|
351
|
+
# Build the playground
|
|
352
|
+
npm run dev:build
|
|
353
|
+
|
|
354
|
+
# Run ESLint
|
|
355
|
+
npm run lint
|
|
356
|
+
|
|
357
|
+
# Run Vitest
|
|
358
|
+
npm run test
|
|
359
|
+
npm run test:watch
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
</details>
|
|
363
|
+
|
|
364
|
+
## License
|
|
365
|
+
|
|
366
|
+
[MIT](./LICENSE)
|
|
367
|
+
|
|
368
|
+
<!-- Badges -->
|
|
369
|
+
[license-src]: https://img.shields.io/npm/l/nuxt-generation-emails.svg?style=flat&colorA=020420&colorB=00DC82
|
|
370
|
+
[license-href]: https://npmjs.com/package/nuxt-generation-emails
|
|
371
|
+
|
|
372
|
+
[nuxt-src]: https://img.shields.io/badge/Nuxt-020420?logo=nuxt
|
|
373
|
+
[nuxt-href]: https://nuxt.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from 'citty';
|
|
3
|
+
import { join, relative } from 'pathe';
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import { consola } from 'consola';
|
|
6
|
+
import { loadNuxtConfig } from '@nuxt/kit';
|
|
7
|
+
|
|
8
|
+
function generateVueTemplate(emailName) {
|
|
9
|
+
const capitalizedEmailName = emailName.charAt(0).toUpperCase() + emailName.slice(1);
|
|
10
|
+
const componentName = `${capitalizedEmailName}Nge`;
|
|
11
|
+
return `<script setup lang="ts">
|
|
12
|
+
import { Body, Button, Font, Head, Hr, Html, Text, Tailwind } from '@vue-email/components'
|
|
13
|
+
|
|
14
|
+
defineOptions({ name: '${componentName}' })
|
|
15
|
+
|
|
16
|
+
const props = withDefaults(defineProps<{
|
|
17
|
+
title?: string
|
|
18
|
+
message?: string
|
|
19
|
+
}>(), {
|
|
20
|
+
title: 'Welcome!',
|
|
21
|
+
message: 'This is the ${emailName} email template.',
|
|
22
|
+
})
|
|
23
|
+
<\/script>
|
|
24
|
+
|
|
25
|
+
<template>
|
|
26
|
+
<Tailwind>
|
|
27
|
+
<Html lang="en">
|
|
28
|
+
<Head />
|
|
29
|
+
<Font
|
|
30
|
+
font-family="DM Sans"
|
|
31
|
+
:fallback-font-family="['Arial', 'Helvetica', 'sans-serif']"
|
|
32
|
+
:web-font="{ url: 'https://fonts.gstatic.com/s/dmsans/v15/rP2Hp2ywxg089UriCZOIHTWEBlw.woff2', format: 'woff2' }"
|
|
33
|
+
/>
|
|
34
|
+
<Body style="font-family: 'DM Sans', Arial, Helvetica, sans-serif;">
|
|
35
|
+
<Text>{{ props.title }}</Text>
|
|
36
|
+
<p>{{ props.message }}</p>
|
|
37
|
+
<Hr />
|
|
38
|
+
<Button href="https://example.com">
|
|
39
|
+
Click me
|
|
40
|
+
</Button>
|
|
41
|
+
</Body>
|
|
42
|
+
</Html>
|
|
43
|
+
</Tailwind>
|
|
44
|
+
</template>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function findEmailsDir() {
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
try {
|
|
51
|
+
const config = await loadNuxtConfig({ cwd });
|
|
52
|
+
const srcDir = config.srcDir || cwd;
|
|
53
|
+
return join(srcDir, "emails");
|
|
54
|
+
} catch {
|
|
55
|
+
if (existsSync(join(cwd, "app"))) {
|
|
56
|
+
return join(cwd, "app", "emails");
|
|
57
|
+
}
|
|
58
|
+
if (existsSync(join(cwd, "src"))) {
|
|
59
|
+
return join(cwd, "src", "emails");
|
|
60
|
+
}
|
|
61
|
+
return join(cwd, "emails");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function getAllDirectories(dirPath, basePath = dirPath) {
|
|
65
|
+
const dirs = [];
|
|
66
|
+
if (!existsSync(dirPath)) {
|
|
67
|
+
return dirs;
|
|
68
|
+
}
|
|
69
|
+
const entries = readdirSync(dirPath);
|
|
70
|
+
for (const entry of entries) {
|
|
71
|
+
const fullPath = join(dirPath, entry);
|
|
72
|
+
if (statSync(fullPath).isDirectory()) {
|
|
73
|
+
const relativePath = relative(basePath, fullPath);
|
|
74
|
+
dirs.push(relativePath);
|
|
75
|
+
dirs.push(...getAllDirectories(fullPath, basePath));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return dirs;
|
|
79
|
+
}
|
|
80
|
+
function parseTemplateName(name) {
|
|
81
|
+
const namePath = name.replace(/^\.\//, "").replace(/\.vue$/, "");
|
|
82
|
+
const parts = namePath.split("/");
|
|
83
|
+
const emailName = parts.pop();
|
|
84
|
+
const subDir = parts.length > 0 ? join(...parts) : "";
|
|
85
|
+
return { emailName, subDir };
|
|
86
|
+
}
|
|
87
|
+
async function promptForDirectory(emailsDir, initialSubDir, argsDir) {
|
|
88
|
+
if (initialSubDir || argsDir) {
|
|
89
|
+
return initialSubDir;
|
|
90
|
+
}
|
|
91
|
+
const existingDirs = getAllDirectories(emailsDir);
|
|
92
|
+
if (existingDirs.length === 0) {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
const useExisting = await consola.prompt("Would you like to select an existing directory?", {
|
|
96
|
+
type: "confirm",
|
|
97
|
+
initial: false
|
|
98
|
+
});
|
|
99
|
+
if (!useExisting) {
|
|
100
|
+
return "";
|
|
101
|
+
}
|
|
102
|
+
const selectedDir = await consola.prompt("Select a directory:", {
|
|
103
|
+
type: "select",
|
|
104
|
+
options: [
|
|
105
|
+
{ label: "emails/ (root)", value: "" },
|
|
106
|
+
...existingDirs.map((dir) => ({ label: `emails/${dir}/`, value: dir }))
|
|
107
|
+
]
|
|
108
|
+
});
|
|
109
|
+
return selectedDir;
|
|
110
|
+
}
|
|
111
|
+
function ensureDirectoryExists(dirPath, description) {
|
|
112
|
+
if (!existsSync(dirPath)) {
|
|
113
|
+
mkdirSync(dirPath, { recursive: true });
|
|
114
|
+
consola.success(`Created ${description}: ${dirPath}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function checkFileExists(filePath) {
|
|
118
|
+
if (existsSync(filePath)) {
|
|
119
|
+
consola.error(`Email template already exists: ${filePath}`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function createEmailFiles(targetDir, emailName, emailPath) {
|
|
124
|
+
const vueFile = join(targetDir, `${emailName}.vue`);
|
|
125
|
+
checkFileExists(vueFile);
|
|
126
|
+
const vueTemplate = generateVueTemplate(emailName);
|
|
127
|
+
writeFileSync(vueFile, vueTemplate, "utf-8");
|
|
128
|
+
consola.success(`Created email template: ${vueFile}`);
|
|
129
|
+
return {
|
|
130
|
+
emailPath,
|
|
131
|
+
vueFile
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
const addCommand = defineCommand({
|
|
135
|
+
meta: {
|
|
136
|
+
name: "add",
|
|
137
|
+
description: "Scaffold a new email template"
|
|
138
|
+
},
|
|
139
|
+
args: {
|
|
140
|
+
name: {
|
|
141
|
+
type: "positional",
|
|
142
|
+
description: "Name of the email template to create",
|
|
143
|
+
required: true
|
|
144
|
+
},
|
|
145
|
+
dir: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: "Directory to create the email in (relative to emails folder)",
|
|
148
|
+
default: ""
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
async run({ args }) {
|
|
152
|
+
const emailsDir = await findEmailsDir();
|
|
153
|
+
const { emailName, subDir: initialSubDir } = parseTemplateName(args.name);
|
|
154
|
+
const subDir = await promptForDirectory(emailsDir, initialSubDir, args.dir);
|
|
155
|
+
const targetDir = join(emailsDir, args.dir, subDir);
|
|
156
|
+
ensureDirectoryExists(targetDir, "emails directory");
|
|
157
|
+
const emailPath = join(subDir, emailName).replace(/\\/g, "/");
|
|
158
|
+
createEmailFiles(targetDir, emailName, emailPath);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
const main = defineCommand({
|
|
163
|
+
meta: {
|
|
164
|
+
name: "nuxt-gen-emails",
|
|
165
|
+
description: "CLI for nuxt-gen-emails module",
|
|
166
|
+
version: "1.0.0"
|
|
167
|
+
},
|
|
168
|
+
subCommands: {
|
|
169
|
+
add: addCommand
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
runMain(main);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
/** Directory containing email templates; resolved from srcDir when relative. */
|
|
5
|
+
emailDir?: string;
|
|
6
|
+
sendGenEmails?: (html: string, data: Record<string, unknown>) => Promise<void> | void;
|
|
7
|
+
}
|
|
8
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
9
|
+
|
|
10
|
+
export { _default as default };
|
|
11
|
+
export type { ModuleOptions };
|
package/dist/module.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as _nuxt_schema from '@nuxt/schema';
|
|
2
|
+
|
|
3
|
+
interface ModuleOptions {
|
|
4
|
+
/** Directory containing email templates; resolved from srcDir when relative. */
|
|
5
|
+
emailDir?: string;
|
|
6
|
+
sendGenEmails?: (html: string, data: Record<string, unknown>) => Promise<void> | void;
|
|
7
|
+
}
|
|
8
|
+
declare const _default: _nuxt_schema.NuxtModule<ModuleOptions, ModuleOptions, false>;
|
|
9
|
+
|
|
10
|
+
export { _default as default };
|
|
11
|
+
export type { ModuleOptions };
|
package/dist/module.json
ADDED