nuxt-generation-emails 1.0.0 → 1.0.2
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 +7 -26
- package/dist/cli/index.mjs +10 -68
- package/dist/module.json +1 -1
- package/dist/module.mjs +80 -0
- package/package.json +7 -9
package/README.md
CHANGED
|
@@ -133,16 +133,8 @@ Every email is a pair of files:
|
|
|
133
133
|
|
|
134
134
|
```vue
|
|
135
135
|
<script setup lang="ts">
|
|
136
|
-
import { computed } from 'vue'
|
|
137
|
-
import mjml2html from 'mjml-browser'
|
|
138
|
-
import Handlebars from 'handlebars'
|
|
139
|
-
import mjmlSource from './example.mjml?raw'
|
|
140
|
-
|
|
141
136
|
defineOptions({ name: 'ExampleNge' })
|
|
142
137
|
|
|
143
|
-
// Auto-imported: registers all .mjml files from components/ as Handlebars partials
|
|
144
|
-
registerMjmlComponents()
|
|
145
|
-
|
|
146
138
|
interface ContentSection {
|
|
147
139
|
heading: string
|
|
148
140
|
body: string
|
|
@@ -163,27 +155,13 @@ const props = withDefaults(defineProps<{
|
|
|
163
155
|
],
|
|
164
156
|
})
|
|
165
157
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const renderedHtml = computed(() => {
|
|
169
|
-
try {
|
|
170
|
-
const mjmlString = compiledTemplate({ ...props })
|
|
171
|
-
return mjml2html(mjmlString).html
|
|
172
|
-
}
|
|
173
|
-
catch (e: unknown) {
|
|
174
|
-
return `<pre style="color:red;">${e instanceof Error ? e.message : String(e)}</pre>`
|
|
175
|
-
}
|
|
176
|
-
})
|
|
158
|
+
useNgeTemplate('example', props)
|
|
177
159
|
</script>
|
|
178
|
-
|
|
179
|
-
<template>
|
|
180
|
-
<div v-html="renderedHtml" />
|
|
181
|
-
</template>
|
|
182
160
|
```
|
|
183
161
|
|
|
184
162
|
### Key architecture rules
|
|
185
163
|
|
|
186
|
-
1. **Every Handlebars variable must be a direct prop** — No computed values or transformations. The server-side API route
|
|
164
|
+
1. **Every Handlebars variable must be a direct prop** — No computed values or transformations. The server-side API route passes `templateData` straight to the Handlebars template, so if a variable isn't a prop, it won't render when sending.
|
|
187
165
|
2. **Use `withDefaults(defineProps<{...}>())`** — Props and defaults are extracted at build time for the preview UI, API docs, and OpenAPI metadata.
|
|
188
166
|
3. **Make all props optional** (`?:`) — Defaults provide sensible preview values.
|
|
189
167
|
|
|
@@ -241,7 +219,9 @@ Components have access to all the props passed to the parent template — Handle
|
|
|
241
219
|
|
|
242
220
|
### Client-side registration
|
|
243
221
|
|
|
244
|
-
|
|
222
|
+
`useNgeTemplate()` automatically registers all components on first call — no manual setup needed.
|
|
223
|
+
|
|
224
|
+
If you need to register components separately (e.g. in a custom composable), `registerMjmlComponents()` is also auto-imported:
|
|
245
225
|
|
|
246
226
|
```ts
|
|
247
227
|
registerMjmlComponents()
|
|
@@ -411,7 +391,8 @@ export default defineNuxtConfig({
|
|
|
411
391
|
|
|
412
392
|
| Function | Description |
|
|
413
393
|
|----------|-------------|
|
|
414
|
-
| `
|
|
394
|
+
| `useNgeTemplate(name, props)` | Loads the MJML template, compiles with Handlebars, registers components, and sets the render function — no `<template>` block needed |
|
|
395
|
+
| `registerMjmlComponents()` | Manually registers all `.mjml` files from `components/` as Handlebars partials (called automatically by `useNgeTemplate`) |
|
|
415
396
|
| `encodeStoreToUrlParams(store)` | Encode a props object into URL search parameters |
|
|
416
397
|
| `generateShareableUrl(store)` | Generate a shareable URL with encoded props |
|
|
417
398
|
|
package/dist/cli/index.mjs
CHANGED
|
@@ -5,30 +5,14 @@ import { existsSync, mkdirSync, writeFileSync, readdirSync, statSync } from 'nod
|
|
|
5
5
|
import { consola } from 'consola';
|
|
6
6
|
import { loadNuxtConfig } from '@nuxt/kit';
|
|
7
7
|
|
|
8
|
-
function generateVueTemplate(emailName) {
|
|
8
|
+
function generateVueTemplate(emailName, emailRelativePath) {
|
|
9
9
|
const capitalizedEmailName = emailName.charAt(0).toUpperCase() + emailName.slice(1);
|
|
10
10
|
const componentName = `${capitalizedEmailName}Nge`;
|
|
11
|
+
const templatePath = emailRelativePath ?? emailName;
|
|
11
12
|
const scriptClose = "<\/script>";
|
|
12
|
-
const templateOpen = "<template>";
|
|
13
|
-
const templateClose = "</template>";
|
|
14
13
|
return `<script setup lang="ts">
|
|
15
|
-
import { computed } from 'vue'
|
|
16
|
-
import mjml2html from 'mjml-browser'
|
|
17
|
-
import Handlebars from 'handlebars'
|
|
18
|
-
import mjmlSource from './${emailName}.mjml?raw'
|
|
19
|
-
|
|
20
14
|
defineOptions({ name: '${componentName}' })
|
|
21
15
|
|
|
22
|
-
/**
|
|
23
|
-
* MJML Components (reusable MJML snippets)
|
|
24
|
-
* Place .mjml files in emails/components/ and they are auto-registered
|
|
25
|
-
* as Handlebars partials on the server side.
|
|
26
|
-
*
|
|
27
|
-
* For client-side preview, uncomment the line below to register them here too.
|
|
28
|
-
* Then use {{> componentName}} in your .mjml template.
|
|
29
|
-
*/
|
|
30
|
-
// registerMjmlComponents()
|
|
31
|
-
|
|
32
16
|
/**
|
|
33
17
|
* Define interfaces for complex prop types here.
|
|
34
18
|
* Every Handlebars variable in the .mjml template must map to a prop
|
|
@@ -60,28 +44,13 @@ const props = withDefaults(defineProps<{
|
|
|
60
44
|
],
|
|
61
45
|
})
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return result.html
|
|
70
|
-
}
|
|
71
|
-
catch (e: unknown) {
|
|
72
|
-
console.error('[${emailName}.vue] Error rendering MJML:', e)
|
|
73
|
-
return \`<pre style="color:red;">\${
|
|
74
|
-
e instanceof Error ? e.message : String(e)
|
|
75
|
-
}\\n\${
|
|
76
|
-
e instanceof Error ? e.stack : ''
|
|
77
|
-
}</pre>\`
|
|
78
|
-
}
|
|
79
|
-
})
|
|
47
|
+
/**
|
|
48
|
+
* useNgeTemplate auto-loads the sibling .mjml file, compiles it with
|
|
49
|
+
* Handlebars, and returns a reactive ComputedRef<string> of rendered HTML.
|
|
50
|
+
* MJML components from components/ are registered automatically.
|
|
51
|
+
*/
|
|
52
|
+
useNgeTemplate('${templatePath}', props)
|
|
80
53
|
${scriptClose}
|
|
81
|
-
|
|
82
|
-
${templateOpen}
|
|
83
|
-
<div v-html="renderedHtml" />
|
|
84
|
-
${templateClose}
|
|
85
54
|
`;
|
|
86
55
|
}
|
|
87
56
|
|
|
@@ -231,7 +200,7 @@ function createEmailFiles(targetDir, emailName, emailPath) {
|
|
|
231
200
|
const mjmlFile = join(targetDir, `${emailName}.mjml`);
|
|
232
201
|
checkFileExists(vueFile);
|
|
233
202
|
checkFileExists(mjmlFile);
|
|
234
|
-
const vueTemplate = generateVueTemplate(emailName);
|
|
203
|
+
const vueTemplate = generateVueTemplate(emailName, emailPath);
|
|
235
204
|
const mjmlTemplate = generateMjmlTemplate(emailName);
|
|
236
205
|
writeFileSync(vueFile, vueTemplate, "utf-8");
|
|
237
206
|
consola.success(`Created email template: ${vueFile}`);
|
|
@@ -379,20 +348,9 @@ function exampleMjml() {
|
|
|
379
348
|
}
|
|
380
349
|
function exampleVue() {
|
|
381
350
|
const scriptClose = "<\/script>";
|
|
382
|
-
const templateOpen = "<template>";
|
|
383
|
-
const templateClose = "</template>";
|
|
384
|
-
const bt = "`";
|
|
385
|
-
const ds = "$";
|
|
386
351
|
return `<script setup lang="ts">
|
|
387
|
-
import { computed } from 'vue'
|
|
388
|
-
import mjml2html from 'mjml-browser'
|
|
389
|
-
import Handlebars from 'handlebars'
|
|
390
|
-
import mjmlSource from './example.mjml?raw'
|
|
391
|
-
|
|
392
352
|
defineOptions({ name: 'ExampleNge' })
|
|
393
353
|
|
|
394
|
-
registerMjmlComponents()
|
|
395
|
-
|
|
396
354
|
interface ContentSection {
|
|
397
355
|
heading: string
|
|
398
356
|
body: string
|
|
@@ -422,24 +380,8 @@ const props = withDefaults(defineProps<{
|
|
|
422
380
|
],
|
|
423
381
|
})
|
|
424
382
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const renderedHtml = computed(() => {
|
|
428
|
-
try {
|
|
429
|
-
const mjmlString = compiledTemplate({ ...props })
|
|
430
|
-
const result = mjml2html(mjmlString)
|
|
431
|
-
return result.html
|
|
432
|
-
}
|
|
433
|
-
catch (e: unknown) {
|
|
434
|
-
console.error('[example.vue] Error rendering MJML:', e)
|
|
435
|
-
return ${bt}<pre style="color:red;">${ds}{e instanceof Error ? e.message : String(e)}\\n${ds}{e instanceof Error ? e.stack : ''}</pre>${bt}
|
|
436
|
-
}
|
|
437
|
-
})
|
|
383
|
+
useNgeTemplate('example', props)
|
|
438
384
|
${scriptClose}
|
|
439
|
-
|
|
440
|
-
${templateOpen}
|
|
441
|
-
<div v-html="renderedHtml" />
|
|
442
|
-
${templateClose}
|
|
443
385
|
`;
|
|
444
386
|
}
|
|
445
387
|
const setupCommand = defineCommand({
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -597,6 +597,82 @@ export function registerMjmlComponents(): void {
|
|
|
597
597
|
Handlebars.registerPartial(name, source as string)
|
|
598
598
|
}
|
|
599
599
|
}
|
|
600
|
+
`
|
|
601
|
+
});
|
|
602
|
+
const templateGlobPath = `~/${configuredEmailDir}`;
|
|
603
|
+
addTemplate({
|
|
604
|
+
filename: "nge/use-template.ts",
|
|
605
|
+
write: true,
|
|
606
|
+
getContents: () => `import { computed, h, getCurrentInstance } from 'vue'
|
|
607
|
+
import type { ComputedRef } from 'vue'
|
|
608
|
+
import mjml2html from 'mjml-browser'
|
|
609
|
+
import Handlebars from 'handlebars'
|
|
610
|
+
import { registerMjmlComponents } from './register-components'
|
|
611
|
+
|
|
612
|
+
const mjmlTemplates: Record<string, string> = import.meta.glob(
|
|
613
|
+
['${templateGlobPath}/**/*.mjml', '!${templateGlobPath}/components/**'],
|
|
614
|
+
{ query: '?raw', import: 'default', eager: true }
|
|
615
|
+
) as Record<string, string>
|
|
616
|
+
|
|
617
|
+
// Build a lookup map: 'example' | 'v1/test' | 'v1/rmi/bid' \u2192 raw MJML source
|
|
618
|
+
const templateMap: Record<string, string> = {}
|
|
619
|
+
for (const [path, source] of Object.entries(mjmlTemplates)) {
|
|
620
|
+
const segments = path.split('/')
|
|
621
|
+
const dirIdx = segments.lastIndexOf('${configuredEmailDir}')
|
|
622
|
+
const relativeParts = dirIdx >= 0 ? segments.slice(dirIdx + 1) : [segments[segments.length - 1]]
|
|
623
|
+
const name = relativeParts.join('/').replace('.mjml', '')
|
|
624
|
+
templateMap[name] = source
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
let _componentsRegistered = false
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Load and render an MJML email template by name.
|
|
631
|
+
* Registers MJML components (Handlebars partials) automatically on first call.
|
|
632
|
+
* Sets the component's render function so no <template> block is needed.
|
|
633
|
+
*
|
|
634
|
+
* @param name - Template name relative to the emails directory (e.g. 'example', 'v1/test')
|
|
635
|
+
* @param props - Reactive props object from defineProps
|
|
636
|
+
* @returns ComputedRef<string> containing the rendered HTML
|
|
637
|
+
*/
|
|
638
|
+
export function useNgeTemplate(name: string, props: Record<string, unknown>): ComputedRef<string> {
|
|
639
|
+
if (!_componentsRegistered) {
|
|
640
|
+
registerMjmlComponents()
|
|
641
|
+
_componentsRegistered = true
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const source = templateMap[name]
|
|
645
|
+
if (!source) {
|
|
646
|
+
const available = Object.keys(templateMap).join(', ')
|
|
647
|
+
console.error(\`[nuxt-gen-emails] Template "\${name}" not found. Available: \${available}\`)
|
|
648
|
+
const fallback = computed(() => \`<pre style="color:red;">Template "\${name}" not found</pre>\`)
|
|
649
|
+
const instance = getCurrentInstance()
|
|
650
|
+
if (instance) instance.render = () => h('div', { innerHTML: fallback.value })
|
|
651
|
+
return fallback
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
const compiled = Handlebars.compile(source)
|
|
655
|
+
|
|
656
|
+
const renderedHtml = computed(() => {
|
|
657
|
+
try {
|
|
658
|
+
const mjmlString = compiled({ ...props })
|
|
659
|
+
const result = mjml2html(mjmlString)
|
|
660
|
+
return result.html
|
|
661
|
+
}
|
|
662
|
+
catch (e: unknown) {
|
|
663
|
+
console.error(\`[\${name}] Error rendering MJML:\`, e)
|
|
664
|
+
return \`<pre style="color:red;">\${e instanceof Error ? e.message : String(e)}\\n\${e instanceof Error ? e.stack : ''}</pre>\`
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
|
|
668
|
+
// Set render function on the component instance so no <template> block is needed
|
|
669
|
+
const instance = getCurrentInstance()
|
|
670
|
+
if (instance) {
|
|
671
|
+
instance.render = () => h('div', { innerHTML: renderedHtml.value })
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return renderedHtml
|
|
675
|
+
}
|
|
600
676
|
`
|
|
601
677
|
});
|
|
602
678
|
nuxt.options.runtimeConfig.nuxtGenEmails = {
|
|
@@ -614,6 +690,10 @@ export function registerMjmlComponents(): void {
|
|
|
614
690
|
{
|
|
615
691
|
name: "registerMjmlComponents",
|
|
616
692
|
from: join(nuxt.options.buildDir, "nge/register-components")
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "useNgeTemplate",
|
|
696
|
+
from: join(nuxt.options.buildDir, "nge/use-template")
|
|
617
697
|
}
|
|
618
698
|
]);
|
|
619
699
|
addServerImports([
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nuxt-generation-emails",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A Nuxt module for authoring, previewing, and sending transactional email templates with MJML and Handlebars.",
|
|
5
5
|
"author": "nullcarry@icloud.com",
|
|
6
6
|
"repository": {
|
|
@@ -63,8 +63,8 @@
|
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@nuxt/kit": "^4.3.1",
|
|
66
|
-
"citty": "^0.1
|
|
67
|
-
"consola": "^3.4.
|
|
66
|
+
"citty": "^0.2.1",
|
|
67
|
+
"consola": "^3.4.2",
|
|
68
68
|
"handlebars": "^4.7.8",
|
|
69
69
|
"mjml": "^4.18.0",
|
|
70
70
|
"mjml-browser": "^4.18.0",
|
|
@@ -75,8 +75,7 @@
|
|
|
75
75
|
"vue": "^3.5.0"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
-
"@nuxt/
|
|
79
|
-
"@nuxt/eslint-config": "^1.14.0",
|
|
78
|
+
"@nuxt/eslint-config": "^1.15.2",
|
|
80
79
|
"@nuxt/module-builder": "^1.0.2",
|
|
81
80
|
"@nuxt/schema": "^4.3.1",
|
|
82
81
|
"@nuxt/test-utils": "^4.0.0",
|
|
@@ -84,11 +83,10 @@
|
|
|
84
83
|
"@types/mjml-browser": "^4.15.0",
|
|
85
84
|
"@types/node": "latest",
|
|
86
85
|
"changelogen": "^0.6.2",
|
|
87
|
-
"eslint": "^10.0.
|
|
88
|
-
"nuxt": "^4.3
|
|
89
|
-
"tailwindcss": "^4.1.18",
|
|
86
|
+
"eslint": "^10.0.2",
|
|
87
|
+
"nuxt": "^4.1.3",
|
|
90
88
|
"typescript": "~5.9.3",
|
|
91
89
|
"vitest": "^4.0.18",
|
|
92
|
-
"vue-tsc": "^3.2.
|
|
90
|
+
"vue-tsc": "^3.2.5"
|
|
93
91
|
}
|
|
94
92
|
}
|