nuxt-generation-emails 1.4.5 → 1.4.6

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.4.5",
7
+ "version": "1.4.6",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "3.6.1"
package/dist/module.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { defineNuxtModule, createResolver, addTypeTemplate, addTemplate, addImports, addServerImports, extendPages, addServerHandler } from '@nuxt/kit';
2
2
  import fs from 'node:fs';
3
- import { join, relative } from 'pathe';
3
+ import { resolve, dirname, join, relative } from 'pathe';
4
4
  import { consola } from 'consola';
5
5
  import { parse, compileScript } from 'vue/compiler-sfc';
6
6
 
@@ -175,7 +175,7 @@ function extractPropsFromSFC(filePath) {
175
175
  const type = propTypes[name] ?? "unknown";
176
176
  props.push({ name, type });
177
177
  }
178
- const defaultValues = extractDefaults(compiled.scriptSetupAst, descriptor.scriptSetup.content);
178
+ const defaultValues = extractDefaults(compiled.scriptSetupAst, descriptor.scriptSetup.content, filePath);
179
179
  for (const [name, value] of Object.entries(defaultValues)) {
180
180
  defaults[name] = value;
181
181
  const existing = props.find((p) => p.name === name);
@@ -194,12 +194,209 @@ function asAstNode(value) {
194
194
  if (typeof node.type !== "string") return null;
195
195
  return node;
196
196
  }
197
- function extractDefaults(scriptSetupAst, scriptContent) {
198
- const astDefaults = extractDefaultsFromAst(scriptSetupAst);
197
+ function isValidIdentifierName(value) {
198
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(value);
199
+ }
200
+ function parseNamedValueImports(scriptContent) {
201
+ const imports = [];
202
+ const importPattern = /import\s+(?!type\b)(?:[A-Za-z_$][A-Za-z0-9_$]*\s*,\s*)?\{([\s\S]*?)\}\s*from\s*['"]([^'"]+)['"]/g;
203
+ let match;
204
+ while ((match = importPattern.exec(scriptContent)) !== null) {
205
+ const specifiers = match[1] ?? "";
206
+ const source = match[2] ?? "";
207
+ for (const rawToken of specifiers.split(",")) {
208
+ const token = rawToken.trim();
209
+ if (!token) continue;
210
+ const cleaned = token.replace(/^type\s+/, "").trim();
211
+ if (!cleaned) continue;
212
+ const aliasMatch = cleaned.match(/^([A-Za-z_$][A-Za-z0-9_$]*)\s+as\s+([A-Za-z_$][A-Za-z0-9_$]*)$/);
213
+ if (aliasMatch) {
214
+ imports.push({
215
+ source,
216
+ importedName: aliasMatch[1],
217
+ localName: aliasMatch[2]
218
+ });
219
+ continue;
220
+ }
221
+ if (isValidIdentifierName(cleaned)) {
222
+ imports.push({
223
+ source,
224
+ importedName: cleaned,
225
+ localName: cleaned
226
+ });
227
+ }
228
+ }
229
+ }
230
+ return imports;
231
+ }
232
+ function resolveImportModulePath(sfcFilePath, importSource) {
233
+ if (!importSource.startsWith(".")) return null;
234
+ const basePath = resolve(dirname(sfcFilePath), importSource);
235
+ const candidates = [
236
+ basePath,
237
+ `${basePath}.ts`,
238
+ `${basePath}.mts`,
239
+ `${basePath}.cts`,
240
+ `${basePath}.js`,
241
+ `${basePath}.mjs`,
242
+ `${basePath}.cjs`,
243
+ join(basePath, "index.ts"),
244
+ join(basePath, "index.mts"),
245
+ join(basePath, "index.cts"),
246
+ join(basePath, "index.js"),
247
+ join(basePath, "index.mjs"),
248
+ join(basePath, "index.cjs")
249
+ ];
250
+ for (const candidate of candidates) {
251
+ if (!fs.existsSync(candidate)) continue;
252
+ try {
253
+ if (fs.statSync(candidate).isFile()) {
254
+ return candidate;
255
+ }
256
+ } catch {
257
+ continue;
258
+ }
259
+ }
260
+ return null;
261
+ }
262
+ function extractTopLevelExpression(source, startIndex) {
263
+ let i = startIndex;
264
+ while (i < source.length && /\s/.test(source[i])) i++;
265
+ if (i >= source.length) return null;
266
+ const expressionStart = i;
267
+ let depthParen = 0;
268
+ let depthBrace = 0;
269
+ let depthBracket = 0;
270
+ let seenContent = false;
271
+ while (i < source.length) {
272
+ const ch = source[i];
273
+ if (ch === "'" || ch === '"') {
274
+ seenContent = true;
275
+ const quote = ch;
276
+ i++;
277
+ while (i < source.length) {
278
+ if (source[i] === "\\") {
279
+ i += 2;
280
+ continue;
281
+ }
282
+ if (source[i] === quote) break;
283
+ i++;
284
+ }
285
+ i++;
286
+ continue;
287
+ }
288
+ if (ch === "`") {
289
+ seenContent = true;
290
+ i++;
291
+ while (i < source.length) {
292
+ if (source[i] === "\\") {
293
+ i += 2;
294
+ continue;
295
+ }
296
+ if (source[i] === "`") break;
297
+ if (source[i] === "$" && source[i + 1] === "{") {
298
+ i += 2;
299
+ let templateDepth = 1;
300
+ while (i < source.length && templateDepth > 0) {
301
+ if (source[i] === "{") templateDepth++;
302
+ else if (source[i] === "}") templateDepth--;
303
+ if (templateDepth > 0) i++;
304
+ }
305
+ }
306
+ i++;
307
+ }
308
+ i++;
309
+ continue;
310
+ }
311
+ if (ch === "/" && source[i + 1] === "/") {
312
+ i = source.indexOf("\n", i);
313
+ if (i === -1) break;
314
+ continue;
315
+ }
316
+ if (ch === "/" && source[i + 1] === "*") {
317
+ const blockEnd = source.indexOf("*/", i + 2);
318
+ if (blockEnd === -1) break;
319
+ i = blockEnd + 2;
320
+ continue;
321
+ }
322
+ if (ch === "(") depthParen++;
323
+ else if (ch === ")") depthParen = Math.max(0, depthParen - 1);
324
+ else if (ch === "{") depthBrace++;
325
+ else if (ch === "}") {
326
+ if (depthBrace > 0) depthBrace--;
327
+ else break;
328
+ } else if (ch === "[") depthBracket++;
329
+ else if (ch === "]") depthBracket = Math.max(0, depthBracket - 1);
330
+ if (!/\s/.test(ch)) {
331
+ seenContent = true;
332
+ }
333
+ const atTopLevel = depthParen === 0 && depthBrace === 0 && depthBracket === 0;
334
+ if (atTopLevel && ch === ";") {
335
+ break;
336
+ }
337
+ if (atTopLevel && ch === "\n" && seenContent) {
338
+ const rest = source.slice(i + 1).trimStart();
339
+ if (!rest || rest.startsWith("export ") || rest.startsWith("const ") || rest.startsWith("interface ") || rest.startsWith("type ")) {
340
+ break;
341
+ }
342
+ }
343
+ i++;
344
+ }
345
+ const expressionText = source.slice(expressionStart, i).trim();
346
+ if (!expressionText) return null;
347
+ return { text: expressionText, end: i + 1 };
348
+ }
349
+ function evaluateExpressionText(expressionText) {
350
+ const normalized = expressionText.replace(/\s+as\s+const\s*$/, "").replace(/\s+satisfies\s+[A-Za-z_$][A-Za-z0-9_$<>,\s.\[\]|&?]*\s*$/, "");
351
+ try {
352
+ const fn = new Function(`return (${normalized})`);
353
+ return { ok: true, value: fn() };
354
+ } catch {
355
+ return { ok: false, value: void 0 };
356
+ }
357
+ }
358
+ function extractExportedConstValues(moduleFilePath) {
359
+ const source = fs.readFileSync(moduleFilePath, "utf-8");
360
+ const exportsMap = {};
361
+ const exportConstPattern = /export\s+const\s+([A-Za-z_$][A-Za-z0-9_$]*)(?:\s*:[^=]+)?\s*=/g;
362
+ let match;
363
+ while ((match = exportConstPattern.exec(source)) !== null) {
364
+ const exportName = match[1];
365
+ const expression = extractTopLevelExpression(source, exportConstPattern.lastIndex);
366
+ if (!expression) continue;
367
+ const evaluated = evaluateExpressionText(expression.text);
368
+ if (evaluated.ok) {
369
+ exportsMap[exportName] = evaluated.value;
370
+ }
371
+ exportConstPattern.lastIndex = expression.end;
372
+ }
373
+ return exportsMap;
374
+ }
375
+ function buildImportScope(scriptContent, sfcFilePath) {
376
+ const scope = {};
377
+ const imports = parseNamedValueImports(scriptContent);
378
+ const moduleExportsCache = /* @__PURE__ */ new Map();
379
+ for (const importInfo of imports) {
380
+ const modulePath = resolveImportModulePath(sfcFilePath, importInfo.source);
381
+ if (!modulePath) continue;
382
+ let moduleExports = moduleExportsCache.get(modulePath);
383
+ if (!moduleExports) {
384
+ moduleExports = extractExportedConstValues(modulePath);
385
+ moduleExportsCache.set(modulePath, moduleExports);
386
+ }
387
+ if (Object.prototype.hasOwnProperty.call(moduleExports, importInfo.importedName)) {
388
+ scope[importInfo.localName] = moduleExports[importInfo.importedName];
389
+ }
390
+ }
391
+ return scope;
392
+ }
393
+ function extractDefaults(scriptSetupAst, scriptContent, sfcFilePath) {
394
+ const evalScope = buildImportScope(scriptContent, sfcFilePath);
395
+ const astDefaults = extractDefaultsFromAst(scriptSetupAst, evalScope);
199
396
  if (astDefaults) return astDefaults;
200
- return extractDefaultsFromSource(scriptContent);
397
+ return extractDefaultsFromSource(scriptContent, evalScope);
201
398
  }
202
- function extractDefaultsFromAst(scriptSetupAst) {
399
+ function extractDefaultsFromAst(scriptSetupAst, evalScope) {
203
400
  if (!Array.isArray(scriptSetupAst)) return null;
204
401
  for (const root of scriptSetupAst) {
205
402
  const rootNode = asAstNode(root);
@@ -207,12 +404,11 @@ function extractDefaultsFromAst(scriptSetupAst) {
207
404
  const found = findWithDefaultsSecondArg(rootNode);
208
405
  if (!found) continue;
209
406
  try {
210
- const evaluated = evaluateAstExpression(found);
407
+ const evaluated = evaluateAstExpression(found, evalScope);
211
408
  if (evaluated && typeof evaluated === "object" && !Array.isArray(evaluated)) {
212
409
  return evaluated;
213
410
  }
214
411
  } catch (error) {
215
- debugLog("Failed AST-based withDefaults extraction; falling back to source parser", error);
216
412
  }
217
413
  }
218
414
  return null;
@@ -243,7 +439,7 @@ function findWithDefaultsSecondArg(node) {
243
439
  }
244
440
  return null;
245
441
  }
246
- function evaluateAstExpression(node) {
442
+ function evaluateAstExpression(node, evalScope) {
247
443
  switch (node.type) {
248
444
  case "ObjectExpression": {
249
445
  const result = {};
@@ -257,13 +453,13 @@ function evaluateAstExpression(node) {
257
453
  if (!key) continue;
258
454
  const valueNode = asAstNode(prop.value);
259
455
  if (!valueNode) continue;
260
- result[key] = evaluateAstExpression(valueNode);
456
+ result[key] = evaluateAstExpression(valueNode, evalScope);
261
457
  }
262
458
  return result;
263
459
  }
264
460
  case "ArrayExpression": {
265
461
  const elements = Array.isArray(node.elements) ? node.elements : [];
266
- return elements.map((element) => asAstNode(element)).filter((element) => !!element).map((element) => evaluateAstExpression(element));
462
+ return elements.map((element) => asAstNode(element)).filter((element) => !!element).map((element) => evaluateAstExpression(element, evalScope));
267
463
  }
268
464
  case "StringLiteral":
269
465
  return node.value;
@@ -292,20 +488,57 @@ function evaluateAstExpression(node) {
292
488
  const statements = Array.isArray(body.body) ? body.body : [];
293
489
  const returnStmt = statements.map((stmt) => asAstNode(stmt)).find((stmt) => stmt?.type === "ReturnStatement");
294
490
  const arg = returnStmt ? asAstNode(returnStmt.argument) : null;
295
- return arg ? evaluateAstExpression(arg) : void 0;
491
+ return arg ? evaluateAstExpression(arg, evalScope) : void 0;
296
492
  }
297
- return evaluateAstExpression(body);
493
+ return evaluateAstExpression(body, evalScope);
298
494
  }
299
495
  case "ParenthesizedExpression": {
300
496
  const expr = asAstNode(node.expression);
301
- return expr ? evaluateAstExpression(expr) : void 0;
497
+ return expr ? evaluateAstExpression(expr, evalScope) : void 0;
302
498
  }
303
499
  case "TSAsExpression": {
304
500
  const expr = asAstNode(node.expression);
305
- return expr ? evaluateAstExpression(expr) : void 0;
501
+ return expr ? evaluateAstExpression(expr, evalScope) : void 0;
502
+ }
503
+ case "TSSatisfiesExpression": {
504
+ const expr = asAstNode(node.expression);
505
+ return expr ? evaluateAstExpression(expr, evalScope) : void 0;
506
+ }
507
+ case "TSNonNullExpression": {
508
+ const expr = asAstNode(node.expression);
509
+ return expr ? evaluateAstExpression(expr, evalScope) : void 0;
510
+ }
511
+ case "MemberExpression": {
512
+ const objectNode = asAstNode(node.object);
513
+ if (!objectNode) {
514
+ throw new Error("Unsupported MemberExpression object in defaults");
515
+ }
516
+ const targetValue = evaluateAstExpression(objectNode, evalScope);
517
+ if (targetValue === null || typeof targetValue !== "object") {
518
+ return void 0;
519
+ }
520
+ const propertyNode = asAstNode(node.property);
521
+ const computed = node.computed === true;
522
+ let propertyKey = null;
523
+ if (computed) {
524
+ if (!propertyNode) {
525
+ throw new Error("Unsupported computed MemberExpression property in defaults");
526
+ }
527
+ const keyValue = evaluateAstExpression(propertyNode, evalScope);
528
+ if (typeof keyValue === "string" || typeof keyValue === "number") {
529
+ propertyKey = String(keyValue);
530
+ }
531
+ } else {
532
+ propertyKey = propertyNode?.type === "Identifier" ? String(propertyNode.name) : propertyNode?.type === "StringLiteral" ? String(propertyNode.value) : null;
533
+ }
534
+ if (!propertyKey) return void 0;
535
+ return targetValue[propertyKey];
306
536
  }
307
537
  case "Identifier": {
308
538
  if (node.name === "undefined") return void 0;
539
+ if (typeof node.name === "string" && Object.prototype.hasOwnProperty.call(evalScope, node.name)) {
540
+ return evalScope[node.name];
541
+ }
309
542
  throw new Error(`Unsupported identifier in defaults: ${String(node.name)}`);
310
543
  }
311
544
  default:
@@ -434,11 +667,14 @@ function parsePropEntries(typeBody, result) {
434
667
  if (i < cleaned.length && (cleaned[i] === ";" || cleaned[i] === "\n")) i++;
435
668
  }
436
669
  }
437
- function extractDefaultsFromSource(scriptContent) {
670
+ function extractDefaultsFromSource(scriptContent, evalScope) {
438
671
  const defaultsText = extractDefaultsObjectText(scriptContent);
439
672
  if (!defaultsText) return {};
440
673
  try {
441
- const fn = new Function(`
674
+ const scopeEntries = Object.entries(evalScope).filter(([name]) => isValidIdentifierName(name));
675
+ const argNames = scopeEntries.map(([name]) => name);
676
+ const argValues = scopeEntries.map(([, value]) => value);
677
+ const fn = new Function(...argNames, `
442
678
  const _defaults = (${defaultsText});
443
679
  const _result = {};
444
680
  for (const [k, v] of Object.entries(_defaults)) {
@@ -446,7 +682,7 @@ function extractDefaultsFromSource(scriptContent) {
446
682
  }
447
683
  return _result;
448
684
  `);
449
- return fn();
685
+ return fn(...argValues);
450
686
  } catch {
451
687
  return extractPrimitivesFromObjectLiteral(defaultsText);
452
688
  }
@@ -589,11 +825,11 @@ import Handlebars from 'handlebars'
589
825
  * Register all .mjml files from the components directory as Handlebars partials.
590
826
  * This lets templates use {{> partialName}} to include reusable MJML snippets.
591
827
  */
592
- function registerMjmlPartials(emailsDir: string): void {
828
+ function registerMjmlPartials(emailsDir) {
593
829
  const componentsDir = join(emailsDir, 'components')
594
830
  if (!existsSync(componentsDir)) return
595
831
 
596
- const walk = (dir: string) => {
832
+ const walk = (dir) => {
597
833
  const entries = readdirSync(dir, { withFileTypes: true })
598
834
 
599
835
  for (const entry of entries) {
@@ -620,21 +856,6 @@ function registerMjmlPartials(emailsDir: string): void {
620
856
  walk(componentsDir)
621
857
  }
622
858
 
623
- type SendData<TAdditional extends Record<string, unknown> = Record<string, unknown>> = {
624
- to?: string
625
- from?: string
626
- subject?: string
627
- } & TAdditional
628
-
629
- type NuxtGenEmailsApiBody<
630
- TTemplateData extends Record<string, unknown> = Record<string, unknown>,
631
- TSendData extends Record<string, unknown> = SendData,
632
- > = {
633
- templateData: TTemplateData
634
- sendData: TSendData
635
- stopSend?: boolean
636
- }
637
-
638
859
  defineRouteMeta({
639
860
  openAPI: {
640
861
  tags: ['nuxt-generation-emails'],
@@ -712,14 +933,14 @@ defineRouteMeta({
712
933
  })
713
934
 
714
935
  export default defineEventHandler(async (event) => {
715
- const body = await readBody<NuxtGenEmailsApiBody>(event)
936
+ const body = await readBody(event)
716
937
 
717
938
  const templateData = body?.templateData ?? {}
718
939
  const sendData = body?.sendData ?? {}
719
940
  const stopSend = body?.stopSend === true
720
941
 
721
942
  try {
722
- const { emailsDir } = useRuntimeConfig().nuxtGenEmails as { emailsDir: string }
943
+ const { emailsDir } = useRuntimeConfig().nuxtGenEmails
723
944
  registerMjmlPartials(emailsDir)
724
945
  const mjmlSource = readFileSync(join(emailsDir, '${mjmlPath}.mjml'), 'utf-8')
725
946
  const compiledTemplate = Handlebars.compile(mjmlSource)
@@ -738,7 +959,7 @@ export default defineEventHandler(async (event) => {
738
959
 
739
960
  return { success: true, message: 'Email rendered successfully', html }
740
961
  }
741
- catch (error: unknown) {
962
+ catch (error) {
742
963
  const message = error instanceof Error ? error.message : 'Failed to render or send email'
743
964
  throw createError({ statusCode: 500, statusMessage: message })
744
965
  }
@@ -774,13 +995,9 @@ function normalizeApiEmailPath(emailsDir, routePrefix, emailName) {
774
995
  }
775
996
  return rawPath;
776
997
  }
777
- function generateServerRoutes(emailsDir, buildDir) {
998
+ function generateServerRoutes(emailsDir, _buildDir) {
778
999
  if (!fs.existsSync(emailsDir)) return [];
779
- const handlersDir = join(buildDir, "email-handlers");
780
1000
  const handlers = [];
781
- if (!fs.existsSync(handlersDir)) {
782
- fs.mkdirSync(handlersDir, { recursive: true });
783
- }
784
1001
  function processEmailDirectory(dirPath, routePrefix = "") {
785
1002
  const entries = fs.readdirSync(dirPath);
786
1003
  for (const entry of entries) {
@@ -804,23 +1021,18 @@ function generateServerRoutes(emailsDir, buildDir) {
804
1021
  console.warn(`[nuxt-generation-emails] MJML template "${mjmlTemplateName}.mjml" referenced by ${emailName}.vue not found \u2014 skipping API route. Expected: ${mjmlPath}`);
805
1022
  continue;
806
1023
  }
807
- const handlerDir = routePrefix ? join(handlersDir, routePrefix.replace(/^\//, "")) : handlersDir;
808
- if (!fs.existsSync(handlerDir)) {
809
- fs.mkdirSync(handlerDir, { recursive: true });
810
- }
811
1024
  const { defaults } = extractPropsFromSFC(fullPath);
812
1025
  const sanitized = sanitizeForOpenApi(defaults);
813
1026
  const sanitizedDefaults = sanitized && typeof sanitized === "object" && !Array.isArray(sanitized) ? sanitized : {};
814
1027
  const examplePayload = Object.keys(sanitizedDefaults).length > 0 ? JSON.stringify(sanitizedDefaults, null, 2) : "{}";
815
- const handlerFileName = `${emailName}.ts`;
816
- const handlerFilePath = join(handlerDir, handlerFileName);
1028
+ const handlerFilePath = routePrefix ? join("email-handlers", routePrefix.replace(/^\//, ""), `${emailName}.ts`) : join("email-handlers", `${emailName}.ts`);
817
1029
  const handlerContent = generateApiRoute(emailName, emailPath, examplePayload, mjmlTemplateName);
818
- fs.writeFileSync(handlerFilePath, handlerContent, "utf-8");
819
- console.log(`[nuxt-generation-emails] Generated API handler: ${handlerFilePath}`);
1030
+ console.log(`[nuxt-generation-emails] Generated API handler template: ${handlerFilePath}`);
820
1031
  handlers.push({
821
1032
  route: `/api/emails/${emailPath}`,
822
1033
  method: "post",
823
- handlerPath: handlerFilePath
1034
+ handlerPath: handlerFilePath,
1035
+ handlerContent
824
1036
  });
825
1037
  }
826
1038
  }
@@ -1036,10 +1248,15 @@ export function useNgeTemplate(name: string, props: Record<string, unknown>): vo
1036
1248
  }
1037
1249
  const handlers = generateServerRoutes(emailsDir, nuxt.options.buildDir);
1038
1250
  for (const handler of handlers) {
1251
+ const template = addTemplate({
1252
+ filename: handler.handlerPath,
1253
+ write: true,
1254
+ getContents: () => handler.handlerContent
1255
+ });
1039
1256
  addServerHandler({
1040
1257
  route: handler.route,
1041
1258
  method: handler.method,
1042
- handler: handler.handlerPath
1259
+ handler: template.dst
1043
1260
  });
1044
1261
  }
1045
1262
  if (nuxt.options.dev && fs.existsSync(emailsDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nuxt-generation-emails",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
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": {