@lupinum/nuxt-email 1.0.0-beta.1 → 1.0.0-beta.3

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 (40) hide show
  1. package/CHANGELOG.md +40 -2
  2. package/README.md +24 -16
  3. package/dist/build.d.mts +13 -0
  4. package/dist/build.mjs +218 -0
  5. package/dist/module.json +1 -1
  6. package/dist/module.mjs +11 -137
  7. package/dist/runtime/render/index.d.ts +6 -0
  8. package/dist/runtime/render/index.js +3 -0
  9. package/dist/runtime/tailwind/css-tree.d.ts +10 -0
  10. package/dist/runtime/tailwind/css-tree.js +11 -0
  11. package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.d.ts +1 -1
  12. package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +1 -1
  13. package/dist/runtime/tailwind/engine/css/extract-rules-per-class.d.ts +1 -1
  14. package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +1 -1
  15. package/dist/runtime/tailwind/engine/css/get-custom-properties.d.ts +1 -1
  16. package/dist/runtime/tailwind/engine/css/get-custom-properties.js +1 -1
  17. package/dist/runtime/tailwind/engine/css/is-part-inlinable.d.ts +1 -1
  18. package/dist/runtime/tailwind/engine/css/is-part-inlinable.js +1 -1
  19. package/dist/runtime/tailwind/engine/css/is-rule-inlinable.d.ts +1 -1
  20. package/dist/runtime/tailwind/engine/css/is-rule-inlinable.js +1 -1
  21. package/dist/runtime/tailwind/engine/css/make-inline-styles-for.d.ts +1 -1
  22. package/dist/runtime/tailwind/engine/css/make-inline-styles-for.js +1 -1
  23. package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.d.ts +1 -1
  24. package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.js +1 -1
  25. package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.d.ts +1 -1
  26. package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.js +1 -1
  27. package/dist/runtime/tailwind/engine/css/sanitize-declarations.d.ts +1 -1
  28. package/dist/runtime/tailwind/engine/css/sanitize-declarations.js +1 -1
  29. package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.d.ts +1 -1
  30. package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.js +1 -1
  31. package/dist/runtime/tailwind/engine/css/split-mixed-rule.d.ts +1 -1
  32. package/dist/runtime/tailwind/engine/css/split-mixed-rule.js +1 -1
  33. package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.d.ts +1 -1
  34. package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.js +1 -1
  35. package/dist/runtime/tailwind/engine/index.js +1 -1
  36. package/dist/runtime/tailwind/engine/setup-tailwind.d.ts +1 -1
  37. package/dist/runtime/tailwind/engine/setup-tailwind.js +1 -1
  38. package/dist/runtime/tailwind/post-render.js +1 -1
  39. package/dist/shared/nuxt-email.B5ux0eBv.mjs +133 -0
  40. package/package.json +21 -9
package/CHANGELOG.md CHANGED
@@ -1,8 +1,46 @@
1
1
  # Changelog
2
2
 
