@lupinum/nuxt-email 0.1.1 → 1.0.0-beta.2

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.
Files changed (55) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +63 -18
  3. package/dist/build.d.mts +13 -0
  4. package/dist/build.mjs +218 -0
  5. package/dist/module.d.mts +1 -1
  6. package/dist/module.json +1 -1
  7. package/dist/module.mjs +54 -145
  8. package/dist/runtime/components/EHtml.d.ts +3 -1
  9. package/dist/runtime/components/EHtml.js +12 -2
  10. package/dist/runtime/components/ELink.js +1 -6
  11. package/dist/runtime/dev-preview/render.get.d.ts +26 -1
  12. package/dist/runtime/dev-preview/render.get.js +2 -2
  13. package/dist/runtime/render/define-email.d.ts +19 -10
  14. package/dist/runtime/render/define-email.js +21 -4
  15. package/dist/runtime/render/errors.d.ts +2 -2
  16. package/dist/runtime/render/errors.js +4 -4
  17. package/dist/runtime/render/index.d.ts +6 -0
  18. package/dist/runtime/render/index.js +3 -0
  19. package/dist/runtime/render/render-email-component.d.ts +3 -6
  20. package/dist/runtime/render/render-email-component.js +16 -16
  21. package/dist/runtime/render/types.d.ts +7 -0
  22. package/dist/runtime/tailwind/css-tree.d.ts +10 -0
  23. package/dist/runtime/tailwind/css-tree.js +11 -0
  24. package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.d.ts +8 -38
  25. package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +186 -72
  26. package/dist/runtime/tailwind/engine/css/extract-rules-per-class.d.ts +3 -1
  27. package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +9 -2
  28. package/dist/runtime/tailwind/engine/css/get-custom-properties.d.ts +1 -1
  29. package/dist/runtime/tailwind/engine/css/get-custom-properties.js +1 -1
  30. package/dist/runtime/tailwind/engine/css/is-part-inlinable.d.ts +1 -1
  31. package/dist/runtime/tailwind/engine/css/is-part-inlinable.js +1 -1
  32. package/dist/runtime/tailwind/engine/css/is-rule-inlinable.d.ts +1 -1
  33. package/dist/runtime/tailwind/engine/css/is-rule-inlinable.js +1 -1
  34. package/dist/runtime/tailwind/engine/css/make-inline-styles-for.d.ts +1 -1
  35. package/dist/runtime/tailwind/engine/css/make-inline-styles-for.js +1 -1
  36. package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.d.ts +1 -1
  37. package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.js +1 -1
  38. package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.d.ts +1 -1
  39. package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.js +1 -1
  40. package/dist/runtime/tailwind/engine/css/sanitize-declarations.d.ts +1 -1
  41. package/dist/runtime/tailwind/engine/css/sanitize-declarations.js +1 -1
  42. package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.d.ts +1 -1
  43. package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.js +1 -1
  44. package/dist/runtime/tailwind/engine/css/split-mixed-rule.d.ts +1 -1
  45. package/dist/runtime/tailwind/engine/css/split-mixed-rule.js +1 -1
  46. package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.d.ts +1 -1
  47. package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.js +1 -1
  48. package/dist/runtime/tailwind/engine/index.d.ts +6 -6
  49. package/dist/runtime/tailwind/engine/index.js +70 -7
  50. package/dist/runtime/tailwind/engine/setup-tailwind.d.ts +1 -1
  51. package/dist/runtime/tailwind/engine/setup-tailwind.js +1 -1
  52. package/dist/runtime/tailwind/post-render.js +7 -8
  53. package/dist/shared/nuxt-email.B5ux0eBv.mjs +133 -0
  54. package/dist/types.d.mts +1 -1
  55. package/package.json +18 -7
