@lupinum/nuxt-email 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/CHANGELOG.md +49 -0
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/THIRD_PARTY_NOTICES +75 -0
- package/dist/chunks/setup.mjs +149 -0
- package/dist/module.d.mts +15 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +300 -0
- package/dist/runtime/code-block/create-component.d.ts +16 -0
- package/dist/runtime/code-block/create-component.js +106 -0
- package/dist/runtime/components/EBody.d.ts +4 -0
- package/dist/runtime/components/EBody.js +75 -0
- package/dist/runtime/components/EButton.d.ts +11 -0
- package/dist/runtime/components/EButton.js +89 -0
- package/dist/runtime/components/EButton.padding.d.ts +12 -0
- package/dist/runtime/components/EButton.padding.js +118 -0
- package/dist/runtime/components/ECodeInline.d.ts +13 -0
- package/dist/runtime/components/ECodeInline.js +43 -0
- package/dist/runtime/components/EColumn.d.ts +4 -0
- package/dist/runtime/components/EColumn.js +12 -0
- package/dist/runtime/components/EContainer.d.ts +4 -0
- package/dist/runtime/components/EContainer.js +35 -0
- package/dist/runtime/components/EFont.d.ts +24 -0
- package/dist/runtime/components/EFont.js +124 -0
- package/dist/runtime/components/EHead.d.ts +4 -0
- package/dist/runtime/components/EHead.js +17 -0
- package/dist/runtime/components/EHeading.d.ts +7 -0
- package/dist/runtime/components/EHeading.js +23 -0
- package/dist/runtime/components/EHr.d.ts +4 -0
- package/dist/runtime/components/EHr.js +26 -0
- package/dist/runtime/components/EHtml.d.ts +4 -0
- package/dist/runtime/components/EHtml.js +17 -0
- package/dist/runtime/components/EImg.d.ts +7 -0
- package/dist/runtime/components/EImg.js +44 -0
- package/dist/runtime/components/ELink.d.ts +6 -0
- package/dist/runtime/components/ELink.js +35 -0
- package/dist/runtime/components/EMarkdown.d.ts +8 -0
- package/dist/runtime/components/EMarkdown.js +64 -0
- package/dist/runtime/components/EPreview.d.ts +8 -0
- package/dist/runtime/components/EPreview.js +85 -0
- package/dist/runtime/components/ERow.d.ts +4 -0
- package/dist/runtime/components/ERow.js +53 -0
- package/dist/runtime/components/ESection.d.ts +4 -0
- package/dist/runtime/components/ESection.js +35 -0
- package/dist/runtime/components/ETailwind.d.ts +24 -0
- package/dist/runtime/components/ETailwind.js +49 -0
- package/dist/runtime/components/EText.d.ts +4 -0
- package/dist/runtime/components/EText.js +20 -0
- package/dist/runtime/components/EText.margins.d.ts +18 -0
- package/dist/runtime/components/EText.margins.js +81 -0
- package/dist/runtime/components/attributes.d.ts +4 -0
- package/dist/runtime/components/attributes.js +11 -0
- package/dist/runtime/components/email-component-names.d.ts +2 -0
- package/dist/runtime/components/email-component-names.js +20 -0
- package/dist/runtime/components/email-component-registry.d.ts +8 -0
- package/dist/runtime/components/email-component-registry.js +2 -0
- package/dist/runtime/components/index.d.ts +23 -0
- package/dist/runtime/components/index.js +18 -0
- package/dist/runtime/components/markdown/parse-css-in-js-to-inline-css.d.ts +2 -0
- package/dist/runtime/components/markdown/parse-css-in-js-to-inline-css.js +60 -0
- package/dist/runtime/components/markdown/render-markdown.d.ts +2 -0
- package/dist/runtime/components/markdown/render-markdown.js +137 -0
- package/dist/runtime/components/markdown/styles.d.ts +30 -0
- package/dist/runtime/components/markdown/styles.js +91 -0
- package/dist/runtime/components/presentation-table.d.ts +7 -0
- package/dist/runtime/components/presentation-table.js +11 -0
- package/dist/runtime/components/style.d.ts +5 -0
- package/dist/runtime/components/style.js +32 -0
- package/dist/runtime/components/table-padding.d.ts +6 -0
- package/dist/runtime/components/table-padding.js +20 -0
- package/dist/runtime/define-email.d.ts +2 -0
- package/dist/runtime/define-email.js +5 -0
- package/dist/runtime/dev-preview/page-script.d.ts +1 -0
- package/dist/runtime/dev-preview/page-script.js +403 -0
- package/dist/runtime/dev-preview/page.css.d.ts +1 -0
- package/dist/runtime/dev-preview/page.css.js +602 -0
- package/dist/runtime/dev-preview/page.get.d.ts +4 -0
- package/dist/runtime/dev-preview/page.get.js +119 -0
- package/dist/runtime/dev-preview/render.get.d.ts +3 -0
- package/dist/runtime/dev-preview/render.get.js +123 -0
- package/dist/runtime/dev-preview/templates.get.d.ts +7 -0
- package/dist/runtime/dev-preview/templates.get.js +14 -0
- package/dist/runtime/errors.d.ts +4 -0
- package/dist/runtime/errors.js +7 -0
- package/dist/runtime/render/define-email.d.ts +56 -0
- package/dist/runtime/render/define-email.js +40 -0
- package/dist/runtime/render/document.d.ts +4 -0
- package/dist/runtime/render/document.js +75 -0
- package/dist/runtime/render/errors.d.ts +4 -0
- package/dist/runtime/render/errors.js +8 -0
- package/dist/runtime/render/plain-text.d.ts +1 -0
- package/dist/runtime/render/plain-text.js +15 -0
- package/dist/runtime/render/render-component.d.ts +4 -0
- package/dist/runtime/render/render-component.js +46 -0
- package/dist/runtime/render/render-email-component.d.ts +16 -0
- package/dist/runtime/render/render-email-component.js +46 -0
- package/dist/runtime/render/types.d.ts +9 -0
- package/dist/runtime/render/types.js +0 -0
- package/dist/runtime/tailwind/engine/compatibility/escape-class-name.d.ts +8 -0
- package/dist/runtime/tailwind/engine/compatibility/escape-class-name.js +10 -0
- package/dist/runtime/tailwind/engine/compatibility/sanitize-class-name.d.ts +7 -0
- package/dist/runtime/tailwind/engine/compatibility/sanitize-class-name.js +17 -0
- package/dist/runtime/tailwind/engine/compatibility/unescape-class.d.ts +1 -0
- package/dist/runtime/tailwind/engine/compatibility/unescape-class.js +3 -0
- package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.d.ts +63 -0
- package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +107 -0
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.d.ts +6 -0
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +54 -0
- package/dist/runtime/tailwind/engine/css/get-css-property.d.ts +17 -0
- package/dist/runtime/tailwind/engine/css/get-css-property.js +3 -0
- package/dist/runtime/tailwind/engine/css/get-custom-properties.d.ts +8 -0
- package/dist/runtime/tailwind/engine/css/get-custom-properties.js +37 -0
- package/dist/runtime/tailwind/engine/css/is-part-inlinable.d.ts +2 -0
- package/dist/runtime/tailwind/engine/css/is-part-inlinable.js +9 -0
- package/dist/runtime/tailwind/engine/css/is-rule-inlinable.d.ts +2 -0
- package/dist/runtime/tailwind/engine/css/is-rule-inlinable.js +9 -0
- package/dist/runtime/tailwind/engine/css/make-inline-styles-for.d.ts +21 -0
- package/dist/runtime/tailwind/engine/css/make-inline-styles-for.js +62 -0
- package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.d.ts +8 -0
- package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.js +138 -0
- package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.d.ts +5 -0
- package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.js +101 -0
- package/dist/runtime/tailwind/engine/css/sanitize-declarations.d.ts +15 -0
- package/dist/runtime/tailwind/engine/css/sanitize-declarations.js +358 -0
- package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.d.ts +17 -0
- package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.js +30 -0
- package/dist/runtime/tailwind/engine/css/split-mixed-rule.d.ts +12 -0
- package/dist/runtime/tailwind/engine/css/split-mixed-rule.js +37 -0
- package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.d.ts +19 -0
- package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.js +44 -0
- package/dist/runtime/tailwind/engine/css/unwrap-value.d.ts +2 -0
- package/dist/runtime/tailwind/engine/css/unwrap-value.js +6 -0
- package/dist/runtime/tailwind/engine/index.d.ts +63 -0
- package/dist/runtime/tailwind/engine/index.js +53 -0
- package/dist/runtime/tailwind/engine/pixel-based-preset.d.ts +9 -0
- package/dist/runtime/tailwind/engine/pixel-based-preset.js +58 -0
- package/dist/runtime/tailwind/engine/sanitize-stylesheet.d.ts +10 -0
- package/dist/runtime/tailwind/engine/sanitize-stylesheet.js +8 -0
- package/dist/runtime/tailwind/engine/setup-tailwind.d.ts +23 -0
- package/dist/runtime/tailwind/engine/setup-tailwind.js +98 -0
- package/dist/runtime/tailwind/engine/stylesheets.d.ts +4 -0
- package/dist/runtime/tailwind/engine/stylesheets.js +1756 -0
- package/dist/runtime/tailwind/engine/types.d.ts +25 -0
- package/dist/runtime/tailwind/engine/types.js +0 -0
- package/dist/runtime/tailwind/errors.d.ts +7 -0
- package/dist/runtime/tailwind/errors.js +9 -0
- package/dist/runtime/tailwind/inline-utils.d.ts +19 -0
- package/dist/runtime/tailwind/inline-utils.js +24 -0
- package/dist/runtime/tailwind/nested.d.ts +59 -0
- package/dist/runtime/tailwind/nested.js +47 -0
- package/dist/runtime/tailwind/post-render.d.ts +7 -0
- package/dist/runtime/tailwind/post-render.js +156 -0
- package/dist/runtime/template-registry/errors.d.ts +5 -0
- package/dist/runtime/template-registry/errors.js +11 -0
- package/dist/runtime/template-registry/registry.d.ts +13 -0
- package/dist/runtime/testing/index.d.ts +13 -0
- package/dist/runtime/testing/index.js +2 -0
- package/dist/types.d.mts +5 -0
- package/package.json +122 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { emailTemplates, renderEmail } from "#nuxt-email/registry";
|
|
2
|
+
import {
|
|
3
|
+
defineEventHandler,
|
|
4
|
+
getQuery,
|
|
5
|
+
setHeaders,
|
|
6
|
+
setResponseStatus
|
|
7
|
+
} from "h3";
|
|
8
|
+
export const PREVIEW_RENDER_CSP = [
|
|
9
|
+
"default-src 'none'",
|
|
10
|
+
"script-src 'none'",
|
|
11
|
+
"style-src 'unsafe-inline'",
|
|
12
|
+
"img-src data: https: http:",
|
|
13
|
+
"font-src data: https: http:",
|
|
14
|
+
"frame-ancestors 'self'",
|
|
15
|
+
"base-uri 'none'",
|
|
16
|
+
"object-src 'none'",
|
|
17
|
+
"form-action 'none'"
|
|
18
|
+
].join("; ");
|
|
19
|
+
class PreviewRequestError extends Error {
|
|
20
|
+
statusCode;
|
|
21
|
+
constructor(statusCode, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "PreviewRequestError";
|
|
24
|
+
this.statusCode = statusCode;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function serializeError(error, seen = /* @__PURE__ */ new Set()) {
|
|
28
|
+
if (!(error instanceof Error)) {
|
|
29
|
+
return {
|
|
30
|
+
name: typeof error,
|
|
31
|
+
message: String(error)
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (seen.has(error)) {
|
|
35
|
+
return {
|
|
36
|
+
name: error.name,
|
|
37
|
+
message: "[circular error cause]"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
seen.add(error);
|
|
41
|
+
const serialized = {
|
|
42
|
+
name: error.name,
|
|
43
|
+
message: error.message
|
|
44
|
+
};
|
|
45
|
+
if (error.stack) {
|
|
46
|
+
serialized.stack = error.stack;
|
|
47
|
+
}
|
|
48
|
+
if ("componentName" in error && typeof error.componentName === "string") {
|
|
49
|
+
serialized.componentName = error.componentName;
|
|
50
|
+
}
|
|
51
|
+
if ("requestedName" in error && typeof error.requestedName === "string") {
|
|
52
|
+
serialized.requestedName = error.requestedName;
|
|
53
|
+
}
|
|
54
|
+
if ("knownNames" in error && Array.isArray(error.knownNames)) {
|
|
55
|
+
serialized.knownNames = error.knownNames.filter((name) => typeof name === "string");
|
|
56
|
+
}
|
|
57
|
+
if ("cause" in error && error.cause !== void 0) {
|
|
58
|
+
serialized.cause = serializeError(error.cause, seen);
|
|
59
|
+
}
|
|
60
|
+
return serialized;
|
|
61
|
+
}
|
|
62
|
+
function requiredString(value, parameter) {
|
|
63
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
64
|
+
throw new PreviewRequestError(400, `Query parameter "${parameter}" must be one non-empty string`);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function fixtureProps(value, name) {
|
|
69
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
70
|
+
throw new Error(`Preview fixture ${name}.fixtures.ts must default-export a props object`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
export default defineEventHandler(async (event) => {
|
|
75
|
+
setHeaders(event, {
|
|
76
|
+
"cache-control": "no-store",
|
|
77
|
+
"referrer-policy": "no-referrer",
|
|
78
|
+
"x-content-type-options": "nosniff"
|
|
79
|
+
});
|
|
80
|
+
const query = getQuery(event);
|
|
81
|
+
const requestedName = typeof query.name === "string" ? query.name : void 0;
|
|
82
|
+
try {
|
|
83
|
+
const name = requiredString(query.name, "name");
|
|
84
|
+
const format = query.format === void 0 ? "html" : requiredString(query.format, "format");
|
|
85
|
+
if (format !== "html" && format !== "json") {
|
|
86
|
+
throw new PreviewRequestError(400, 'Query parameter "format" must be "html" or "json"');
|
|
87
|
+
}
|
|
88
|
+
const template = Object.hasOwn(emailTemplates, name) ? emailTemplates[name] : void 0;
|
|
89
|
+
if (template === void 0) {
|
|
90
|
+
throw new PreviewRequestError(
|
|
91
|
+
404,
|
|
92
|
+
`Unknown email template "${name}"; known templates: ${Object.keys(emailTemplates).join(", ")}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
if (!template.fixture) {
|
|
96
|
+
throw new PreviewRequestError(404, `Email template "${name}" has no colocated ${name}.fixtures.ts file`);
|
|
97
|
+
}
|
|
98
|
+
const fixtureModule = await template.fixture();
|
|
99
|
+
const output = await renderEmail(name, fixtureProps(fixtureModule.default, name));
|
|
100
|
+
if (format === "json") {
|
|
101
|
+
setHeaders(event, { "content-type": "application/json; charset=utf-8" });
|
|
102
|
+
return { name, ...output, bytes: Buffer.byteLength(output.html, "utf8") };
|
|
103
|
+
}
|
|
104
|
+
setHeaders(event, {
|
|
105
|
+
"content-security-policy": PREVIEW_RENDER_CSP,
|
|
106
|
+
"content-type": "text/html; charset=utf-8"
|
|
107
|
+
});
|
|
108
|
+
return output.html;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const statusCode = error instanceof PreviewRequestError ? error.statusCode : 500;
|
|
111
|
+
const serializedError = serializeError(error);
|
|
112
|
+
setResponseStatus(event, statusCode);
|
|
113
|
+
setHeaders(event, { "content-type": "application/json; charset=utf-8" });
|
|
114
|
+
return {
|
|
115
|
+
statusCode,
|
|
116
|
+
statusMessage: serializedError.message,
|
|
117
|
+
data: {
|
|
118
|
+
templateName: requestedName,
|
|
119
|
+
error: serializedError
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { emailTemplates } from "#nuxt-email/registry";
|
|
2
|
+
import { defineEventHandler, setHeaders } from "h3";
|
|
3
|
+
export default defineEventHandler((event) => {
|
|
4
|
+
setHeaders(event, {
|
|
5
|
+
"cache-control": "no-store",
|
|
6
|
+
"content-type": "application/json; charset=utf-8"
|
|
7
|
+
});
|
|
8
|
+
return {
|
|
9
|
+
templates: Object.entries(emailTemplates).map(([name, template]) => ({
|
|
10
|
+
name,
|
|
11
|
+
hasFixture: template.fixture !== void 0
|
|
12
|
+
})).sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)
|
|
13
|
+
};
|
|
14
|
+
});
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { DefineEmailOutsideRenderError, DuplicateEmailDefinitionError, } from './render/define-email.js';
|
|
2
|
+
export { EmailRenderError } from './render/errors.js';
|
|
3
|
+
export { UnknownEmailTemplateError } from './template-registry/errors.js';
|
|
4
|
+
export { TailwindMissingHeadError } from './tailwind/errors.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export {
|
|
2
|
+
DefineEmailOutsideRenderError,
|
|
3
|
+
DuplicateEmailDefinitionError
|
|
4
|
+
} from "./render/define-email.js";
|
|
5
|
+
export { EmailRenderError } from "./render/errors.js";
|
|
6
|
+
export { UnknownEmailTemplateError } from "./template-registry/errors.js";
|
|
7
|
+
export { TailwindMissingHeadError } from "./tailwind/errors.js";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { TailwindRegion } from '../tailwind/nested.js';
|
|
2
|
+
/**
|
|
3
|
+
* Options declared by an email template via {@link defineEmail}.
|
|
4
|
+
*
|
|
5
|
+
* `subject` is a zero-argument closure over the template's real `defineProps()`
|
|
6
|
+
* value and returns the computed subject line.
|
|
7
|
+
*/
|
|
8
|
+
export interface DefineEmailOptions {
|
|
9
|
+
subject: () => string;
|
|
10
|
+
}
|
|
11
|
+
declare const EMAIL_RENDER_BRAND: unique symbol;
|
|
12
|
+
/**
|
|
13
|
+
* Per-render registry created by the renderer. A fresh instance is created for
|
|
14
|
+
* every render, so concurrent renders never share mutable state. `defineEmail`
|
|
15
|
+
* reaches it through the async-local render scope.
|
|
16
|
+
*/
|
|
17
|
+
export interface EmailRenderContext {
|
|
18
|
+
readonly [EMAIL_RENDER_BRAND]: true;
|
|
19
|
+
subject?: () => string;
|
|
20
|
+
/**
|
|
21
|
+
* Tailwind regions registered by `<ETailwind>` boundaries during render, in
|
|
22
|
+
* registration order. Consumed by the post-render pass to complete each head
|
|
23
|
+
* `<style>` and inline nested plain elements. Absent when no boundary rendered.
|
|
24
|
+
*/
|
|
25
|
+
tailwindRegions?: TailwindRegion[];
|
|
26
|
+
}
|
|
27
|
+
export declare class DefineEmailOutsideRenderError extends Error {
|
|
28
|
+
constructor();
|
|
29
|
+
}
|
|
30
|
+
export declare class DuplicateEmailDefinitionError extends Error {
|
|
31
|
+
constructor();
|
|
32
|
+
}
|
|
33
|
+
export declare function createEmailRenderContext(): EmailRenderContext;
|
|
34
|
+
/**
|
|
35
|
+
* The active email render context, or `undefined` outside a render. Used by
|
|
36
|
+
* `<ETailwind>` to register its region on the same context `renderComponentToHtml`
|
|
37
|
+
* reads back after render.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getEmailRenderContext(): EmailRenderContext | undefined;
|
|
40
|
+
/**
|
|
41
|
+
* Run `fn` (the render) with `context` as the active email render context so any
|
|
42
|
+
* `defineEmail()` call inside the rendered template — sync or after an await —
|
|
43
|
+
* resolves to it.
|
|
44
|
+
*/
|
|
45
|
+
export declare function runWithEmailRenderContext<T>(context: EmailRenderContext, fn: () => T): T;
|
|
46
|
+
/**
|
|
47
|
+
* Declare email metadata (currently the subject) from an email template's
|
|
48
|
+
* `<script setup>`. The zero-argument subject closure captures the template's
|
|
49
|
+
* real `defineProps()` value, so there is no separate generic that can lie about
|
|
50
|
+
* the template contract. The computed value surfaces as `subject`.
|
|
51
|
+
*
|
|
52
|
+
* Must run during an email render; calling it elsewhere throws
|
|
53
|
+
* {@link DefineEmailOutsideRenderError}.
|
|
54
|
+
*/
|
|
55
|
+
export declare function defineEmail(options: DefineEmailOptions): void;
|
|
56
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
const EMAIL_RENDER_BRAND = /* @__PURE__ */ Symbol("nuxt-email:render-context");
|
|
3
|
+
const renderContextStorage = new AsyncLocalStorage();
|
|
4
|
+
export class DefineEmailOutsideRenderError extends Error {
|
|
5
|
+
constructor() {
|
|
6
|
+
super("defineEmail() must be called during an email render, from an email template rendered by renderEmail().");
|
|
7
|
+
this.name = "DefineEmailOutsideRenderError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export class DuplicateEmailDefinitionError extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("defineEmail() may only be called once during one email render.");
|
|
13
|
+
this.name = "DuplicateEmailDefinitionError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export function createEmailRenderContext() {
|
|
17
|
+
return { [EMAIL_RENDER_BRAND]: true };
|
|
18
|
+
}
|
|
19
|
+
export function getEmailRenderContext() {
|
|
20
|
+
return renderContextStorage.getStore();
|
|
21
|
+
}
|
|
22
|
+
export function runWithEmailRenderContext(context, fn) {
|
|
23
|
+
return renderContextStorage.run(context, fn);
|
|
24
|
+
}
|
|
25
|
+
function isEmailRenderContext(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && value[EMAIL_RENDER_BRAND] === true;
|
|
27
|
+
}
|
|
28
|
+
export function defineEmail(options) {
|
|
29
|
+
const context = renderContextStorage.getStore();
|
|
30
|
+
if (!isEmailRenderContext(context)) {
|
|
31
|
+
throw new DefineEmailOutsideRenderError();
|
|
32
|
+
}
|
|
33
|
+
if (context.subject !== void 0) {
|
|
34
|
+
throw new DuplicateEmailDefinitionError();
|
|
35
|
+
}
|
|
36
|
+
if (!options || typeof options.subject !== "function") {
|
|
37
|
+
throw new TypeError("defineEmail() subject must be a function returning a string.");
|
|
38
|
+
}
|
|
39
|
+
context.subject = options.subject;
|
|
40
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export declare const EMAIL_DOCTYPE = "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">";
|
|
2
|
+
export declare function assembleEmailDocument(renderedHtml: string): string;
|
|
3
|
+
export declare function assertNoUnresolvedEmailComponents(renderedHtml: string): void;
|
|
4
|
+
export declare function assertCompleteEmailDocument(html: string): void;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Parser } from "htmlparser2";
|
|
2
|
+
export const EMAIL_DOCTYPE = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">';
|
|
3
|
+
const LEADING_DOCTYPE = /^\s*<!doctype[^>]*>/i;
|
|
4
|
+
const EMAIL_COMPONENT_TAG = /^E[A-Za-z0-9]+$/;
|
|
5
|
+
export function assembleEmailDocument(renderedHtml) {
|
|
6
|
+
return `${EMAIL_DOCTYPE}${renderedHtml.replace(LEADING_DOCTYPE, "")}`;
|
|
7
|
+
}
|
|
8
|
+
export function assertNoUnresolvedEmailComponents(renderedHtml) {
|
|
9
|
+
let unresolvedComponent;
|
|
10
|
+
const parser = new Parser({
|
|
11
|
+
onopentag(name) {
|
|
12
|
+
if (unresolvedComponent === void 0 && EMAIL_COMPONENT_TAG.test(name)) {
|
|
13
|
+
unresolvedComponent = name;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}, { xmlMode: true });
|
|
17
|
+
parser.end(renderedHtml);
|
|
18
|
+
if (unresolvedComponent !== void 0) {
|
|
19
|
+
throw new TypeError(
|
|
20
|
+
`Unknown email component <${unresolvedComponent}>. Configure it or use a registered E* component.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function assertCompleteEmailDocument(html) {
|
|
25
|
+
const document = html.startsWith(EMAIL_DOCTYPE) ? html.slice(EMAIL_DOCTYPE.length) : html;
|
|
26
|
+
const stack = [];
|
|
27
|
+
let bodyCount = 0;
|
|
28
|
+
let bodyClosed = false;
|
|
29
|
+
let headCount = 0;
|
|
30
|
+
let htmlCount = 0;
|
|
31
|
+
let htmlClosed = false;
|
|
32
|
+
let invalid = false;
|
|
33
|
+
const parser = new Parser({
|
|
34
|
+
onopentag(name) {
|
|
35
|
+
const parent = stack.at(-1);
|
|
36
|
+
if (name === "html") {
|
|
37
|
+
htmlCount++;
|
|
38
|
+
if (stack.length > 0 || htmlClosed) invalid = true;
|
|
39
|
+
} else if (name === "head") {
|
|
40
|
+
headCount++;
|
|
41
|
+
if (parent !== "html" || bodyCount > 0) invalid = true;
|
|
42
|
+
} else if (name === "body") {
|
|
43
|
+
bodyCount++;
|
|
44
|
+
if (parent !== "html" || bodyClosed) invalid = true;
|
|
45
|
+
} else if (!stack.includes("head") && !stack.includes("body")) {
|
|
46
|
+
invalid = true;
|
|
47
|
+
}
|
|
48
|
+
stack.push(name);
|
|
49
|
+
},
|
|
50
|
+
ontext(value) {
|
|
51
|
+
if (value.trim() !== "" && !stack.includes("head") && !stack.includes("body")) {
|
|
52
|
+
invalid = true;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
onclosetag(name, isImplied) {
|
|
56
|
+
const index = stack.lastIndexOf(name);
|
|
57
|
+
if (index === -1) {
|
|
58
|
+
invalid = true;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if ((name === "html" || name === "head" || name === "body") && isImplied) {
|
|
62
|
+
invalid = true;
|
|
63
|
+
}
|
|
64
|
+
stack.splice(index);
|
|
65
|
+
if (name === "body") bodyClosed = true;
|
|
66
|
+
if (name === "html") htmlClosed = true;
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
parser.end(document);
|
|
70
|
+
if (invalid || stack.length > 0 || htmlCount !== 1 || !htmlClosed || headCount > 1 || bodyCount !== 1 || !bodyClosed) {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
"Email templates must render exactly one <html> root containing exactly one <body>; wrap the template in EHtml and EBody"
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function renderPlainText(html: string): string;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { convert } from "html-to-text";
|
|
2
|
+
export function renderPlainText(html) {
|
|
3
|
+
return convert(html, {
|
|
4
|
+
wordwrap: false,
|
|
5
|
+
selectors: [
|
|
6
|
+
{ selector: "img", format: "skip" },
|
|
7
|
+
{ selector: "[data-skip-in-text=true]", format: "skip" },
|
|
8
|
+
{
|
|
9
|
+
selector: "a",
|
|
10
|
+
options: { linkBrackets: false, hideLinkHrefIfSameAsText: true }
|
|
11
|
+
},
|
|
12
|
+
{ selector: '[data-text-format="dataTable"]', format: "dataTable" }
|
|
13
|
+
]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Component } from 'vue';
|
|
2
|
+
import type { EmailRenderContext } from './define-email.js';
|
|
3
|
+
export type EmailComponentRegistry = Readonly<Record<string, Component>>;
|
|
4
|
+
export declare function renderComponentToHtml(component: Component, props?: Record<string, unknown>, context?: EmailRenderContext, componentRegistry?: EmailComponentRegistry): Promise<string>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { createSSRApp } from "vue";
|
|
2
|
+
import { renderToString } from "vue/server-renderer";
|
|
3
|
+
import { emailComponentRegistry } from "../components/email-component-registry.js";
|
|
4
|
+
import { applyTailwindPostRender } from "../tailwind/post-render.js";
|
|
5
|
+
import { createEmailRenderContext, runWithEmailRenderContext } from "./define-email.js";
|
|
6
|
+
import { assembleEmailDocument, assertNoUnresolvedEmailComponents } from "./document.js";
|
|
7
|
+
function assertKnownProps(component, props) {
|
|
8
|
+
const declaration = component.props;
|
|
9
|
+
if (typeof component === "function" && declaration === void 0) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const knownProps = new Set(Array.isArray(declaration) ? declaration : Object.keys(declaration ?? {}));
|
|
13
|
+
const unknownProps = Object.keys(props).filter((name) => !knownProps.has(name)).sort();
|
|
14
|
+
if (unknownProps.length > 0) {
|
|
15
|
+
throw new TypeError(`Unknown email component prop${unknownProps.length === 1 ? "" : "s"}: ${unknownProps.join(", ")}`);
|
|
16
|
+
}
|
|
17
|
+
if (Array.isArray(declaration) || declaration === void 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const missingProps = Object.entries(declaration).filter(([name, option]) => {
|
|
21
|
+
return typeof option === "object" && option !== null && "required" in option && option.required === true && !Object.hasOwn(props, name);
|
|
22
|
+
}).map(([name]) => name).sort();
|
|
23
|
+
if (missingProps.length > 0) {
|
|
24
|
+
throw new TypeError(`Missing required email component prop${missingProps.length === 1 ? "" : "s"}: ${missingProps.join(", ")}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export async function renderComponentToHtml(component, props = {}, context = createEmailRenderContext(), componentRegistry = emailComponentRegistry) {
|
|
28
|
+
assertKnownProps(component, props);
|
|
29
|
+
const app = createSSRApp(component, props);
|
|
30
|
+
for (const [name, emailComponent] of Object.entries(componentRegistry)) {
|
|
31
|
+
app.component(name, emailComponent);
|
|
32
|
+
}
|
|
33
|
+
let renderFailure;
|
|
34
|
+
app.config.errorHandler = (error) => {
|
|
35
|
+
renderFailure ??= { cause: error };
|
|
36
|
+
};
|
|
37
|
+
const renderedHtml = await runWithEmailRenderContext(context, () => renderToString(app, context));
|
|
38
|
+
if (renderFailure !== void 0) {
|
|
39
|
+
throw renderFailure.cause;
|
|
40
|
+
}
|
|
41
|
+
assertNoUnresolvedEmailComponents(renderedHtml);
|
|
42
|
+
const document = assembleEmailDocument(
|
|
43
|
+
renderedHtml.replaceAll("<!--[-->", "").replaceAll("<!--]-->", "").replaceAll("<!---->", "")
|
|
44
|
+
);
|
|
45
|
+
return applyTailwindPostRender(document, context.tailwindRegions);
|
|
46
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Component, FunctionalComponent, PublicProps } from 'vue';
|
|
2
|
+
import type { EmailComponentRegistry } from './render-component.js';
|
|
3
|
+
import type { RenderedEmail } from './types.js';
|
|
4
|
+
type DeclaredComponentProps<ComponentType extends Component> = ComponentType extends abstract new (...args: infer _Arguments) => infer Instance ? Instance extends {
|
|
5
|
+
$props: infer Props;
|
|
6
|
+
} ? Omit<Props, keyof PublicProps> : Record<string, unknown> : ComponentType extends FunctionalComponent<infer Props> ? Props : Record<string, unknown>;
|
|
7
|
+
type RequiredPropKeys<Props> = {
|
|
8
|
+
[Key in keyof Props]-?: Record<never, never> extends Pick<Props, Key> ? never : Key;
|
|
9
|
+
}[keyof Props];
|
|
10
|
+
type RenderEmailComponentArguments<ComponentType extends Component> = keyof DeclaredComponentProps<ComponentType> extends never ? [props?: Record<string, never>] : RequiredPropKeys<DeclaredComponentProps<ComponentType>> extends never ? [props?: DeclaredComponentProps<ComponentType>] : [props: DeclaredComponentProps<ComponentType>];
|
|
11
|
+
export interface RenderEmailComponent {
|
|
12
|
+
<ComponentType extends Component>(component: ComponentType, ...args: RenderEmailComponentArguments<ComponentType>): Promise<RenderedEmail>;
|
|
13
|
+
}
|
|
14
|
+
export declare function createRenderEmailComponent(componentRegistry: EmailComponentRegistry): RenderEmailComponent;
|
|
15
|
+
export declare const renderEmailComponent: RenderEmailComponent;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { emailComponentRegistry } from "../components/email-component-registry.js";
|
|
2
|
+
import { createEmailRenderContext } from "./define-email.js";
|
|
3
|
+
import { assertCompleteEmailDocument } from "./document.js";
|
|
4
|
+
import { EmailRenderError } from "./errors.js";
|
|
5
|
+
import { renderPlainText } from "./plain-text.js";
|
|
6
|
+
import { renderComponentToHtml } from "./render-component.js";
|
|
7
|
+
function componentName(component) {
|
|
8
|
+
if (typeof component === "object" && component !== null || typeof component === "function") {
|
|
9
|
+
if ("name" in component && typeof component.name === "string" && component.name.length > 0) {
|
|
10
|
+
return component.name;
|
|
11
|
+
}
|
|
12
|
+
if ("__name" in component && typeof component.__name === "string" && component.__name.length > 0) {
|
|
13
|
+
return component.__name;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return "AnonymousEmail";
|
|
17
|
+
}
|
|
18
|
+
export function createRenderEmailComponent(componentRegistry) {
|
|
19
|
+
return async (component, ...args) => {
|
|
20
|
+
const props = args[0] ?? {};
|
|
21
|
+
try {
|
|
22
|
+
const context = createEmailRenderContext();
|
|
23
|
+
const html = await renderComponentToHtml(component, props, context, componentRegistry);
|
|
24
|
+
assertCompleteEmailDocument(html);
|
|
25
|
+
const subjectFactory = context.subject;
|
|
26
|
+
if (subjectFactory === void 0) {
|
|
27
|
+
return {
|
|
28
|
+
html,
|
|
29
|
+
text: renderPlainText(html)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const subject = subjectFactory();
|
|
33
|
+
if (typeof subject !== "string") {
|
|
34
|
+
throw new TypeError(`defineEmail() subject must return a string; received ${typeof subject}`);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
html,
|
|
38
|
+
text: renderPlainText(html),
|
|
39
|
+
subject
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
throw new EmailRenderError(componentName(component), error);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
export const renderEmailComponent = createRenderEmailComponent(emailComponentRegistry);
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Escapes all characters that may not be accepted on
|
|
3
|
+
* CSS selectors by using the regex "[^a-zA-Z0-9\-_]".
|
|
4
|
+
*
|
|
5
|
+
* Also does a bit more trickery to avoid escaping already
|
|
6
|
+
* escaped characters.
|
|
7
|
+
*/
|
|
8
|
+
export declare function escapeClassName(className: string): string;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function escapeClassName(className) {
|
|
2
|
+
return className.replace(
|
|
3
|
+
/* we need this look ahead capturing group to avoid using negative look behinds */
|
|
4
|
+
/([^\\]|^)(?=([^\w-]))/g,
|
|
5
|
+
(match, prefixCharacter, characterToEscape) => {
|
|
6
|
+
if (prefixCharacter === "" && characterToEscape === "\\") return match;
|
|
7
|
+
return `${prefixCharacter}\\`;
|
|
8
|
+
}
|
|
9
|
+
);
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replaces special characters to avoid problems on email clients.
|
|
3
|
+
*
|
|
4
|
+
* @param className - This should not come with any escaped characters, it should come the same
|
|
5
|
+
* as is on the `class` attribute of the element.
|
|
6
|
+
*/
|
|
7
|
+
export declare function sanitizeClassName(className: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const digitToNameMap = {
|
|
2
|
+
0: "zero",
|
|
3
|
+
1: "one",
|
|
4
|
+
2: "two",
|
|
5
|
+
3: "three",
|
|
6
|
+
4: "four",
|
|
7
|
+
5: "five",
|
|
8
|
+
6: "six",
|
|
9
|
+
7: "seven",
|
|
10
|
+
8: "eight",
|
|
11
|
+
9: "nine"
|
|
12
|
+
};
|
|
13
|
+
export function sanitizeClassName(className) {
|
|
14
|
+
return className.replaceAll("+", "plus").replaceAll("[", "").replaceAll("%", "pc").replaceAll("]", "").replaceAll("(", "").replaceAll(")", "").replaceAll("!", "imprtnt").replaceAll(">", "gt").replaceAll("<", "lt").replaceAll("=", "eq").replace(/^\d/, (digit) => {
|
|
15
|
+
return digitToNameMap[digit];
|
|
16
|
+
}).replace(/[^\w-]/g, "_");
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function unescapeClass(singleClass: string): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Downlevels modern CSS features that email clients don't support,
|
|
3
|
+
* operating on a css-tree StyleSheet AST.
|
|
4
|
+
*
|
|
5
|
+
* 1. CSS Nesting: unnests @media rules from inside selectors
|
|
6
|
+
* `.sm_p-4{@media (min-width:40rem){padding:1rem!important}}`
|
|
7
|
+
* → `@media (min-width:40rem){.sm_p-4{padding:1rem!important}}`
|
|
8
|
+
*
|
|
9
|
+
* 2. Media Queries Level 4 range syntax → legacy min-width/max-width
|
|
10
|
+
* `(width>=40rem)` → `(min-width:40rem)`
|
|
11
|
+
*
|
|
12
|
+
* Gmail, Outlook, Yahoo, and most email clients don't support either feature.
|
|
13
|
+
* See: https://www.caniemail.com/features/css-at-media/
|
|
14
|
+
* https://www.caniemail.com/features/css-nesting/
|
|
15
|
+
*/
|
|
16
|
+
import { type StyleSheet } from 'css-tree';
|
|
17
|
+
/**
|
|
18
|
+
* css-tree 3.x introduced new AST node types for query-related at-rules that
|
|
19
|
+
* `@types/css-tree` (still on 2.x at the time of writing) doesn't expose.
|
|
20
|
+
* Augmenting the module here lets the rest of this file work with strong
|
|
21
|
+
* types instead of scattering `as` casts everywhere.
|
|
22
|
+
*
|
|
23
|
+
* - `FeatureRange` is what the parser emits for Media Queries Level 4 range
|
|
24
|
+
* syntax: `(width >= 40rem)`.
|
|
25
|
+
* - `Feature` is the legacy form (`(min-width: 40rem)`) we construct as the
|
|
26
|
+
* downleveled output.
|
|
27
|
+
*
|
|
28
|
+
* Note: we cannot extend the `CssNode` union itself (it's a `type` alias, not
|
|
29
|
+
* an interface), so two narrow casts remain in this file:
|
|
30
|
+
* 1. Widening `node` inside `walk()` so we can narrow against
|
|
31
|
+
* `FeatureRange.type` (`CssNode` doesn't list `'FeatureRange'`).
|
|
32
|
+
* 2. Assigning a constructed `Feature` back to `ListItem<CssNode>.data`.
|
|
33
|
+
*
|
|
34
|
+
* Both are flagged inline and reference back to this block.
|
|
35
|
+
*
|
|
36
|
+
* See:
|
|
37
|
+
* https://github.com/csstree/csstree/blob/master/lib/syntax/node/FeatureRange.js
|
|
38
|
+
* https://github.com/csstree/csstree/blob/master/lib/syntax/node/Feature.js
|
|
39
|
+
*/
|
|
40
|
+
declare module 'css-tree' {
|
|
41
|
+
interface FeatureRange extends CssNodeCommon {
|
|
42
|
+
type: 'FeatureRange';
|
|
43
|
+
kind: string;
|
|
44
|
+
left: CssNode;
|
|
45
|
+
leftComparison: string;
|
|
46
|
+
middle: CssNode;
|
|
47
|
+
rightComparison: string | null;
|
|
48
|
+
right: CssNode | null;
|
|
49
|
+
}
|
|
50
|
+
interface Feature extends CssNodeCommon {
|
|
51
|
+
type: 'Feature';
|
|
52
|
+
kind: string;
|
|
53
|
+
name: string;
|
|
54
|
+
value: CssNode | null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Unnest @media at-rules from inside regular rules, and downlevel
|
|
59
|
+
* range media query syntax to legacy min-width/max-width.
|
|
60
|
+
*
|
|
61
|
+
* Mutates the stylesheet in place.
|
|
62
|
+
*/
|
|
63
|
+
export declare function downlevelForEmailClients(styleSheet: StyleSheet): void;
|