@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 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.gg/RPH6SeA36N"><img src="https://img.shields.io/badge/Discord-18181B?logo=discord" alt="Discord"></a>
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
- > [`1.0.0-beta.3`](https://www.npmjs.com/package/@lupinum/nuxt-email/v/1.0.0-beta.3) 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.
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
- ## Discord
116
+ Questions? Ask in the [Lupinum OSS Discord](https://discord.lupinum.com).
145
117
 
146
- Join the Lupinum OSS community to discuss Nuxt Email, ask questions, and share
147
- what you build.
118
+ ## Agent setup
148
119
 
149
- <p align="center">
150
- <a href="https://discord.gg/RPH6SeA36N">
151
- <picture>
152
- <source media="(prefers-color-scheme: dark)" srcset="docs/public/discord-dark.svg">
153
- <source media="(prefers-color-scheme: light)" srcset="docs/public/discord-light.svg">
154
- <img src="docs/public/discord-light.svg" width="500" alt="Join the Lupinum OSS Discord">
155
- </picture>
156
- </a>
157
- </p>
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 [MAINTAINING.md](https://github.com/lupinum-dev/nuxt-email/blob/main/MAINTAINING.md) for releases.
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.gg/RPH6SeA36N) for project discussion.
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
 
@@ -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.9.0, pinned at source commit
6
- 6eb428924c4c2774228a07cbec1977ad8898f143. The adapted surface covers button
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/6eb428924c4c2774228a07cbec1977ad8898f143.
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.1.18 — utility compilation used by ETailwind; its stylesheet CSS
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
 
@@ -1,47 +1,43 @@
1
- # @lupinum/nuxt-email
1
+ # @lupinum/nuxt-email 1.0.0-beta.5 documentation
2
2
 
3
- Documentation for installed version 1.0.0-beta.4.
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
- Read the relevant local pages before changing this package’s integration.
6
- Project instructions govern architecture, style and permissions.
8
+ > Typed transactional email for Nuxt, with email-safe Tailwind v4.
7
9
 
8
- ## Start here
10
+ ## When to use
9
11
 
10
- - [Installation](./pages/docs/getting-started/installation.md) — /docs/getting-started/installation
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
- ## Find a page
14
+ ## Documentation
14
15
 
15
- The index below maps public documentation routes to this installed snapshot.
16
- For links to this documentation site, use the matching local page in manifest.json
17
- instead of fetching a possibly different website version. Preserve URL fragments.
18
- External dependencies have their own versioned contracts. Report missing guidance.
19
-
20
- - [Overview](./pages/docs/components.md) — /docs/components
21
- - [Code](./pages/docs/components/code.md) — /docs/components/code
22
- - [Content](./pages/docs/components/content.md) — /docs/components/content
23
- - [Layout](./pages/docs/components/layout.md) — /docs/components/layout
24
- - [Markdown](./pages/docs/components/markdown.md) — /docs/components/markdown
25
- - [Structure](./pages/docs/components/structure.md) — /docs/components/structure
26
- - [Tailwind](./pages/docs/components/tailwind.md) — /docs/components/tailwind
27
- - [Introduction](./pages/docs/getting-started.md) — /docs/getting-started
28
- - [Choose the right tool](./pages/docs/getting-started/choose-a-tool.md) — /docs/getting-started/choose-a-tool
29
- - [Your first email](./pages/docs/getting-started/first-email.md) — /docs/getting-started/first-email
30
- - [Installation](./pages/docs/getting-started/installation.md) — /docs/getting-started/installation
31
- - [Render from Nitro](./pages/docs/getting-started/render-from-nitro.md) — /docs/getting-started/render-from-nitro
32
- - [Guides](./pages/docs/guides.md) — /docs/guides
33
- - [Client compatibility and proofs](./pages/docs/guides/client-compatibility.md) — /docs/guides/client-compatibility
34
- - [Plain text](./pages/docs/guides/plain-text.md) — /docs/guides/plain-text
35
- - [Preview workflow](./pages/docs/guides/preview-workflow.md) — /docs/guides/preview-workflow
36
- - [Sending email](./pages/docs/guides/sending-email.md) — /docs/guides/sending-email
37
- - [Render outside Nitro](./pages/docs/guides/standalone-rendering.md) — /docs/guides/standalone-rendering
38
- - [Subject lines](./pages/docs/guides/subject-lines.md) — /docs/guides/subject-lines
39
- - [Using Tailwind](./pages/docs/guides/tailwind.md) — /docs/guides/tailwind
40
- - [Testing your emails](./pages/docs/guides/testing-your-emails.md) — /docs/guides/testing-your-emails
41
- - [Reference](./pages/docs/reference.md) — /docs/reference
42
- - [Conformance and divergences](./pages/docs/reference/conformance.md) — /docs/reference/conformance
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
- ### Use a coding agent
36
+ ### Set up a coding agent
37
37
 
38
- A coding agent is a development tool that can inspect and change your project.
39
- After installation, copy this prompt into your coding agent:
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
- Add Nuxt Email to this Nuxt application and render one transactional email from
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
- If the installed package has no `agent-docs` export, read its packaged README,
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.9.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.
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
- [`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`.
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.9.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:
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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=4.5.1 <5"
6
6
  },
7
- "version": "1.0.0-beta.4",
7
+ "version": "1.0.0-beta.5",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "unknown"
@@ -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 6eb428924c4c2774228a07cbec1977ad8898f143.
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 === "media" || atrule.name === "supports";
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
- walk(root, {
18
- visit: "Rule",
19
- enter(rule) {
20
- const firstSelector = rule.prelude.type === "SelectorList" ? rule.prelude.children.first : null;
21
- if (firstSelector?.type === "Selector" && firstSelector.children.first?.type === "NestingSelector") {
22
- return;
23
- }
24
- const selectorClasses = [];
25
- walk(rule.prelude, {
26
- visit: "ClassSelector",
27
- enter(classSelector) {
28
- selectorClasses.push(string.decode(classSelector.name));
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
- if (isRuleInlinable(rule)) {
32
- for (const className of selectorClasses) {
33
- if (classSet.has(className)) {
34
- appendRule(inlinableRules, className, rule);
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
- } else {
38
- const { inlinablePart, nonInlinablePart } = splitMixedRule(rule);
39
- let includesRequestedClass = false;
40
- for (const className of selectorClasses) {
41
- if (!classSet.has(className)) continue;
42
- includesRequestedClass = true;
43
- if (inlinablePart) {
44
- appendRule(inlinableRules, className, inlinablePart);
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 (includesRequestedClass && nonInlinablePart) {
51
- orderedNonInlinableRules.push(nonInlinablePart);
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 (!isRuleInlinable(rule)) {
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
  }