3
+ ## v1.0.0-beta.3
4
+
5
+ [compare changes](https://github.com/lupinum-dev/nuxt-email/compare/v1.0.0-beta.2...v1.0.0-beta.3)
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - **deps:** Enforce expiring quarantine exceptions ([#47](https://github.com/lupinum-dev/nuxt-email/pull/47))
10
+ - **preview:** Bundle configured renderer during development ([#48](https://github.com/lupinum-dev/nuxt-email/pull/48))
11
+ - **compatibility:** Prove supported Nuxt and Vue ranges ([#50](https://github.com/lupinum-dev/nuxt-email/pull/50))
12
+
13
+ ### 🏡 Chore
14
+
15
+ - **maintenance:** Verify Email commands without repeated CI work ([#49](https://github.com/lupinum-dev/nuxt-email/pull/49))
16
+
17
+ ### ❤️ Contributors
18
+
19
+ - Matthias Amon
20
+
21
+ ## v1.0.0-beta.2
22
+
23
+ ### Added
24
+
25
+ - Added `@lupinum/nuxt-email/build` to compile the existing typed email
26
+ registry for Node runtimes outside Nitro.
27
+ - Added `@lupinum/nuxt-email/render` so compiled templates reuse the same
28
+ deterministic renderer, metadata, plain-text generation, and email-safe
29
+ components as Nuxt applications.
30
+
31
+ ### Production boundaries
32
+
33
+ - Keep delivery, retries, idempotency, provider credentials, and audit state
34
+ application-owned. The package compiles and renders email; it does not send.
35
+ - Reject source directories outside the application root, refuse to replace
36
+ output not owned by Nuxt Email, and preserve the last valid registry when a
37
+ build fails.
38
+ - Exclude preview fixtures and source templates from the generated production
39
+ registry.
40
+
3
41
  ## v1.0.0-beta.1
4
42
 
5
- Status: certified beta candidate. Feature development is frozen until stable `1.0.0`; manual email-client QA and the external transactional beta remain pending.
43
+ Status: published on npm's `next` tag. Feature development is frozen until stable `1.0.0`. The three-client beta smoke test passed; the full eight-client checklist and external transactional beta remain pending.
6
44
 
7
45
  ### Added
8
46
 
@@ -89,6 +127,6 @@ Status: pre-1.0 candidate. The package identity is `@lupinum/nuxt-email`; the un
89
127
 
90
128
  - Node.js `^22.18.0 || ^24.11.0 || ^26.0.0`.
91
129
  - Nuxt `>=4.5.1 <5`.
92
- - Vue `^3.5.35`.
130
+ - Vue `^3.5.40`.
93
131
 
94
132
  The repository's v1.0 readiness gate records the current contract and remaining approval steps; the live release ledger is intentionally excluded from the npm tarball.
package/README.md CHANGED
@@ -25,7 +25,7 @@
25
25
  </p>
26
26
 
27
27
  > [!WARNING]
28
- > Nuxt Email is in the `1.0.0-beta.1` contract freeze. Review the changelog before each upgrade. Manual inbox QA and the external beta are still pending. The unscoped `nuxt-email` package on npm is unrelated to this project.
28
+ > [`1.0.0-beta.2`](https://www.npmjs.com/package/@lupinum/nuxt-email/v/1.0.0-beta.2) is published on npm's `next` tag. The [three-client beta smoke test](https://github.com/lupinum-dev/nuxt-email/blob/main/docs/release/client-qa-checklist.md#100-beta1-smoke-test) passed against beta.1; the full eight-client checklist and an external transactional beta still block stable `1.0.0`. The unscoped `nuxt-email` package on npm is unrelated to this project.
29
29
 
30
30
  ## Why use Nuxt Email?
31
31
 
@@ -43,16 +43,16 @@ Do not use it for marketing campaigns or a workflow shared by non-Nuxt applicati
43
43
 
44
44
  - Node.js `^22.18.0 || ^24.11.0 || ^26.0.0`.
45
45
  - Nuxt `>=4.5.1 <5`.
46
- - Vue `^3.5.35`.
46
+ - Vue `^3.5.40`.
47
47
 
48
- CI tests every supported Node major. The release consumer installs Nuxt `4.5.2` without a dependency shim.
48
+ CI tests every supported Node major. Release consumers verify the declared minimum and current Nuxt 4 and Vue 3 versions without a dependency shim.
49
49
 
50
50
  ## Installation
51
51
 
52
52
  Install and register the module:
53
53
 
54
54
  ```bash
55
- pnpm add @lupinum/nuxt-email
55
+ pnpm add @lupinum/nuxt-email@next
56
56
  ```
57
57
 
58
58
  ```ts
@@ -69,19 +69,30 @@ Create `app/emails/welcome.vue`:
69
69
  <script setup lang="ts">
70
70
  import { defineEmail } from '@lupinum/nuxt-email/define-email'
71
71
 
72
- const props = defineProps<{ firstName: string; activationUrl: string }>()
72
+ const props = defineProps<{
73
+ firstName: string
74
+ activationUrl: string
75
+ }>()
73
76
 
74
- defineEmail({ subject: () => `Welcome, ${props.firstName}` })
77
+ defineEmail({
78
+ subject: () => `Welcome aboard, ${props.firstName}`,
79
+ })
75
80
  </script>
76
81
 
77
82
  <template>
78
83
  <ETailwind>
79
84
  <EHtml lang="en">
80
- <EBody class="bg-slate-100 p-6">
85
+ <EHead>
86
+ <title>Activate your account</title>
87
+ </EHead>
88
+ <EBody class="m-0 bg-slate-100 p-6">
81
89
  <EPreview>Your account is ready.</EPreview>
82
- <EContainer class="bg-white p-6">
83
- <EHeading>Welcome, {{ firstName }}</EHeading>
84
- <EButton :href="activationUrl">Activate account</EButton>
90
+ <EContainer class="rounded-lg bg-white p-6">
91
+ <EHeading class="m-0 text-2xl text-slate-900">Welcome, {{ firstName }}</EHeading>
92
+ <EText class="text-slate-600">Finish setting up your account.</EText>
93
+ <EButton class="rounded-md bg-blue-600 px-5 py-3 text-white" :href="activationUrl">
94
+ Activate account
95
+ </EButton>
85
96
  </EContainer>
86
97
  </EBody>
87
98
  </EHtml>
@@ -100,7 +111,7 @@ export default defineEventHandler(async () => {
100
111
  })
101
112
  ```
102
113
 
103
- The template name and props are generated from the Vue file. Invalid names and props fail during type checking.
114
+ The template name and props are generated from the Vue file. Invalid names and props fail during type checking. Use a sibling fixture to inspect it in `/__email`, then [pass the rendered result to your provider](https://nuxt-email.lupinum.com/docs/guides/sending-email).
104
115
 
105
116
  ## Discord
106
117
 
@@ -175,14 +186,11 @@ Complete the [manual client QA checklist](https://github.com/lupinum-dev/nuxt-em
175
186
 
176
187
  ## Package exports
177
188
 
178
- - `@lupinum/nuxt-email` provides the Nuxt module plus the `RenderedEmail` and `EmailComponentProps` types.
179
- - `@lupinum/nuxt-email/define-email` provides typed template metadata.
180
- - `@lupinum/nuxt-email/testing` provides standalone component rendering.
181
- - `@lupinum/nuxt-email/errors` provides supported runtime error classes.
189
+ The package has a module entry point plus focused build, production rendering, metadata, testing, and error subpaths. Use `@lupinum/nuxt-email/build` to compile a server registry and `@lupinum/nuxt-email/render` for compiled components outside Nitro. The [standalone guide](https://nuxt-email.lupinum.com/docs/guides/standalone-rendering) explains the build and runtime boundaries. The [canonical entry-point table](https://nuxt-email.lupinum.com/docs/reference/module#package-entry-points) lists every runtime and type-only export.
182
190
 
183
191
  ## Documentation
184
192
 
185
- Read the [Nuxt Email documentation](https://nuxt-email.lupinum.com/docs). It contains the [installation guide](https://nuxt-email.lupinum.com/docs/getting-started/installation), [component reference](https://nuxt-email.lupinum.com/docs/components), [preview guide](https://nuxt-email.lupinum.com/docs/guides/preview-workflow), [testing guide](https://nuxt-email.lupinum.com/docs/guides/testing-your-emails), and [sending guide](https://nuxt-email.lupinum.com/docs/guides/sending-email).
193
+ Read the [Nuxt Email documentation](https://nuxt-email.lupinum.com/docs). Start with the [installation guide](https://nuxt-email.lupinum.com/docs/getting-started/installation) and [component reference](https://nuxt-email.lupinum.com/docs/components). Use the guides for [preview](https://nuxt-email.lupinum.com/docs/guides/preview-workflow), [testing](https://nuxt-email.lupinum.com/docs/guides/testing-your-emails), and [sending](https://nuxt-email.lupinum.com/docs/guides/sending-email).
186
194
 
187
195
  See the [changelog](CHANGELOG.md) for released changes.
188
196
 
@@ -0,0 +1,13 @@
1
+ interface BuildEmailRegistryOptions {
2
+ /** Application/project root, without loading its Nuxt configuration. */
3
+ rootDir: string;
4
+ /** Template directory relative to rootDir. Defaults to app/emails. */
5
+ templatesDir?: string;
6
+ /** Dedicated directory inside rootDir. Must be absent or previously created by this helper. */
7
+ outDir: string;
8
+ }
9
+ /** Compile trusted application templates. Never call this from a request or action. */
10
+ declare function buildEmailRegistry(options: BuildEmailRegistryOptions): Promise<void>;
11
+
12
+ export { buildEmailRegistry };
13
+ export type { BuildEmailRegistryOptions };
package/dist/build.mjs ADDED
@@ -0,0 +1,218 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile, lstat, mkdir, mkdtemp, writeFile, rm, readdir, rename } from 'node:fs/promises';
3
+ import { createRequire } from 'node:module';
4
+ import { relative, dirname, resolve, isAbsolute, join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ import { build } from 'esbuild';
7
+ import ts from 'typescript';
8
+ import { d as discoverEmailTemplates, g as generateEmailRegistry, a as generateEmailTypes } from './shared/nuxt-email.B5ux0eBv.mjs';
9
+ import { createHash } from 'node:crypto';
10
+ import { parse, compileScript, compileTemplate } from 'vue/compiler-sfc';
11
+ import '@nuxt/kit';
12
+
13
+ function emailTemplatesPlugin(directory) {
14
+ return {
15
+ name: "nuxt-email-templates",
16
+ setup(build) {
17
+ build.onLoad({ filter: /\.vue$/ }, async ({ path }) => {
18
+ const { descriptor, errors } = parse(await readFile(path, "utf8"), { filename: path });
19
+ if (errors.length > 0) throw errors[0];
20
+ if (descriptor.styles.length || descriptor.customBlocks.length) {
21
+ throw new Error("Standalone email templates do not support style or custom blocks. Use ETailwind or inline styles.");
22
+ }
23
+ if ([descriptor.script, descriptor.scriptSetup, descriptor.template].some((block) => block?.src)) {
24
+ throw new Error("Standalone email templates do not support external SFC blocks. Import local components instead.");
25
+ }
26
+ if ([descriptor.script, descriptor.scriptSetup].some((block) => block?.lang && !["js", "ts"].includes(block.lang)) || descriptor.template?.lang && descriptor.template.lang !== "html") {
27
+ throw new Error("Standalone email templates support JavaScript/TypeScript scripts and HTML templates only.");
28
+ }
29
+ const id = createHash("sha256").update(relative(directory, path).replaceAll("\\", "/")).digest("hex").slice(0, 8);
30
+ const script = descriptor.script || descriptor.scriptSetup ? compileScript(descriptor, { id, genDefaultAs: "__emailComponent", inlineTemplate: false, isProd: true }) : void 0;
31
+ const template = descriptor.template ? compileTemplate({
32
+ source: descriptor.template.content,
33
+ filename: path,
34
+ id,
35
+ isProd: true,
36
+ compilerOptions: { bindingMetadata: script?.bindings },
37
+ transformAssetUrls: false
38
+ }) : void 0;
39
+ if (template?.errors.length) throw template.errors[0];
40
+ return {
41
+ contents: [
42
+ script?.content ?? "const __emailComponent = {}",
43
+ template?.code ?? "",
44
+ template ? "__emailComponent.render = render" : "",
45
+ "export default __emailComponent"
46
+ ].join("\n"),
47
+ loader: "ts",
48
+ resolveDir: dirname(path)
49
+ };
50
+ });
51
+ }
52
+ };
53
+ }
54
+
55
+ const execute = promisify(execFile);
56
+ const require$1 = createRequire(import.meta.url);
57
+ const renderImport = "@lupinum/nuxt-email/render";
58
+ const ownershipMarker = ".nuxt-email-generated";
59
+ const ownershipContent = "Generated by @lupinum/nuxt-email/build. Do not edit.\n";
60
+ function contains(parent, child) {
61
+ const path = relative(parent, child);
62
+ return path === "" || !path.startsWith(`..`) && !isAbsolute(path);
63
+ }
64
+ async function replaceOutputDirectory(staging, outDir, replaceOutput) {
65
+ const previousRoot = await mkdtemp(join(dirname(outDir), ".nuxt-email-previous-"));
66
+ const previous = join(previousRoot, "output");
67
+ let removePreviousRoot = true;
68
+ try {
69
+ if (replaceOutput) await rename(outDir, previous);
70
+ try {
71
+ await rename(staging, outDir);
72
+ } catch (error) {
73
+ if (replaceOutput) {
74
+ try {
75
+ await rename(previous, outDir);
76
+ } catch (restoreError) {
77
+ removePreviousRoot = false;
78
+ throw new AggregateError(
79
+ [error, restoreError],
80
+ `Failed to replace generated email output and restore it. The prior output remains at ${previous}.`,
81
+ { cause: restoreError }
82
+ );
83
+ }
84
+ }
85
+ throw error;
86
+ }
87
+ } finally {
88
+ if (removePreviousRoot) await rm(previousRoot, { recursive: true, force: true });
89
+ }
90
+ }
91
+ async function normalizeDeclarationImports(directory) {
92
+ for (const file of await readdir(directory, { recursive: true })) {
93
+ if (!/\.d\.[cm]?ts$/.test(file)) continue;
94
+ const path = join(directory, file);
95
+ const source = ts.createSourceFile(path, await readFile(path, "utf8"), ts.ScriptTarget.Latest, true);
96
+ const transformed = ts.transform(source, [(context) => {
97
+ const visit = (node) => {
98
+ if (ts.isStringLiteral(node) && node.text.startsWith(".")) {
99
+ const parent = node.parent;
100
+ const isImport = ts.isImportDeclaration(parent) || ts.isExportDeclaration(parent) || ts.isLiteralTypeNode(parent) && ts.isImportTypeNode(parent.parent);
101
+ if (isImport && !node.text.endsWith(".json")) {
102
+ const module = ts.resolveModuleName(node.text, path, { moduleResolution: ts.ModuleResolutionKind.Bundler }, ts.sys).resolvedModule;
103
+ if (!module) throw new Error(`Cannot resolve emitted declaration import ${JSON.stringify(node.text)} from ${path}. Keep local imports inside the email directory.`);
104
+ const target = relative(dirname(path), module.resolvedFileName).replaceAll("\\", "/").replace(/\.d\.([cm]?)ts$/, ".$1js");
105
+ return ts.factory.createStringLiteral(target.startsWith(".") ? target : `./${target}`);
106
+ }
107
+ }
108
+ return ts.visitEachChild(node, visit, context);
109
+ };
110
+ return (node) => ts.visitNode(node, visit);
111
+ }]);
112
+ try {
113
+ await writeFile(path, ts.createPrinter().printFile(transformed.transformed[0]));
114
+ } finally {
115
+ transformed.dispose();
116
+ }
117
+ }
118
+ }
119
+ async function buildEmailRegistry(options) {
120
+ if (Object.keys(options).some((key) => key !== "rootDir" && key !== "outDir" && key !== "templatesDir")) {
121
+ throw new TypeError("buildEmailRegistry accepts only rootDir, outDir and templatesDir.");
122
+ }
123
+ const rootDir = resolve(options.rootDir);
124
+ const templatesDir = options.templatesDir ?? "app/emails";
125
+ if (isAbsolute(templatesDir)) throw new TypeError("templatesDir must be relative to rootDir.");
126
+ const directory = resolve(rootDir, templatesDir);
127
+ if (!contains(rootDir, directory)) throw new TypeError("templatesDir must remain inside rootDir.");
128
+ const outDir = resolve(rootDir, options.outDir);
129
+ if (!contains(rootDir, outDir)) throw new TypeError("outDir must remain inside rootDir so the generated module can resolve project dependencies.");
130
+ if (contains(directory, outDir) || contains(outDir, directory)) {
131
+ throw new TypeError("Generated output must be outside and must not contain the email directory.");
132
+ }
133
+ let replaceOutput = false;
134
+ try {
135
+ const output = await lstat(outDir);
136
+ if (!output.isDirectory() || output.isSymbolicLink()) throw new TypeError("outDir must be a generated directory, not a file or symlink.");
137
+ const marker = await readFile(join(outDir, ownershipMarker), "utf8").catch(() => "");
138
+ if (marker !== ownershipContent) throw new TypeError("Refusing to replace an outDir not owned by this build.");
139
+ replaceOutput = true;
140
+ } catch (error) {
141
+ if (!(error instanceof Error && "code" in error && error.code === "ENOENT")) throw error;
142
+ }
143
+ const templates = await discoverEmailTemplates(directory);
144
+ if (templates.length === 0) throw new TypeError("The email directory contains no templates.");
145
+ await mkdir(dirname(outDir), { recursive: true });
146
+ const staging = await mkdtemp(join(dirname(outDir), ".nuxt-email-build-"));
147
+ try {
148
+ const declarations = join(staging, "types");
149
+ const globals = join(staging, "components.d.ts");
150
+ await writeFile(globals, `import type { EmailComponents } from '${renderImport}'
151
+ declare module 'vue' { interface GlobalComponents extends EmailComponents {} }
152
+ export {}
153
+ `);
154
+ const config = join(staging, "tsconfig.json");
155
+ await writeFile(config, JSON.stringify({
156
+ compilerOptions: {
157
+ target: "ES2022",
158
+ module: "ESNext",
159
+ moduleResolution: "Bundler",
160
+ strict: true,
161
+ skipLibCheck: true,
162
+ declaration: true,
163
+ emitDeclarationOnly: true,
164
+ noEmitOnError: true,
165
+ rootDir: directory,
166
+ declarationDir: declarations,
167
+ types: [],
168
+ lib: ["ES2022", "DOM", "DOM.Iterable"]
169
+ },
170
+ vueCompilerOptions: { strictTemplates: true },
171
+ files: [globals, ...templates.map((template) => template.sourcePath)]
172
+ }));
173
+ try {
174
+ await execute(process.execPath, [require$1.resolve("vue-tsc/bin/vue-tsc.js"), "-p", config], {
175
+ cwd: directory,
176
+ maxBuffer: 4 * 1024 * 1024
177
+ });
178
+ } catch (error) {
179
+ const diagnostics = error instanceof Error && "stdout" in error && typeof error.stdout === "string" ? error.stdout.trim() : "";
180
+ throw new Error(`Email declaration build failed${diagnostics ? `:
181
+ ${diagnostics}` : "."}`, { cause: error });
182
+ }
183
+ await normalizeDeclarationImports(declarations);
184
+ const registry = generateEmailRegistry(templates, {
185
+ emailRenderError: renderImport,
186
+ renderEmailComponent: renderImport,
187
+ templateRegistryErrors: renderImport
188
+ });
189
+ await build({
190
+ absWorkingDir: rootDir,
191
+ tsconfigRaw: { compilerOptions: { target: "ES2022", module: "ESNext" } },
192
+ stdin: { contents: registry, resolveDir: directory, sourcefile: "email-registry.js" },
193
+ outfile: join(staging, "index.mjs"),
194
+ bundle: true,
195
+ format: "esm",
196
+ platform: "node",
197
+ target: "node22",
198
+ packages: "external",
199
+ logLevel: "silent",
200
+ plugins: [emailTemplatesPlugin(directory)]
201
+ });
202
+ await writeFile(join(staging, "index.d.mts"), generateEmailTypes(
203
+ templates.map((template) => ({
204
+ name: template.name,
205
+ sourcePath: `./types/${template.name}.vue.js`
206
+ })),
207
+ { renderedEmail: renderImport }
208
+ ));
209
+ await writeFile(join(staging, ownershipMarker), ownershipContent);
210
+ await rm(config);
211
+ await rm(globals);
212
+ await replaceOutputDirectory(staging, outDir, replaceOutput);
213
+ } finally {
214
+ await rm(staging, { recursive: true, force: true });
215
+ }
216
+ }
217
+
218
+ export { buildEmailRegistry };
package/dist/module.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.5.1 <5"
6
6
  },
7
- "version": "1.0.0-beta.1",
7
+ "version": "1.0.0-beta.3",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "unknown"
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,135 +27,6 @@ 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;
38
- }
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;
46
- }
47
- }
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("\\", "/");
54
- }
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"));
60
- }
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
- }
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
- }
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 _EmailProps<Component> = keyof import(${importPath(typePaths.renderedEmail)}).EmailComponentProps<Component> extends never
144
- ? Record<string, never>
145
- : import(${importPath(typePaths.renderedEmail)}).EmailComponentProps<Component>
146
-
147
- export type EmailTemplateProps = {
148
- [Name in EmailTemplateName]: _EmailProps<Awaited<ReturnType<(typeof emailTemplates)[Name]['component']>>['default']>
149
- }
150
-
151
- export function renderEmail<Name extends EmailTemplateName>(
152
- name: Name,
153
- props: NoInfer<EmailTemplateProps[Name]>,
154
- ): Promise<import(${importPath(typePaths.renderedEmail)}).RenderedEmail>
155
- `;
156
- }
157
-
158
30
  function configureEmailTypeProject(tsConfig, emailDirectory, registryId, registryPath) {
159
31
  const normalizedDirectory = emailDirectory.replaceAll("\\", "/");
160
32
  const include = tsConfig.include ??= [];
@@ -199,10 +71,12 @@ const module$1 = defineNuxtModule({
199
71
  const resolver = createResolver(import.meta.url);
200
72
  const defineEmailPublicPath = await resolver.resolvePath("./runtime/define-email");
201
73
  const errorsPublicPath = await resolver.resolvePath("./runtime/errors");
202
- let renderEmailComponentPath = await resolver.resolvePath("./runtime/testing");
74
+ let renderEmailComponentPath = await resolver.resolvePath("./runtime/render/index");
75
+ let testingRendererPath = await resolver.resolvePath("./runtime/testing");
203
76
  if (options.codeBlock !== void 0) {
204
77
  const { setupCodeBlock } = await import('./chunks/setup.mjs');
205
78
  renderEmailComponentPath = setupCodeBlock(options.codeBlock, resolver);
79
+ testingRendererPath = renderEmailComponentPath;
206
80
  }
207
81
  const emailDirectory = resolve(nuxt.options.srcDir, "emails");
208
82
  const loadTemplates = async () => {
@@ -312,20 +186,20 @@ const module$1 = defineNuxtModule({
312
186
  const nuxtOptions = nuxt.options;
313
187
  const nitro = nuxtOptions.nitro ??= {};
314
188
  const alias = nitro.alias ??= {};
315
- alias["#nuxt-email/testing"] = renderEmailComponentPath;
189
+ alias["#nuxt-email/testing"] = testingRendererPath;
316
190
  alias["@lupinum/nuxt-email/define-email"] = defineEmailPublicPath;
317
191
  alias["@lupinum/nuxt-email/errors"] = errorsPublicPath;
318
192
  const externals = nitro.externals ??= {};
319
193
  const inline = externals.inline ??= [];
320
- for (const publicRuntimePath of [defineEmailPublicPath, errorsPublicPath]) {
321
- if (!inline.includes(publicRuntimePath)) {
322
- inline.push(publicRuntimePath);
194
+ for (const runtimePath of [defineEmailPublicPath, errorsPublicPath, testingRendererPath]) {
195
+ if (!inline.includes(runtimePath)) {
196
+ inline.push(runtimePath);
323
197
  }
324
198
  }
325
199
  const typescript = nitro.typescript ??= {};
326
200
  const tsConfig = typescript.tsConfig ??= {};
327
201
  const paths = configureEmailTypeProject(tsConfig, emailDirectory, registryId, typeTemplate.dst);
328
- paths["#nuxt-email/testing"] = [renderEmailComponentPath];
202
+ paths["#nuxt-email/testing"] = [testingRendererPath];
329
203
  nitro.rollupConfig ??= {};
330
204
  const existingPlugins = nitro.rollupConfig.plugins;
331
205
  nitro.rollupConfig.plugins = existingPlugins ? [existingPlugins, vueRollup()] : [vueRollup()];
@@ -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";
@@ -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;
@@ -3,7 +3,7 @@
3
3
  * can parse. The transform is deliberately strict: leaving one nested rule in
4
4
  * the output is worse than failing the render with an actionable error.
5
5
  */
6
- import { type StyleSheet } from 'css-tree';
6
+ import { type StyleSheet } from '../../css-tree.js';
7
7
  /**
8
8
  * css-tree 3.x parses media range syntax, but the currently published type
9
9
  * package does not expose the corresponding nodes yet.
@@ -4,7 +4,7 @@ import {
4
4
  List,
5
5
  parse,
6
6
  walk
7
- } from "css-tree";
7
+ } from "../../css-tree.js";
8
8
  export function downlevelForEmailClients(styleSheet) {
9
9
  styleSheet.children = flattenStyleSheet(styleSheet);
10
10
  downlevelRangeMediaQueries(styleSheet);
@@ -1,4 +1,4 @@
1
- import { type CssNode, type Rule } from 'css-tree';
1
+ import { type CssNode, type Rule } from '../../css-tree.js';
2
2
  export interface ExtractedRules {
3
3
  inlinable: Map<string, Rule[]>;
4
4
  nonInlinable: Map<string, Rule[]>;
@@ -1,4 +1,4 @@
1
- import { string, walk } from "css-tree";
1
+ import { string, walk } from "../../css-tree.js";
2
2
  import { isRuleInlinable } from "./is-rule-inlinable.js";
3
3
  import { splitMixedRule } from "./split-mixed-rule.js";
4
4
  export function extractRulesPerClass(root, classes) {
@@ -1,4 +1,4 @@
1
- import { type CssNode, type Declaration } from 'css-tree';
1
+ import { type CssNode, type Declaration } from '../../css-tree.js';
2
2
  export interface CustomProperty {
3
3
  syntax?: Declaration;
4
4
  inherits?: Declaration;
@@ -1,4 +1,4 @@
1
- import { generate, walk } from "css-tree";
1
+ import { generate, walk } from "../../css-tree.js";
2
2
  export function getCustomProperties(node) {
3
3
  const customProperties = /* @__PURE__ */ new Map();
4
4
  walk(node, {
@@ -1,2 +1,2 @@
1
- import { type CssNode } from 'css-tree';
1
+ import { type CssNode } from '../../css-tree.js';
2
2
  export declare function isPartInlinable(part: CssNode): boolean;
@@ -1,4 +1,4 @@
1
- import { find } from "css-tree";
1
+ import { find } from "../../css-tree.js";
2
2
  export function isPartInlinable(part) {
3
3
  const hasAtRuleInside = find(part, (node) => node.type === "Atrule") !== null;
4
4
  const hasPseudoSelector = find(
@@ -1,2 +1,2 @@
1
- import { type Rule } from 'css-tree';
1
+ import { type Rule } from '../../css-tree.js';
2
2
  export declare function isRuleInlinable(rule: Rule): boolean;
@@ -1,4 +1,4 @@
1
- import { find } from "css-tree";
1
+ import { find } from "../../css-tree.js";
2
2
  export function isRuleInlinable(rule) {
3
3
  const hasAtRuleInside = find(rule, (node) => node.type === "Atrule") !== null;
4
4
  const hasPseudoSelector = find(
@@ -1,4 +1,4 @@
1
- import { type CssNode } from 'css-tree';
1
+ import { type CssNode } from '../../css-tree.js';
2
2
  import type { CustomProperties } from './get-custom-properties.js';
3
3
  /**
4
4
  * Adapted from React Email's `make-inline-styles-for.ts`.
@@ -1,4 +1,4 @@
1
- import { generate, walk } from "css-tree";
1
+ import { generate, walk } from "../../css-tree.js";
2
2
  import { getCssProperty } from "./get-css-property.js";
3
3
  import { stripEmptyTailwindVars } from "./strip-empty-tailwind-vars.js";
4
4
  import { unwrapValue } from "./unwrap-value.js";
@@ -1,4 +1,4 @@
1
- import { type CssNode, type Declaration } from 'css-tree';
1
+ import { type CssNode, type Declaration } from '../../css-tree.js';
2
2
  export interface VariableDefinition {
3
3
  declaration: Declaration;
4
4
  path: CssNode[];
@@ -2,7 +2,7 @@ import {
2
2
  generate,
3
3
  parse,
4
4
  walk
5
- } from "css-tree";
5
+ } from "../../css-tree.js";
6
6
  function doSelectorsIntersect(first, second) {
7
7
  const firstStringified = generate(first);
8
8
  const secondStringified = generate(second);
@@ -1,4 +1,4 @@
1
- import { type CssNode } from 'css-tree';
1
+ import { type CssNode } from '../../css-tree.js';
2
2
  /**
3
3
  * Intentionally only resolves `*` and `/` operations without dealing with parenthesis, because this is the only thing required to run Tailwind v4
4
4
  */
@@ -1,4 +1,4 @@
1
- import { walk } from "css-tree";
1
+ import { walk } from "../../css-tree.js";
2
2
  export function resolveCalcExpressions(node) {
3
3
  walk(node, {
4
4
  visit: "Function",
@@ -1,4 +1,4 @@
1
- import { type CssNode } from 'css-tree';
1
+ import { type CssNode } from '../../css-tree.js';
2
2
  /**
3
3
  * Meant to do all the things necessary, in a per-declaration basis, to have the best email client
4
4
  * support possible.
@@ -3,7 +3,7 @@ import {
3
3
  List,
4
4
  parse,
5
5
  walk
6
- } from "css-tree";
6
+ } from "../../css-tree.js";
7
7
  function rgbNode(r, g, b, alpha) {
8
8
  const children = new List();
9
9
  children.appendData({
@@ -1,4 +1,4 @@
1
- import { type CssNode } from 'css-tree';
1
+ import { type CssNode } from '../../css-tree.js';
2
2
  /**
3
3
  * This function goes through a few steps to ensure the best email client support and
4
4
  * to ensure that media queries and pseudo classes are applied correctly alongside
@@ -1,4 +1,4 @@
1
- import { string, walk } from "css-tree";
1
+ import { string, walk } from "../../css-tree.js";
2
2
  import { sanitizeClassName } from "../compatibility/sanitize-class-name.js";
3
3
  import { isRuleInlinable } from "./is-rule-inlinable.js";
4
4
  import { stripEmptyTailwindVars } from "./strip-empty-tailwind-vars.js";
@@ -1,4 +1,4 @@
1
- import { type Rule } from 'css-tree';
1
+ import { type Rule } from '../../css-tree.js';
2
2
  /**
3
3
  * Split a rule into inlinable and non-inlinable parts. Caller must only pass rules
4
4
  * for which isRuleInlinable(rule) is false. Returns clones so the original stylesheet is never mutated.
@@ -1,4 +1,4 @@
1
- import { clone, find, List } from "css-tree";
1
+ import { clone, find, List } from "../../css-tree.js";
2
2
  import { isPartInlinable } from "./is-part-inlinable.js";
3
3
  export function splitMixedRule(rule) {
4
4
  const selectorHasPseudo = rule.prelude !== null && find(
@@ -1,4 +1,4 @@
1
- import { type CssNode } from 'css-tree';
1
+ import { type CssNode } from '../../css-tree.js';
2
2
  /**
3
3
  * Tailwind v4 emits variant-stacking idioms like
4
4
  * font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) tabular-nums var(--tw-numeric-fraction,)
@@ -1,4 +1,4 @@
1
- import { generate, walk } from "css-tree";
1
+ import { generate, walk } from "../../css-tree.js";
2
2
  export function stripEmptyTailwindVars(node) {
3
3
  walk(node, {
4
4
  visit: "Function",
@@ -3,7 +3,7 @@ import {
3
3
  generate,
4
4
  List,
5
5
  walk
6
- } from "css-tree";
6
+ } from "../css-tree.js";
7
7
  import { downlevelForEmailClients } from "./css/downlevel-for-email-clients.js";
8
8
  import { extractRulesPerClass } from "./css/extract-rules-per-class.js";
9
9
  import { getCustomProperties } from "./css/get-custom-properties.js";
@@ -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";
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lupinum/nuxt-email",
3
- "version": "1.0.0-beta.1",
3
+ "version": "1.0.0-beta.3",
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"
@@ -53,7 +61,7 @@
53
61
  ],
54
62
  "peerDependencies": {
55
63
  "nuxt": ">=4.5.1 <5",
56
- "vue": "^3.5.35"
64
+ "vue": "^3.5.40"
57
65
  },
58
66
  "dependencies": {
59
67
  "@nuxt/kit": ">=4.5.1 <5",
@@ -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,8 @@
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",
96
105
  "yaml": "2.9.0"
97
106
  },
98
107
  "scripts": {
@@ -111,15 +120,18 @@
111
120
  "proofs:generate": "node --import tsx scripts/proofs/generate.ts",
112
121
  "proofs:send": "node --import tsx scripts/proofs/send.ts",
113
122
  "repo:check": "node scripts/check-repository-policy.mjs",
114
- "release:verify": "pnpm verify && node --import tsx scripts/verify-release.ts",
123
+ "release:verify": "pnpm verify && pnpm release:pack",
115
124
  "release:prepare": "changelogen --bump --clean --hideAuthorEmail",
116
125
  "release:artifact": "pnpm release:verify --output .release/lupinum-nuxt-email.tgz && node scripts/write-release-manifest.mjs",
117
126
  "lint": "eslint .",
118
- "test": "vitest run --exclude test/preview.test.ts && pnpm test:preview",
127
+ "test": "pnpm prepack && vitest run --exclude test/preview.test.ts --reporter=default --reporter=./test/conformance/tooling/reporter.ts && pnpm test:preview",
119
128
  "test:preview": "vitest run test/preview.test.ts",
120
129
  "test:conformance": "vitest run test/conformance",
121
130
  "test:watch": "vitest watch",
122
131
  "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",
123
- "verify": "pnpm audit:all && pnpm repo:check && pnpm lint && pnpm test:types && pnpm test && pnpm conformance:check && pnpm docs:build"
132
+ "verify": "pnpm check:dependencies && pnpm audit:all && pnpm repo:check && pnpm lint && pnpm test:types && pnpm test && pnpm docs:build",
133
+ "check:dependencies": "node scripts/check-dependency-policy.mjs",
134
+ "build": "nuxt-module-build build",
135
+ "release:pack": "node --import tsx scripts/verify-release.ts"
124
136
  }
125
137
  }