@menapo/email 0.0.0-stage → 0.1.0

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/vite.js ADDED
@@ -0,0 +1,957 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+ //#region src/vite/caniemail.ts
4
+ const CHECKS = {
5
+ "display:flex": {
6
+ clients: "Outlook (Windows)",
7
+ fix: "Write a flex row or column of elements with no wrapping and start alignment (render() makes it a table), or use a <table>."
8
+ },
9
+ "display:grid": {
10
+ clients: "Outlook (Windows)",
11
+ fix: "Use a flex row of elements, which render() makes a table row, or a <table>."
12
+ },
13
+ position: {
14
+ clients: "Gmail (web), Gmail (iOS), Gmail (Android), Outlook (Windows)",
15
+ fix: "Lay it out in the flow: a flex row, padding or margins."
16
+ },
17
+ "flex-wrap": {
18
+ clients: "Gmail (web), Gmail (iOS), Gmail (Android), Outlook (Windows), Yahoo (web)",
19
+ fix: "Drop the wrapping: render() makes a non-wrapping flex row a table row."
20
+ },
21
+ "justify-content": {
22
+ clients: "Gmail (web), Gmail (iOS), Gmail (Android), Outlook (Windows), Yahoo (web)",
23
+ fix: "Give the items widths, or set text-align on the container."
24
+ },
25
+ transform: {
26
+ clients: "Gmail (web), Gmail (iOS), Gmail (Android), Outlook (Windows), Outlook.com, Outlook (Mac), Yahoo (web)",
27
+ fix: "Use an image for a rotated or scaled element."
28
+ },
29
+ "aspect-ratio": {
30
+ clients: "Gmail (web), Gmail (iOS), Gmail (Android), Outlook (Windows), Outlook.com, Outlook (Mac), Yahoo (web)",
31
+ fix: "Give the element a width and a height."
32
+ },
33
+ "object-fit": {
34
+ clients: "Outlook (Windows), Yahoo (web)",
35
+ fix: "Crop the image to its displayed size before sending it."
36
+ },
37
+ gap: {
38
+ clients: "Outlook (Windows), Yahoo (web)",
39
+ fix: "Put the gap on a flex row or column, which render() makes cell padding, or use margins."
40
+ },
41
+ "background-image": {
42
+ clients: "Outlook (Windows)",
43
+ fix: "Keep a background color as well, so the text stays readable where the image is dropped."
44
+ },
45
+ "var()": {
46
+ clients: "Gmail (web), Gmail (iOS), Gmail (Android), Outlook (Windows), Outlook.com, Outlook (Mac), Yahoo (web)",
47
+ fix: "Use a vicss token or a literal value; a runtime value goes in the element's own style."
48
+ }
49
+ };
50
+ //#endregion
51
+ //#region src/vite/lower.ts
52
+ /**
53
+ * Values mail clients read (PLAN D6), at build time: `rem` as px at a 16 px
54
+ * root, `calc()` of literals folded, `oklch()` and `oklab()` as hex (or
55
+ * `rgba()` with alpha). Anything else stays as written; the dev checks warn
56
+ * about what is left (`var()`).
57
+ */
58
+ const fmt = (n) => String(Math.round(n * 1e5) / 1e5);
59
+ /** A `calc()` body of numbers with one unit, or undefined when it holds anything else. */
60
+ function evaluate(expr) {
61
+ const tokens = expr.match(/\d*\.?\d+(?:e[+-]?\d+)?[a-z%]*|[-+*/()]/gi);
62
+ if (!tokens || tokens.join("") !== expr.replace(/\s+/g, "")) return void 0;
63
+ let i = 0;
64
+ const primary = () => {
65
+ const t = tokens[i++];
66
+ if (t === "(") {
67
+ const v = sum();
68
+ return tokens[i++] === ")" ? v : void 0;
69
+ }
70
+ if (t === "-") {
71
+ const v = primary();
72
+ return v && {
73
+ n: -v.n,
74
+ unit: v.unit
75
+ };
76
+ }
77
+ const m = t && /^(\d*\.?\d+(?:e[+-]?\d+)?)([a-z%]*)$/i.exec(t);
78
+ return m ? {
79
+ n: Number(m[1]),
80
+ unit: m[2].toLowerCase()
81
+ } : void 0;
82
+ };
83
+ const product = () => {
84
+ let a = primary();
85
+ while (a && (tokens[i] === "*" || tokens[i] === "/")) {
86
+ const op = tokens[i++];
87
+ const b = primary();
88
+ if (!b) return void 0;
89
+ if (op === "*") {
90
+ if (a.unit && b.unit) return void 0;
91
+ a = {
92
+ n: a.n * b.n,
93
+ unit: a.unit || b.unit
94
+ };
95
+ } else {
96
+ if (b.unit || b.n === 0) return void 0;
97
+ a = {
98
+ n: a.n / b.n,
99
+ unit: a.unit
100
+ };
101
+ }
102
+ }
103
+ return a;
104
+ };
105
+ const sum = () => {
106
+ let a = product();
107
+ while (a && (tokens[i] === "+" || tokens[i] === "-")) {
108
+ const op = tokens[i++];
109
+ const b = product();
110
+ if (!b || a.unit !== b.unit && a.n !== 0 && b.n !== 0) return void 0;
111
+ a = {
112
+ n: op === "+" ? a.n + b.n : a.n - b.n,
113
+ unit: a.unit || b.unit
114
+ };
115
+ }
116
+ return a;
117
+ };
118
+ const v = sum();
119
+ return i === tokens.length ? v : void 0;
120
+ }
121
+ function hex(r, g, b, alpha) {
122
+ const channel = (c) => {
123
+ const linear = Math.min(1, Math.max(0, c));
124
+ const srgb = linear <= .0031308 ? 12.92 * linear : 1.055 * linear ** (1 / 2.4) - .055;
125
+ return Math.round(srgb * 255);
126
+ };
127
+ const [R, G, B] = [
128
+ channel(r),
129
+ channel(g),
130
+ channel(b)
131
+ ];
132
+ if (alpha < 1) return `rgba(${R},${G},${B},${fmt(alpha)})`;
133
+ return `#${[
134
+ R,
135
+ G,
136
+ B
137
+ ].map((c) => c.toString(16).padStart(2, "0")).join("")}`;
138
+ }
139
+ /** OKLab to sRGB (Björn Ottosson's matrices), clipped to the gamut. */
140
+ function fromOklab(L, a, b, alpha) {
141
+ const l = (L + .3963377774 * a + .2158037573 * b) ** 3;
142
+ const m = (L - .1055613458 * a - .0638541728 * b) ** 3;
143
+ const s = (L - .0894841775 * a - 1.291485548 * b) ** 3;
144
+ return hex(4.0767416621 * l - 3.3077115913 * m + .2309699292 * s, -1.2684380046 * l + 2.6097574011 * m - .3413193965 * s, -.0041960863 * l - .7034186147 * m + 1.707614701 * s, alpha);
145
+ }
146
+ const num = (t, percentOf = 1) => t === "none" ? 0 : t.endsWith("%") ? parseFloat(t) / 100 * percentOf : parseFloat(t);
147
+ function color(fn, body) {
148
+ const [main, alphaPart] = body.split("/");
149
+ const [x, y, z] = main.trim().split(/[\s,]+/);
150
+ if (x === void 0 || y === void 0 || z === void 0) return void 0;
151
+ const alpha = alphaPart ? num(alphaPart.trim()) : 1;
152
+ const L = num(x);
153
+ if (fn === "oklab") return fromOklab(L, num(y, .4), num(z, .4), alpha);
154
+ const C = num(y, .4);
155
+ const h = parseFloat(z) * Math.PI / 180;
156
+ return fromOklab(L, C * Math.cos(h), C * Math.sin(h), alpha);
157
+ }
158
+ /** `value` in the form mail clients read. */
159
+ function lower(value) {
160
+ let out = value.replace(/(-?\d*\.?\d+)rem\b/g, (_, n) => `${fmt(Number(n) * 16)}px`);
161
+ for (let again = true; again;) {
162
+ again = false;
163
+ out = out.replace(/calc\(([^()]*(?:\([^()]*\)[^()]*)*)\)/g, (match, body) => {
164
+ const v = evaluate(body);
165
+ if (!v) return match;
166
+ again = true;
167
+ return `${fmt(v.n)}${v.n === 0 ? "" : v.unit}`;
168
+ });
169
+ }
170
+ return out.replace(/\b(oklch|oklab)\(([^()]*)\)/g, (match, fn, body) => color(fn, body) ?? match);
171
+ }
172
+ /**
173
+ * An at-rule mail clients parse: media range syntax (`width >= 768px`), which
174
+ * vicss prints and Gmail and Outlook.com reject, as `min-width`/`max-width`.
175
+ */
176
+ function lowerAtRule(rule) {
177
+ return lower(rule).replace(/\((width|height)\s*(>=|>|<=|<)\s*(-?\d*\.?\d+)(px|em)\)/g, (_, side, op, n, unit) => {
178
+ const value = Number(n);
179
+ const bound = op === "<" ? value - (unit === "px" ? .02 : .00125) : op === ">" ? value + (unit === "px" ? .02 : .00125) : value;
180
+ return `(${op.startsWith(">") ? "min" : "max"}-${side}: ${fmt(bound)}${unit})`;
181
+ });
182
+ }
183
+ //#endregion
184
+ //#region src/vite/entries.ts
185
+ const DARK = "@media (prefers-color-scheme: dark)";
186
+ function dark(property, value, selector, atRules) {
187
+ const outlook = property.startsWith("background") ? "[data-ogsb]" : "[data-ogsc]";
188
+ return [[
189
+ selector,
190
+ [...atRules, DARK],
191
+ property,
192
+ value
193
+ ], [
194
+ `${outlook} ${selector}`,
195
+ atRules,
196
+ property,
197
+ value
198
+ ]];
199
+ }
200
+ function entryOf(literals) {
201
+ const i = [];
202
+ const r = [];
203
+ for (const l of literals) {
204
+ const value = lower(l.value);
205
+ const darkValue = l.modes?.dark === void 0 ? void 0 : lower(l.modes.dark);
206
+ const isDark = l.chain.includes("dark");
207
+ for (const rule of l.rules) {
208
+ const atRules = rule.atRules.map(lowerAtRule);
209
+ const always = rule.selector === "&" && atRules.length === 0;
210
+ if (isDark) r.push(...dark(l.property, value, rule.selector, atRules));
211
+ else if (always) i.push([
212
+ l.tier,
213
+ l.property,
214
+ value
215
+ ]);
216
+ else r.push([
217
+ rule.selector,
218
+ atRules,
219
+ l.property,
220
+ value
221
+ ]);
222
+ if (darkValue !== void 0 && !isDark) r.push(...dark(l.property, darkValue, rule.selector, atRules));
223
+ }
224
+ }
225
+ return {
226
+ ...i.length && { i },
227
+ ...r.length && { r }
228
+ };
229
+ }
230
+ //#endregion
231
+ //#region src/email.ts
232
+ const KIND = Symbol.for("@menapo/email:email");
233
+ /** Whether `value` is an `email()`, whichever copy of this package made it. */
234
+ const isEmail = (value) => typeof value === "object" && value !== null && value[KIND] === true;
235
+ //#endregion
236
+ //#region src/style.ts
237
+ const UNITLESS = /* @__PURE__ */ new Set([
238
+ "flex",
239
+ "flex-grow",
240
+ "flex-shrink",
241
+ "font-weight",
242
+ "line-height",
243
+ "opacity",
244
+ "order",
245
+ "z-index"
246
+ ]);
247
+ const kebab = (name) => name.startsWith("--") ? name : name.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`);
248
+ /** Space-separated parts at the top level: `1px solid rgb(0 0 0)` → three. */
249
+ function parts(value) {
250
+ const out = [];
251
+ let depth = 0;
252
+ let start = 0;
253
+ for (let i = 0; i <= value.length; i++) {
254
+ const c = value[i];
255
+ if (c === "(") depth++;
256
+ else if (c === ")") depth--;
257
+ else if ((c === void 0 || c === " ") && depth === 0) {
258
+ if (i > start) out.push(value.slice(start, i));
259
+ start = i + 1;
260
+ }
261
+ }
262
+ return out;
263
+ }
264
+ const SIDES = [
265
+ "top",
266
+ "right",
267
+ "bottom",
268
+ "left"
269
+ ];
270
+ /** `8px 16px` → top, right, bottom, left. */
271
+ function box(value) {
272
+ const [t = "0", r = t, b = t, l = r] = parts(value);
273
+ return [
274
+ t,
275
+ r,
276
+ b,
277
+ l
278
+ ];
279
+ }
280
+ /**
281
+ * Physical longhands for one declaration. Gmail and Outlook ignore logical
282
+ * properties, and box shorthands are expanded so a later longhand or
283
+ * shorthand overrides exactly what CSS would.
284
+ */
285
+ function physical(property, value, dir) {
286
+ const start = dir === "rtl" ? "right" : "left";
287
+ const end = dir === "rtl" ? "left" : "right";
288
+ const m = /^(padding|margin|inset|border)(?:-(inline|block))?(?:-(start|end))?(-width|-style|-color)?$/.exec(property);
289
+ if (m && (m[1] !== "border" || m[2])) {
290
+ const [, kind, axis, edge, suffix = ""] = m;
291
+ const side = (s) => kind === "inset" ? s : `${kind}-${s}${suffix}`;
292
+ if (!axis) {
293
+ if (kind === "inset") {
294
+ const v = box(value);
295
+ return SIDES.map((s, i) => [s, v[i]]);
296
+ }
297
+ if (kind === "border") return [[property, value]];
298
+ const v = box(value);
299
+ return SIDES.map((s, i) => [side(s), v[i]]);
300
+ }
301
+ const [a, b = a] = kind === "border" ? [value] : parts(value);
302
+ const pair = axis === "inline" ? [start, end] : ["top", "bottom"];
303
+ if (edge) return [[side(edge === "start" ? pair[0] : pair[1]), value]];
304
+ return [[side(pair[0]), a], [side(pair[1]), b]];
305
+ }
306
+ switch (property) {
307
+ case "text-align": return [[property, value === "start" ? start : value === "end" ? end : value]];
308
+ case "inline-size":
309
+ case "block-size":
310
+ case "min-inline-size":
311
+ case "max-inline-size":
312
+ case "min-block-size":
313
+ case "max-block-size": return [[property.replace("inline-size", "width").replace("block-size", "height"), value]];
314
+ case "background": return parts(value).length === 1 && !/url\(|gradient\(/.test(value) ? [["background-color", value]] : [[property, value]];
315
+ }
316
+ return [[property, value]];
317
+ }
318
+ const VAR = /var\((--[\w-]+)(?:,\s*([^()]*))?\)/g;
319
+ /** Sets a declaration as CSS would apply it after the ones already set. */
320
+ function apply(style, property, value, dir) {
321
+ for (const [p, v] of physical(property, value, dir)) {
322
+ style.delete(p);
323
+ style.set(p, v);
324
+ }
325
+ }
326
+ /**
327
+ * The element's style. Conditional rules of its classes are added to `rules`
328
+ * as CSS text.
329
+ */
330
+ function compute(node, reg, dir, rules) {
331
+ const style = /* @__PURE__ */ new Map();
332
+ const kept = [];
333
+ const cls = node.props.class;
334
+ if (cls) {
335
+ const inline = [];
336
+ for (const name of cls.split(" ")) {
337
+ if (!name) continue;
338
+ const entry = reg.css[name];
339
+ if (!entry) {
340
+ kept.push(name);
341
+ continue;
342
+ }
343
+ if (entry.i) inline.push(...entry.i);
344
+ if (entry.r) {
345
+ kept.push(name);
346
+ for (const rule of entry.r) rules.add(ruleCss(name, rule, dir));
347
+ }
348
+ }
349
+ inline.sort((a, b) => a[0] - b[0]);
350
+ for (const [, p, v] of inline) apply(style, p, v, dir);
351
+ }
352
+ const own = node.props.style;
353
+ if (own) for (const key in own) {
354
+ const value = own[key];
355
+ if (value == null) continue;
356
+ const p = kebab(key);
357
+ apply(style, p, typeof value === "number" && !UNITLESS.has(p) && value !== 0 ? `${value}px` : String(value), dir);
358
+ }
359
+ let custom = false;
360
+ for (const p of style.keys()) if (p.startsWith("--")) custom = true;
361
+ if (custom) {
362
+ for (const [p, v] of style) if (v.includes("var(")) style.set(p, v.replace(VAR, (match, name, fallback) => style.get(name) ?? fallback ?? match));
363
+ for (const p of style.keys()) if (p.startsWith("--")) style.delete(p);
364
+ }
365
+ return {
366
+ style,
367
+ classes: kept
368
+ };
369
+ }
370
+ /** CSS identifier escaping for a class in a selector (readable vicss names hold `:` and `.`). */
371
+ const escapeClass = (name) => name.replace(/^(\d)|[^\w-]/g, (c, digit) => digit ? `\\3${digit} ` : `\\${c}`);
372
+ function ruleCss(name, [selector, atRules, property, value], dir) {
373
+ const body = physical(property, value, dir).map(([p, v]) => `${p}:${v}!important`).join(";");
374
+ const block = `${selector.replaceAll("&", `.${escapeClass(name)}`)}{${body}}`;
375
+ return atRules.reduceRight((inner, at) => `${at}{${inner}}`, block);
376
+ }
377
+ /** The `style` attribute: padding and margin written as shorthands when all four sides are set. */
378
+ function styleText(style) {
379
+ let out = "";
380
+ const done = /* @__PURE__ */ new Set();
381
+ for (const [p, v] of style) {
382
+ if (done.has(p)) continue;
383
+ const m = /^(padding|margin)-(top|right|bottom|left)$/.exec(p);
384
+ if (m) {
385
+ const all = SIDES.map((s) => style.get(`${m[1]}-${s}`));
386
+ if (all.every((x) => x !== void 0)) {
387
+ for (const s of SIDES) done.add(`${m[1]}-${s}`);
388
+ const [t, r, b, l] = all;
389
+ const short = t === b && r === l ? t === r ? t : `${t} ${r}` : r === l ? `${t} ${r} ${b}` : `${t} ${r} ${b} ${l}`;
390
+ out += `${m[1]}:${short};`;
391
+ continue;
392
+ }
393
+ }
394
+ out += `${p}:${v};`;
395
+ }
396
+ return out.slice(0, -1);
397
+ }
398
+ //#endregion
399
+ //#region src/html.ts
400
+ const VOID = /* @__PURE__ */ new Set([
401
+ "area",
402
+ "br",
403
+ "col",
404
+ "hr",
405
+ "img",
406
+ "meta",
407
+ "source",
408
+ "wbr"
409
+ ]);
410
+ const FORBIDDEN = /* @__PURE__ */ new Set([
411
+ "script",
412
+ "iframe",
413
+ "object",
414
+ "embed",
415
+ "form",
416
+ "input",
417
+ "style",
418
+ "link",
419
+ "base",
420
+ "meta"
421
+ ]);
422
+ const SAFE_URL = /^(?:https?:|mailto:|tel:|cid:|#)/i;
423
+ const BOXES = /* @__PURE__ */ new Set([
424
+ "div",
425
+ "section",
426
+ "header",
427
+ "footer",
428
+ "main",
429
+ "article",
430
+ "aside",
431
+ "center"
432
+ ]);
433
+ const TABLE = "role=\"presentation\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\"";
434
+ const escapeText = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
435
+ const escapeAttr = (s) => escapeText(s).replace(/"/g, "&quot;");
436
+ function warn(ctx, message) {
437
+ const text = `email "${ctx.name}": ${message}`;
438
+ if (ctx.warned.has(text)) return;
439
+ ctx.warned.add(text);
440
+ (ctx.reg.warn ?? console.warn)(text);
441
+ }
442
+ function computedOf(node, ctx) {
443
+ let c = ctx.computed.get(node);
444
+ if (!c) ctx.computed.set(node, c = compute(node, ctx.reg, ctx.dir, ctx.rules));
445
+ return c;
446
+ }
447
+ function attributes(node, classes, style, extra = "") {
448
+ let out = "";
449
+ for (const name in node.props) {
450
+ if (name === "class" || name === "style") continue;
451
+ const value = node.props[name];
452
+ if (/^on/i.test(name)) throw new Error(`email: <${node.type} ${name}> is an event handler; mail clients run no script. Remove it.`);
453
+ if (value === true) {
454
+ out += ` ${name}`;
455
+ continue;
456
+ }
457
+ if (typeof value === "function" || typeof value === "object" && value !== null) throw new TypeError(`email: <${node.type} ${name}=…> is an object; pass a string or a number.`);
458
+ const text = String(value);
459
+ if ((name === "href" || name === "src") && !SAFE_URL.test(text.trim())) throw new Error(`email: <${node.type} ${name}=${JSON.stringify(text)}> needs an absolute https, http, mailto, tel or cid URL; mail has no base URL and clients block other schemes.`);
460
+ out += ` ${name}="${escapeAttr(text)}"`;
461
+ }
462
+ if (classes.length) out += ` class="${escapeAttr(classes.join(" "))}"`;
463
+ const css = styleText(style);
464
+ if (css) out += ` style="${escapeAttr(css)}"`;
465
+ return out + extra;
466
+ }
467
+ const px = (value) => value && /^\d+(\.\d+)?px$/.test(value) ? parseFloat(value) : void 0;
468
+ const take = (style, ...names) => {
469
+ for (const name of names) style.delete(name);
470
+ };
471
+ const elementsOf = (node) => node.children.filter((c) => typeof c !== "string" || c.trim());
472
+ function children(node, ctx, align) {
473
+ let out = "";
474
+ for (const child of node.children) out += write(child, ctx, align);
475
+ return out;
476
+ }
477
+ function features(style) {
478
+ const out = [];
479
+ for (const [p, v] of style) {
480
+ const d = `${p}:${v}`;
481
+ if (p === "display" && /flex/.test(v)) out.push(["display:flex", d]);
482
+ else if (p === "display" && /grid/.test(v)) out.push(["display:grid", d]);
483
+ else if (p === "position" && v !== "static") out.push(["position", d]);
484
+ else if (/^(flex-wrap|justify-content|transform|aspect-ratio|object-fit|background-image)$/.test(p)) out.push([p, d]);
485
+ else if (/^(row-|column-)?gap$/.test(p)) out.push(["gap", d]);
486
+ if (v.includes("var(")) out.push(["var()", d]);
487
+ }
488
+ return out;
489
+ }
490
+ function check(node, style, ctx) {
491
+ const checks = ctx.reg.checks;
492
+ if (!checks) return;
493
+ for (const [key, declaration] of features(style)) {
494
+ const c = checks[key];
495
+ if (c) warn(ctx, `<${node.type}> has ${declaration}, which ${c.clients} drop. ${c.fix}`);
496
+ }
497
+ if (node.type === "img" && node.props.alt == null) warn(ctx, `<img src="${String(node.props.src)}"> has no alt. Add alt="…" (alt="" for a decorative image).`);
498
+ const cls = node.props.class;
499
+ if (cls && /(?:^|\s)mi-/.test(cls)) warn(ctx, "an icon() is SVG, which Gmail and Outlook do not show. Use <img src=\"https://…/icon.png\" alt=\"…\" width=\"16\" height=\"16\"> with a PNG at an absolute URL.");
500
+ }
501
+ /** An `<a>` with a background and padding: a one-cell table Outlook pads (D8). */
502
+ function button(node, c, ctx, align) {
503
+ const style = new Map(c.style);
504
+ const bg = style.get("background-color");
505
+ const pad = [
506
+ "top",
507
+ "right",
508
+ "bottom",
509
+ "left"
510
+ ].map((s) => style.get(`padding-${s}`) ?? "0");
511
+ const outer = /* @__PURE__ */ new Map();
512
+ for (const s of [
513
+ "top",
514
+ "right",
515
+ "bottom",
516
+ "left"
517
+ ]) {
518
+ const m = style.get(`margin-${s}`);
519
+ if (m !== void 0) outer.set(`margin-${s}`, m);
520
+ style.delete(`margin-${s}`);
521
+ }
522
+ const full = style.get("display") === "block" || style.get("width") === "100%";
523
+ style.set("display", full ? "block" : "inline-block");
524
+ if (!style.has("text-decoration")) style.set("text-decoration", "none");
525
+ const cell = /* @__PURE__ */ new Map([["background-color", bg]]);
526
+ const radius = style.get("border-radius");
527
+ if (radius) cell.set("border-radius", radius);
528
+ cell.set("mso-padding-alt", pad.join(" "));
529
+ const center = align === "center";
530
+ if (center) outer.set("margin-left", "auto").set("margin-right", "auto");
531
+ check(node, style, ctx);
532
+ return `${`<table ${TABLE}${center ? " align=\"center\"" : ""}${full ? " width=\"100%\"" : ""}${outer.size ? ` style="${escapeAttr(styleText(outer))}"` : ""}>`}<tr><td align="center" bgcolor="${escapeAttr(bg)}" style="${escapeAttr(styleText(cell))}"><a${attributes(node, c.classes, style)}>${children(node, ctx, void 0)}</a></td></tr></table>`;
533
+ }
534
+ /** A `max-width` (or fixed-width, auto-margin) box: a table Outlook gives a width (D9). */
535
+ function boxTable(node, c, ctx, width, max) {
536
+ const style = new Map(c.style);
537
+ const centered = style.get("margin-left") === "auto" && style.get("margin-right") === "auto";
538
+ const outer = /* @__PURE__ */ new Map();
539
+ if (max) outer.set("max-width", `${width}px`);
540
+ outer.set("width", "100%");
541
+ for (const s of ["top", "bottom"]) {
542
+ const m = style.get(`margin-${s}`);
543
+ if (m !== void 0) outer.set(`margin-${s}`, m);
544
+ }
545
+ if (centered) outer.set("margin-left", "auto").set("margin-right", "auto");
546
+ take(style, "max-width", "width", "margin-top", "margin-right", "margin-bottom", "margin-left");
547
+ const align = centered ? "center" : ctx.dir === "rtl" ? "right" : "left";
548
+ const bg = style.get("background-color");
549
+ check(node, style, ctx);
550
+ return `<!--[if mso]><table ${TABLE} align="${align}" width="${width}"><tr><td><![endif]--><table ${TABLE} align="${align}" width="100%" style="${escapeAttr(styleText(outer))}"><tr><td${attributes(node, c.classes, style, bg ? ` bgcolor="${escapeAttr(bg)}"` : "")}>${children(node, ctx, style.get("text-align"))}</td></tr></table><!--[if mso]></td></tr></table><![endif]-->`;
551
+ }
552
+ const VALIGN = {
553
+ center: "middle",
554
+ "flex-end": "bottom",
555
+ end: "bottom"
556
+ };
557
+ const SIMPLE_JUSTIFY = /* @__PURE__ */ new Set([
558
+ "flex-start",
559
+ "start",
560
+ "normal",
561
+ "left"
562
+ ]);
563
+ /** A flex row or column of elements: table cells, `gap` as their padding (D9). */
564
+ function flexTable(node, c, ctx, align) {
565
+ const style = c.style;
566
+ const direction = style.get("flex-direction") ?? "row";
567
+ const wrap = style.get("flex-wrap");
568
+ const justify = style.get("justify-content");
569
+ const items = elementsOf(node);
570
+ if (style.get("display") !== "flex" || direction !== "row" && direction !== "column" || wrap && wrap !== "nowrap" || justify && !SIMPLE_JUSTIFY.has(justify) || items.some((i) => typeof i === "string")) return void 0;
571
+ const rest = new Map(style);
572
+ const [rowGap, columnGap = rowGap] = parts(style.get("gap") ?? "");
573
+ const gap = direction === "row" ? style.get("column-gap") ?? columnGap : style.get("row-gap") ?? rowGap;
574
+ const alignItems = style.get("align-items") ?? "";
575
+ take(rest, "display", "flex-direction", "flex-wrap", "gap", "row-gap", "column-gap", "align-items", "justify-content");
576
+ const inner = rest.get("text-align") ?? align;
577
+ const start = ctx.dir === "rtl" ? "right" : "left";
578
+ let cells = "";
579
+ items.forEach((item, i) => {
580
+ const cell = /* @__PURE__ */ new Map();
581
+ if (i > 0 && gap && gap !== "0" && gap !== "0px") cell.set(direction === "row" ? `padding-${start}` : "padding-top", gap);
582
+ const own = computedOf(item, ctx).style;
583
+ const w = own.get("width") ?? own.get("flex-basis");
584
+ const td = `<td${(w && /^\d+(\.\d+)?(px|%)$/.test(w) ? ` width="${w.endsWith("%") ? w : parseFloat(w)}"` : "") + (direction === "row" ? ` valign="${VALIGN[alignItems] ?? "top"}"` : alignItems === "center" ? " align=\"center\"" : /end/.test(alignItems) ? ` align="${ctx.dir === "rtl" ? "left" : "right"}"` : "") + (cell.size ? ` style="${escapeAttr(styleText(cell))}"` : "")}>${write(item, ctx, inner)}</td>`;
585
+ cells += direction === "row" ? td : `<tr>${td}</tr>`;
586
+ });
587
+ const grid = `<table ${TABLE} width="100%">${direction === "row" ? `<tr>${cells}</tr>` : cells}</table>`;
588
+ check(node, rest, ctx);
589
+ const outer = /* @__PURE__ */ new Map();
590
+ for (const s of [
591
+ "top",
592
+ "right",
593
+ "bottom",
594
+ "left"
595
+ ]) {
596
+ const m = rest.get(`margin-${s}`);
597
+ if (m !== void 0) outer.set(`margin-${s}`, m);
598
+ rest.delete(`margin-${s}`);
599
+ }
600
+ const bg = rest.get("background-color");
601
+ const attrs = attributes(node, c.classes, rest, bg ? ` bgcolor="${escapeAttr(bg)}"` : "");
602
+ const margins = outer.size ? ` style="${escapeAttr(styleText(outer))}"` : "";
603
+ if (attrs) return `<table ${TABLE} width="100%"${margins}><tr><td${attrs}>${grid}</td></tr></table>`;
604
+ return margins ? grid.replace("width=\"100%\">", `width="100%"${margins}>`) : grid;
605
+ }
606
+ /** Whether `node` is written as a table: a button, a flex row or column, a box. */
607
+ function becomesTable(node, ctx) {
608
+ const { style } = computedOf(node, ctx);
609
+ const tag = node.type.toLowerCase();
610
+ if (tag === "a") return style.has("background-color") && [...style.keys()].some((p) => p.startsWith("padding"));
611
+ if (style.get("display") === "flex") return true;
612
+ return BOXES.has(tag) && (px(style.get("max-width")) !== void 0 || px(style.get("width")) !== void 0);
613
+ }
614
+ /** One child as HTML; `align` is the text-align it inherits, which a button table follows. */
615
+ function write(child, ctx, align) {
616
+ if (typeof child === "string") return escapeText(child);
617
+ const node = child;
618
+ const tag = node.type.toLowerCase();
619
+ if (FORBIDDEN.has(tag)) throw new Error(`email: <${tag}> is not allowed in a template; mail clients strip it. The <head> and its <style> are render()'s own.`);
620
+ const c = computedOf(node, ctx);
621
+ const style = c.style;
622
+ if (tag === "a" && style.has("background-color") && [...style.keys()].some((p) => p.startsWith("padding"))) return button(node, c, ctx, align);
623
+ if (style.get("display") === "flex") {
624
+ const table = flexTable(node, c, ctx, align);
625
+ if (table) return table;
626
+ }
627
+ if (BOXES.has(tag)) {
628
+ const max = px(style.get("max-width"));
629
+ if (max) return boxTable(node, c, ctx, max, true);
630
+ const width = px(style.get("width"));
631
+ if (width && style.get("margin-left") === "auto" && style.get("margin-right") === "auto") return boxTable(node, c, ctx, width, false);
632
+ }
633
+ let extra = "";
634
+ let wrap;
635
+ let out = style;
636
+ let name = tag;
637
+ if (/^(p|h[1-6])$/.test(tag) && node.children.some((k) => typeof k !== "string" && becomesTable(k, ctx))) {
638
+ name = "div";
639
+ if (tag === "p") {
640
+ out = new Map(style);
641
+ if (!out.has("margin-top")) out.set("margin-top", "1em");
642
+ if (!out.has("margin-bottom")) out.set("margin-bottom", "1em");
643
+ }
644
+ }
645
+ if (tag === "img") {
646
+ out = new Map(style);
647
+ if (!out.has("display")) out.set("display", "block");
648
+ for (const side of ["width", "height"]) {
649
+ const n = px(out.get(side));
650
+ if (n !== void 0 && node.props[side] == null) extra += ` ${side}="${n}"`;
651
+ }
652
+ if (out.get("margin-left") === "auto" && out.get("margin-right") === "auto") {
653
+ take(out, "margin-left", "margin-right");
654
+ wrap = [`<table ${TABLE} align="center"><tr><td>`, "</td></tr></table>"];
655
+ }
656
+ } else if ((tag === "td" || tag === "table") && style.get("background-color")) extra += ` bgcolor="${escapeAttr(style.get("background-color"))}"`;
657
+ check(node, out, ctx);
658
+ const open = `<${name}${attributes(node, c.classes, out, extra)}>`;
659
+ const html = VOID.has(tag) ? open : `${open}${children(node, ctx, style.get("text-align") ?? align)}</${name}>`;
660
+ return wrap ? wrap[0] + html + wrap[1] : html;
661
+ }
662
+ //#endregion
663
+ //#region src/registry.ts
664
+ const KEY = Symbol.for("@menapo/email");
665
+ function registry() {
666
+ const g = globalThis;
667
+ return g[KEY] ??= { css: {} };
668
+ }
669
+ //#endregion
670
+ //#region src/text.ts
671
+ const BLOCKS = /* @__PURE__ */ new Set([
672
+ "address",
673
+ "article",
674
+ "aside",
675
+ "blockquote",
676
+ "center",
677
+ "dd",
678
+ "div",
679
+ "dl",
680
+ "dt",
681
+ "footer",
682
+ "h1",
683
+ "h2",
684
+ "h3",
685
+ "h4",
686
+ "h5",
687
+ "h6",
688
+ "header",
689
+ "main",
690
+ "ol",
691
+ "p",
692
+ "pre",
693
+ "section",
694
+ "table",
695
+ "tr",
696
+ "ul"
697
+ ]);
698
+ const BREAK = "\0";
699
+ function textOf(root, styles) {
700
+ let out = "";
701
+ const walk = (child, list) => {
702
+ if (typeof child === "string") {
703
+ out += child.replace(/\s+/g, " ");
704
+ return;
705
+ }
706
+ const { type, props } = child;
707
+ const style = styles.get(child)?.style;
708
+ if (props.hidden != null || props["aria-hidden"] === "true" || style?.get("display") === "none") return;
709
+ switch (type) {
710
+ case "br":
711
+ out += "\n";
712
+ return;
713
+ case "hr":
714
+ out += `${BREAK}---${BREAK}`;
715
+ return;
716
+ case "img":
717
+ if (typeof props.alt === "string" && props.alt) out += props.alt;
718
+ return;
719
+ case "a": {
720
+ const start = out.length;
721
+ for (const c of child.children) walk(c, list);
722
+ const label = out.slice(start).trim();
723
+ const href = typeof props.href === "string" ? props.href : "";
724
+ const shown = href.replace(/^mailto:|^tel:/, "");
725
+ if (href && shown !== label) out = `${out.slice(0, start)}${label ? `${label}: ` : ""}${href}`;
726
+ return;
727
+ }
728
+ case "li":
729
+ out += `\n${list?.ordered ? `${++list.n}. ` : "- "}`;
730
+ for (const c of child.children) walk(c);
731
+ out += "\n";
732
+ return;
733
+ case "td":
734
+ case "th":
735
+ for (const c of child.children) walk(c, list);
736
+ out += " ";
737
+ return;
738
+ }
739
+ const block = BLOCKS.has(type);
740
+ if (block) out += BREAK;
741
+ const inner = type === "ol" || type === "ul" ? {
742
+ ordered: type === "ol",
743
+ n: 0
744
+ } : list;
745
+ for (const c of child.children) walk(c, inner);
746
+ if (block) out += BREAK;
747
+ };
748
+ for (const child of root) walk(child);
749
+ return out.split(BREAK).map((block) => block.split("\n").map((line) => line.replace(/[ \t]+/g, " ").trim()).filter(Boolean).join("\n")).filter(Boolean).join("\n\n");
750
+ }
751
+ //#endregion
752
+ //#region src/index.ts
753
+ function direction(locale) {
754
+ try {
755
+ const l = new Intl.Locale(locale);
756
+ return (l.getTextInfo?.() ?? l.textInfo)?.direction === "rtl" ? "rtl" : "ltr";
757
+ } catch {
758
+ return "ltr";
759
+ }
760
+ }
761
+ const escape$1 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
762
+ const RESET = "body{margin:0;padding:0;width:100%!important;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%}table,td{border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt}img{border:0;outline:none;text-decoration:none;-ms-interpolation-mode:bicubic;height:auto;line-height:100%}a[x-apple-data-detectors]{color:inherit!important;text-decoration:none!important;font-size:inherit!important;font-family:inherit!important;font-weight:inherit!important;line-height:inherit!important}";
763
+ const MSO = "<!--[if mso]><noscript><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml></noscript><style>td,th,div,p,a,h1,h2,h3,h4,h5,h6{mso-line-height-rule:exactly}</style><![endif]-->";
764
+ const FILLER = "&#847;&zwnj;&nbsp;".repeat(60);
765
+ function shell(subject, preheader, body, rules, lang, dir) {
766
+ const dark = [...rules].some((r) => r.includes("prefers-color-scheme") || r.includes("[data-ogsc]") || r.includes("[data-ogsb]"));
767
+ const langAttrs = `${lang ? ` lang="${escape$1(lang)}"` : ""}${dir ? ` dir="${dir}"` : ""}`;
768
+ return `<!doctype html><html${langAttrs} xmlns="http://www.w3.org/1999/xhtml" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="x-apple-disable-message-reformatting"><meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">` + (dark ? "<meta name=\"color-scheme\" content=\"light dark\"><meta name=\"supported-color-schemes\" content=\"light dark\">" : "") + `<title>${escape$1(subject)}</title>${MSO}<style>${RESET}${[...rules].join("")}</style></head><body style="margin:0;padding:0;word-spacing:normal"><div role="article" aria-roledescription="email" aria-label="${escape$1(subject)}"${langAttrs}>` + (preheader ? `<div style="display:none;max-height:0;overflow:hidden;mso-hide:all">${escape$1(preheader)}${FILLER}</div>` : "") + `<table role="presentation" width="100%" border="0" cellpadding="0" cellspacing="0" align="center"><tr><td>${body}</td></tr></table></div></body></html>`;
769
+ }
770
+ const list = (markup) => Array.isArray(markup) ? markup : [markup];
771
+ /** Renders an email for one recipient: its subject, HTML and plain text. */
772
+ async function render(mail, props, options = {}) {
773
+ const reg = registry();
774
+ const { locale, timeZone } = options;
775
+ const run = async () => {
776
+ const message = await mail.template(props);
777
+ const { lang, dir } = reg.i18n ? reg.i18n.htmlAttributes() : {
778
+ lang: locale,
779
+ dir: locale ? direction(locale) : void 0
780
+ };
781
+ const ctx = {
782
+ reg,
783
+ dir: dir ?? "ltr",
784
+ rules: /* @__PURE__ */ new Set(),
785
+ name: options.name ?? "email",
786
+ computed: /* @__PURE__ */ new WeakMap(),
787
+ warned: /* @__PURE__ */ new Set()
788
+ };
789
+ const roots = list(message.body);
790
+ let body = "";
791
+ for (const child of roots) body += write(child, ctx);
792
+ const text = textOf(roots, ctx.computed);
793
+ if (reg.checks && body.length > 1e5) warn(ctx, `the HTML is ${Math.round(body.length / 1e3)} KB; Gmail clips mail over about 102 KB. Shorten it.`);
794
+ return {
795
+ subject: message.subject,
796
+ html: shell(message.subject, message.preheader, body, ctx.rules, lang, dir),
797
+ text
798
+ };
799
+ };
800
+ if (reg.i18n) {
801
+ if (locale !== void 0) return reg.i18n.withLocale(locale, run, timeZone === void 0 ? {} : { timeZone });
802
+ try {
803
+ reg.i18n.htmlAttributes();
804
+ } catch (cause) {
805
+ throw new Error(`email: render(${options.name ?? "…"}) has no locale and runs outside a request, so t() has none. Pass the recipient's: render(email, props, { locale: user.locale }).`, { cause });
806
+ }
807
+ }
808
+ return run();
809
+ }
810
+ //#endregion
811
+ //#region src/vite/preview.ts
812
+ /**
813
+ * The dev preview at `/__email` (PLAN D15): every `email()` export of the
814
+ * project's files with each sample, and one page per sample with the
815
+ * subject, the HTML, the text part, its size and the warnings.
816
+ */
817
+ const PREFIX = "/__email";
818
+ const SKIP = /* @__PURE__ */ new Set([
819
+ "node_modules",
820
+ "dist",
821
+ "build",
822
+ "coverage",
823
+ "test-results"
824
+ ]);
825
+ const SOURCE = /\.(?:[cm]?[jt]sx?)$/;
826
+ const escape = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
827
+ /** Files that import `@menapo/email` and call `email(`: where the emails are. */
828
+ function emailFiles(root, dir = root, out = []) {
829
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
830
+ if (entry.name.startsWith(".") || SKIP.has(entry.name)) continue;
831
+ const path = join(dir, entry.name);
832
+ if (entry.isDirectory()) emailFiles(root, path, out);
833
+ else if (SOURCE.test(entry.name) && !/\.(test|spec)\./.test(entry.name)) {
834
+ const code = readFileSync(path, "utf8");
835
+ if (code.includes("@menapo/email") && /\bemail\s*\(/.test(code)) out.push(relative(root, path));
836
+ }
837
+ }
838
+ return out;
839
+ }
840
+ async function emails(server) {
841
+ const root = server.config.root;
842
+ const out = [];
843
+ for (const file of emailFiles(root).sort()) {
844
+ const mod = await server.ssrLoadModule(`/${file}`);
845
+ for (const [name, value] of Object.entries(mod)) if (isEmail(value)) out.push({
846
+ file,
847
+ name,
848
+ email: value
849
+ });
850
+ }
851
+ return out;
852
+ }
853
+ async function locales(server) {
854
+ const file = join(server.config.root, "vi18n.config.ts");
855
+ if (!existsSync(file)) return [];
856
+ return (await server.ssrLoadModule("/vi18n.config.ts")).default?.locales ?? [];
857
+ }
858
+ const page = (title, body) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><title>${escape(title)}</title><style>body{font:14px/1.5 system-ui,sans-serif;margin:0 auto;max-width:960px;padding:24px;color:#111}a{color:#2563eb}iframe{width:100%;height:640px;border:1px solid #ddd;border-radius:6px}pre{white-space:pre-wrap;background:#f6f6f6;padding:12px;border-radius:6px}.warn{color:#9a3412}</style></head><body>${body}</body></html>`;
859
+ function preview(server) {
860
+ return (req, res, next) => {
861
+ const url = new URL(req.url ?? "/", "http://localhost");
862
+ if (url.pathname !== PREFIX && url.pathname !== `${PREFIX}/`) return next();
863
+ (async () => {
864
+ const all = await emails(server);
865
+ const file = url.searchParams.get("file");
866
+ const name = url.searchParams.get("email");
867
+ const sample = url.searchParams.get("sample") ?? "default";
868
+ const known = await locales(server);
869
+ const locale = url.searchParams.get("locale") ?? known[0];
870
+ let body;
871
+ const found = all.find((e) => e.file === file && e.name === name);
872
+ if (!found) {
873
+ const items = all.map(({ file, name, email }) => {
874
+ const samples = Object.keys(email.samples);
875
+ const links = samples.length ? samples.map((s) => `<a href="${PREFIX}?file=${encodeURIComponent(file)}&email=${name}&sample=${encodeURIComponent(s)}">${escape(s)}</a>`).join(" · ") : "<em>no samples: add email(…, { samples: { default: {…} } })</em>";
876
+ return `<li><strong>${escape(name)}</strong> <small>${escape(file)}</small>: ${links}</li>`;
877
+ }).join("");
878
+ body = `<h1>Emails</h1>${items ? `<ul>${items}</ul>` : "<p>No email() exports found.</p>"}`;
879
+ } else {
880
+ const props = found.email.samples[sample];
881
+ if (props === void 0) throw new Error(`email: ${found.name} has no sample "${sample}".`);
882
+ const warnings = [];
883
+ const reg = registry();
884
+ const before = reg.warn;
885
+ reg.warn = (m) => warnings.push(m);
886
+ let out;
887
+ try {
888
+ out = await render(found.email, props, {
889
+ locale,
890
+ name: found.name
891
+ });
892
+ } finally {
893
+ reg.warn = before;
894
+ }
895
+ const here = `${PREFIX}?file=${encodeURIComponent(found.file)}&email=${found.name}&sample=${encodeURIComponent(sample)}`;
896
+ const switcher = known.map((l) => l === locale ? `<strong>${l}</strong>` : `<a href="${here}&locale=${l}">${l}</a>`).join(" · ");
897
+ body = `<p><a href="${PREFIX}">All emails</a>${switcher ? ` · ${switcher}` : ""}</p><h1>${escape(found.name)} <small>${escape(sample)}</small></h1><p>Subject: <strong id="subject">${escape(out.subject)}</strong> · ${(out.html.length / 1e3).toFixed(1)} KB</p>` + (warnings.length ? `<ul id="warnings">${warnings.map((w) => `<li class="warn">${escape(w)}</li>`).join("")}</ul>` : "") + `<iframe id="html" title="HTML part" srcdoc="${escape(out.html)}"></iframe><h2>Text</h2><pre id="text">${escape(out.text)}</pre>`;
898
+ }
899
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
900
+ res.setHeader("Cache-Control", "no-cache");
901
+ res.end(page("Emails", body));
902
+ })().catch((error) => {
903
+ server.ssrFixStacktrace(error);
904
+ next(error);
905
+ });
906
+ };
907
+ }
908
+ //#endregion
909
+ //#region src/vite/index.ts
910
+ const REGISTRY = "globalThis[Symbol.for(\"@menapo/email\")] ??= { css: {} }";
911
+ function email() {
912
+ let vicss;
913
+ let i18n = false;
914
+ let dev = false;
915
+ return {
916
+ name: "menapo:email",
917
+ enforce: "post",
918
+ configResolved(config) {
919
+ vicss = config.plugins.find((p) => p.name === "vicss")?.api;
920
+ i18n = config.plugins.some((p) => p.name.startsWith("vi18n:"));
921
+ dev = config.command === "serve";
922
+ },
923
+ async transform(code, id) {
924
+ if (this.environment.config.consumer !== "server") return null;
925
+ if (id.startsWith("\0") || id.includes("/node_modules/")) return null;
926
+ let imports = "";
927
+ let body = "";
928
+ if (vicss) {
929
+ const literals = await vicss.literals(code, this);
930
+ const css = {};
931
+ let any = false;
932
+ for (const name in literals) {
933
+ css[name] = entryOf(literals[name]);
934
+ any = true;
935
+ }
936
+ if (any) body += `Object.assign(r.css, ${JSON.stringify(css)});`;
937
+ }
938
+ if (/["']@menapo\/email["']/.test(code)) {
939
+ if (i18n) {
940
+ imports += "import { withLocale as __mew, htmlAttributes as __meh } from \"@menapo/vi18n/server\";\n";
941
+ body += "r.i18n ??= { withLocale: __mew, htmlAttributes: __meh };";
942
+ }
943
+ if (dev) body += `r.checks ??= ${JSON.stringify(CHECKS)};`;
944
+ }
945
+ if (!body) return null;
946
+ return {
947
+ code: `${code}\n${imports}{ const r = (${REGISTRY}); ${body} }\n`,
948
+ map: null
949
+ };
950
+ },
951
+ configureServer(server) {
952
+ server.middlewares.use(preview(server));
953
+ }
954
+ };
955
+ }
956
+ //#endregion
957
+ export { email as default, email };