@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.
Files changed (159) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +172 -0
  4. package/THIRD_PARTY_NOTICES +75 -0
  5. package/dist/chunks/setup.mjs +149 -0
  6. package/dist/module.d.mts +15 -0
  7. package/dist/module.json +12 -0
  8. package/dist/module.mjs +300 -0
  9. package/dist/runtime/code-block/create-component.d.ts +16 -0
  10. package/dist/runtime/code-block/create-component.js +106 -0
  11. package/dist/runtime/components/EBody.d.ts +4 -0
  12. package/dist/runtime/components/EBody.js +75 -0
  13. package/dist/runtime/components/EButton.d.ts +11 -0
  14. package/dist/runtime/components/EButton.js +89 -0
  15. package/dist/runtime/components/EButton.padding.d.ts +12 -0
  16. package/dist/runtime/components/EButton.padding.js +118 -0
  17. package/dist/runtime/components/ECodeInline.d.ts +13 -0
  18. package/dist/runtime/components/ECodeInline.js +43 -0
  19. package/dist/runtime/components/EColumn.d.ts +4 -0
  20. package/dist/runtime/components/EColumn.js +12 -0
  21. package/dist/runtime/components/EContainer.d.ts +4 -0
  22. package/dist/runtime/components/EContainer.js +35 -0
  23. package/dist/runtime/components/EFont.d.ts +24 -0
  24. package/dist/runtime/components/EFont.js +124 -0
  25. package/dist/runtime/components/EHead.d.ts +4 -0
  26. package/dist/runtime/components/EHead.js +17 -0
  27. package/dist/runtime/components/EHeading.d.ts +7 -0
  28. package/dist/runtime/components/EHeading.js +23 -0
  29. package/dist/runtime/components/EHr.d.ts +4 -0
  30. package/dist/runtime/components/EHr.js +26 -0
  31. package/dist/runtime/components/EHtml.d.ts +4 -0
  32. package/dist/runtime/components/EHtml.js +17 -0
  33. package/dist/runtime/components/EImg.d.ts +7 -0
  34. package/dist/runtime/components/EImg.js +44 -0
  35. package/dist/runtime/components/ELink.d.ts +6 -0
  36. package/dist/runtime/components/ELink.js +35 -0
  37. package/dist/runtime/components/EMarkdown.d.ts +8 -0
  38. package/dist/runtime/components/EMarkdown.js +64 -0
  39. package/dist/runtime/components/EPreview.d.ts +8 -0
  40. package/dist/runtime/components/EPreview.js +85 -0
  41. package/dist/runtime/components/ERow.d.ts +4 -0
  42. package/dist/runtime/components/ERow.js +53 -0
  43. package/dist/runtime/components/ESection.d.ts +4 -0
  44. package/dist/runtime/components/ESection.js +35 -0
  45. package/dist/runtime/components/ETailwind.d.ts +24 -0
  46. package/dist/runtime/components/ETailwind.js +49 -0
  47. package/dist/runtime/components/EText.d.ts +4 -0
  48. package/dist/runtime/components/EText.js +20 -0
  49. package/dist/runtime/components/EText.margins.d.ts +18 -0
  50. package/dist/runtime/components/EText.margins.js +81 -0
  51. package/dist/runtime/components/attributes.d.ts +4 -0
  52. package/dist/runtime/components/attributes.js +11 -0
  53. package/dist/runtime/components/email-component-names.d.ts +2 -0
  54. package/dist/runtime/components/email-component-names.js +20 -0
  55. package/dist/runtime/components/email-component-registry.d.ts +8 -0
  56. package/dist/runtime/components/email-component-registry.js +2 -0
  57. package/dist/runtime/components/index.d.ts +23 -0
  58. package/dist/runtime/components/index.js +18 -0
  59. package/dist/runtime/components/markdown/parse-css-in-js-to-inline-css.d.ts +2 -0
  60. package/dist/runtime/components/markdown/parse-css-in-js-to-inline-css.js +60 -0
  61. package/dist/runtime/components/markdown/render-markdown.d.ts +2 -0
  62. package/dist/runtime/components/markdown/render-markdown.js +137 -0
  63. package/dist/runtime/components/markdown/styles.d.ts +30 -0
  64. package/dist/runtime/components/markdown/styles.js +91 -0
  65. package/dist/runtime/components/presentation-table.d.ts +7 -0
  66. package/dist/runtime/components/presentation-table.js +11 -0
  67. package/dist/runtime/components/style.d.ts +5 -0
  68. package/dist/runtime/components/style.js +32 -0
  69. package/dist/runtime/components/table-padding.d.ts +6 -0
  70. package/dist/runtime/components/table-padding.js +20 -0
  71. package/dist/runtime/define-email.d.ts +2 -0
  72. package/dist/runtime/define-email.js +5 -0
  73. package/dist/runtime/dev-preview/page-script.d.ts +1 -0
  74. package/dist/runtime/dev-preview/page-script.js +403 -0
  75. package/dist/runtime/dev-preview/page.css.d.ts +1 -0
  76. package/dist/runtime/dev-preview/page.css.js +602 -0
  77. package/dist/runtime/dev-preview/page.get.d.ts +4 -0
  78. package/dist/runtime/dev-preview/page.get.js +119 -0
  79. package/dist/runtime/dev-preview/render.get.d.ts +3 -0
  80. package/dist/runtime/dev-preview/render.get.js +123 -0
  81. package/dist/runtime/dev-preview/templates.get.d.ts +7 -0
  82. package/dist/runtime/dev-preview/templates.get.js +14 -0
  83. package/dist/runtime/errors.d.ts +4 -0
  84. package/dist/runtime/errors.js +7 -0
  85. package/dist/runtime/render/define-email.d.ts +56 -0
  86. package/dist/runtime/render/define-email.js +40 -0
  87. package/dist/runtime/render/document.d.ts +4 -0
  88. package/dist/runtime/render/document.js +75 -0
  89. package/dist/runtime/render/errors.d.ts +4 -0
  90. package/dist/runtime/render/errors.js +8 -0
  91. package/dist/runtime/render/plain-text.d.ts +1 -0
  92. package/dist/runtime/render/plain-text.js +15 -0
  93. package/dist/runtime/render/render-component.d.ts +4 -0
  94. package/dist/runtime/render/render-component.js +46 -0
  95. package/dist/runtime/render/render-email-component.d.ts +16 -0
  96. package/dist/runtime/render/render-email-component.js +46 -0
  97. package/dist/runtime/render/types.d.ts +9 -0
  98. package/dist/runtime/render/types.js +0 -0
  99. package/dist/runtime/tailwind/engine/compatibility/escape-class-name.d.ts +8 -0
  100. package/dist/runtime/tailwind/engine/compatibility/escape-class-name.js +10 -0
  101. package/dist/runtime/tailwind/engine/compatibility/sanitize-class-name.d.ts +7 -0
  102. package/dist/runtime/tailwind/engine/compatibility/sanitize-class-name.js +17 -0
  103. package/dist/runtime/tailwind/engine/compatibility/unescape-class.d.ts +1 -0
  104. package/dist/runtime/tailwind/engine/compatibility/unescape-class.js +3 -0
  105. package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.d.ts +63 -0
  106. package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +107 -0
  107. package/dist/runtime/tailwind/engine/css/extract-rules-per-class.d.ts +6 -0
  108. package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +54 -0
  109. package/dist/runtime/tailwind/engine/css/get-css-property.d.ts +17 -0
  110. package/dist/runtime/tailwind/engine/css/get-css-property.js +3 -0
  111. package/dist/runtime/tailwind/engine/css/get-custom-properties.d.ts +8 -0
  112. package/dist/runtime/tailwind/engine/css/get-custom-properties.js +37 -0
  113. package/dist/runtime/tailwind/engine/css/is-part-inlinable.d.ts +2 -0
  114. package/dist/runtime/tailwind/engine/css/is-part-inlinable.js +9 -0
  115. package/dist/runtime/tailwind/engine/css/is-rule-inlinable.d.ts +2 -0
  116. package/dist/runtime/tailwind/engine/css/is-rule-inlinable.js +9 -0
  117. package/dist/runtime/tailwind/engine/css/make-inline-styles-for.d.ts +21 -0
  118. package/dist/runtime/tailwind/engine/css/make-inline-styles-for.js +62 -0
  119. package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.d.ts +8 -0
  120. package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.js +138 -0
  121. package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.d.ts +5 -0
  122. package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.js +101 -0
  123. package/dist/runtime/tailwind/engine/css/sanitize-declarations.d.ts +15 -0
  124. package/dist/runtime/tailwind/engine/css/sanitize-declarations.js +358 -0
  125. package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.d.ts +17 -0
  126. package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.js +30 -0
  127. package/dist/runtime/tailwind/engine/css/split-mixed-rule.d.ts +12 -0
  128. package/dist/runtime/tailwind/engine/css/split-mixed-rule.js +37 -0
  129. package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.d.ts +19 -0
  130. package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.js +44 -0
  131. package/dist/runtime/tailwind/engine/css/unwrap-value.d.ts +2 -0
  132. package/dist/runtime/tailwind/engine/css/unwrap-value.js +6 -0
  133. package/dist/runtime/tailwind/engine/index.d.ts +63 -0
  134. package/dist/runtime/tailwind/engine/index.js +53 -0
  135. package/dist/runtime/tailwind/engine/pixel-based-preset.d.ts +9 -0
  136. package/dist/runtime/tailwind/engine/pixel-based-preset.js +58 -0
  137. package/dist/runtime/tailwind/engine/sanitize-stylesheet.d.ts +10 -0
  138. package/dist/runtime/tailwind/engine/sanitize-stylesheet.js +8 -0
  139. package/dist/runtime/tailwind/engine/setup-tailwind.d.ts +23 -0
  140. package/dist/runtime/tailwind/engine/setup-tailwind.js +98 -0
  141. package/dist/runtime/tailwind/engine/stylesheets.d.ts +4 -0
  142. package/dist/runtime/tailwind/engine/stylesheets.js +1756 -0
  143. package/dist/runtime/tailwind/engine/types.d.ts +25 -0
  144. package/dist/runtime/tailwind/engine/types.js +0 -0
  145. package/dist/runtime/tailwind/errors.d.ts +7 -0
  146. package/dist/runtime/tailwind/errors.js +9 -0
  147. package/dist/runtime/tailwind/inline-utils.d.ts +19 -0
  148. package/dist/runtime/tailwind/inline-utils.js +24 -0
  149. package/dist/runtime/tailwind/nested.d.ts +59 -0
  150. package/dist/runtime/tailwind/nested.js +47 -0
  151. package/dist/runtime/tailwind/post-render.d.ts +7 -0
  152. package/dist/runtime/tailwind/post-render.js +156 -0
  153. package/dist/runtime/template-registry/errors.d.ts +5 -0
  154. package/dist/runtime/template-registry/errors.js +11 -0
  155. package/dist/runtime/template-registry/registry.d.ts +13 -0
  156. package/dist/runtime/testing/index.d.ts +13 -0
  157. package/dist/runtime/testing/index.js +2 -0
  158. package/dist/types.d.mts +5 -0
  159. package/package.json +122 -0
