@lupinum/nuxt-email 0.1.1 → 1.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -0
- package/README.md +63 -18
- package/dist/build.d.mts +13 -0
- package/dist/build.mjs +218 -0
- package/dist/module.d.mts +1 -1
- package/dist/module.json +1 -1
- package/dist/module.mjs +54 -145
- package/dist/runtime/components/EHtml.d.ts +3 -1
- package/dist/runtime/components/EHtml.js +12 -2
- package/dist/runtime/components/ELink.js +1 -6
- package/dist/runtime/dev-preview/render.get.d.ts +26 -1
- package/dist/runtime/dev-preview/render.get.js +2 -2
- package/dist/runtime/render/define-email.d.ts +19 -10
- package/dist/runtime/render/define-email.js +21 -4
- package/dist/runtime/render/errors.d.ts +2 -2
- package/dist/runtime/render/errors.js +4 -4
- package/dist/runtime/render/index.d.ts +6 -0
- package/dist/runtime/render/index.js +3 -0
- package/dist/runtime/render/render-email-component.d.ts +3 -6
- package/dist/runtime/render/render-email-component.js +16 -16
- package/dist/runtime/render/types.d.ts +7 -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 +8 -38
- package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +186 -72
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.d.ts +3 -1
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +9 -2
- 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.d.ts +6 -6
- package/dist/runtime/tailwind/engine/index.js +70 -7
- 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 +7 -8
- package/dist/shared/nuxt-email.B5ux0eBv.mjs +133 -0
- package/dist/types.d.mts +1 -1
- package/package.json +18 -7
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.0.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- Added `@lupinum/nuxt-email/build` to compile the existing typed email
|
|
8
|
+
registry for Node runtimes outside Nitro.
|
|
9
|
+
- Added `@lupinum/nuxt-email/render` so compiled templates reuse the same
|
|
10
|
+
deterministic renderer, metadata, plain-text generation, and email-safe
|
|
11
|
+
components as Nuxt applications.
|
|
12
|
+
|
|
13
|
+
### Production boundaries
|
|
14
|
+
|
|
15
|
+
- Keep delivery, retries, idempotency, provider credentials, and audit state
|
|
16
|
+
application-owned. The package compiles and renders email; it does not send.
|
|
17
|
+
- Reject source directories outside the application root, refuse to replace
|
|
18
|
+
output not owned by Nuxt Email, and preserve the last valid registry when a
|
|
19
|
+
build fails.
|
|
20
|
+
- Exclude preview fixtures and source templates from the generated production
|
|
21
|
+
registry.
|
|
22
|
+
|
|
23
|
+
## v1.0.0-beta.1
|
|
24
|
+
|
|
25
|
+
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.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added synchronous `subject` and authored plain-text `text` metadata to `defineEmail`, including prop-derived functions. Authored text takes precedence over automatic HTML-to-text derivation.
|
|
30
|
+
- Added the type-only `EmailComponentProps<Component>` helper for deterministic, component-checked preview fixtures.
|
|
31
|
+
- Added a baseURL-aware Nuxt DevTools shortcut to the existing development-only `/__email` preview.
|
|
32
|
+
- Added direct Resend and local Nodemailer/Mailpit sending recipes while keeping all delivery behavior application-owned.
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
|
|
36
|
+
- Rebuilt Tailwind v4 nested-selector and nested `@media`/`@supports` handling so declarations and rules preserve source order, media ranges are downleveled, and unhandled nesting fails instead of emitting raw `&` CSS.
|
|
37
|
+
- Limited emitted keyframes to animations referenced by the current render. Animation remains best-effort progressive enhancement because email-client support is limited.
|
|
38
|
+
- Made `EHtml.lang` a required, non-empty value. `dir` still defaults to `ltr`.
|
|
39
|
+
- Made `ELink` visually neutral by removing its built-in color and underline. `target="_blank"` remains the default and can be overridden.
|
|
40
|
+
- Renamed `EmailRenderError.componentName` to `templateName` and kept the original error as `cause`.
|
|
41
|
+
- Limited module-driven registry rebuilds to structural template and fixture changes; ordinary edits continue through Nitro's normal watcher.
|
|
42
|
+
|
|
43
|
+
### Breaking changes from v0.1.1
|
|
44
|
+
|
|
45
|
+
- Every `EHtml` now requires an explicit non-empty `lang`.
|
|
46
|
+
- `ELink` no longer supplies color or text-decoration styles. Add those styles explicitly where required.
|
|
47
|
+
- Read `EmailRenderError.templateName` instead of `componentName`; there is no compatibility alias.
|
|
48
|
+
- `defineEmail` rejects an empty object and invalid or asynchronous metadata values. Provide at least one synchronous `subject` or `text` value.
|
|
49
|
+
|
|
3
50
|
## v0.1.1
|
|
4
51
|
|
|
5
52
|
[Compare changes](https://github.com/lupinum-dev/nuxt-email/compare/e45025a...v0.1.1)
|
package/README.md
CHANGED
|
@@ -1,19 +1,31 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/public/icon-dark.svg">
|
|
4
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/public/icon-light.svg">
|
|
5
|
+
<img src="docs/public/icon-light.svg" width="128" alt="Nuxt Email icon">
|
|
6
|
+
</picture>
|
|
3
7
|
</p>
|
|
4
8
|
|
|
5
|
-
<h1 align="center">
|
|
9
|
+
<h1 align="center">
|
|
10
|
+
<picture>
|
|
11
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/public/wordmark-dark.svg">
|
|
12
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/public/wordmark-light.svg">
|
|
13
|
+
<img src="docs/public/wordmark-light.svg" width="256" alt="Nuxt Email">
|
|
14
|
+
</picture>
|
|
15
|
+
</h1>
|
|
6
16
|
|
|
7
17
|
<p align="center">Write typed transactional email as Vue components and render it directly in Nuxt.</p>
|
|
8
18
|
|
|
9
19
|
<p align="center">
|
|
10
|
-
<a href="https://www.npmjs.com/package/@lupinum/nuxt-email"><img src="https://img.shields.io/npm/v/@lupinum/nuxt-email?
|
|
20
|
+
<a href="https://www.npmjs.com/package/@lupinum/nuxt-email"><img src="https://img.shields.io/npm/v/@lupinum/nuxt-email?color=00DC82" alt="npm version"></a>
|
|
11
21
|
<a href="https://github.com/lupinum-dev/nuxt-email/actions/workflows/ci.yml"><img src="https://github.com/lupinum-dev/nuxt-email/actions/workflows/ci.yml/badge.svg" alt="CI status"></a>
|
|
12
|
-
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-
|
|
22
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-00DC82" alt="MIT license"></a>
|
|
23
|
+
<a href="https://discord.gg/RPH6SeA36N"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord" alt="Discord"></a>
|
|
24
|
+
<a href="https://deepwiki.com/lupinum-dev/nuxt-email"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
|
13
25
|
</p>
|
|
14
26
|
|
|
15
27
|
> [!WARNING]
|
|
16
|
-
> The
|
|
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.
|
|
17
29
|
|
|
18
30
|
## Why use Nuxt Email?
|
|
19
31
|
|
|
@@ -40,7 +52,7 @@ CI tests every supported Node major. The release consumer installs Nuxt `4.5.2`
|
|
|
40
52
|
Install and register the module:
|
|
41
53
|
|
|
42
54
|
```bash
|
|
43
|
-
pnpm add @lupinum/nuxt-email
|
|
55
|
+
pnpm add @lupinum/nuxt-email@next
|
|
44
56
|
```
|
|
45
57
|
|
|
46
58
|
```ts
|
|
@@ -57,19 +69,30 @@ Create `app/emails/welcome.vue`:
|
|
|
57
69
|
<script setup lang="ts">
|
|
58
70
|
import { defineEmail } from '@lupinum/nuxt-email/define-email'
|
|
59
71
|
|
|
60
|
-
const props = defineProps<{
|
|
72
|
+
const props = defineProps<{
|
|
73
|
+
firstName: string
|
|
74
|
+
activationUrl: string
|
|
75
|
+
}>()
|
|
61
76
|
|
|
62
|
-
defineEmail({
|
|
77
|
+
defineEmail({
|
|
78
|
+
subject: () => `Welcome aboard, ${props.firstName}`,
|
|
79
|
+
})
|
|
63
80
|
</script>
|
|
64
81
|
|
|
65
82
|
<template>
|
|
66
83
|
<ETailwind>
|
|
67
84
|
<EHtml lang="en">
|
|
68
|
-
<
|
|
85
|
+
<EHead>
|
|
86
|
+
<title>Activate your account</title>
|
|
87
|
+
</EHead>
|
|
88
|
+
<EBody class="m-0 bg-slate-100 p-6">
|
|
69
89
|
<EPreview>Your account is ready.</EPreview>
|
|
70
|
-
<EContainer class="bg-white p-6">
|
|
71
|
-
<EHeading>Welcome, {{ firstName }}</EHeading>
|
|
72
|
-
<
|
|
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>
|
|
73
96
|
</EContainer>
|
|
74
97
|
</EBody>
|
|
75
98
|
</EHtml>
|
|
@@ -88,7 +111,22 @@ export default defineEventHandler(async () => {
|
|
|
88
111
|
})
|
|
89
112
|
```
|
|
90
113
|
|
|
91
|
-
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).
|
|
115
|
+
|
|
116
|
+
## Discord
|
|
117
|
+
|
|
118
|
+
Join the Lupinum OSS community to discuss Nuxt Email, ask questions, and share
|
|
119
|
+
what you build.
|
|
120
|
+
|
|
121
|
+
<p align="center">
|
|
122
|
+
<a href="https://discord.gg/RPH6SeA36N">
|
|
123
|
+
<picture>
|
|
124
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/public/discord-dark.svg">
|
|
125
|
+
<source media="(prefers-color-scheme: light)" srcset="docs/public/discord-light.svg">
|
|
126
|
+
<img src="docs/public/discord-light.svg" width="500" alt="Join the Lupinum OSS Discord">
|
|
127
|
+
</picture>
|
|
128
|
+
</a>
|
|
129
|
+
</p>
|
|
92
130
|
|
|
93
131
|
## Email components
|
|
94
132
|
|
|
@@ -114,6 +152,16 @@ Run the Nuxt application and open `/__email`. The development-only page shows th
|
|
|
114
152
|
|
|
115
153
|
Add one sibling fixture file for deterministic sample props. Preview routes and fixtures are excluded from production builds.
|
|
116
154
|
|
|
155
|
+
```ts
|
|
156
|
+
import type { EmailComponentProps } from '@lupinum/nuxt-email'
|
|
157
|
+
import type WelcomeEmail from './welcome.vue'
|
|
158
|
+
|
|
159
|
+
export default {
|
|
160
|
+
firstName: 'Ada',
|
|
161
|
+
activationUrl: 'https://example.com/activate',
|
|
162
|
+
} satisfies EmailComponentProps<typeof WelcomeEmail>
|
|
163
|
+
```
|
|
164
|
+
|
|
117
165
|
## Testing
|
|
118
166
|
|
|
119
167
|
Render a Vue email without starting Nuxt:
|
|
@@ -138,14 +186,11 @@ Complete the [manual client QA checklist](https://github.com/lupinum-dev/nuxt-em
|
|
|
138
186
|
|
|
139
187
|
## Package exports
|
|
140
188
|
|
|
141
|
-
- `@lupinum/nuxt-email`
|
|
142
|
-
- `@lupinum/nuxt-email/define-email` provides typed template metadata.
|
|
143
|
-
- `@lupinum/nuxt-email/testing` provides standalone component rendering.
|
|
144
|
-
- `@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.
|
|
145
190
|
|
|
146
191
|
## Documentation
|
|
147
192
|
|
|
148
|
-
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).
|
|
149
194
|
|
|
150
195
|
See the [changelog](CHANGELOG.md) for released changes.
|
|
151
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.d.mts
CHANGED