@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
@@ -1,4 +1,9 @@
1
- import { generate, List } from "css-tree";
1
+ import {
2
+ clone,
3
+ generate,
4
+ List,
5
+ walk
6
+ } from "../css-tree.js";
2
7
  import { downlevelForEmailClients } from "./css/downlevel-for-email-clients.js";
3
8
  import { extractRulesPerClass } from "./css/extract-rules-per-class.js";
4
9
  import { getCustomProperties } from "./css/get-custom-properties.js";
@@ -7,6 +12,35 @@ import { sanitizeClassName } from "./compatibility/sanitize-class-name.js";
7
12
  import { sanitizeNonInlinableRules } from "./css/sanitize-non-inlinable-rules.js";
8
13
  import { sanitizeStyleSheet } from "./sanitize-stylesheet.js";
9
14
  import { setupTailwind } from "./setup-tailwind.js";
15
+ function collectKeyframesDefinitions(styleSheet) {
16
+ const definitions = [];
17
+ styleSheet.children.forEach((node) => {
18
+ if (node.type === "Atrule" && (node.name === "keyframes" || node.name === "-webkit-keyframes") && node.prelude) {
19
+ definitions.push({ name: generate(node.prelude).trim(), node });
20
+ }
21
+ });
22
+ return definitions;
23
+ }
24
+ function collectReferencedKeyframes(rulesByClass, knownNames) {
25
+ const references = /* @__PURE__ */ new Map();
26
+ for (const [className, rules] of rulesByClass) {
27
+ const classReferences = /* @__PURE__ */ new Set();
28
+ for (const rule of rules) {
29
+ walk(rule, {
30
+ visit: "Declaration",
31
+ enter(declaration) {
32
+ if (declaration.property !== "animation" && declaration.property !== "animation-name") return;
33
+ walk(declaration.value, (node) => {
34
+ const name = node.type === "Identifier" ? node.name : node.type === "String" ? node.value : null;
35
+ if (name && knownNames.has(name)) classReferences.add(name);
36
+ });
37
+ }
38
+ });
39
+ }
40
+ if (classReferences.size > 0) references.set(className, classReferences);
41
+ }
42
+ return references;
43
+ }
10
44
  export async function createTailwindEngine(options = {}) {
11
45
  const { config, theme, utility } = options;
12
46
  const tailwindSetup = await setupTailwind({
@@ -17,17 +51,42 @@ export async function createTailwindEngine(options = {}) {
17
51
  tailwindSetup.addUtilities(classNames);
18
52
  const styleSheet = tailwindSetup.getStyleSheet();
19
53
  sanitizeStyleSheet(styleSheet);
20
- const { inlinable: inlinableRules, nonInlinable: nonInlinableRules } = extractRulesPerClass(styleSheet, classNames);
54
+ const {
55
+ inlinable: inlinableRules,
56
+ nonInlinable: nonInlinableRules,
57
+ orderedNonInlinable
58
+ } = extractRulesPerClass(styleSheet, classNames);
21
59
  const customProperties = getCustomProperties(styleSheet);
60
+ const keyframesDefinitions = collectKeyframesDefinitions(styleSheet);
61
+ const knownKeyframeNames = new Set(
62
+ keyframesDefinitions.map((definition) => definition.name)
63
+ );
64
+ const animationReferences = /* @__PURE__ */ new Map();
65
+ for (const rules of [inlinableRules, nonInlinableRules]) {
66
+ for (const [className, names] of collectReferencedKeyframes(
67
+ rules,
68
+ knownKeyframeNames
69
+ )) {
70
+ const existing = animationReferences.get(className) ?? /* @__PURE__ */ new Set();
71
+ for (const name of names) existing.add(name);
72
+ animationReferences.set(className, existing);
73
+ }
74
+ }
75
+ const referencedKeyframeNames = new Set(
76
+ Array.from(animationReferences.values()).flatMap((names) => [...names])
77
+ );
22
78
  const nonInlineStyles = {
23
79
  type: "StyleSheet",
24
- children: new List().fromArray(
25
- Array.from(nonInlinableRules.values()).flat()
26
- )
80
+ children: new List().fromArray(orderedNonInlinable)
27
81
  };
28
82
  sanitizeNonInlinableRules(nonInlineStyles);
83
+ for (const definition of keyframesDefinitions) {
84
+ if (referencedKeyframeNames.has(definition.name)) {
85
+ nonInlineStyles.children.appendData(clone(definition.node));
86
+ }
87
+ }
29
88
  downlevelForEmailClients(nonInlineStyles);
30
- const nonInlinableCss = nonInlinableRules.size > 0 ? generate(nonInlineStyles) : "";
89
+ const nonInlinableCss = nonInlineStyles.children.isEmpty ? "" : generate(nonInlineStyles);
31
90
  const inlinable = /* @__PURE__ */ new Map();
32
91
  for (const [className, rules] of inlinableRules) {
33
92
  inlinable.set(className, makeInlineStylesFor(rules, customProperties));
@@ -41,11 +100,15 @@ export async function createTailwindEngine(options = {}) {
41
100
  residualClassMap.set(className, className);
42
101
  }
43
102
  }
103
+ const nonInlinableClassNames = new Set(nonInlinableRules.keys());
104
+ for (const className of classNames) {
105
+ if (animationReferences.has(className) && !nonInlinableRules.has(className)) nonInlinableClassNames.add(className);
106
+ }
44
107
  return {
45
108
  inlinable,
46
109
  nonInlinableCss,
47
110
  residualClassMap,
48
- nonInlinableClassNames: Array.from(nonInlinableRules.keys())
111
+ nonInlinableClassNames: [...nonInlinableClassNames]
49
112
  };
50
113
  };
51
114
  return { computeStyles };
@@ -1,4 +1,4 @@
1
- import { type StyleSheet } from 'css-tree';
1
+ import { type StyleSheet } from '../css-tree.js';
2
2
  import type { TailwindConfig } from './types.js';
3
3
  export type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
4
4
  interface CSSConfigs {
@@ -1,4 +1,4 @@
1
- import { parse } from "css-tree";
1
+ import { parse } from "../css-tree.js";
2
2
  import { compile } from "tailwindcss";
3
3
  import {
4
4
  indexCss,
@@ -1,4 +1,4 @@
1
- import { generate, parse, walk } from "css-tree";
1
+ import { generate, parse, walk } from "./css-tree.js";
2
2
  import { Parser } from "htmlparser2";
3
3
  import { classTokens, mergeInlinableStyle, residualClasses } from "./inline-utils.js";
4
4
  import { TailwindMissingHeadError } from "./errors.js";
@@ -63,12 +63,7 @@ function rewriteOpenTag(openTag, newClass, newStyle) {
63
63
  }
64
64
  return tag;
65
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);
66
+ function inlinePlainElements(fragment, hits, computed) {
72
67
  const edits = [];
73
68
  for (const hit of hits) {
74
69
  const tokens = classTokens(hit.classValue);
@@ -131,8 +126,12 @@ function processRegion(html, region) {
131
126
  if (startIndex === -1 || endIndex === -1) return html;
132
127
  const fragmentStart = startIndex + startComment.length;
133
128
  const fragment = html.slice(fragmentStart, endIndex);
134
- const inlined = inlinePlainElements(fragment, region);
129
+ const hits = collectElements(fragment);
130
+ for (const hit of hits) {
131
+ region.classNames.push(...classTokens(hit.classValue));
132
+ }
135
133
  const computed = region.engine.computeStyles(region.classNames);
134
+ const inlined = inlinePlainElements(fragment, hits, computed);
136
135
  if (inlined.includes(region.placeholder)) {
137
136
  const completed = inlined.replace(region.placeholder, computed.nonInlinableCss);
138
137
  return html.slice(0, startIndex) + completed + html.slice(endIndex + endComment.length);
@@ -0,0 +1,133 @@
1
+ import { relative, isAbsolute, sep } from 'node:path';
2
+ import { resolveFiles } from '@nuxt/kit';
3
+
4
+ class DuplicateEmailTemplateError extends Error {
5
+ templateName;
6
+ sourcePaths;
7
+ constructor(templateName, sourcePaths) {
8
+ const sortedSourcePaths = [...sourcePaths].sort();
9
+ super(`Duplicate email template name "${templateName}" from: ${sortedSourcePaths.join(", ")}`);
10
+ this.name = "DuplicateEmailTemplateError";
11
+ this.templateName = templateName;
12
+ this.sourcePaths = sortedSourcePaths;
13
+ }
14
+ }
15
+ class EmailTemplateDiscoveryError extends Error {
16
+ sourcePath;
17
+ constructor(sourcePath, cause) {
18
+ super(`Failed to discover email templates under ${sourcePath}`, { cause });
19
+ this.name = "EmailTemplateDiscoveryError";
20
+ this.sourcePath = sourcePath;
21
+ }
22
+ }
23
+
24
+ function compareCodePoints(left, right) {
25
+ return left < right ? -1 : left > right ? 1 : 0;
26
+ }
27
+ function normalizeSeparators(value) {
28
+ return value.split(sep).join("/").replaceAll("\\", "/");
29
+ }
30
+ function templatesFromSourcePaths(emailDirectory, sourcePaths) {
31
+ const templates = sourcePaths.map((sourcePath) => {
32
+ const relativePath = normalizeSeparators(relative(emailDirectory, sourcePath));
33
+ if (relativePath.startsWith("../") || relativePath === ".." || isAbsolute(relativePath)) {
34
+ throw new EmailTemplateDiscoveryError(sourcePath, new TypeError("Template path is outside app/emails"));
35
+ }
36
+ return {
37
+ name: relativePath.slice(0, -".vue".length),
38
+ sourcePath: normalizeSeparators(sourcePath)
39
+ };
40
+ }).sort((left, right) => {
41
+ return compareCodePoints(left.name, right.name) || compareCodePoints(left.sourcePath, right.sourcePath);
42
+ });
43
+ const pathsByName = /* @__PURE__ */ new Map();
44
+ for (const template of templates) {
45
+ const paths = pathsByName.get(template.name) ?? [];
46
+ paths.push(template.sourcePath);
47
+ pathsByName.set(template.name, paths);
48
+ }
49
+ const duplicate = [...pathsByName.entries()].filter(([, paths]) => paths.length > 1).sort(([left], [right]) => compareCodePoints(left, right))[0];
50
+ if (duplicate) {
51
+ throw new DuplicateEmailTemplateError(duplicate[0], duplicate[1]);
52
+ }
53
+ return templates;
54
+ }
55
+ async function discoverEmailTemplates(emailDirectory) {
56
+ let sourcePaths;
57
+ try {
58
+ sourcePaths = await resolveFiles(emailDirectory, "**/*.vue", {
59
+ followSymbolicLinks: false,
60
+ ignore: ["components/**"]
61
+ });
62
+ } catch (error) {
63
+ throw new EmailTemplateDiscoveryError(emailDirectory, error);
64
+ }
65
+ return templatesFromSourcePaths(emailDirectory, sourcePaths);
66
+ }
67
+
68
+ function importPath(path) {
69
+ return JSON.stringify(path.replaceAll("\\", "/"));
70
+ }
71
+ function generateEmailRegistry(templates, runtimePaths) {
72
+ const entries = templates.map((template) => {
73
+ const fixture = template.fixturePath ? `
74
+ fixture: () => import(${importPath(template.fixturePath)}),` : "";
75
+ return ` [${JSON.stringify(template.name)}]: {
76
+ component: () => import(${importPath(template.sourcePath)}),${fixture}
77
+ },`;
78
+ });
79
+ return `import { EmailRenderError } from ${importPath(runtimePaths.emailRenderError)}
80
+ import { renderEmailComponent } from ${importPath(runtimePaths.renderEmailComponent)}
81
+ import { UnknownEmailTemplateError } from ${importPath(runtimePaths.templateRegistryErrors)}
82
+
83
+ export const emailTemplates = Object.freeze({
84
+ ${entries.join("\n")}
85
+ })
86
+
87
+ export async function renderEmail(name, props) {
88
+ const loader = typeof name === 'string' && Object.hasOwn(emailTemplates, name)
89
+ ? emailTemplates[name]
90
+ : undefined
91
+ if (!loader) {
92
+ throw new UnknownEmailTemplateError(String(name), Object.keys(emailTemplates))
93
+ }
94
+
95
+ try {
96
+ const templateModule = await loader.component()
97
+ return await renderEmailComponent(templateModule.default, props)
98
+ }
99
+ catch (error) {
100
+ throw new EmailRenderError(name, error instanceof EmailRenderError ? error.cause : error)
101
+ }
102
+ }
103
+ `;
104
+ }
105
+ function generateEmailTypes(templates, typePaths) {
106
+ const entries = templates.map((template) => {
107
+ const fixture = template.fixturePath ? `
108
+ fixture: () => Promise<typeof import(${importPath(template.fixturePath)})>` : "";
109
+ return ` ${JSON.stringify(template.name)}: {
110
+ component: () => Promise<typeof import(${importPath(template.sourcePath)})>${fixture}
111
+ }`;
112
+ });
113
+ return `export const emailTemplates: Readonly<{
114
+ ${entries.join("\n")}
115
+ }>
116
+
117
+ export type EmailTemplateName = keyof typeof emailTemplates
118
+ type _EmailProps<Component> = keyof import(${importPath(typePaths.renderedEmail)}).EmailComponentProps<Component> extends never
119
+ ? Record<string, never>
120
+ : import(${importPath(typePaths.renderedEmail)}).EmailComponentProps<Component>
121
+
122
+ export type EmailTemplateProps = {
123
+ [Name in EmailTemplateName]: _EmailProps<Awaited<ReturnType<(typeof emailTemplates)[Name]['component']>>['default']>
124
+ }
125
+
126
+ export function renderEmail<Name extends EmailTemplateName>(
127
+ name: Name,
128
+ props: NoInfer<EmailTemplateProps[Name]>,
129
+ ): Promise<import(${importPath(typePaths.renderedEmail)}).RenderedEmail>
130
+ `;
131
+ }
132
+
133
+ export { generateEmailTypes as a, discoverEmailTemplates as d, generateEmailRegistry as g };
package/dist/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { type RenderedEmail } from '../dist/runtime/render/types.js'
1
+ export { type EmailComponentProps, type RenderedEmail } from '../dist/runtime/render/types.js'
2
2
 
3
3
  export { default } from './module.mjs'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/nuxt-email",
3
- "version": "0.1.1",
3
+ "version": "1.0.0-beta.2",
4
4
  "description": "Typed transactional email for Nuxt with email-safe Tailwind v4",
5
5
  "keywords": [
6
6
  "email",
@@ -27,6 +27,14 @@
27
27
  "node": "^22.18.0 || ^24.11.0 || ^26.0.0"
28
28
  },
29
29
  "exports": {
30
+ "./build": {
31
+ "types": "./dist/build.d.mts",
32
+ "import": "./dist/build.mjs"
33
+ },
34
+ "./render": {
35
+ "types": "./dist/runtime/render/index.d.ts",
36
+ "import": "./dist/runtime/render/index.js"
37
+ },
30
38
  ".": {
31
39
  "types": "./dist/module.d.mts",
32
40
  "import": "./dist/module.mjs"
@@ -62,12 +70,15 @@
62
70
  "@shikijs/langs": "4.3.1",
63
71
  "@shikijs/themes": "4.3.1",
64
72
  "css-tree": "3.2.1",
73
+ "esbuild": "0.28.1",
65
74
  "h3": "1.15.11",
66
75
  "html-to-text": "9.0.5",
67
76
  "htmlparser2": "^10.1.0",
68
77
  "marked": "15.0.12",
69
78
  "tailwindcss": "4.1.18",
70
- "unplugin-vue": "7.2.0"
79
+ "typescript": "~5.9.3",
80
+ "unplugin-vue": "7.2.0",
81
+ "vue-tsc": "^3.3.3"
71
82
  },
72
83
  "devDependencies": {
73
84
  "@nuxt/eslint-config": "^1.15.2",
@@ -89,10 +100,9 @@
89
100
  "react-dom": "19.2.7",
90
101
  "react-email": "6.9.0",
91
102
  "tsx": "4.23.1",
92
- "typescript": "~5.9.3",
93
103
  "vitest": "^4.1.8",
94
- "vue": "3.5.40",
95
- "vue-tsc": "^3.3.3"
104
+ "vue": "3.5.42",
105
+ "yaml": "2.9.0"
96
106
  },
97
107
  "scripts": {
98
108
  "changelog": "changelogen --no-output",
@@ -105,7 +115,8 @@
105
115
  "oracle:check": "node --import tsx test/conformance/tooling/generate-react-oracle.ts --check",
106
116
  "oracle:write": "node --import tsx test/conformance/tooling/generate-react-oracle.ts --write",
107
117
  "performance:measure": "node --expose-gc --import tsx test/performance/measure.ts",
108
- "docs:build": "nuxt-module-build prepare && pnpm prepack && pnpm --dir docs build",
118
+ "docs:build": "pnpm docs:theme && nuxt-module-build prepare && pnpm prepack && pnpm --dir docs build",
119
+ "docs:theme": "node scripts/check-docs-theme.mjs",
109
120
  "proofs:generate": "node --import tsx scripts/proofs/generate.ts",
110
121
  "proofs:send": "node --import tsx scripts/proofs/send.ts",
111
122
  "repo:check": "node scripts/check-repository-policy.mjs",
@@ -113,7 +124,7 @@
113
124
  "release:prepare": "changelogen --bump --clean --hideAuthorEmail",
114
125
  "release:artifact": "pnpm release:verify --output .release/lupinum-nuxt-email.tgz && node scripts/write-release-manifest.mjs",
115
126
  "lint": "eslint .",
116
- "test": "vitest run --exclude test/preview.test.ts && pnpm test:preview",
127
+ "test": "pnpm prepack && vitest run --exclude test/preview.test.ts && pnpm test:preview",
117
128
  "test:preview": "vitest run test/preview.test.ts",
118
129
  "test:conformance": "vitest run test/conformance",
119
130
  "test:watch": "vitest watch",