@@ -0,0 +1,25 @@
1
+ import type { Config } from 'tailwindcss';
2
+ /**
3
+ * A Tailwind v4 JS config, minus `content` (email rendering derives its
4
+ * candidate set from the rendered tree, not from filesystem globs).
5
+ *
6
+ * Mirrors React Email's `TailwindConfig`.
7
+ */
8
+ export type TailwindConfig = Omit<Config, 'content'>;
9
+ /**
10
+ * Options for {@link createTailwindEngine}.
11
+ *
12
+ * - `config` — a Tailwind JS config object (theme, plugins, …).
13
+ * - `theme` — raw CSS injected as an extra `layer(theme)` import
14
+ * (`@theme { … }` / custom properties).
15
+ * - `utility` — raw CSS injected as an extra `layer(utilities)` import
16
+ * (`@utility …`).
17
+ *
18
+ * Matches the public `<Tailwind config theme utility>` prop surface in React
19
+ * Email; the two CSS strings map to setup-tailwind's `cssConfigs`.
20
+ */
21
+ export interface TailwindEngineOptions {
22
+ config?: TailwindConfig;
23
+ theme?: string;
24
+ utility?: string;
25
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Thrown when a Tailwind region produces rules that must live in a `<head>`
3
+ * (media queries / pseudo-classes) but no `<head>` exists inside it.
4
+ */
5
+ export declare class TailwindMissingHeadError extends Error {
6
+ constructor(nonInlinableClassNames: string[]);
7
+ }
@@ -0,0 +1,9 @@
1
+ export class TailwindMissingHeadError extends Error {
2
+ constructor(nonInlinableClassNames) {
3
+ super(
4
+ `Tailwind: <head> not found inside <ETailwind>.
5
+ Move <EHead /> inside <ETailwind>, or remove these classes that require a <head>: ${nonInlinableClassNames.join(" ")}.`
6
+ );
7
+ this.name = "TailwindMissingHeadError";
8
+ }
9
+ }
@@ -0,0 +1,19 @@
1
+ import type { ComputedStyles } from './engine/index.js';
2
+ /**
3
+ * Shared class/style helpers for the Tailwind inlining paths. The render-time
4
+ * VNode transform ({@link ./transform}), the nested primitive self-inliner
5
+ * ({@link ./nested}), and the post-render leftover pass ({@link ./post-render})
6
+ * all resolve class tokens against a {@link ComputedStyles} the exact same way,
7
+ * so the resolution lives here once.
8
+ */
9
+ /** Split a raw `class` value into non-empty tokens in author order (duplicates kept). */
10
+ export declare function classTokens(rawClass: unknown): string[];
11
+ /**
12
+ * Merge the inlinable declarations of an element's classes, in class order.
13
+ * Returns kebab-case `property -> value` pairs (the shape stored on
14
+ * {@link ComputedStyles.inlinable}); later classes overwrite earlier values
15
+ * while keeping the first position, mirroring object-spread merge order.
16
+ */
17
+ export declare function mergeInlinableStyle(tokens: string[], computed: ComputedStyles): Map<string, string>;
18
+ /** Residual classes to keep on the element (original order), mapped ORIGINAL -> OUTPUT. */
19
+ export declare function residualClasses(tokens: string[], computed: ComputedStyles): string[];
@@ -0,0 +1,24 @@
1
+ import { normalizeClass } from "vue";
2
+ export function classTokens(rawClass) {
3
+ if (rawClass == null) return [];
4
+ return normalizeClass(rawClass).split(/\s+/).filter((token) => token.length > 0);
5
+ }
6
+ export function mergeInlinableStyle(tokens, computed) {
7
+ const merged = /* @__PURE__ */ new Map();
8
+ for (const token of tokens) {
9
+ const declarations = computed.inlinable.get(token);
10
+ if (!declarations) continue;
11
+ for (const [property, value] of declarations) {
12
+ merged.set(property, value);
13
+ }
14
+ }
15
+ return merged;
16
+ }
17
+ export function residualClasses(tokens, computed) {
18
+ const residual = [];
19
+ for (const token of tokens) {
20
+ const output = computed.residualClassMap.get(token);
21
+ if (output !== void 0) residual.push(output);
22
+ }
23
+ return residual;
24
+ }
@@ -0,0 +1,59 @@
1
+ import type { TailwindEngine } from './engine/index.js';
2
+ import type { InjectionKey } from 'vue';
3
+ /**
4
+ * Nested-component Tailwind support.
5
+ *
6
+ * A Vue component cannot be safely invoked out of band to inspect its eventual
7
+ * output: doing so breaks scoped slots and executes user code more than once.
8
+ * Tailwind therefore follows Vue's normal render lifecycle:
9
+ *
10
+ * 1. Every E* primitive with style-derivation logic (Text margins, Section /
11
+ * Container td-padding split, Button MSO spacer derivation, Link/Img/Hr
12
+ * defaults) resolves its classes through this context before deriving markup.
13
+ * 2. Structural primitives and native elements are inlined once, after SSR, by
14
+ * {@link ./post-render}.
15
+ */
16
+ /**
17
+ * A region of accumulated Tailwind state for one `<ETailwind>` boundary. Created
18
+ * during the boundary's render, registered on the per-render context, and
19
+ * consumed by the post-render pass. `classNames` is mutated in place as nested
20
+ * primitives (and later the leftover pass) discover classes, so the head `<style>`
21
+ * — injected before the body renders — can be completed with the full set.
22
+ */
23
+ export interface TailwindRegion {
24
+ readonly id: string;
25
+ readonly engine: TailwindEngine;
26
+ /** Every class token seen in the region, in discovery order (duplicates kept). */
27
+ readonly classNames: string[];
28
+ /** Unique token injected into the head `<style>` body, replaced post-render. */
29
+ readonly placeholder: string;
30
+ /** Comment marker content wrapping the region output (splice scope for the leftover pass). */
31
+ readonly startMarker: string;
32
+ readonly endMarker: string;
33
+ }
34
+ /**
35
+ * Provided by `<ETailwind>` to its subtree. The engine and region are filled in
36
+ * during setup/render; primitives read them at their own render time (always
37
+ * after the boundary's render has populated the holder).
38
+ */
39
+ export interface NestedTailwindHolder {
40
+ engine: TailwindEngine | null;
41
+ region: TailwindRegion | null;
42
+ }
43
+ export declare const TAILWIND_NESTED_KEY: InjectionKey<NestedTailwindHolder>;
44
+ /** Create a region with a document-unique id for its markers and style placeholder. */
45
+ export declare function createTailwindRegion(engine: TailwindEngine, classNames: string[]): TailwindRegion;
46
+ export interface NestedResolution {
47
+ /** Effective style input for the primitive's own style logic. */
48
+ style: unknown;
49
+ /** True only when a class or style actually changed (keeps the inactive path byte-identical). */
50
+ changed: boolean;
51
+ }
52
+ /**
53
+ * Resolve a primitive's raw class tokens against the injected engine and return
54
+ * the merged `{ ...tailwind, ...author }` style to feed into the primitive's own
55
+ * derivation. Mutates `attributes.class` in place to the residual classes (or
56
+ * removes it) only when something actually changes, so a primitive outside a
57
+ * Tailwind region remains untouched.
58
+ */
59
+ export declare function resolveNestedTailwindStyle(holder: NestedTailwindHolder | null, attributes: Record<string, unknown>, styleValue: unknown): NestedResolution;
@@ -0,0 +1,47 @@
1
+ import { camelize, normalizeClass } from "vue";
2
+ import { normalizeEmailStyle } from "../components/style.js";
3
+ import { classTokens, mergeInlinableStyle, residualClasses } from "./inline-utils.js";
4
+ export const TAILWIND_NESTED_KEY = /* @__PURE__ */ Symbol("nuxt-email:tailwind-nested");
5
+ let regionCounter = 0;
6
+ export function createTailwindRegion(engine, classNames) {
7
+ const id = `${(regionCounter++).toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
8
+ return {
9
+ id,
10
+ engine,
11
+ classNames,
12
+ placeholder: `/*nuxt-email-tw-css:${id}*/`,
13
+ startMarker: `nuxt-email-tw:${id}`,
14
+ endMarker: `/nuxt-email-tw:${id}`
15
+ };
16
+ }
17
+ export function resolveNestedTailwindStyle(holder, attributes, styleValue) {
18
+ if (!holder || !holder.engine || !holder.region) {
19
+ return { style: styleValue, changed: false };
20
+ }
21
+ const tokens = classTokens(attributes.class);
22
+ if (tokens.length === 0) {
23
+ return { style: styleValue, changed: false };
24
+ }
25
+ holder.region.classNames.push(...tokens);
26
+ const computed = holder.engine.computeStyles(tokens);
27
+ const tw = mergeInlinableStyle(tokens, computed);
28
+ const residual = residualClasses(tokens, computed);
29
+ const classUnchanged = residual.length === tokens.length && residual.every((name, index) => name === tokens[index]);
30
+ if (tw.size === 0 && classUnchanged) {
31
+ return { style: styleValue, changed: false };
32
+ }
33
+ if (residual.length > 0) {
34
+ attributes.class = normalizeClass(residual.join(" "));
35
+ } else {
36
+ delete attributes.class;
37
+ }
38
+ if (tw.size === 0) {
39
+ return { style: styleValue, changed: true };
40
+ }
41
+ const tailwindStyle = {};
42
+ for (const [property, value] of tw) {
43
+ tailwindStyle[camelize(property)] = value;
44
+ }
45
+ const author = normalizeEmailStyle(styleValue);
46
+ return { style: { ...tailwindStyle, ...author ?? {} }, changed: true };
47
+ }
@@ -0,0 +1,7 @@
1
+ import type { TailwindRegion } from './nested.js';
2
+ /**
3
+ * Complete every registered Tailwind region. A no-op (byte-identical passthrough)
4
+ * when no region was registered — the zero-cost guarantee for emails that do not
5
+ * use `<ETailwind>`.
6
+ */
7
+ export declare function applyTailwindPostRender(html: string, regions: TailwindRegion[] | undefined): string;
@@ -0,0 +1,156 @@
1
+ import { generate, parse, walk } from "css-tree";
2
+ import { Parser } from "htmlparser2";
3
+ import { classTokens, mergeInlinableStyle, residualClasses } from "./inline-utils.js";
4
+ import { TailwindMissingHeadError } from "./errors.js";
5
+ function collectElements(fragment) {
6
+ const hits = [];
7
+ const parser = new Parser({
8
+ onopentag(_name, attributes) {
9
+ if (typeof attributes.class !== "string" || attributes.class.length === 0) return;
10
+ hits.push({
11
+ openStart: parser.startIndex,
12
+ openEnd: parser.endIndex + 1,
13
+ classValue: attributes.class,
14
+ styleValue: typeof attributes.style === "string" ? attributes.style : void 0
15
+ });
16
+ }
17
+ });
18
+ parser.write(fragment);
19
+ parser.end();
20
+ return hits;
21
+ }
22
+ function parseStylePairs(style) {
23
+ if (!style) return [];
24
+ const pairs = [];
25
+ try {
26
+ const declarations = parse(style, { context: "declarationList" });
27
+ walk(declarations, {
28
+ visit: "Declaration",
29
+ enter(declaration) {
30
+ pairs.push([
31
+ declaration.property,
32
+ generate(declaration.value).trim() + (declaration.important ? "!important" : "")
33
+ ]);
34
+ }
35
+ });
36
+ } catch {
37
+ return null;
38
+ }
39
+ return pairs;
40
+ }
41
+ function serializeStyle(style) {
42
+ let out = "";
43
+ for (const [property, value] of style) {
44
+ out += `${property}:${value};`;
45
+ }
46
+ return out;
47
+ }
48
+ const CLASS_ATTRIBUTE = /\s+class\s*=\s*("[^"]*"|'[^']*')/;
49
+ const STYLE_ATTRIBUTE = /\s+style\s*=\s*("[^"]*"|'[^']*')/;
50
+ function escapeAttributeValue(value) {
51
+ return value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
52
+ }
53
+ function rewriteOpenTag(openTag, newClass, newStyle) {
54
+ let tag = newClass === null ? openTag.replace(CLASS_ATTRIBUTE, "") : openTag.replace(CLASS_ATTRIBUTE, ` class="${escapeAttributeValue(newClass)}"`);
55
+ if (newStyle !== null && newStyle.length > 0) {
56
+ const escapedStyle = escapeAttributeValue(newStyle);
57
+ if (STYLE_ATTRIBUTE.test(tag)) {
58
+ tag = tag.replace(STYLE_ATTRIBUTE, ` style="${escapedStyle}"`);
59
+ } else {
60
+ const closeLength = tag.endsWith("/>") ? 2 : 1;
61
+ tag = `${tag.slice(0, tag.length - closeLength)} style="${escapedStyle}"${tag.slice(tag.length - closeLength)}`;
62
+ }
63
+ }
64
+ return tag;
65
+ }
66
+ function inlinePlainElements(fragment, region) {
67
+ const hits = collectElements(fragment);
68
+ for (const hit of hits) {
69
+ region.classNames.push(...classTokens(hit.classValue));
70
+ }
71
+ const computed = region.engine.computeStyles(region.classNames);
72
+ const edits = [];
73
+ for (const hit of hits) {
74
+ const tokens = classTokens(hit.classValue);
75
+ const tw = mergeInlinableStyle(tokens, computed);
76
+ const residual = residualClasses(tokens, computed);
77
+ const classUnchanged = residual.length === tokens.length && residual.every((name, index) => name === tokens[index]);
78
+ if (tw.size === 0 && classUnchanged) continue;
79
+ const authorStyles = parseStylePairs(hit.styleValue);
80
+ const merged = new Map(tw);
81
+ if (authorStyles !== null) {
82
+ for (const [property, value] of authorStyles) {
83
+ merged.set(property, value);
84
+ }
85
+ }
86
+ const openTag = fragment.slice(hit.openStart, hit.openEnd);
87
+ const newClass = residual.length > 0 ? residual.join(" ") : null;
88
+ const newStyle = authorStyles === null ? `${serializeStyle(merged)}${hit.styleValue ?? ""}` : merged.size > 0 ? serializeStyle(merged) : null;
89
+ edits.push({ start: hit.openStart, end: hit.openEnd, text: rewriteOpenTag(openTag, newClass, newStyle) });
90
+ }
91
+ edits.sort((left, right) => right.start - left.start);
92
+ let out = fragment;
93
+ for (const edit of edits) {
94
+ out = out.slice(0, edit.start) + edit.text + out.slice(edit.end);
95
+ }
96
+ return out;
97
+ }
98
+ function insertStyleIntoHead(html, css) {
99
+ let depth = 0;
100
+ let headDepth;
101
+ let headCloseStart;
102
+ let firstDirectStyleStart;
103
+ const parser = new Parser({
104
+ onopentag(name) {
105
+ if (headDepth === void 0 && name === "head") {
106
+ headDepth = depth;
107
+ } else if (headDepth !== void 0 && headCloseStart === void 0 && depth === headDepth + 1 && name === "style" && firstDirectStyleStart === void 0) {
108
+ firstDirectStyleStart = parser.startIndex;
109
+ }
110
+ depth += 1;
111
+ },
112
+ onclosetag(name) {
113
+ depth = Math.max(0, depth - 1);
114
+ if (name === "head" && headDepth === depth && headCloseStart === void 0) {
115
+ headCloseStart = parser.startIndex;
116
+ }
117
+ }
118
+ });
119
+ parser.write(html);
120
+ parser.end();
121
+ const insertionIndex = firstDirectStyleStart ?? headCloseStart;
122
+ if (insertionIndex === void 0) return null;
123
+ const style = `<style>${css}</style>`;
124
+ return html.slice(0, insertionIndex) + style + html.slice(insertionIndex);
125
+ }
126
+ function processRegion(html, region) {
127
+ const startComment = `<!--${region.startMarker}-->`;
128
+ const endComment = `<!--${region.endMarker}-->`;
129
+ const startIndex = html.indexOf(startComment);
130
+ const endIndex = html.indexOf(endComment);
131
+ if (startIndex === -1 || endIndex === -1) return html;
132
+ const fragmentStart = startIndex + startComment.length;
133
+ const fragment = html.slice(fragmentStart, endIndex);
134
+ const inlined = inlinePlainElements(fragment, region);
135
+ const computed = region.engine.computeStyles(region.classNames);
136
+ if (inlined.includes(region.placeholder)) {
137
+ const completed = inlined.replace(region.placeholder, computed.nonInlinableCss);
138
+ return html.slice(0, startIndex) + completed + html.slice(endIndex + endComment.length);
139
+ }
140
+ const withHeadStyle = insertStyleIntoHead(inlined, computed.nonInlinableCss);
141
+ if (withHeadStyle !== null) {
142
+ return html.slice(0, startIndex) + withHeadStyle + html.slice(endIndex + endComment.length);
143
+ }
144
+ if (computed.nonInlinableCss !== "") {
145
+ throw new TailwindMissingHeadError(computed.nonInlinableClassNames);
146
+ }
147
+ return html.slice(0, startIndex) + inlined + html.slice(endIndex + endComment.length);
148
+ }
149
+ export function applyTailwindPostRender(html, regions) {
150
+ if (!regions || regions.length === 0) return html;
151
+ let result = html;
152
+ for (const region of regions) {
153
+ result = processRegion(result, region);
154
+ }
155
+ return result;
156
+ }
@@ -0,0 +1,5 @@
1
+ export declare class UnknownEmailTemplateError extends Error {
2
+ readonly requestedName: string;
3
+ readonly knownNames: readonly string[];
4
+ constructor(requestedName: string, knownNames: readonly string[]);
5
+ }
@@ -0,0 +1,11 @@
1
+ export class UnknownEmailTemplateError extends Error {
2
+ requestedName;
3
+ knownNames;
4
+ constructor(requestedName, knownNames) {
5
+ const sortedKnownNames = [...knownNames].sort();
6
+ super(sortedKnownNames.length === 0 ? `Unknown email template "${requestedName}"; no email templates are registered` : `Unknown email template "${requestedName}"; known templates: ${sortedKnownNames.join(", ")}`);
7
+ this.name = "UnknownEmailTemplateError";
8
+ this.requestedName = requestedName;
9
+ this.knownNames = sortedKnownNames;
10
+ }
11
+ }
@@ -0,0 +1,13 @@
1
+ declare module '#nuxt-email/registry' {
2
+ interface EmailTemplateRegistryEntry {
3
+ component: () => Promise<{ default: import('vue').Component }>
4
+ fixture?: () => Promise<{ default: unknown }>
5
+ }
6
+
7
+ export const emailTemplates: Readonly<Record<string, EmailTemplateRegistryEntry>>
8
+
9
+ export function renderEmail(
10
+ name: string,
11
+ props: Record<string, unknown>,
12
+ ): Promise<import('../render/types').RenderedEmail>
13
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * First-class testing utilities for nuxt-email templates.
3
+ *
4
+ * Import from the `@lupinum/nuxt-email/testing` subpath to render and assert on your
5
+ * emails in a unit test without booting a Nuxt app:
6
+ *
7
+ * ```ts
8
+ * import { renderEmailComponent } from '@lupinum/nuxt-email/testing'
9
+ * ```
10
+ */
11
+ export { renderEmailComponent } from '../render/render-email-component.js';
12
+ export { EmailRenderError } from '../render/errors.js';
13
+ export type { RenderedEmail } from '../render/types.js';
@@ -0,0 +1,2 @@
1
+ export { renderEmailComponent } from "../render/render-email-component.js";
2
+ export { EmailRenderError } from "../render/errors.js";
@@ -0,0 +1,5 @@
1
+ export { type RenderedEmail } from '../dist/runtime/render/types.js'
2
+
3
+ export { default } from './module.mjs'
4
+
5
+ export { type ModuleOptions } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,122 @@
1
+ {
2
+ "name": "@lupinum/nuxt-email",
3
+ "version": "0.1.0",
4
+ "description": "Typed transactional email for Nuxt with email-safe Tailwind v4",
5
+ "keywords": [
6
+ "email",
7
+ "nuxt",
8
+ "tailwindcss",
9
+ "transactional-email",
10
+ "vue"
11
+ ],
12
+ "license": "MIT",
13
+ "author": "Lupinum OG <info@lupinum.com> (https://lupinum.com)",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/lupinum-dev/nuxt-email.git"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "homepage": "https://nuxt-email.lupinum.com",
22
+ "bugs": {
23
+ "url": "https://github.com/lupinum-dev/nuxt-email/issues"
24
+ },
25
+ "type": "module",
26
+ "engines": {
27
+ "node": "^22.18.0 || ^24.11.0 || ^26.0.0"
28
+ },
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/module.d.mts",
32
+ "import": "./dist/module.mjs"
33
+ },
34
+ "./testing": {
35
+ "types": "./dist/runtime/testing/index.d.ts",
36
+ "import": "./dist/runtime/testing/index.js"
37
+ },
38
+ "./define-email": {
39
+ "types": "./dist/runtime/define-email.d.ts",
40
+ "import": "./dist/runtime/define-email.js"
41
+ },
42
+ "./errors": {
43
+ "types": "./dist/runtime/errors.d.ts",
44
+ "import": "./dist/runtime/errors.js"
45
+ }
46
+ },
47
+ "main": "./dist/module.mjs",
48
+ "types": "./dist/module.d.mts",
49
+ "files": [
50
+ "CHANGELOG.md",
51
+ "dist",
52
+ "THIRD_PARTY_NOTICES"
53
+ ],
54
+ "peerDependencies": {
55
+ "nuxt": ">=4.5.1 <5",
56
+ "vue": "^3.5.35"
57
+ },
58
+ "dependencies": {
59
+ "@nuxt/kit": ">=4.5.1 <5",
60
+ "@shikijs/core": "4.3.1",
61
+ "@shikijs/engine-javascript": "4.3.1",
62
+ "@shikijs/langs": "4.3.1",
63
+ "@shikijs/themes": "4.3.1",
64
+ "css-tree": "3.2.1",
65
+ "h3": "1.15.11",
66
+ "html-to-text": "9.0.5",
67
+ "htmlparser2": "^10.1.0",
68
+ "marked": "15.0.12",
69
+ "tailwindcss": "4.1.18",
70
+ "unplugin-vue": "7.2.0"
71
+ },
72
+ "devDependencies": {
73
+ "@nuxt/eslint-config": "^1.15.2",
74
+ "@nuxt/module-builder": "^1.0.2",
75
+ "@nuxt/schema": "4.5.2",
76
+ "@nuxt/test-utils": "^4.0.3",
77
+ "@react-email/render": "2.1.0",
78
+ "@types/css-tree": "2.3.11",
79
+ "@types/html-to-text": "9.0.4",
80
+ "@types/node": "latest",
81
+ "@types/react": "19.2.17",
82
+ "@types/react-dom": "19.2.3",
83
+ "@vitejs/plugin-vue": "6.0.8",
84
+ "changelogen": "0.6.2",
85
+ "eslint": "^10.4.1",
86
+ "nuxt": "4.5.2",
87
+ "pkg-pr-new": "0.0.87",
88
+ "react": "19.2.7",
89
+ "react-dom": "19.2.7",
90
+ "react-email": "6.9.0",
91
+ "tsx": "4.23.1",
92
+ "typescript": "~5.9.3",
93
+ "vitest": "^4.1.8",
94
+ "vue": "3.5.40",
95
+ "vue-tsc": "^3.3.3"
96
+ },
97
+ "scripts": {
98
+ "changelog": "changelogen --no-output",
99
+ "audit:all": "pnpm audit",
100
+ "conformance:check": "NUXT_EMAIL_CONFORMANCE_REPORT=check vitest run test/conformance --reporter=default --reporter=./test/conformance/tooling/reporter.ts",
101
+ "conformance:write": "NUXT_EMAIL_CONFORMANCE_REPORT=write vitest run test/conformance --reporter=default --reporter=./test/conformance/tooling/reporter.ts",
102
+ "dev": "pnpm dev:prepare && nuxt dev playground",
103
+ "dev:build": "nuxt build playground",
104
+ "dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground",
105
+ "oracle:check": "node --import tsx test/conformance/tooling/generate-react-oracle.ts --check",
106
+ "oracle:write": "node --import tsx test/conformance/tooling/generate-react-oracle.ts --write",
107
+ "performance:measure": "node --expose-gc --import tsx test/performance/measure.ts",
108
+ "docs:build": "nuxt-module-build prepare && pnpm prepack && pnpm --dir docs build",
109
+ "proofs:generate": "node --import tsx scripts/proofs/generate.ts",
110
+ "proofs:send": "node --import tsx scripts/proofs/send.ts",
111
+ "repo:check": "node scripts/check-repository-policy.mjs",
112
+ "release:verify": "pnpm verify && node --import tsx scripts/verify-release.ts",
113
+ "release:prepare": "changelogen --bump --clean --hideAuthorEmail",
114
+ "release:artifact": "pnpm release:verify --output .release/lupinum-nuxt-email.tgz && node scripts/write-release-manifest.mjs",
115
+ "lint": "eslint .",
116
+ "test": "vitest run",
117
+ "test:conformance": "vitest run test/conformance",
118
+ "test:watch": "vitest watch",
119
+ "test:types": "pnpm dev:prepare && nuxt prepare test/fixtures/basic && nuxt prepare test/fixtures/code-block && nuxt prepare test/fixtures/preview && vue-tsc --noEmit && vue-tsc --noEmit -p test/fixtures/basic/.nuxt/tsconfig.server.json && vue-tsc --noEmit -p test/fixtures/code-block/.nuxt/tsconfig.server.json && vue-tsc --noEmit -p test/fixtures/preview/.nuxt/tsconfig.server.json && cd playground && vue-tsc --noEmit",
120
+ "verify": "pnpm audit:all && pnpm repo:check && pnpm lint && pnpm test:types && pnpm test && pnpm conformance:check && pnpm docs:build"
121
+ }
122
+ }