@lupinum/nuxt-email 1.0.0-beta.4 → 1.0.0-beta.5
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 +10 -0
- package/README.md +49 -45
- package/THIRD_PARTY_NOTICES +4 -4
- package/dist/agent/AGENTS.md +37 -41
- package/dist/agent/pages/docs/getting-started/installation.md +6 -18
- package/dist/agent/pages/docs/getting-started.md +4 -2
- package/dist/agent/pages/docs/reference/conformance.md +1 -1
- package/dist/module.json +1 -1
- package/dist/runtime/components/EButton.d.ts +1 -1
- package/dist/runtime/tailwind/engine/css/constants.d.ts +1 -0
- package/dist/runtime/tailwind/engine/css/constants.js +1 -0
- package/dist/runtime/tailwind/engine/css/downlevel-for-email-clients.js +2 -1
- package/dist/runtime/tailwind/engine/css/extract-rules-per-class.js +91 -33
- package/dist/runtime/tailwind/engine/css/sanitize-non-inlinable-rules.js +7 -2
- package/dist/runtime/tailwind/engine/stylesheets.d.ts +3 -3
- package/dist/runtime/tailwind/engine/stylesheets.js +141 -34
- package/package.json +20 -28
- package/dist/agent/manifest.json +0 -208
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#63](https://github.com/lupinum-dev/nuxt-email/pull/63) [`8ceae4a`](https://github.com/lupinum-dev/nuxt-email/commit/8ceae4a7cf9439962e03f01ceb50bffa0b8fdd0f) Thanks [@Mat4m0](https://github.com/Mat4m0)! - Fix conditional and child utility styles with the current Tailwind compiler, and fail instead of dropping unsupported conditions.
|
|
8
|
+
|
|
9
|
+
`ETailwind` keeps media-query, pseudo-class, child (`*:`, `**:`) and arbitrary selector variants such as `[:where(&)]:` with their conditions. Supported wrappers around a utility are `@media` and `@supports` (`@layer` is ignored). A utility inside any other at-rule now throws, for example `@container` variants (`@sm:`) or custom CSS in the `ETailwind` `utility` prop that wraps a class in `@scope` or `@container`. It throws `Unable to render Tailwind CSS: @<name> rules are not supported.` Before, the condition was dropped silently and the style applied everywhere.
|
|
10
|
+
|
|
11
|
+
Migration: remove `@container` variants (`@sm:`, `@md:` and so on) from email templates, and move custom CSS in the `ETailwind` `utility` prop out of `@container`, `@scope` and other unsupported at-rules, or rewrite the condition with `@media` or `@supports`.
|
|
12
|
+
|
|
3
13
|
## v1.0.0-beta.4
|
|
4
14
|
|
|
5
15
|
[compare changes](https://github.com/lupinum-dev/nuxt-email/compare/v1.0.0-beta.3...v1.0.0-beta.4)
|
package/README.md
CHANGED
|
@@ -20,12 +20,12 @@
|
|
|
20
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>
|
|
21
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>
|
|
22
22
|
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-00DC82" alt="MIT license"></a>
|
|
23
|
-
<a href="https://discord.
|
|
23
|
+
<a href="https://discord.lupinum.com"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord" alt="Discord"></a>
|
|
24
24
|
<a href="https://deepwiki.com/lupinum-dev/nuxt-email"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
|
|
25
25
|
</p>
|
|
26
26
|
|
|
27
27
|
> [!WARNING]
|
|
28
|
-
>
|
|
28
|
+
> The 1.0.0 beta is published on npm's [`next` tag](https://www.npmjs.com/package/@lupinum/nuxt-email?activeTab=versions). 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
|
|
|
@@ -61,34 +61,6 @@ export default defineNuxtConfig({
|
|
|
61
61
|
})
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
<!-- BEGIN:consumer-onboarding -->
|
|
65
|
-
|
|
66
|
-
### Use a coding agent
|
|
67
|
-
|
|
68
|
-
A coding agent is a development tool that can inspect and change your project.
|
|
69
|
-
After installation, copy this prompt into your coding agent:
|
|
70
|
-
|
|
71
|
-
```text
|
|
72
|
-
Add Nuxt Email to this Nuxt application and render one transactional email from
|
|
73
|
-
a Vue template. Read the project's existing instructions first. Resolve
|
|
74
|
-
@lupinum/nuxt-email/agent-docs from this application's directory and read its
|
|
75
|
-
starting pages. Use the installed version's examples and public types. Preserve
|
|
76
|
-
existing routes, security boundaries, conventions, and AGENTS.md instructions.
|
|
77
|
-
Add or update one short Nuxt Email pointer in AGENTS.md if the project allows
|
|
78
|
-
it; do not duplicate the documentation. If the file is absent, create only that
|
|
79
|
-
pointer. Report missing guidance. Verify the server render, HTML and plain-text
|
|
80
|
-
output, and one invalid request. Keep delivery in application-owned code.
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
If the installed package has no `agent-docs` export, read its packaged README,
|
|
84
|
-
types, and `CONFORMANCE.md`. Use documentation from the matching source tag when
|
|
85
|
-
more detail is needed. Installing or updating the package does not edit project
|
|
86
|
-
instructions. The pointer resolves the installed package, so upgrades and
|
|
87
|
-
rollbacks select the matching documentation without copying it into your
|
|
88
|
-
application.
|
|
89
|
-
|
|
90
|
-
<!-- END:consumer-onboarding -->
|
|
91
|
-
|
|
92
64
|
## Quick start
|
|
93
65
|
|
|
94
66
|
Create `app/emails/welcome.vue`:
|
|
@@ -141,20 +113,52 @@ export default defineEventHandler(async () => {
|
|
|
141
113
|
|
|
142
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).
|
|
143
115
|
|
|
144
|
-
|
|
116
|
+
Questions? Ask in the [Lupinum OSS Discord](https://discord.lupinum.com).
|
|
145
117
|
|
|
146
|
-
|
|
147
|
-
what you build.
|
|
118
|
+
## Agent setup
|
|
148
119
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
120
|
+
`@lupinum/nuxt-email` ships its documentation inside the package for coding
|
|
121
|
+
agents such as Claude Code, Codex and Cursor. The pages match the installed
|
|
122
|
+
version, so an agent does not follow the website for a different version.
|
|
123
|
+
|
|
124
|
+
To set it up, give your coding agent this task:
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
Set up @lupinum/nuxt-email for coding agents as described under "Agent setup" in its README.
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Steps for the agent
|
|
131
|
+
|
|
132
|
+
1. Find `node_modules/@lupinum/nuxt-email/dist/agent/AGENTS.md`. Look in the
|
|
133
|
+
directory of the project that depends on `@lupinum/nuxt-email` first, then
|
|
134
|
+
in its parent directories up to the repository root (workspaces can hoist
|
|
135
|
+
packages). Read it; it lists the documentation pages.
|
|
136
|
+
2. Add the section below to the project's agent instructions: `AGENTS.md`, or
|
|
137
|
+
`CLAUDE.md` if the project has only that file. If it has neither, create
|
|
138
|
+
`AGENTS.md`. Write the path relative to the repository root, through
|
|
139
|
+
`node_modules/@lupinum/nuxt-email` (for example
|
|
140
|
+
`apps/web/node_modules/@lupinum/nuxt-email/...` in a workspace). Never write
|
|
141
|
+
a resolved path such as `node_modules/.pnpm/...`: it contains the version and
|
|
142
|
+
breaks after an upgrade. If a section for `@lupinum/nuxt-email` already
|
|
143
|
+
exists, leave it as it is.
|
|
144
|
+
|
|
145
|
+
Use the path you found in place of the sample path:
|
|
146
|
+
|
|
147
|
+
```md
|
|
148
|
+
## @lupinum/nuxt-email
|
|
149
|
+
|
|
150
|
+
Before you change code that uses @lupinum/nuxt-email, read
|
|
151
|
+
`node_modules/@lupinum/nuxt-email/dist/agent/AGENTS.md` and the pages it
|
|
152
|
+
lists. They document the installed version. Prefer them over what you
|
|
153
|
+
remember about this package and over the website.
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
3. Do not copy the documentation into the project and do not install a skill.
|
|
157
|
+
The section points into the installed package, so it stays correct after
|
|
158
|
+
every upgrade or downgrade.
|
|
159
|
+
|
|
160
|
+
If the file does not exist, the installed version has no packaged
|
|
161
|
+
documentation. Read the package README and its TypeScript types instead.
|
|
158
162
|
|
|
159
163
|
## Email components
|
|
160
164
|
|
|
@@ -224,7 +228,7 @@ See the [changelog](CHANGELOG.md) for released changes.
|
|
|
224
228
|
|
|
225
229
|
## Contributing and development
|
|
226
230
|
|
|
227
|
-
Read the [contribution guide](https://github.com/lupinum-dev/nuxt-email/blob/main/CONTRIBUTING.md) before you open a pull request. Run the normal handoff gate before you submit a change:
|
|
231
|
+
Read the [contribution guide](https://github.com/lupinum-dev/nuxt-email/blob/main/.github/CONTRIBUTING.md) before you open a pull request. Run the normal handoff gate before you submit a change:
|
|
228
232
|
|
|
229
233
|
```bash
|
|
230
234
|
corepack enable
|
|
@@ -232,11 +236,11 @@ pnpm install --frozen-lockfile
|
|
|
232
236
|
pnpm verify
|
|
233
237
|
```
|
|
234
238
|
|
|
235
|
-
Maintainers use the protected workflow in [
|
|
239
|
+
Maintainers use the protected workflow in [Lupinum OSS](https://oss.lupinum.com/docs/releasing) for releases.
|
|
236
240
|
|
|
237
241
|
## Support and security
|
|
238
242
|
|
|
239
|
-
Open a [GitHub issue](https://github.com/lupinum-dev/nuxt-email/issues) for bugs and focused proposals. Join the [Lupinum OSS Discord](https://discord.
|
|
243
|
+
Open a [GitHub issue](https://github.com/lupinum-dev/nuxt-email/issues) for bugs and focused proposals. Join the [Lupinum OSS Discord](https://discord.lupinum.com) for project discussion.
|
|
240
244
|
|
|
241
245
|
Use the [private security process](https://github.com/lupinum-dev/nuxt-email/security/policy) to report a vulnerability. Do not report a vulnerability in a public issue.
|
|
242
246
|
|
package/THIRD_PARTY_NOTICES
CHANGED
|
@@ -2,12 +2,12 @@ React Email
|
|
|
2
2
|
===========
|
|
3
3
|
|
|
4
4
|
Nuxt Email contains Vue adaptations of selected compatibility algorithms and
|
|
5
|
-
markup strategies from React Email 6.
|
|
6
|
-
|
|
5
|
+
markup strategies from React Email 6.11.0, pinned at source commit
|
|
6
|
+
15419ff1f4cd0e32ed2c15a4a9182cc47a200a60. The adapted surface covers button
|
|
7
7
|
padding and Outlook spacing, text margin expansion, table padding placement,
|
|
8
8
|
hidden preview text, the font-face declaration, inline code rendering, Markdown
|
|
9
9
|
rendering, and Tailwind class inlining. The pinned upstream source is available at
|
|
10
|
-
https://github.com/resend/react-email/tree/
|
|
10
|
+
https://github.com/resend/react-email/tree/15419ff1f4cd0e32ed2c15a4a9182cc47a200a60.
|
|
11
11
|
|
|
12
12
|
Copyright 2024 Plus Five Five, Inc
|
|
13
13
|
|
|
@@ -44,7 +44,7 @@ whose text is reproduced once below.
|
|
|
44
44
|
- css-tree 3.2.1 — CSS parsing and generation in the Tailwind inlining engine.
|
|
45
45
|
Copyright (C) 2016-2026 by Roman Dvornov
|
|
46
46
|
|
|
47
|
-
- tailwindcss 4.
|
|
47
|
+
- tailwindcss 4.3.3 — utility compilation used by ETailwind; its stylesheet CSS
|
|
48
48
|
is also vendored byte-for-byte into the engine.
|
|
49
49
|
Copyright (c) Tailwind Labs, Inc.
|
|
50
50
|
|
package/dist/agent/AGENTS.md
CHANGED
|
@@ -1,47 +1,43 @@
|
|
|
1
|
-
# @lupinum/nuxt-email
|
|
1
|
+
# @lupinum/nuxt-email 1.0.0-beta.5 documentation
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
These pages document the installed version 1.0.0-beta.5. Prefer them over what you
|
|
4
|
+
remember about this package and over the website, which may describe another version.
|
|
5
|
+
Read the pages that match the task before you write code. If they do not answer a question,
|
|
6
|
+
the user can ask in the Lupinum OSS Discord: https://discord.lupinum.com
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
Project instructions govern architecture, style and permissions.
|
|
8
|
+
> Typed transactional email for Nuxt, with email-safe Tailwind v4.
|
|
7
9
|
|
|
8
|
-
##
|
|
10
|
+
## When to use
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
- [Module behavior](./pages/docs/reference/module.md) — /docs/reference/module
|
|
12
|
+
Use this site to author and render transactional email with Nuxt Email.
|
|
12
13
|
|
|
13
|
-
##
|
|
14
|
+
## Documentation
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
- [Overview](./pages/docs/components.md)
|
|
21
|
-
- [
|
|
22
|
-
- [
|
|
23
|
-
- [
|
|
24
|
-
- [
|
|
25
|
-
- [
|
|
26
|
-
- [Tailwind](./pages/docs/components/tailwind.md)
|
|
27
|
-
- [
|
|
28
|
-
- [
|
|
29
|
-
- [
|
|
30
|
-
- [
|
|
31
|
-
- [
|
|
32
|
-
- [
|
|
33
|
-
- [
|
|
34
|
-
- [
|
|
35
|
-
- [
|
|
36
|
-
- [
|
|
37
|
-
- [
|
|
38
|
-
- [
|
|
39
|
-
- [
|
|
40
|
-
- [
|
|
41
|
-
- [
|
|
42
|
-
- [
|
|
43
|
-
- [defineEmail](./pages/docs/reference/define-email.md) — /docs/reference/define-email
|
|
44
|
-
- [Error types](./pages/docs/reference/errors.md) — /docs/reference/errors
|
|
45
|
-
- [Migration from React Email](./pages/docs/reference/migration.md) — /docs/reference/migration
|
|
46
|
-
- [Module behavior](./pages/docs/reference/module.md) — /docs/reference/module
|
|
47
|
-
- [renderEmail](./pages/docs/reference/render-email.md) — /docs/reference/render-email
|
|
16
|
+
- [Introduction](./pages/docs/getting-started.md): What Nuxt Email is, why its output is trustworthy, and how it is compared with a fixed React Email version.
|
|
17
|
+
- [Choose the right tool](./pages/docs/getting-started/choose-a-tool.md): Compare Nuxt Email with the closest Vue, Nuxt, standalone, and React email workflows.
|
|
18
|
+
- [Installation](./pages/docs/getting-started/installation.md): Add @lupinum/nuxt-email to a supported Nuxt application and optionally configure syntax highlighting.
|
|
19
|
+
- [Your first email](./pages/docs/getting-started/first-email.md): Write a Vue email template under app/emails/, add a typed preview fixture, and understand template naming.
|
|
20
|
+
- [Render from Nitro](./pages/docs/getting-started/render-from-nitro.md): Call the typed, server-only renderEmail auto-import and pass its html and text to your provider SDK.
|
|
21
|
+
- [Overview](./pages/docs/components.md): The built-in E-prefixed components, optional ECodeBlock, and their shared safety rules.
|
|
22
|
+
- [Structure](./pages/docs/components/structure.md): EHtml, EHead, EBody, EPreview, and EFont — the document shell, head, body, inbox preview text, and font declarations.
|
|
23
|
+
- [Layout](./pages/docs/components/layout.md): EContainer, ESection, ERow, and EColumn — presentation-table primitives for stable email layouts.
|
|
24
|
+
- [Content](./pages/docs/components/content.md): EText, EHeading, ELink, EImg, EHr, and EButton for visible email content.
|
|
25
|
+
- [Code](./pages/docs/components/code.md): Compatibility-safe inline code and opt-in syntax-highlighted blocks.
|
|
26
|
+
- [Markdown](./pages/docs/components/markdown.md): EMarkdown renders Markdown to email-safe HTML with inline styles and per-element overrides.
|
|
27
|
+
- [Tailwind](./pages/docs/components/tailwind.md): ETailwind compiles Tailwind v4 utilities into email-safe inline styles and head rules during server rendering.
|
|
28
|
+
- [Guides](./pages/docs/guides.md): Task-focused guides for authoring, previewing, testing, and sending transactional email.
|
|
29
|
+
- [Preview workflow](./pages/docs/guides/preview-workflow.md): Use the development-only /__email preview, add fixtures, and monitor exact rendered size without treating it as a client guarantee.
|
|
30
|
+
- [Sending email](./pages/docs/guides/sending-email.md): Hand Nuxt Email output to a provider SDK or a local Mailpit SMTP server.
|
|
31
|
+
- [Using Tailwind](./pages/docs/guides/tailwind.md): How ETailwind inlines utilities, how nested-component classes are reached, and the exact documented limitations.
|
|
32
|
+
- [Subject lines](./pages/docs/guides/subject-lines.md): Compute a subject line from the same typed props with defineEmail, and read it back from the render result.
|
|
33
|
+
- [Plain text](./pages/docs/guides/plain-text.md): How the plain-text fallback is produced from the rendered HTML, what it excludes, and how to control table output.
|
|
34
|
+
- [Testing your emails](./pages/docs/guides/testing-your-emails.md): Render templates and assert on their output in unit tests with the @lupinum/nuxt-email/testing utilities.
|
|
35
|
+
- [Client compatibility and proofs](./pages/docs/guides/client-compatibility.md): What is automatically proven, and the manual email-client QA checklist that closes the remaining gap.
|
|
36
|
+
- [Render outside Nitro](./pages/docs/guides/standalone-rendering.md): Compile the same Vue templates for a Node backend without booting Nuxt.
|
|
37
|
+
- [Reference](./pages/docs/reference.md): The precise contracts — renderEmail, module behavior, error types, conformance, and migration.
|
|
38
|
+
- [renderEmail](./pages/docs/reference/render-email.md): The one public render API — its signature, deterministic result shape, plain-text contract, and security boundary.
|
|
39
|
+
- [defineEmail](./pages/docs/reference/define-email.md): Declare a template subject or authored plain-text alternative from script setup.
|
|
40
|
+
- [Module behavior](./pages/docs/reference/module.md): What the module registers, its fixed conventions, and the opt-in codeBlock configuration.
|
|
41
|
+
- [Error types](./pages/docs/reference/errors.md): Public error classes, their stable fields, common symptoms, and recovery steps.
|
|
42
|
+
- [Conformance and divergences](./pages/docs/reference/conformance.md): How compatibility is measured against React Email, the current pass numbers, and every intentional divergence.
|
|
43
|
+
- [Migration from React Email](./pages/docs/reference/migration.md): Translate JSX templates, props, and rendering calls to Nuxt Email's Vue single-file components and generated server API.
|
|
@@ -33,29 +33,17 @@ export default defineNuxtConfig({
|
|
|
33
33
|
})
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
###
|
|
36
|
+
### Set up a coding agent
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
The package ships its documentation for coding agents such as Claude Code,
|
|
39
|
+
Codex and Cursor, so they read pages that match the installed version. Give
|
|
40
|
+
your coding agent this task:
|
|
40
41
|
|
|
41
42
|
```text
|
|
42
|
-
|
|
43
|
-
a Vue template. Read the project's existing instructions first. Resolve
|
|
44
|
-
@lupinum/nuxt-email/agent-docs from this application's directory and read its
|
|
45
|
-
starting pages. Use the installed version's examples and public types. Preserve
|
|
46
|
-
existing routes, security boundaries, conventions, and AGENTS.md instructions.
|
|
47
|
-
Add or update one short Nuxt Email pointer in AGENTS.md if the project allows
|
|
48
|
-
it; do not duplicate the documentation. If the file is absent, create only that
|
|
49
|
-
pointer. Report missing guidance. Verify the server render, HTML and plain-text
|
|
50
|
-
output, and one invalid request. Keep delivery in application-owned code.
|
|
43
|
+
Set up @lupinum/nuxt-email for coding agents as described under "Agent setup" in its README.
|
|
51
44
|
```
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
types, and `CONFORMANCE.md`. Use documentation from the matching source tag when
|
|
55
|
-
more detail is needed. Installing or updating the package does not edit project
|
|
56
|
-
instructions. The pointer resolves the installed package, so upgrades and
|
|
57
|
-
rollbacks select the matching documentation without copying it into your
|
|
58
|
-
application.
|
|
46
|
+
The steps are in the [README's Agent setup section](https://github.com/lupinum-dev/nuxt-email#agent-setup).
|
|
59
47
|
|
|
60
48
|
## 2. Confirm the TypeScript entry point
|
|
61
49
|
|
|
@@ -27,7 +27,7 @@ Nuxt Email is one focused option among Vue, Nuxt, standalone, and React email to
|
|
|
27
27
|
|
|
28
28
|
Email HTML is unforgiving: clients strip `<style>`, break flexbox, and mangle padding. Nuxt Email combines conservative primitives with reproducible checks while keeping live-client claims separate.
|
|
29
29
|
|
|
30
|
-
- **Checked against React Email.** Covered behaviors are compared, case by case, with fixed versions of [React Email](https://react.email) (`6.
|
|
30
|
+
- **Checked against React Email.** Covered behaviors are compared, case by case, with fixed versions of [React Email](https://react.email) (`6.11.0`) and `@react-email/render` (`2.1.0`). The [conformance report](/raw/docs/reference/conformance.md) records the tested scope and intentional differences. It is regression evidence, not a universal inbox-rendering guarantee.
|
|
31
31
|
- **Tailwind v4 for email.** `ETailwind` inlines compatible utilities, preserves author-style precedence, and moves media-query and pseudo-class rules into the head.
|
|
32
32
|
- **Deterministic.** Two renders of the same template and props return byte-identical HTML and a matching plain-text fallback. No IDs, timestamps, hydration payload, or mutable renderer state.
|
|
33
33
|
- **Server-only and typed.** `renderEmail('welcome', props)` is generated from your `app/emails/` folder. Unknown template names, missing props, and wrong types fail at compile time.
|
|
@@ -47,7 +47,7 @@ Email HTML is unforgiving: clients strip `<style>`, break flexbox, and mangle pa
|
|
|
47
47
|
|
|
48
48
|
## Release status
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
The 1.0.0 beta is published on npm's [`next` tag](https://www.npmjs.com/package/@lupinum/nuxt-email?activeTab=versions). 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`.
|
|
51
51
|
|
|
52
52
|
The package identity is `@lupinum/nuxt-email`; the unscoped npm package is unrelated.
|
|
53
53
|
|
|
@@ -60,3 +60,5 @@ The package identity is `@lupinum/nuxt-email`; the unscoped npm package is unrel
|
|
|
60
60
|
Nuxt `4.5.1` and Vue `3.5.40` are the compatibility baseline. Release consumers verify that minimum and the current compatible Nuxt 4 and Vue 3 versions without a dependency shim. CI covers Node 22, 24, and 26. Other Node majors, Nuxt 3, Nuxt 5, edge runtimes, and client-side email rendering are outside the support contract.
|
|
61
61
|
|
|
62
62
|
The Node range follows the supported even-numbered Node release lines that can run the Nuxt baseline. Keeping that range narrow lets the renderer use the platform Nuxt already requires and avoids compatibility code for old or short-lived Node releases.
|
|
63
|
+
|
|
64
|
+
Questions? Ask in the [Lupinum OSS Discord](https://discord.lupinum.com).
|
|
@@ -17,7 +17,7 @@ Compatibility is reported **per behavior**. No global "React Email compatible" p
|
|
|
17
17
|
|
|
18
18
|
## How it is measured
|
|
19
19
|
|
|
20
|
-
The current Nuxt Email candidate is compared against **React Email `6.
|
|
20
|
+
The current Nuxt Email candidate is compared against **React Email `6.11.0`** and **`@react-email/render` `2.1.0`**. Each behavior is a case that renders the same input through both renderers and asserts one of these classifications:
|
|
21
21
|
|
|
22
22
|
- **exact** — byte-identical output.
|
|
23
23
|
- **normalized** — identical after the internal conformance helper canonicalizes insignificant serialization differences such as attribute order, trailing `style` semicolons, class whitespace, and self-closing form.
|
package/dist/module.json
CHANGED
|
@@ -5,7 +5,7 @@ export type EButtonProps = Omit<SafeEmailAttributes<AnchorHTMLAttributes>, 'href
|
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* Outlook spacing behavior adapted from React Email's MIT-licensed Button.
|
|
8
|
-
* Copyright 2024 Plus Five Five, Inc; pinned source commit
|
|
8
|
+
* Copyright 2024 Plus Five Five, Inc; pinned source commit 15419ff1f4cd0e32ed2c15a4a9182cc47a200a60.
|
|
9
9
|
*/
|
|
10
10
|
export declare function computeMsoFontWidthAndSpaceCount(expectedWidth: number): readonly [number, number];
|
|
11
11
|
export declare const EButton: DefineComponent<EButtonProps>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const NON_INLINABLE_ATRULES: Set<string>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const NON_INLINABLE_ATRULES = /* @__PURE__ */ new Set(["media", "supports"]);
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
parse,
|
|
6
6
|
walk
|
|
7
7
|
} from "../../css-tree.js";
|
|
8
|
+
import { NON_INLINABLE_ATRULES } from "./constants.js";
|
|
8
9
|
export function downlevelForEmailClients(styleSheet) {
|
|
9
10
|
styleSheet.children = flattenStyleSheet(styleSheet);
|
|
10
11
|
downlevelRangeMediaQueries(styleSheet);
|
|
@@ -162,7 +163,7 @@ function assertNoNestingSelector(selectors) {
|
|
|
162
163
|
});
|
|
163
164
|
}
|
|
164
165
|
function isConditionalAtrule(atrule) {
|
|
165
|
-
return atrule.name
|
|
166
|
+
return NON_INLINABLE_ATRULES.has(atrule.name.toLowerCase());
|
|
166
167
|
}
|
|
167
168
|
function clonePrelude(prelude) {
|
|
168
169
|
return prelude ? clone(prelude) : null;
|
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
import { string, walk } from "../../css-tree.js";
|
|
1
|
+
import { clone, List, string, walk } from "../../css-tree.js";
|
|
2
|
+
import { NON_INLINABLE_ATRULES } from "./constants.js";
|
|
2
3
|
import { isRuleInlinable } from "./is-rule-inlinable.js";
|
|
3
4
|
import { splitMixedRule } from "./split-mixed-rule.js";
|
|
5
|
+
const OWNER_FUNCTIONS = /* @__PURE__ */ new Set(["is", "where", "matches"]);
|
|
6
|
+
const SELECTOR_FUNCTIONS = /* @__PURE__ */ new Set([...OWNER_FUNCTIONS, "not", "has"]);
|
|
7
|
+
function isOwnArbitraryVariant(className, functions) {
|
|
8
|
+
return className.includes("&") && functions.filter((name) => !OWNER_FUNCTIONS.has(name)).every((name) => className.toLowerCase().includes(`:${name}(`));
|
|
9
|
+
}
|
|
4
10
|
export function extractRulesPerClass(root, classes) {
|
|
5
11
|
const classSet = new Set(classes);
|
|
6
12
|
const inlinableRules = /* @__PURE__ */ new Map();
|
|
@@ -14,43 +20,95 @@ export function extractRulesPerClass(root, classes) {
|
|
|
14
20
|
map.set(className, [rule]);
|
|
15
21
|
}
|
|
16
22
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
const enclosingAtRules = [];
|
|
24
|
+
const handleRule = (rule) => {
|
|
25
|
+
const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
|
|
26
|
+
if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const selectorClasses = [];
|
|
30
|
+
if (rule.prelude.type === "SelectorList") {
|
|
31
|
+
rule.prelude.children.forEach((selector) => {
|
|
32
|
+
const owners = [];
|
|
33
|
+
const collect = (insideFunctions) => {
|
|
34
|
+
const functions = [];
|
|
35
|
+
walk(selector, {
|
|
36
|
+
enter(node) {
|
|
37
|
+
if (node.type === "PseudoElementSelector") return walk.skip;
|
|
38
|
+
if (node.type === "PseudoClassSelector") {
|
|
39
|
+
const name = node.name.toLowerCase();
|
|
40
|
+
if (!insideFunctions || !SELECTOR_FUNCTIONS.has(name)) return walk.skip;
|
|
41
|
+
functions.push(name);
|
|
42
|
+
}
|
|
43
|
+
if (node.type === "ClassSelector") {
|
|
44
|
+
const className = string.decode(node.name);
|
|
45
|
+
if (functions.every((name) => OWNER_FUNCTIONS.has(name)) || isOwnArbitraryVariant(className, functions)) owners.push(className);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
leave(node) {
|
|
49
|
+
if (node.type === "PseudoClassSelector" && insideFunctions && SELECTOR_FUNCTIONS.has(node.name.toLowerCase())) functions.pop();
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
collect(false);
|
|
54
|
+
if (!owners.length) collect(true);
|
|
55
|
+
selectorClasses.push(...owners);
|
|
30
56
|
});
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
57
|
+
}
|
|
58
|
+
if (enclosingAtRules.length > 0) {
|
|
59
|
+
const unsupported = enclosingAtRules.find((atRule) => !NON_INLINABLE_ATRULES.has(atRule.name.toLowerCase()));
|
|
60
|
+
if (unsupported && selectorClasses.some((className) => classSet.has(className))) {
|
|
61
|
+
throw new TypeError(`Unable to render Tailwind CSS: @${unsupported.name} rules are not supported.`);
|
|
62
|
+
}
|
|
63
|
+
const conditional = clone(rule);
|
|
64
|
+
for (const atRule of [...enclosingAtRules].reverse()) {
|
|
65
|
+
conditional.block.children = new List().fromArray([{
|
|
66
|
+
type: "Atrule",
|
|
67
|
+
name: atRule.name,
|
|
68
|
+
prelude: atRule.prelude ? clone(atRule.prelude) : null,
|
|
69
|
+
block: { type: "Block", children: conditional.block.children }
|
|
70
|
+
}]);
|
|
71
|
+
}
|
|
72
|
+
let includesRequestedClass = false;
|
|
73
|
+
for (const className of selectorClasses) {
|
|
74
|
+
if (!classSet.has(className)) continue;
|
|
75
|
+
includesRequestedClass = true;
|
|
76
|
+
appendRule(nonInlinableRules, className, conditional);
|
|
77
|
+
}
|
|
78
|
+
if (includesRequestedClass) orderedNonInlinableRules.push(conditional);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (isRuleInlinable(rule)) {
|
|
82
|
+
for (const className of selectorClasses) {
|
|
83
|
+
if (classSet.has(className)) {
|
|
84
|
+
appendRule(inlinableRules, className, rule);
|
|
36
85
|
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (nonInlinablePart) {
|
|
47
|
-
appendRule(nonInlinableRules, className, nonInlinablePart);
|
|
48
|
-
}
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
|
|
89
|
+
let includesRequestedClass = false;
|
|
90
|
+
for (const className of selectorClasses) {
|
|
91
|
+
if (!classSet.has(className)) continue;
|
|
92
|
+
includesRequestedClass = true;
|
|
93
|
+
if (inlinablePart) {
|
|
94
|
+
appendRule(inlinableRules, className, inlinablePart);
|
|
49
95
|
}
|
|
50
|
-
if (
|
|
51
|
-
|
|
96
|
+
if (nonInlinablePart) {
|
|
97
|
+
appendRule(nonInlinableRules, className, nonInlinablePart);
|
|
52
98
|
}
|
|
53
99
|
}
|
|
100
|
+
if (includesRequestedClass && nonInlinablePart) {
|
|
101
|
+
orderedNonInlinableRules.push(nonInlinablePart);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
walk(root, {
|
|
106
|
+
enter(node) {
|
|
107
|
+
if (node.type === "Atrule" && node.name.toLowerCase() !== "layer") enclosingAtRules.push(node);
|
|
108
|
+
else if (node.type === "Rule") handleRule(node);
|
|
109
|
+
},
|
|
110
|
+
leave(node) {
|
|
111
|
+
if (node.type === "Atrule" && node.name.toLowerCase() !== "layer") enclosingAtRules.pop();
|
|
54
112
|
}
|
|
55
113
|
});
|
|
56
114
|
return {
|
|
@@ -1,12 +1,14 @@
|
|
|
1
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
|
+
import { NON_INLINABLE_ATRULES } from "./constants.js";
|
|
4
5
|
import { stripEmptyTailwindVars } from "./strip-empty-tailwind-vars.js";
|
|
5
6
|
export function sanitizeNonInlinableRules(node) {
|
|
7
|
+
let conditionalDepth = 0;
|
|
6
8
|
walk(node, {
|
|
7
|
-
visit: "Rule",
|
|
8
9
|
enter(rule) {
|
|
9
|
-
if (
|
|
10
|
+
if (rule.type === "Atrule" && NON_INLINABLE_ATRULES.has(rule.name.toLowerCase())) conditionalDepth++;
|
|
11
|
+
if (rule.type === "Rule" && (conditionalDepth > 0 || !isRuleInlinable(rule))) {
|
|
10
12
|
walk(rule.prelude, (node2) => {
|
|
11
13
|
if (node2.type === "ClassSelector") {
|
|
12
14
|
const unescapedClassName = string.decode(node2.name);
|
|
@@ -25,6 +27,9 @@ export function sanitizeNonInlinableRules(node) {
|
|
|
25
27
|
}
|
|
26
28
|
});
|
|
27
29
|
}
|
|
30
|
+
},
|
|
31
|
+
leave(rule) {
|
|
32
|
+
if (rule.type === "Atrule" && NON_INLINABLE_ATRULES.has(rule.name.toLowerCase())) conditionalDepth--;
|
|
28
33
|
}
|
|
29
34
|
});
|
|
30
35
|
}
|