nuxt-generation-emails 1.0.4 → 1.0.7

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/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.0.0"
6
6
  },
7
- "version": "1.0.4",
7
+ "version": "1.0.7",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -4,7 +4,7 @@ import { join, relative } from 'pathe';
4
4
  import { consola } from 'consola';
5
5
  import { parse, compileScript } from 'vue/compiler-sfc';
6
6
 
7
- function generateWrapperComponent(emailsLayoutPath, emailComponentPath) {
7
+ function generateWrapperComponent(emailsLayoutPath, emailComponentPath, extractedPropTypes = {}) {
8
8
  const scriptClose = "<\/script>";
9
9
  const templateOpen = "<template>";
10
10
  const templateClose = "</template>";
@@ -37,6 +37,12 @@ function inferType(ctor: unknown): 'string' | 'number' | 'boolean' | 'object' |
37
37
  if (ctor === Number) return 'number'
38
38
  if (ctor === Boolean) return 'boolean'
39
39
  if (ctor === Object || ctor === Array) return 'object'
40
+ if (Array.isArray(ctor)) {
41
+ if (ctor.includes(String)) return 'string'
42
+ if (ctor.includes(Number)) return 'number'
43
+ if (ctor.includes(Boolean)) return 'boolean'
44
+ if (ctor.includes(Object) || ctor.includes(Array)) return 'object'
45
+ }
40
46
  return 'unknown'
41
47
  }
42
48
 
@@ -45,6 +51,8 @@ interface PropDefinition {
45
51
  type: 'string' | 'number' | 'boolean' | 'object' | 'unknown'
46
52
  }
47
53
 
54
+ const extractedTypes = ${JSON.stringify(extractedPropTypes, null, 2)} as Record<string, PropDefinition['type']>
55
+
48
56
  const introspected = computed(() => {
49
57
  const comp = emailComponent.value as any
50
58
  const raw = comp?.props
@@ -55,12 +63,13 @@ const introspected = computed(() => {
55
63
 
56
64
  if (Array.isArray(raw)) {
57
65
  for (const name of raw) {
58
- defs.push({ name, type: 'unknown' })
66
+ defs.push({ name, type: extractedTypes[name] ?? 'unknown' })
59
67
  }
60
68
  } else {
61
69
  for (const [name, spec] of Object.entries(raw as Record<string, any>)) {
62
70
  const ctor = spec?.type ?? spec
63
- defs.push({ name, type: inferType(ctor) })
71
+ const runtimeType = inferType(ctor)
72
+ defs.push({ name, type: runtimeType !== 'unknown' ? runtimeType : (extractedTypes[name] ?? 'unknown') })
64
73
 
65
74
  if (spec?.default !== undefined) {
66
75
  defaults[name] = typeof spec.default === 'function' ? spec.default() : spec.default
@@ -135,6 +144,298 @@ ${templateClose}
135
144
  `;
136
145
  }
137
146
 
147
+ function extractPropsFromSFC(filePath) {
148
+ const source = fs.readFileSync(filePath, "utf-8");
149
+ const { descriptor } = parse(source, { filename: filePath });
150
+ if (!descriptor.scriptSetup) {
151
+ return { props: [], defaults: {} };
152
+ }
153
+ try {
154
+ const compiled = compileScript(descriptor, {
155
+ id: filePath,
156
+ isProd: false
157
+ });
158
+ const props = [];
159
+ const defaults = {};
160
+ const propNames = [];
161
+ if (compiled.bindings) {
162
+ for (const [name, binding] of Object.entries(compiled.bindings)) {
163
+ if (binding === "props") {
164
+ propNames.push(name);
165
+ }
166
+ }
167
+ }
168
+ const propTypes = extractPropTypesFromSource(descriptor.scriptSetup.content);
169
+ for (const name of propNames) {
170
+ const type = propTypes[name] ?? "unknown";
171
+ props.push({ name, type });
172
+ }
173
+ const defaultValues = extractDefaults(compiled.scriptSetupAst, descriptor.scriptSetup.content);
174
+ for (const [name, value] of Object.entries(defaultValues)) {
175
+ defaults[name] = value;
176
+ const existing = props.find((p) => p.name === name);
177
+ if (existing) {
178
+ existing.default = value;
179
+ }
180
+ }
181
+ return { props, defaults };
182
+ } catch {
183
+ return { props: [], defaults: {} };
184
+ }
185
+ }
186
+ function asAstNode(value) {
187
+ if (!value || typeof value !== "object") return null;
188
+ const node = value;
189
+ if (typeof node.type !== "string") return null;
190
+ return node;
191
+ }
192
+ function extractDefaults(scriptSetupAst, scriptContent) {
193
+ const astDefaults = extractDefaultsFromAst(scriptSetupAst);
194
+ if (astDefaults) return astDefaults;
195
+ return extractDefaultsFromSource(scriptContent);
196
+ }
197
+ function extractDefaultsFromAst(scriptSetupAst) {
198
+ if (!Array.isArray(scriptSetupAst)) return null;
199
+ for (const root of scriptSetupAst) {
200
+ const rootNode = asAstNode(root);
201
+ if (!rootNode) continue;
202
+ const found = findWithDefaultsSecondArg(rootNode);
203
+ if (!found) continue;
204
+ try {
205
+ const evaluated = evaluateAstExpression(found);
206
+ if (evaluated && typeof evaluated === "object" && !Array.isArray(evaluated)) {
207
+ return evaluated;
208
+ }
209
+ } catch (error) {
210
+ debugLog("Failed AST-based withDefaults extraction; falling back to source parser", error);
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+ function findWithDefaultsSecondArg(node) {
216
+ if (node.type === "CallExpression") {
217
+ const callee = asAstNode(node.callee);
218
+ if (callee?.type === "Identifier" && callee.name === "withDefaults") {
219
+ const args = Array.isArray(node.arguments) ? node.arguments : [];
220
+ const secondArg = asAstNode(args[1]);
221
+ if (secondArg) return secondArg;
222
+ }
223
+ }
224
+ for (const value of Object.values(node)) {
225
+ if (Array.isArray(value)) {
226
+ for (const item of value) {
227
+ const child2 = asAstNode(item);
228
+ if (!child2) continue;
229
+ const found2 = findWithDefaultsSecondArg(child2);
230
+ if (found2) return found2;
231
+ }
232
+ continue;
233
+ }
234
+ const child = asAstNode(value);
235
+ if (!child) continue;
236
+ const found = findWithDefaultsSecondArg(child);
237
+ if (found) return found;
238
+ }
239
+ return null;
240
+ }
241
+ function evaluateAstExpression(node) {
242
+ switch (node.type) {
243
+ case "ObjectExpression": {
244
+ const result = {};
245
+ const properties = Array.isArray(node.properties) ? node.properties : [];
246
+ for (const propLike of properties) {
247
+ const prop = asAstNode(propLike);
248
+ if (!prop) continue;
249
+ if (prop.type !== "ObjectProperty") continue;
250
+ const keyNode = asAstNode(prop.key);
251
+ const key = keyNode?.type === "Identifier" ? String(keyNode.name) : keyNode?.type === "StringLiteral" ? String(keyNode.value) : null;
252
+ if (!key) continue;
253
+ const valueNode = asAstNode(prop.value);
254
+ if (!valueNode) continue;
255
+ result[key] = evaluateAstExpression(valueNode);
256
+ }
257
+ return result;
258
+ }
259
+ case "ArrayExpression": {
260
+ const elements = Array.isArray(node.elements) ? node.elements : [];
261
+ return elements.map((element) => asAstNode(element)).filter((element) => !!element).map((element) => evaluateAstExpression(element));
262
+ }
263
+ case "StringLiteral":
264
+ return node.value;
265
+ case "NumericLiteral":
266
+ return node.value;
267
+ case "BooleanLiteral":
268
+ return node.value;
269
+ case "NullLiteral":
270
+ return null;
271
+ case "TemplateLiteral": {
272
+ const expressions = Array.isArray(node.expressions) ? node.expressions : [];
273
+ if (expressions.length > 0) {
274
+ throw new Error("Unsupported TemplateLiteral with expressions in defaults");
275
+ }
276
+ const quasis = Array.isArray(node.quasis) ? node.quasis : [];
277
+ return quasis.map((quasi) => {
278
+ const quasiNode = asAstNode(quasi);
279
+ const cooked = quasiNode?.value && typeof quasiNode.value === "object" ? quasiNode.value.cooked : void 0;
280
+ return typeof cooked === "string" ? cooked : "";
281
+ }).join("");
282
+ }
283
+ case "ArrowFunctionExpression": {
284
+ const body = asAstNode(node.body);
285
+ if (!body) return void 0;
286
+ if (body.type === "BlockStatement") {
287
+ const statements = Array.isArray(body.body) ? body.body : [];
288
+ const returnStmt = statements.map((stmt) => asAstNode(stmt)).find((stmt) => stmt?.type === "ReturnStatement");
289
+ const arg = returnStmt ? asAstNode(returnStmt.argument) : null;
290
+ return arg ? evaluateAstExpression(arg) : void 0;
291
+ }
292
+ return evaluateAstExpression(body);
293
+ }
294
+ case "ParenthesizedExpression": {
295
+ const expr = asAstNode(node.expression);
296
+ return expr ? evaluateAstExpression(expr) : void 0;
297
+ }
298
+ case "TSAsExpression": {
299
+ const expr = asAstNode(node.expression);
300
+ return expr ? evaluateAstExpression(expr) : void 0;
301
+ }
302
+ case "Identifier": {
303
+ if (node.name === "undefined") return void 0;
304
+ throw new Error(`Unsupported identifier in defaults: ${String(node.name)}`);
305
+ }
306
+ default:
307
+ throw new Error(`Unsupported AST node in defaults: ${node.type}`);
308
+ }
309
+ }
310
+ function extractPropTypesFromSource(scriptContent) {
311
+ const result = {};
312
+ const match = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}\s*>\s*\(/);
313
+ if (!match) return result;
314
+ const typeBody = match[1];
315
+ const propPattern = /(\w+)\s*(?:\?\s*)?:\s*(\w+)/g;
316
+ let propMatch;
317
+ while ((propMatch = propPattern.exec(typeBody)) !== null) {
318
+ const name = propMatch[1];
319
+ const tsType = propMatch[2];
320
+ switch (tsType.toLowerCase()) {
321
+ case "string":
322
+ result[name] = "string";
323
+ break;
324
+ case "number":
325
+ result[name] = "number";
326
+ break;
327
+ case "boolean":
328
+ result[name] = "boolean";
329
+ break;
330
+ default:
331
+ result[name] = "object";
332
+ break;
333
+ }
334
+ }
335
+ return result;
336
+ }
337
+ function extractDefaultsFromSource(scriptContent) {
338
+ const defaultsText = extractDefaultsObjectText(scriptContent);
339
+ if (!defaultsText) return {};
340
+ try {
341
+ const fn = new Function(`
342
+ const _defaults = (${defaultsText});
343
+ const _result = {};
344
+ for (const [k, v] of Object.entries(_defaults)) {
345
+ _result[k] = typeof v === 'function' ? v() : v;
346
+ }
347
+ return _result;
348
+ `);
349
+ return fn();
350
+ } catch {
351
+ return extractPrimitivesFromObjectLiteral(defaultsText);
352
+ }
353
+ }
354
+ function extractDefaultsObjectText(scriptContent) {
355
+ const wdIdx = scriptContent.indexOf("withDefaults(");
356
+ if (wdIdx === -1) return null;
357
+ const start = scriptContent.indexOf("(", wdIdx + "withDefaults".length);
358
+ if (start === -1) return null;
359
+ let depth = 1;
360
+ let i = start + 1;
361
+ let separatorComma = -1;
362
+ while (i < scriptContent.length && depth > 0) {
363
+ const ch = scriptContent[i];
364
+ if (ch === "'" || ch === '"') {
365
+ const quote = ch;
366
+ i++;
367
+ while (i < scriptContent.length) {
368
+ if (scriptContent[i] === "\\") {
369
+ i += 2;
370
+ continue;
371
+ }
372
+ if (scriptContent[i] === quote) break;
373
+ i++;
374
+ }
375
+ i++;
376
+ continue;
377
+ }
378
+ if (ch === "`") {
379
+ i++;
380
+ while (i < scriptContent.length) {
381
+ if (scriptContent[i] === "\\") {
382
+ i += 2;
383
+ continue;
384
+ }
385
+ if (scriptContent[i] === "`") break;
386
+ if (scriptContent[i] === "$" && scriptContent[i + 1] === "{") {
387
+ i += 2;
388
+ let tdepth = 1;
389
+ while (i < scriptContent.length && tdepth > 0) {
390
+ if (scriptContent[i] === "{") tdepth++;
391
+ else if (scriptContent[i] === "}") tdepth--;
392
+ if (tdepth > 0) i++;
393
+ }
394
+ }
395
+ i++;
396
+ }
397
+ i++;
398
+ continue;
399
+ }
400
+ if (ch === "/" && scriptContent[i + 1] === "/") {
401
+ i = scriptContent.indexOf("\n", i);
402
+ if (i === -1) break;
403
+ i++;
404
+ continue;
405
+ }
406
+ if (ch === "/" && scriptContent[i + 1] === "*") {
407
+ i = scriptContent.indexOf("*/", i) + 2;
408
+ if (i < 2) break;
409
+ continue;
410
+ }
411
+ if (ch === "(" || ch === "{" || ch === "[") {
412
+ depth++;
413
+ } else if (ch === ")" || ch === "}" || ch === "]") {
414
+ depth--;
415
+ if (depth === 0) break;
416
+ } else if (ch === "," && depth === 1 && separatorComma === -1) {
417
+ separatorComma = i;
418
+ }
419
+ i++;
420
+ }
421
+ if (separatorComma === -1) return null;
422
+ const secondArg = scriptContent.slice(separatorComma + 1, i).trim();
423
+ return secondArg || null;
424
+ }
425
+ function extractPrimitivesFromObjectLiteral(text) {
426
+ const result = {};
427
+ const linePattern = /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|(\d+(?:\.\d+)?)|(true|false))\s*[,}]?/g;
428
+ let match;
429
+ while ((match = linePattern.exec(text)) !== null) {
430
+ const key = match[1];
431
+ if (match[2] != null) result[key] = match[2];
432
+ else if (match[3] != null) result[key] = match[3];
433
+ else if (match[4] != null) result[key] = Number(match[4]);
434
+ else if (match[5] != null) result[key] = match[5] === "true";
435
+ }
436
+ return result;
437
+ }
438
+
138
439
  function addEmailPages(dirPath, pages, options, routePrefix = "") {
139
440
  const entries = fs.readdirSync(dirPath);
140
441
  for (const entry of entries) {
@@ -155,7 +456,11 @@ function addEmailPages(dirPath, pages, options, routePrefix = "") {
155
456
  }
156
457
  const wrapperContent = generateWrapperComponent(
157
458
  options.emailTemplateComponentPath,
158
- fullPath
459
+ fullPath,
460
+ extractPropsFromSFC(fullPath).props.reduce((acc, prop) => {
461
+ acc[prop.name] = prop.type;
462
+ return acc;
463
+ }, {})
159
464
  );
160
465
  fs.writeFileSync(wrapperPath, wrapperContent, "utf-8");
161
466
  const pageName = `email${routePrefix.replace(/\//g, "-")}-${name}`.replace(/^-+/, "");
@@ -175,7 +480,7 @@ function generateApiRoute(emailName, emailPath, examplePayload = "{}") {
175
480
  import { defineEventHandler, readBody, createError } from 'h3'
176
481
  import { useNitroApp, useRuntimeConfig } from '#imports'
177
482
  import { readFileSync, existsSync, readdirSync } from 'node:fs'
178
- import { join, basename } from 'node:path'
483
+ import { join, relative } from 'node:path'
179
484
  import mjml2html from 'mjml'
180
485
  import Handlebars from 'handlebars'
181
486
 
@@ -187,16 +492,31 @@ function registerMjmlPartials(emailsDir: string): void {
187
492
  const componentsDir = join(emailsDir, 'components')
188
493
  if (!existsSync(componentsDir)) return
189
494
 
190
- {
191
- const files = readdirSync(componentsDir)
495
+ const walk = (dir: string) => {
496
+ const entries = readdirSync(dir, { withFileTypes: true })
192
497
 
193
- for (const file of files) {
194
- if (!file.endsWith('.mjml')) continue
195
- const partialName = basename(file, '.mjml')
196
- const partialSource = readFileSync(join(componentsDir, file), 'utf-8')
498
+ for (const entry of entries) {
499
+ const fullPath = join(dir, entry.name)
500
+
501
+ if (entry.isDirectory()) {
502
+ walk(fullPath)
503
+ continue
504
+ }
505
+
506
+ if (!entry.name.endsWith('.mjml')) continue
507
+
508
+ const relativeName = relative(componentsDir, fullPath).replace(/\\\\/g, '/')
509
+ const partialName = relativeName.endsWith('.mjml') ? relativeName.slice(0, -5) : relativeName
510
+ const basename = entry.name.endsWith('.mjml') ? entry.name.slice(0, -5) : entry.name
511
+
512
+ const partialSource = readFileSync(fullPath, 'utf-8')
197
513
  Handlebars.registerPartial(partialName, partialSource)
514
+ // Backward compatibility: allow {{> Header}} for nested components.
515
+ Handlebars.registerPartial(basename, partialSource)
198
516
  }
199
517
  }
518
+
519
+ walk(componentsDir)
200
520
  }
201
521
 
202
522
  type SendData<TAdditional extends Record<string, unknown> = Record<string, unknown>> = {
@@ -211,6 +531,7 @@ type NuxtGenEmailsApiBody<
211
531
  > = {
212
532
  templateData: TTemplateData
213
533
  sendData: TSendData
534
+ stopSend?: boolean
214
535
  }
215
536
 
216
537
  defineRouteMeta({
@@ -242,6 +563,11 @@ defineRouteMeta({
242
563
  subject: { type: 'string', example: 'Sending with Twilio SendGrid is Fun' },
243
564
  },
244
565
  },
566
+ stopSend: {
567
+ type: 'boolean',
568
+ description: 'When true, render the template but skip the send hook.',
569
+ example: true,
570
+ },
245
571
  },
246
572
  required: ['templateData', 'sendData'],
247
573
  },
@@ -289,6 +615,7 @@ export default defineEventHandler(async (event) => {
289
615
 
290
616
  const templateData = body?.templateData ?? {}
291
617
  const sendData = body?.sendData ?? {}
618
+ const stopSend = body?.stopSend === true
292
619
 
293
620
  try {
294
621
  const { emailsDir } = useRuntimeConfig().nuxtGenEmails as { emailsDir: string }
@@ -302,9 +629,11 @@ export default defineEventHandler(async (event) => {
302
629
  throw new Error('MJML compilation errors: ' + errors.map(e => e.formattedMessage).join('; '))
303
630
  }
304
631
 
305
- const nitro = useNitroApp()
306
- // @ts-ignore - custom hook
307
- await nitro.hooks.callHook('nuxt-generation-emails:send', { html, data: sendData })
632
+ if (!stopSend) {
633
+ const nitro = useNitroApp()
634
+ // @ts-ignore - custom hook
635
+ await nitro.hooks.callHook('nuxt-generation-emails:send', { html, data: sendData })
636
+ }
308
637
 
309
638
  return { success: true, message: 'Email rendered successfully', html }
310
639
  }
@@ -316,174 +645,22 @@ export default defineEventHandler(async (event) => {
316
645
  `;
317
646
  }
318
647
 
319
- function extractPropsFromSFC(filePath) {
320
- const source = fs.readFileSync(filePath, "utf-8");
321
- const { descriptor } = parse(source, { filename: filePath });
322
- if (!descriptor.scriptSetup) {
323
- return { props: [], defaults: {} };
648
+ function sanitizeForOpenApi(value) {
649
+ if (typeof value === "string") {
650
+ return value.replace(/"/g, "\\u0022").replace(/\\'/g, "'");
324
651
  }
325
- try {
326
- const compiled = compileScript(descriptor, {
327
- id: filePath,
328
- isProd: false
329
- });
330
- const props = [];
331
- const defaults = {};
332
- const propNames = [];
333
- if (compiled.bindings) {
334
- for (const [name, binding] of Object.entries(compiled.bindings)) {
335
- if (binding === "props") {
336
- propNames.push(name);
337
- }
338
- }
339
- }
340
- const propTypes = extractPropTypesFromSource(descriptor.scriptSetup.content);
341
- for (const name of propNames) {
342
- const type = propTypes[name] ?? "unknown";
343
- props.push({ name, type });
344
- }
345
- const defaultValues = extractDefaultsFromSource(descriptor.scriptSetup.content);
346
- for (const [name, value] of Object.entries(defaultValues)) {
347
- defaults[name] = value;
348
- const existing = props.find((p) => p.name === name);
349
- if (existing) {
350
- existing.default = value;
351
- }
352
- }
353
- return { props, defaults };
354
- } catch {
355
- return { props: [], defaults: {} };
652
+ if (Array.isArray(value)) {
653
+ return value.map(sanitizeForOpenApi);
356
654
  }
357
- }
358
- function extractPropTypesFromSource(scriptContent) {
359
- const result = {};
360
- const match = scriptContent.match(/defineProps\s*<\s*\{([\s\S]*?)\}\s*>\s*\(/);
361
- if (!match) return result;
362
- const typeBody = match[1];
363
- const propPattern = /(\w+)\s*(?:\?\s*)?:\s*(\w+)/g;
364
- let propMatch;
365
- while ((propMatch = propPattern.exec(typeBody)) !== null) {
366
- const name = propMatch[1];
367
- const tsType = propMatch[2];
368
- switch (tsType.toLowerCase()) {
369
- case "string":
370
- result[name] = "string";
371
- break;
372
- case "number":
373
- result[name] = "number";
374
- break;
375
- case "boolean":
376
- result[name] = "boolean";
377
- break;
378
- default:
379
- result[name] = "object";
380
- break;
655
+ if (value !== null && typeof value === "object") {
656
+ const out = {};
657
+ for (const [k, v] of Object.entries(value)) {
658
+ out[k] = sanitizeForOpenApi(v);
381
659
  }
660
+ return out;
382
661
  }
383
- return result;
662
+ return value;
384
663
  }
385
- function extractDefaultsFromSource(scriptContent) {
386
- const defaultsText = extractDefaultsObjectText(scriptContent);
387
- if (!defaultsText) return {};
388
- try {
389
- const fn = new Function(`
390
- const _defaults = (${defaultsText});
391
- const _result = {};
392
- for (const [k, v] of Object.entries(_defaults)) {
393
- _result[k] = typeof v === 'function' ? v() : v;
394
- }
395
- return _result;
396
- `);
397
- return fn();
398
- } catch {
399
- return extractPrimitivesFromObjectLiteral(defaultsText);
400
- }
401
- }
402
- function extractDefaultsObjectText(scriptContent) {
403
- const wdIdx = scriptContent.indexOf("withDefaults(");
404
- if (wdIdx === -1) return null;
405
- const start = scriptContent.indexOf("(", wdIdx + "withDefaults".length);
406
- if (start === -1) return null;
407
- let depth = 1;
408
- let i = start + 1;
409
- let separatorComma = -1;
410
- while (i < scriptContent.length && depth > 0) {
411
- const ch = scriptContent[i];
412
- if (ch === "'" || ch === '"') {
413
- const quote = ch;
414
- i++;
415
- while (i < scriptContent.length) {
416
- if (scriptContent[i] === "\\") {
417
- i += 2;
418
- continue;
419
- }
420
- if (scriptContent[i] === quote) break;
421
- i++;
422
- }
423
- i++;
424
- continue;
425
- }
426
- if (ch === "`") {
427
- i++;
428
- while (i < scriptContent.length) {
429
- if (scriptContent[i] === "\\") {
430
- i += 2;
431
- continue;
432
- }
433
- if (scriptContent[i] === "`") break;
434
- if (scriptContent[i] === "$" && scriptContent[i + 1] === "{") {
435
- i += 2;
436
- let tdepth = 1;
437
- while (i < scriptContent.length && tdepth > 0) {
438
- if (scriptContent[i] === "{") tdepth++;
439
- else if (scriptContent[i] === "}") tdepth--;
440
- if (tdepth > 0) i++;
441
- }
442
- }
443
- i++;
444
- }
445
- i++;
446
- continue;
447
- }
448
- if (ch === "/" && scriptContent[i + 1] === "/") {
449
- i = scriptContent.indexOf("\n", i);
450
- if (i === -1) break;
451
- i++;
452
- continue;
453
- }
454
- if (ch === "/" && scriptContent[i + 1] === "*") {
455
- i = scriptContent.indexOf("*/", i) + 2;
456
- if (i < 2) break;
457
- continue;
458
- }
459
- if (ch === "(" || ch === "{" || ch === "[") {
460
- depth++;
461
- } else if (ch === ")" || ch === "}" || ch === "]") {
462
- depth--;
463
- if (depth === 0) break;
464
- } else if (ch === "," && depth === 1 && separatorComma === -1) {
465
- separatorComma = i;
466
- }
467
- i++;
468
- }
469
- if (separatorComma === -1) return null;
470
- const secondArg = scriptContent.slice(separatorComma + 1, i).trim();
471
- return secondArg || null;
472
- }
473
- function extractPrimitivesFromObjectLiteral(text) {
474
- const result = {};
475
- const linePattern = /(\w+)\s*:\s*(?:'([^']*)'|"([^"]*)"|(\d+(?:\.\d+)?)|(true|false))\s*[,}]?/g;
476
- let match;
477
- while ((match = linePattern.exec(text)) !== null) {
478
- const key = match[1];
479
- if (match[2] != null) result[key] = match[2];
480
- else if (match[3] != null) result[key] = match[3];
481
- else if (match[4] != null) result[key] = Number(match[4]);
482
- else if (match[5] != null) result[key] = match[5] === "true";
483
- }
484
- return result;
485
- }
486
-
487
664
  function generateServerRoutes(emailsDir, buildDir) {
488
665
  if (!fs.existsSync(emailsDir)) return [];
489
666
  const handlersDir = join(buildDir, "email-handlers");
@@ -514,7 +691,8 @@ function generateServerRoutes(emailsDir, buildDir) {
514
691
  fs.mkdirSync(handlerDir, { recursive: true });
515
692
  }
516
693
  const { defaults } = extractPropsFromSFC(fullPath);
517
- const examplePayload = Object.keys(defaults).length > 0 ? JSON.stringify(defaults, null, 2) : "{}";
694
+ const sanitized = sanitizeForOpenApi(defaults);
695
+ const examplePayload = Object.keys(sanitized).length > 0 ? JSON.stringify(sanitized, null, 2) : "{}";
518
696
  const handlerFileName = `${emailName}.ts`;
519
697
  const handlerFilePath = join(handlerDir, handlerFileName);
520
698
  const handlerContent = generateApiRoute(emailName, emailPath, examplePayload);
@@ -583,7 +761,7 @@ declare module 'nitropack/types' {
583
761
  }, { nuxt: true, nitro: true });
584
762
  const configuredEmailDir = options.emailDir ?? "emails";
585
763
  const emailsDir = join(nuxt.options.srcDir, configuredEmailDir);
586
- const globPath = `~/${configuredEmailDir}/components/*.mjml`;
764
+ const globPath = `~/${configuredEmailDir}/components/**/*.mjml`;
587
765
  addTemplate({
588
766
  filename: "nge/register-components.ts",
589
767
  write: true,
@@ -593,8 +771,16 @@ const componentFiles: Record<string, unknown> = import.meta.glob('${globPath}',
593
771
 
594
772
  export function registerMjmlComponents(): void {
595
773
  for (const [path, source] of Object.entries(componentFiles)) {
596
- const name = path.split('/').pop()!.replace('.mjml', '')
597
- Handlebars.registerPartial(name, source as string)
774
+ const normalizedPath = path.split('?')[0] || path
775
+ const parts = normalizedPath.split('/')
776
+ const componentsIdx = parts.lastIndexOf('components')
777
+ const relativeParts = componentsIdx >= 0 ? parts.slice(componentsIdx + 1) : [parts[parts.length - 1]]
778
+ const relativeName = relativeParts.join('/').replace('.mjml', '')
779
+ const basename = relativeParts[relativeParts.length - 1]!.replace('.mjml', '')
780
+
781
+ Handlebars.registerPartial(relativeName, source as string)
782
+ // Backward compatibility: allow {{> Header}} for nested components.
783
+ Handlebars.registerPartial(basename, source as string)
598
784
  }
599
785
  }
600
786
  `
@@ -37,7 +37,8 @@ async function testApi() {
37
37
  method: "POST",
38
38
  body: {
39
39
  templateData: props.dataObject,
40
- sendData: {}
40
+ sendData: {},
41
+ stopSend: true
41
42
  }
42
43
  });
43
44
  responseData.value = result;
@@ -62,7 +63,8 @@ async function sendTestEmail() {
62
63
  sendData: {
63
64
  to: lastUsedEmail.value,
64
65
  subject: `Test email ${Date.now()}`
65
- }
66
+ },
67
+ stopSend: false
66
68
  };
67
69
  isLoading.value = true;
68
70
  sendStatus.value = "sending";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-generation-emails",
3
- "version": "1.0.4",
3
+ "version": "1.0.7",
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": {