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 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
- const compiledTemplate = Handlebars.compile(mjmlSource)
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 calls `compiledTemplate({ ...templateData })` with the raw POST body, so if a variable isn't a prop, it won't render when sending.
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
- Call `registerMjmlComponents()` in your Vue SFC to register components for the preview UI. This is auto-imported by the module — no import needed:
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
- | `registerMjmlComponents()` | Registers all `.mjml` files from `components/` as Handlebars partials |
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
 
@@ -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
- const compiledTemplate = Handlebars.compile(mjmlSource)
64
-
65
- const renderedHtml = computed(() => {
66
- try {
67
- const mjmlString = compiledTemplate({ ...props })
68
- const result = mjml2html(mjmlString)
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
- const compiledTemplate = Handlebars.compile(mjmlSource)
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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "1.0.0",
7
+ "version": "1.0.2",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
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.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.6",
67
- "consola": "^3.4.0",
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/devtools": "^3.1.1",
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.0",
88
- "nuxt": "^4.3.1",
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.4"
90
+ "vue-tsc": "^3.2.5"
93
91
  }
94
92
  }