package/dist/module.mjs CHANGED
@@ -1,8 +1,9 @@
1
- import { relative, isAbsolute, sep, resolve } from 'node:path';
2
- import { resolveFiles, defineNuxtModule, createResolver, addServerTemplate, addTypeTemplate, addServerImports, addComponent, addServerHandler, useNitro, updateTemplates } from '@nuxt/kit';
1
+ import { resolve, isAbsolute, relative } from 'node:path';
2
+ import { defineNuxtModule, createResolver, addServerTemplate, addTypeTemplate, addServerImports, addComponent, addServerHandler, useNitro, updateTemplates } from '@nuxt/kit';
3
3
  import vueRollup from 'unplugin-vue/rollup';
4
4
  import { stat } from 'node:fs/promises';
5
5
  import { EMAIL_COMPONENT_NAMES } from '../dist/runtime/components/email-component-names.js';
6
+ import { g as generateEmailRegistry, a as generateEmailTypes, d as discoverEmailTemplates } from './shared/nuxt-email.B5ux0eBv.mjs';
6
7
 
7
8
  function previewFixturePath(sourcePath) {
8
9
  return `${sourcePath.slice(0, -".vue".length)}.fixtures.ts`;
@@ -26,140 +27,34 @@ async function attachPreviewFixtures(templates) {
26
27
  }));
27
28
  }
28
29
 
