nuxt-generation-emails 1.4.5 → 1.4.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
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
|
|
198
|
-
|
|
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
|
|
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
|
|
828
|
+
function registerMjmlPartials(emailsDir) {
|
|
593
829
|
const componentsDir = join(emailsDir, 'components')
|
|
594
830
|
if (!existsSync(componentsDir)) return
|
|
595
831
|
|
|
596
|
-
const walk = (dir
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
|
|
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:
|
|
1259
|
+
handler: template.dst
|
|
1043
1260
|
});
|
|
1044
1261
|
}
|
|
1045
1262
|
if (nuxt.options.dev && fs.existsSync(emailsDir)) {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
<script setup>
|
|
2
2
|
import { ref, computed } from "vue";
|
|
3
|
-
import { useRoute, useCookie } from "#imports";
|
|
3
|
+
import { useRoute, useCookie, useRouter } from "#imports";
|
|
4
|
+
import { resolveApiEndpointFromPreviewPath } from "../utils/email-route";
|
|
4
5
|
const props = defineProps({
|
|
5
6
|
dataObject: { type: Object, required: true }
|
|
6
7
|
});
|
|
7
8
|
const route = useRoute();
|
|
9
|
+
const router = useRouter();
|
|
8
10
|
const isLoading = ref(false);
|
|
9
11
|
const response = ref("");
|
|
10
12
|
const responseData = ref(null);
|
|
@@ -19,8 +21,8 @@ const testEmailTo = ref(lastUsedEmail.value || "");
|
|
|
19
21
|
const sendStatus = ref("idle");
|
|
20
22
|
let sendStatusTimeout = null;
|
|
21
23
|
const apiEndpoint = computed(() => {
|
|
22
|
-
const
|
|
23
|
-
return
|
|
24
|
+
const availablePreviewPaths = router.getRoutes().filter((route2) => route2.path.startsWith("/__emails/")).map((route2) => route2.path);
|
|
25
|
+
return resolveApiEndpointFromPreviewPath(route.path, availablePreviewPaths);
|
|
24
26
|
});
|
|
25
27
|
const htmlResponse = computed(() => {
|
|
26
28
|
if (!responseData.value || typeof responseData.value !== "object") return "";
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export declare function toTemplatePath(previewPath: string): string;
|
|
2
|
+
export declare function normalizeApiTemplatePath(templatePath: string, availableTemplatePaths: string[]): string;
|
|
3
|
+
export declare function resolveApiEndpointFromPreviewPath(currentPreviewPath: string, availablePreviewPaths: string[]): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const PREVIEW_PREFIX = "/__emails/";
|
|
2
|
+
function stripLeadingSlash(value) {
|
|
3
|
+
return value.replace(/^\/+/, "");
|
|
4
|
+
}
|
|
5
|
+
export function toTemplatePath(previewPath) {
|
|
6
|
+
if (previewPath.startsWith(PREVIEW_PREFIX)) {
|
|
7
|
+
return stripLeadingSlash(previewPath.slice(PREVIEW_PREFIX.length));
|
|
8
|
+
}
|
|
9
|
+
return stripLeadingSlash(previewPath);
|
|
10
|
+
}
|
|
11
|
+
export function normalizeApiTemplatePath(templatePath, availableTemplatePaths) {
|
|
12
|
+
if (!templatePath || templatePath === "index") {
|
|
13
|
+
return templatePath;
|
|
14
|
+
}
|
|
15
|
+
if (!templatePath.endsWith("/index")) {
|
|
16
|
+
return templatePath;
|
|
17
|
+
}
|
|
18
|
+
const indexlessPath = templatePath.slice(0, -"/index".length);
|
|
19
|
+
const hasSiblingTemplate = availableTemplatePaths.includes(indexlessPath);
|
|
20
|
+
if (hasSiblingTemplate) {
|
|
21
|
+
return templatePath;
|
|
22
|
+
}
|
|
23
|
+
return indexlessPath;
|
|
24
|
+
}
|
|
25
|
+
export function resolveApiEndpointFromPreviewPath(currentPreviewPath, availablePreviewPaths) {
|
|
26
|
+
const templatePath = toTemplatePath(currentPreviewPath);
|
|
27
|
+
const availableTemplatePaths = availablePreviewPaths.map(toTemplatePath);
|
|
28
|
+
const normalizedTemplatePath = normalizeApiTemplatePath(templatePath, availableTemplatePaths);
|
|
29
|
+
return `/api/emails/${normalizedTemplatePath}`;
|
|
30
|
+
}
|
package/package.json
CHANGED