nuxt-generation-emails 1.0.4 → 1.0.5
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 +1 -1
- package/dist/module.mjs +367 -181
- package/dist/runtime/components/ApiTester.vue +4 -2
- package/package.json +1 -1
package/dist/module.json
CHANGED
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
|
-
|
|
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,
|
|
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
|
|
495
|
+
const walk = (dir: string) => {
|
|
496
|
+
const entries = readdirSync(dir, { withFileTypes: true })
|
|
192
497
|
|
|
193
|
-
for (const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
597
|
-
|
|
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