29
- class DuplicateEmailTemplateError extends Error {
30
- templateName;
31
- sourcePaths;
32
- constructor(templateName, sourcePaths) {
33
- const sortedSourcePaths = [...sourcePaths].sort();
34
- super(`Duplicate email template name "${templateName}" from: ${sortedSourcePaths.join(", ")}`);
35
- this.name = "DuplicateEmailTemplateError";
36
- this.templateName = templateName;
37
- this.sourcePaths = sortedSourcePaths;
30
+ function configureEmailTypeProject(tsConfig, emailDirectory, registryId, registryPath) {
31
+ const normalizedDirectory = emailDirectory.replaceAll("\\", "/");
32
+ const include = tsConfig.include ??= [];
33
+ const emailTypeGlob = `${normalizedDirectory}/**/*.vue`;
34
+ if (!include.includes(emailTypeGlob)) {
35
+ include.push(emailTypeGlob);
38
36
  }
39
- }
40
- class EmailTemplateDiscoveryError extends Error {
41
- sourcePath;
42
- constructor(sourcePath, cause) {
43
- super(`Failed to discover email templates under ${sourcePath}`, { cause });
44
- this.name = "EmailTemplateDiscoveryError";
45
- this.sourcePath = sourcePath;
37
+ const exclude = tsConfig.exclude ??= [];
38
+ const emailComponentTypeGlob = `${normalizedDirectory}/components/**/*.vue`;
39
+ if (!exclude.includes(emailComponentTypeGlob)) {
40
+ exclude.push(emailComponentTypeGlob);
46
41
  }
42
+ const paths = (tsConfig.compilerOptions ??= {}).paths ??= {};
43
+ paths[registryId] = [registryPath];
44
+ return paths;
47
45
  }
48
-
49
- function compareCodePoints(left, right) {
50
- return left < right ? -1 : left > right ? 1 : 0;
51
- }
52
- function normalizeSeparators(value) {
53
- return value.split(sep).join("/").replaceAll("\\", "/");
46
+ function previewURL(baseURL) {
47
+ return `${baseURL.endsWith("/") ? baseURL : `${baseURL}/`}__email`;
54
48
  }
55
- function templatesFromSourcePaths(emailDirectory, sourcePaths) {
56
- const templates = sourcePaths.map((sourcePath) => {
57
- const relativePath = normalizeSeparators(relative(emailDirectory, sourcePath));
58
- if (relativePath.startsWith("../") || relativePath === ".." || isAbsolute(relativePath)) {
59
- throw new EmailTemplateDiscoveryError(sourcePath, new TypeError("Template path is outside app/emails"));
49
+ function relativeEmailPath(emailDirectory, path, roots) {
50
+ const candidates = isAbsolute(path) ? [path] : roots.map((root) => resolve(root, path));
51
+ for (const candidate of candidates) {
52
+ const relativePath = relative(emailDirectory, candidate).replaceAll("\\", "/");
53
+ if (relativePath !== ".." && !relativePath.startsWith("../") && !isAbsolute(relativePath)) {
54
+ return relativePath;
60
55
  }
61
- return {
62
- name: relativePath.slice(0, -".vue".length),
63
- sourcePath: normalizeSeparators(sourcePath)
64
- };
65
- }).sort((left, right) => {
66
- return compareCodePoints(left.name, right.name) || compareCodePoints(left.sourcePath, right.sourcePath);
67
- });
68
- const pathsByName = /* @__PURE__ */ new Map();
69
- for (const template of templates) {
70
- const paths = pathsByName.get(template.name) ?? [];
71
- paths.push(template.sourcePath);
72
- pathsByName.set(template.name, paths);
73
56
  }
74
- const duplicate = [...pathsByName.entries()].filter(([, paths]) => paths.length > 1).sort(([left], [right]) => compareCodePoints(left, right))[0];
75
- if (duplicate) {
76
- throw new DuplicateEmailTemplateError(duplicate[0], duplicate[1]);
77
- }
78
- return templates;
79
57
  }
80
- async function discoverEmailTemplates(emailDirectory) {
81
- let sourcePaths;
82
- try {
83
- sourcePaths = await resolveFiles(emailDirectory, "**/*.vue", {
84
- followSymbolicLinks: false,
85
- ignore: ["components/**"]
86
- });
87
- } catch (error) {
88
- throw new EmailTemplateDiscoveryError(emailDirectory, error);
89
- }
90
- return templatesFromSourcePaths(emailDirectory, sourcePaths);
91
- }
92
-
93
- function importPath(path) {
94
- return JSON.stringify(path.replaceAll("\\", "/"));
95
- }
96
- function generateEmailRegistry(templates, runtimePaths) {
97
- const entries = templates.map((template) => {
98
- const fixture = template.fixturePath ? `
99
- fixture: () => import(${importPath(template.fixturePath)}),` : "";
100
- return ` [${JSON.stringify(template.name)}]: {
101
- component: () => import(${importPath(template.sourcePath)}),${fixture}
102
- },`;
103
- });
104
- return `import { EmailRenderError } from ${importPath(runtimePaths.emailRenderError)}
105
- import { renderEmailComponent } from ${importPath(runtimePaths.renderEmailComponent)}
106
- import { UnknownEmailTemplateError } from ${importPath(runtimePaths.templateRegistryErrors)}
107
-
108
- export const emailTemplates = Object.freeze({
109
- ${entries.join("\n")}
110
- })
111
-
112
- export async function renderEmail(name, props) {
113
- const loader = typeof name === 'string' && Object.hasOwn(emailTemplates, name)
114
- ? emailTemplates[name]
115
- : undefined
116
- if (!loader) {
117
- throw new UnknownEmailTemplateError(String(name), Object.keys(emailTemplates))
118
- }
119
-
120
- try {
121
- const templateModule = await loader.component()
122
- return await renderEmailComponent(templateModule.default, props)
123
- }
124
- catch (error) {
125
- throw new EmailRenderError(name, error instanceof EmailRenderError ? error.cause : error)
126
- }
127
- }
128
- `;
129
- }
130
- function generateEmailTypes(templates, typePaths) {
131
- const entries = templates.map((template) => {
132
- const fixture = template.fixturePath ? `
133
- fixture: () => Promise<typeof import(${importPath(template.fixturePath)})>` : "";
134
- return ` ${JSON.stringify(template.name)}: {
135
- component: () => Promise<typeof import(${importPath(template.sourcePath)})>${fixture}
136
- }`;
137
- });
138
- return `export const emailTemplates: Readonly<{
139
- ${entries.join("\n")}
140
- }>
141
-
142
- export type EmailTemplateName = keyof typeof emailTemplates
143
- type _EmailComponent = abstract new (...args: never[]) => { $props: object }
144
- type _DeclaredEmailProps<Component extends _EmailComponent> = Omit<
145
- InstanceType<Component>['$props'],
146
- keyof import('vue').PublicProps
147
- >
148
- type _EmailProps<Component extends _EmailComponent> = keyof _DeclaredEmailProps<Component> extends never
149
- ? Record<string, never>
150
- : _DeclaredEmailProps<Component>
151
-
152
- export type EmailTemplateProps = {
153
- [Name in EmailTemplateName]: _EmailProps<Awaited<ReturnType<(typeof emailTemplates)[Name]['component']>>['default']>
154
- }
155
-
156
- export function renderEmail<Name extends EmailTemplateName>(
157
- name: Name,
158
- props: NoInfer<EmailTemplateProps[Name]>,
159
- ): Promise<import(${importPath(typePaths.renderedEmail)}).RenderedEmail>
160
- `;
161
- }
162
-
163
58
  const module$1 = defineNuxtModule({
164
59
  meta: {
165
60
  name: "@lupinum/nuxt-email",
@@ -176,10 +71,12 @@ const module$1 = defineNuxtModule({
176
71
  const resolver = createResolver(import.meta.url);
177
72
  const defineEmailPublicPath = await resolver.resolvePath("./runtime/define-email");
178
73
  const errorsPublicPath = await resolver.resolvePath("./runtime/errors");
179
- let renderEmailComponentPath = await resolver.resolvePath("./runtime/testing");
74
+ let renderEmailComponentPath = await resolver.resolvePath("./runtime/render/index");
75
+ let testingRendererPath = await resolver.resolvePath("./runtime/testing");
180
76
  if (options.codeBlock !== void 0) {
181
77
  const { setupCodeBlock } = await import('./chunks/setup.mjs');
182
78
  renderEmailComponentPath = setupCodeBlock(options.codeBlock, resolver);
79
+ testingRendererPath = renderEmailComponentPath;
183
80
  }
184
81
  const emailDirectory = resolve(nuxt.options.srcDir, "emails");
185
82
  const loadTemplates = async () => {
@@ -205,6 +102,9 @@ const module$1 = defineNuxtModule({
205
102
  write: true,
206
103
  getContents: () => generateEmailTypes(templates, registryTypePaths)
207
104
  }, { nitro: true });
105
+ nuxt.hook("prepare:types", ({ tsConfig: tsConfig2 }) => {
106
+ configureEmailTypeProject(tsConfig2, emailDirectory, registryId, typeTemplate.dst);
107
+ });
208
108
  addServerImports([
209
109
  {
210
110
  name: "renderEmail",
@@ -220,6 +120,17 @@ const module$1 = defineNuxtModule({
220
120
  });
221
121
  }
222
122
  if (nuxt.options.dev) {
123
+ nuxt.hook("devtools:customTabs", (tabs) => {
124
+ tabs.push({
125
+ name: "nuxt-email",
126
+ title: "Nuxt Email",
127
+ icon: "i-lucide-mail",
128
+ view: {
129
+ type: "iframe",
130
+ src: previewURL(nuxt.options.app.baseURL)
131
+ }
132
+ });
133
+ });
223
134
  addServerHandler({
224
135
  route: "/__email",
225
136
  method: "get",
@@ -250,21 +161,21 @@ const module$1 = defineNuxtModule({
250
161
  let registryUpdates = Promise.resolve();
251
162
  nuxt.hook("builder:watch", (event, path) => {
252
163
  const structureChanged = ["add", "unlink", "addDir", "unlinkDir"].includes(event);
253
- if (event !== "change" && !structureChanged) {
164
+ if (!structureChanged) {
254
165
  return;
255
166
  }
256
- const absolutePath = isAbsolute(path) ? path : resolve(nuxt.options.srcDir, path);
257
- const relativePath = relative(emailDirectory, absolutePath).replaceAll("\\", "/");
258
- if (relativePath === ".." || relativePath.startsWith("../") || isAbsolute(relativePath)) {
167
+ const relativePath = relativeEmailPath(emailDirectory, path, [
168
+ nuxt.options.rootDir,
169
+ nuxt.options.srcDir
170
+ ]);
171
+ if (relativePath === void 0) {
259
172
  return;
260
173
  }
261
174
  const update = async () => {
262
- if (structureChanged) {
263
- templates = await loadTemplates();
264
- await updateTemplates({
265
- filter: (template) => template.filename === typeTemplate.filename
266
- });
267
- }
175
+ templates = await loadTemplates();
176
+ await updateTemplates({
177
+ filter: (template) => template.filename === typeTemplate.filename
178
+ });
268
179
  await reloadRegistry();
269
180
  };
270
181
  const queuedUpdate = registryUpdates.then(update, update);
@@ -275,7 +186,7 @@ const module$1 = defineNuxtModule({
275
186
  const nuxtOptions = nuxt.options;
276
187
  const nitro = nuxtOptions.nitro ??= {};
277
188
  const alias = nitro.alias ??= {};
278
- alias["#nuxt-email/testing"] = renderEmailComponentPath;
189
+ alias["#nuxt-email/testing"] = testingRendererPath;
279
190
  alias["@lupinum/nuxt-email/define-email"] = defineEmailPublicPath;
280
191
  alias["@lupinum/nuxt-email/errors"] = errorsPublicPath;
281
192
  const externals = nitro.externals ??= {};
@@ -287,10 +198,8 @@ const module$1 = defineNuxtModule({
287
198
  }
288
199
  const typescript = nitro.typescript ??= {};
289
200
  const tsConfig = typescript.tsConfig ??= {};
290
- const compilerOptions = tsConfig.compilerOptions ??= {};
291
- const paths = compilerOptions.paths ??= {};
292
- paths[registryId] = [typeTemplate.dst];
293
- paths["#nuxt-email/testing"] = [renderEmailComponentPath];
201
+ const paths = configureEmailTypeProject(tsConfig, emailDirectory, registryId, typeTemplate.dst);
202
+ paths["#nuxt-email/testing"] = [testingRendererPath];
294
203
  nitro.rollupConfig ??= {};
295
204
  const existingPlugins = nitro.rollupConfig.plugins;
296
205
  nitro.rollupConfig.plugins = existingPlugins ? [existingPlugins, vueRollup()] : [vueRollup()];
@@ -1,4 +1,6 @@
1
1
  import type { DefineComponent, HTMLAttributes } from 'vue';
2
2
  import type { SafeEmailAttributes } from './attributes.js';
3
- export type EHtmlProps = SafeEmailAttributes<HTMLAttributes>;
3
+ export type EHtmlProps = Omit<SafeEmailAttributes<HTMLAttributes>, 'lang'> & {
4
+ lang: string;
5
+ };
4
6
  export declare const EHtml: DefineComponent<EHtmlProps>;
@@ -3,10 +3,20 @@ import { assertSafeEmailAttributes } from "./attributes.js";
3
3
  export const EHtml = defineComponent({
4
4
  name: "EHtml",
5
5
  inheritAttrs: false,
6
- setup(_props, { attrs, slots }) {
6
+ props: {
7
+ lang: {
8
+ type: String,
9
+ required: true
10
+ }
11
+ },
12
+ setup(props, { attrs, slots }) {
7
13
  return () => {
8
14
  assertSafeEmailAttributes("EHtml", attrs);
9
- const { dir = "ltr", lang = "en", ...attributes } = attrs;
15
+ const { dir = "ltr", ...attributes } = attrs;
16
+ const { lang } = props;
17
+ if (typeof lang !== "string" || lang.trim().length === 0) {
18
+ throw new TypeError("EHtml lang must be a non-empty string");
19
+ }
10
20
  return h("html", {
11
21
  ...attributes,
12
22
  dir,
@@ -1,11 +1,6 @@
1
1
  import { defineComponent, h, inject } from "vue";
2
2
  import { resolveNestedTailwindStyle, TAILWIND_NESTED_KEY } from "../tailwind/nested.js";
3
3
  import { assertSafeEmailAttributes } from "./attributes.js";
4
- import { mergeEmailStyles } from "./style.js";
5
- const DEFAULT_LINK_STYLE = {
6
- color: "#067df7",
7
- textDecorationLine: "none"
8
- };
9
4
  export const ELink = defineComponent({
10
5
  name: "ELink",
11
6
  inheritAttrs: false,
@@ -27,7 +22,7 @@ export const ELink = defineComponent({
27
22
  return h("a", {
28
23
  ...attributes,
29
24
  href: props.href,
30
- style: mergeEmailStyles(DEFAULT_LINK_STYLE, effectiveStyle),
25
+ ...effectiveStyle === void 0 || effectiveStyle === null ? {} : { style: effectiveStyle },
31
26
  target: attrs.target ?? "_blank"
32
27
  }, slots.default?.());
33
28
  };
@@ -1,3 +1,28 @@
1
1
  export declare const PREVIEW_RENDER_CSP: string;
2
- declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<any>>;
2
+ interface SerializedPreviewError {
3
+ name: string;
4
+ message: string;
5
+ stack?: string;
6
+ templateName?: string;
7
+ requestedName?: string;
8
+ knownNames?: readonly string[];
9
+ cause?: SerializedPreviewError;
10
+ }
11
+ declare const _default: import("h3").EventHandler<import("h3").EventHandlerRequest, Promise<string | {
12
+ bytes: any;
13
+ html: string;
14
+ text: string;
15
+ subject?: string;
16
+ name: string;
17
+ statusCode?: undefined;
18
+ statusMessage?: undefined;
19
+ data?: undefined;
20
+ } | {
21
+ statusCode: number;
22
+ statusMessage: string;
23
+ data: {
24
+ templateName: string | undefined;
25
+ error: SerializedPreviewError;
26
+ };
27
+ }>>;
3
28
  export default _default;
@@ -45,8 +45,8 @@ function serializeError(error, seen = /* @__PURE__ */ new Set()) {
45
45
  if (error.stack) {
46
46
  serialized.stack = error.stack;
47
47
  }
48
- if ("componentName" in error && typeof error.componentName === "string") {
49
- serialized.componentName = error.componentName;
48
+ if ("templateName" in error && typeof error.templateName === "string") {
49
+ serialized.templateName = error.templateName;
50
50
  }
51
51
  if ("requestedName" in error && typeof error.requestedName === "string") {
52
52
  serialized.requestedName = error.requestedName;
@@ -2,12 +2,18 @@ import type { TailwindRegion } from '../tailwind/nested.js';
2
2
  /**
3
3
  * Options declared by an email template via {@link defineEmail}.
4
4
  *
5
- * `subject` is a zero-argument closure over the template's real `defineProps()`
6
- * value and returns the computed subject line.
5
+ * String values are constant metadata. Functions are zero-argument closures
6
+ * over the template's real `defineProps()` value and run synchronously after
7
+ * the HTML render completes.
7
8
  */
8
- export interface DefineEmailOptions {
9
- subject: () => string;
10
- }
9
+ export type EmailMetadataValue = string | (() => string);
10
+ export type DefineEmailOptions = {
11
+ subject: EmailMetadataValue;
12
+ text?: EmailMetadataValue;
13
+ } | {
14
+ subject?: EmailMetadataValue;
15
+ text: EmailMetadataValue;
16
+ };
11
17
  declare const EMAIL_RENDER_BRAND: unique symbol;
12
18
  /**
13
19
  * Per-render registry created by the renderer. A fresh instance is created for
@@ -16,7 +22,10 @@ declare const EMAIL_RENDER_BRAND: unique symbol;
16
22
  */
17
23
  export interface EmailRenderContext {
18
24
  readonly [EMAIL_RENDER_BRAND]: true;
19
- subject?: () => string;
25
+ metadata?: {
26
+ subject?: () => string;
27
+ text?: () => string;
28
+ };
20
29
  /**
21
30
  * Tailwind regions registered by `<ETailwind>` boundaries during render, in
22
31
  * registration order. Consumed by the post-render pass to complete each head
@@ -44,10 +53,10 @@ export declare function getEmailRenderContext(): EmailRenderContext | undefined;
44
53
  */
45
54
  export declare function runWithEmailRenderContext<T>(context: EmailRenderContext, fn: () => T): T;
46
55
  /**
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`.
56
+ * Declare subject and/or authored plain text from an email template's
57
+ * `<script setup>`. A zero-argument closure captures the template's real
58
+ * `defineProps()` value, so there is no separate generic that can lie about the
59
+ * template contract.
51
60
  *
52
61
  * Must run during an email render; calling it elsewhere throws
53
62
  * {@link DefineEmailOutsideRenderError}.
@@ -30,11 +30,28 @@ export function defineEmail(options) {
30
30
  if (!isEmailRenderContext(context)) {
31
31
  throw new DefineEmailOutsideRenderError();
32
32
  }
33
- if (context.subject !== void 0) {
33
+ if (context.metadata) {
34
34
  throw new DuplicateEmailDefinitionError();
35
35
  }
36
- if (!options || typeof options.subject !== "function") {
37
- throw new TypeError("defineEmail() subject must be a function returning a string.");
36
+ if (!options || typeof options !== "object") {
37
+ throw new TypeError("defineEmail() requires subject or text metadata.");
38
38
  }
39
- context.subject = options.subject;
39
+ const keys = Object.keys(options);
40
+ const unsupportedKeys = keys.filter((key) => key !== "subject" && key !== "text");
41
+ if (unsupportedKeys.length > 0) {
42
+ throw new TypeError(`defineEmail() received unsupported metadata: ${unsupportedKeys.sort().join(", ")}.`);
43
+ }
44
+ if (options.subject === void 0 && options.text === void 0) {
45
+ throw new TypeError("defineEmail() requires subject or text metadata.");
46
+ }
47
+ const toFactory = (name, value) => {
48
+ if (value === void 0) return void 0;
49
+ if (typeof value === "string") return () => value;
50
+ if (typeof value === "function") return value;
51
+ throw new TypeError(`defineEmail() ${name} must be a string or a function returning a string.`);
52
+ };
53
+ context.metadata = {
54
+ subject: toFactory("subject", options.subject),
55
+ text: toFactory("text", options.text)
56
+ };
40
57
  }
@@ -1,4 +1,4 @@
1
1
  export declare class EmailRenderError extends Error {
2
- readonly componentName: string;
3
- constructor(componentName: string, cause: unknown);
2
+ readonly templateName: string;
3
+ constructor(templateName: string, cause: unknown);
4
4
  }
@@ -1,8 +1,8 @@
1
1
  export class EmailRenderError extends Error {
2
- componentName;
3
- constructor(componentName, cause) {
4
- super(`Failed to render email component ${componentName}`, { cause });
2
+ templateName;
3
+ constructor(templateName, cause) {
4
+ super(`Failed to render email template ${templateName}`, { cause });
5
5
  this.name = "EmailRenderError";
6
- this.componentName = componentName;
6
+ this.templateName = templateName;
7
7
  }
8
8
  }
@@ -0,0 +1,6 @@
1
+ /** Server-only production rendering of already compiled Vue components. */
2
+ export { renderEmailComponent } from './render-email-component.js';
3
+ export { EmailRenderError } from './errors.js';
4
+ export { UnknownEmailTemplateError } from '../template-registry/errors.js';
5
+ export type { EmailComponentProps, RenderedEmail } from './types.js';
6
+ export type EmailComponents = typeof import('../components/email-component-registry').emailComponentRegistry;
@@ -0,0 +1,3 @@
1
+ export { renderEmailComponent } from "./render-email-component.js";
2
+ export { EmailRenderError } from "./errors.js";
3
+ export { UnknownEmailTemplateError } from "../template-registry/errors.js";
@@ -1,13 +1,10 @@
1
- import type { Component, FunctionalComponent, PublicProps } from 'vue';
1
+ import type { Component } from 'vue';
2
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>;
3
+ import type { EmailComponentProps, RenderedEmail } from './types.js';
7
4
  type RequiredPropKeys<Props> = {
8
5
  [Key in keyof Props]-?: Record<never, never> extends Pick<Props, Key> ? never : Key;
9
6
  }[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>];
7
+ type RenderEmailComponentArguments<ComponentType extends Component> = keyof EmailComponentProps<ComponentType> extends never ? [props?: Record<string, never>] : RequiredPropKeys<EmailComponentProps<ComponentType>> extends never ? [props?: EmailComponentProps<ComponentType>] : [props: EmailComponentProps<ComponentType>];
11
8
  export interface RenderEmailComponent {
12
9
  <ComponentType extends Component>(component: ComponentType, ...args: RenderEmailComponentArguments<ComponentType>): Promise<RenderedEmail>;
13
10
  }
@@ -4,7 +4,7 @@ import { assertCompleteEmailDocument } from "./document.js";
4
4
  import { EmailRenderError } from "./errors.js";
5
5
  import { renderPlainText } from "./plain-text.js";
6
6
  import { renderComponentToHtml } from "./render-component.js";
7
- function componentName(component) {
7
+ function templateName(component) {
8
8
  if (typeof component === "object" && component !== null || typeof component === "function") {
9
9
  if ("name" in component && typeof component.name === "string" && component.name.length > 0) {
10
10
  return component.name;
@@ -22,24 +22,24 @@ export function createRenderEmailComponent(componentRegistry) {
22
22
  const context = createEmailRenderContext();
23
23
  const html = await renderComponentToHtml(component, props, context, componentRegistry);
24
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 {
25
+ const resolveMetadata = (name, factory) => {
26
+ if (!factory) return void 0;
27
+ const value = factory();
28
+ if (typeof value !== "string") {
29
+ throw new TypeError(`defineEmail() ${name} must return a string; received ${typeof value}`);
30
+ }
31
+ return value;
32
+ };
33
+ const subject = resolveMetadata("subject", context.metadata?.subject);
34
+ const text = resolveMetadata("text", context.metadata?.text) ?? renderPlainText(html);
35
+ const result = {
37
36
  html,
38
- text: renderPlainText(html),
39
- subject
37
+ text
40
38
  };
39
+ if (subject !== void 0) result.subject = subject;
40
+ return result;
41
41
  } catch (error) {
42
- throw new EmailRenderError(componentName(component), error);
42
+ throw new EmailRenderError(templateName(component), error);
43
43
  }
44
44
  };
45
45
  }
@@ -1,3 +1,9 @@
1
+ import type { FunctionalComponent, PublicProps } from 'vue';
2
+ type VueListenerProp = `on${Capitalize<string>}`;
3
+ type EmailAuthoringProps<Props> = string extends keyof Props ? Props : Omit<Props, keyof PublicProps | VueListenerProp>;
4
+ export type EmailComponentProps<ComponentType> = ComponentType extends abstract new (...args: never[]) => {
5
+ $props: infer Props;
6
+ } ? EmailAuthoringProps<Props> : ComponentType extends FunctionalComponent<infer Props> ? EmailAuthoringProps<Props> : never;
1
7
  export interface RenderedEmail {
2
8
  html: string;
3
9
  text: string;
@@ -7,3 +13,4 @@ export interface RenderedEmail {
7
13
  */
8
14
  subject?: string;
9
15
  }
16
+ export {};
@@ -0,0 +1,10 @@
1
+ import type * as CssTree from 'css-tree';
2
+ export declare const parse: typeof CssTree.parse;
3
+ export declare const generate: typeof CssTree.generate;
4
+ export declare const walk: typeof CssTree.walk;
5
+ export declare const find: typeof CssTree.find;
6
+ export declare const clone: typeof CssTree.clone;
7
+ export declare const List: typeof CssTree.List;
8
+ export type List<T> = CssTree.List<T>;
9
+ export declare const string: typeof CssTree.string;
10
+ export type * from 'css-tree';
@@ -0,0 +1,11 @@
1
+ import parser from "css-tree/parser";
2
+ import generator from "css-tree/generator";
3
+ import walker from "css-tree/walker";
4
+ import { clone as cloneNode, List as NodeList, string as cssString } from "css-tree/utils";
5
+ export const parse = parser;
6
+ export const generate = generator;
7
+ export const walk = walker;
8
+ export const find = walker.find;
9
+ export const clone = cloneNode;
10
+ export const List = NodeList;
11
+ export const string = cssString;