@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.
- package/CHANGELOG.md +40 -2
- package/README.md +24 -16
- package/dist/build.d.mts +13 -0
- package/dist/build.mjs +218 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +11 -137
- package/dist/runtime/render/index.d.ts +6 -0
- package/dist/runtime/render/index.js +3 -0
- package/dist/runtime/tailwind/css-tree.d.ts +10 -0
- package/dist/runtime/tailwind/css-tree.js +11 -0
- package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +1 -1
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +1 -1
- package/dist/runtime/tailwind/engine/css/get-custom-properties.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/get-custom-properties.js +1 -1
- package/dist/runtime/tailwind/engine/css/is-part-inlinable.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/is-part-inlinable.js +1 -1
- package/dist/runtime/tailwind/engine/css/is-rule-inlinable.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/is-rule-inlinable.js +1 -1
- package/dist/runtime/tailwind/engine/css/make-inline-styles-for.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/make-inline-styles-for.js +1 -1
- package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/resolve-all-css-variables.js +1 -1
- package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/resolve-calc-expressions.js +1 -1
- package/dist/runtime/tailwind/engine/css/sanitize-declarations.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/sanitize-declarations.js +1 -1
- package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.js +1 -1
- package/dist/runtime/tailwind/engine/css/split-mixed-rule.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/split-mixed-rule.js +1 -1
- package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/strip-empty-tailwind-vars.js +1 -1
- package/dist/runtime/tailwind/engine/index.js +1 -1
- package/dist/runtime/tailwind/engine/setup-tailwind.d.ts +1 -1
- package/dist/runtime/tailwind/engine/setup-tailwind.js +1 -1
- package/dist/runtime/tailwind/post-render.js +1 -1
- package/dist/shared/nuxt-email.B5ux0eBv.mjs +133 -0
- 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:
|
|
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.
|
|
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
|
-
>
|
|
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.
|
|
46
|
+
- Vue `^3.5.40`.
|
|
47
47
|
|
|
48
|
-
CI tests every supported Node major.
|
|
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<{
|
|
72
|
+
const props = defineProps<{
|
|
73
|
+
firstName: string
|
|
74
|
+
activationUrl: string
|
|
75
|
+
}>()
|
|
73
76
|
|
|
74
|
-
defineEmail({
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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`
|
|
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).
|
|
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
|
|
package/dist/build.d.mts
ADDED
|
@@ -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
package/dist/module.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
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/
|
|
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"] =
|
|
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
|
|
321
|
-
if (!inline.includes(
|
|
322
|
-
inline.push(
|
|
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"] = [
|
|
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,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.
|
|
@@ -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,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 { 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 { 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.
|
|
@@ -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 { 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.
|
|
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.
|
|
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
|
-
"
|
|
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.
|
|
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 &&
|
|
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
|
|
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
|
}
|