diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf0981e..3b7a954 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,38 @@
All notable changes to this project will be documented in this file.
+## Unreleased
+
+### Changed
+
+- Browser-condition imports from `@akin01/solid-email` now resolve to the
+ DOM/CSR preview build, replacing the public `@akin01/solid-email/client`
+ subpath while keeping server, Workerd, and default imports on the SSR/email
+ rendering build.
+- Browser-condition `require('@akin01/solid-email')` now resolves to the
+ DOM/CSR preview CJS build.
+- Default/root ESM and CJS server entries now share Solid's server runtime for
+ Tailwind resource rendering, preventing CJS `Tailwind` renders from splitting
+ Solid SSR context.
+- Tailwind's CSS Tree usage now imports the exported browser/dist ESM bundle to
+ avoid Node `createRequire`/`mdn-data` JSON loading in Workerd bundles.
+
+### Related commits
+
+- Workerd-safe root exports and rendering
+ - [`691ed9560834`](https://github.com/Akin01/solid-email/commit/691ed95608341c664c8d80335e001faea667cf9a) Added Workerd-safe root export conditions, server/runtime fixes, and Cloudflare TanStack Start coverage.
+ - [`9c6bb421f37b`](https://github.com/Akin01/solid-email/commit/9c6bb421f37b427bc9f05305a3bac63a886d4857) Added the TypeScript shim for CSS Tree's dist ESM entry used by Workerd-safe Tailwind imports.
+- Documentation and bundle metrics
+ - [`4a38f8e2af11`](https://github.com/Akin01/solid-email/commit/4a38f8e2af117d53a49535844c78e5a184719a40) Updated browser-root documentation, Solid Email skill guidance, and recomputed bundle-size comparisons.
+
+### Verified
+
+- `pnpm test`
+- `pnpm test:e2e`
+- `pnpm typecheck`
+- `pnpm build`
+- `pnpm lint`
+
## 0.1.4 - 2026-06-26
### Changed
diff --git a/README.md b/README.md
index 0629521..d60ee9e 100644
--- a/README.md
+++ b/README.md
@@ -60,10 +60,12 @@ Bundle size compares built ESM entry files after `pnpm build`; gzip uses Node's
| Package entry | Raw size | Gzip size | Comparison |
| --- | ---: | ---: | --- |
-| `@akin01/solid-email/dist/index.mjs` | 198.7 KiB | 42.3 KiB | Components entry |
-| `@solid-email/render/dist/node/index.mjs` | 13.3 KiB | 3.7 KiB | Renderer entry |
-| Solid Email combined entries | 211.9 KiB | 46.0 KiB | 6.8x smaller raw / 7.5x smaller gzip than React Email |
-| `react-email/dist/index.mjs` | 1,448.0 KiB | 347.4 KiB | React Email baseline |
+| `@akin01/solid-email/dist/index.mjs` | 199.0 KiB | 42.7 KiB | Server/root components and render utility re-exports |
+| `@akin01/solid-email/dist/client/index.mjs` | 105.9 KiB | 19.5 KiB | Browser-condition DOM preview build |
+| `@solid-email/render/dist/node/index.mjs` | 26.3 KiB | 6.2 KiB | Renderer entry |
+| Solid Email server entries | 225.3 KiB | 48.9 KiB | 6.4x smaller raw / 7.1x smaller gzip than React Email |
+| Solid Email all ESM condition entries | 331.2 KiB | 68.4 KiB | 4.4x smaller raw / 5.1x smaller gzip than React Email |
+| `react-email/dist/index.mjs` | 1,448.0 KiB | 348.6 KiB | React Email baseline |
## Install
@@ -112,14 +114,14 @@ const html = renderSync(() => );
## Entrypoints
-`@akin01/solid-email` is the SSR/email-rendering entrypoint. It keeps `render`,
-`compile`, and all email components available even under browser-like import
-conditions, so browser code can still produce email HTML strings.
-
-`@akin01/solid-email/client` is the opt-in DOM/CSR preview entrypoint. It exports
-DOM-safe preview components and intentionally excludes `render`, `compile`, and
+`@akin01/solid-email` is conditionally exported. Server, Workerd, and default
+imports expose `render`, `compile`, and the full email component set, including
`Tailwind`.
+Browser-condition imports of the same package root resolve to the DOM/CSR
+preview build. That build exports DOM-safe preview components and intentionally
+excludes `render`, `compile`, and `Tailwind`.
+
## Compile for repeated renders
When you render the same template multiple times with different data, `compile()` pre-evaluates the Solid components once and reuses the cached HTML on each render.
diff --git a/e2e/cloudflare-tanstack-start/package.json b/e2e/cloudflare-tanstack-start/package.json
new file mode 100644
index 0000000..a78ad7a
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "solid-email-cloudflare-tanstack-start-e2e",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "preview": "vite preview --host 127.0.0.1"
+ },
+ "dependencies": {
+ "@solid-email/html-to-text": "file:../.tmp/packs/solid-email-html-to-text-0.1.1.tgz",
+ "@solid-email/render": "file:../.tmp/packs/solid-email-render-0.1.4.tgz",
+ "@akin01/solid-email": "file:../.tmp/packs/akin01-solid-email-0.1.4.tgz",
+ "solid-js": "1.9.13"
+ },
+ "devDependencies": {
+ "@cloudflare/vite-plugin": "1.43.0",
+ "@tanstack/solid-router": "1.170.16",
+ "@tanstack/solid-start": "1.168.26",
+ "@types/node": "25.0.2",
+ "typescript": "6.0.3",
+ "vite": "7.3.1",
+ "vite-plugin-solid": "2.11.12",
+ "wrangler": "4.107.0"
+ }
+}
diff --git a/e2e/cloudflare-tanstack-start/pnpm-workspace.yaml b/e2e/cloudflare-tanstack-start/pnpm-workspace.yaml
new file mode 100644
index 0000000..005d266
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/pnpm-workspace.yaml
@@ -0,0 +1,9 @@
+overrides:
+ '@akin01/solid-email': file:../.tmp/packs/akin01-solid-email-0.1.4.tgz
+ '@solid-email/html-to-text': file:../.tmp/packs/solid-email-html-to-text-0.1.1.tgz
+ '@solid-email/render': file:../.tmp/packs/solid-email-render-0.1.4.tgz
+
+allowBuilds:
+ esbuild: true
+ sharp: true
+ workerd: true
diff --git a/e2e/cloudflare-tanstack-start/src/email.functions.ts b/e2e/cloudflare-tanstack-start/src/email.functions.ts
new file mode 100644
index 0000000..f31d33a
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/src/email.functions.ts
@@ -0,0 +1,6 @@
+import { createServerFn } from '@tanstack/solid-start';
+import { renderCloudflareEmailReport } from './email.server';
+
+export const renderCloudflareEmailReportFn = createServerFn({
+ method: 'GET',
+}).handler(() => renderCloudflareEmailReport());
diff --git a/e2e/cloudflare-tanstack-start/src/email.server.tsx b/e2e/cloudflare-tanstack-start/src/email.server.tsx
new file mode 100644
index 0000000..8c06a07
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/src/email.server.tsx
@@ -0,0 +1,205 @@
+import {
+ Body,
+ Button,
+ CodeBlock,
+ CodeInline,
+ Column,
+ Container,
+ Font,
+ Head,
+ Heading,
+ Hr,
+ Html,
+ Img,
+ Link,
+ Markdown,
+ Preview,
+ Row,
+ Section,
+ Tailwind,
+ Text,
+ xonokai,
+} from '@akin01/solid-email';
+import { compile, render, Slot, slot } from '@solid-email/render';
+import type { JSX } from 'solid-js';
+
+type WorkerEmailSlots = {
+ ctaUrl: string;
+ recipient: string;
+ routeName: string;
+};
+
+const markdownContent = `# Markdown heading
+
+Paragraph with **bold text**, _italic text_, ~~removed text~~, and [a markdown link](https://example.com/markdown).
+
+> Quoted markdown block
+
+1. First ordered item
+2. Second ordered item
+
+- First unordered item
+- Second unordered item
+
+| Name | Value |
+| ---- | ----- |
+| Component | Markdown |
+
+
+
+\`inline markdown code\`
+
+\`\`\`ts
+const answer = 42;
+\`\`\`
+`;
+
+export type CloudflareEmailReport = {
+ html: string;
+ plainText: string;
+ status: 'rendered';
+};
+
+function WorkerRouteEmail(): JSX.Element {
+ return (
+
+
+
+
+ All components e2e preview
+
+
+
+
+
+ Comprehensive components e2e
+
+
+ Cloudflare TanStack Solid email rendered
+
+
+ Hello !
+
+
+ Loaded from while running in Workerd.
+
+
+
+
+
+
+
+
+ Plain link component
+
+
+
+
+
+ Inline code component:{' '}
+ const solid = true;
+
+
+
+ {markdownContent}
+
+
+
+
+
+
+ );
+}
+
+export async function renderCloudflareEmailReport(): Promise {
+ try {
+ const email = await compile(() => , {
+ withPlainText: true,
+ });
+ const slots = {
+ ctaUrl: 'https://example.com/action',
+ recipient: 'Cloudflare TanStack Worker',
+ routeName: 'Solid Start route graph',
+ } satisfies WorkerEmailSlots;
+
+ const html = await email.render(slots);
+ const plainText = await email.render(slots, { plainText: true });
+ const renderProbe = await render(() => (
+
+ Standalone render confirms @solid-email/render in Workerd
+
+ ));
+
+ if (
+ !html.includes(slots.recipient) ||
+ !html.includes(slots.routeName) ||
+ !html.includes(slots.ctaUrl) ||
+ !plainText.includes(slots.recipient) ||
+ !renderProbe.includes('@solid-email/render in Workerd')
+ ) {
+ throw new Error(
+ 'Cloudflare Worker email render missed expected slot content',
+ );
+ }
+
+ return {
+ html: `${html}\n${renderProbe}`,
+ plainText,
+ status: 'rendered',
+ };
+ } catch (error) {
+ const message =
+ error instanceof Error ? (error.stack ?? error.message) : String(error);
+ throw new Error(`Cloudflare Worker email render failed: ${message}`);
+ }
+}
diff --git a/e2e/cloudflare-tanstack-start/src/router.tsx b/e2e/cloudflare-tanstack-start/src/router.tsx
new file mode 100644
index 0000000..7e47566
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/src/router.tsx
@@ -0,0 +1,6 @@
+import { createRouter } from '@tanstack/solid-router';
+import { routeTree } from './routeTree.gen';
+
+export function getRouter() {
+ return createRouter({ routeTree });
+}
diff --git a/e2e/cloudflare-tanstack-start/src/routes/__root.tsx b/e2e/cloudflare-tanstack-start/src/routes/__root.tsx
new file mode 100644
index 0000000..ac0bb2c
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/src/routes/__root.tsx
@@ -0,0 +1,35 @@
+import {
+ createRootRoute,
+ HeadContent,
+ Outlet,
+ Scripts,
+} from '@tanstack/solid-router';
+import { type JSX, Suspense } from 'solid-js';
+import { HydrationScript } from 'solid-js/web';
+
+export const Route = createRootRoute({
+ component: RootComponent,
+});
+
+function RootComponent(): JSX.Element {
+ return (
+
+
+
+ );
+}
+
+function RootDocument(props: Readonly<{ children: JSX.Element }>): JSX.Element {
+ return (
+
+
+
+
+
+
+ {props.children}
+
+
+
+ );
+}
diff --git a/e2e/cloudflare-tanstack-start/src/routes/index.tsx b/e2e/cloudflare-tanstack-start/src/routes/index.tsx
new file mode 100644
index 0000000..d85f06c
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/src/routes/index.tsx
@@ -0,0 +1,18 @@
+import { createFileRoute } from '@tanstack/solid-router';
+import { renderCloudflareEmailReportFn } from '../email.functions';
+
+export const Route = createFileRoute('/')({
+ loader: () => renderCloudflareEmailReportFn(),
+ component: Home,
+});
+
+function Home() {
+ const report = Route.useLoaderData();
+ return (
+
+ Cloudflare TanStack Start Solid route loaded
+ {report().plainText}
+ {report().html}
+
+ );
+}
diff --git a/e2e/cloudflare-tanstack-start/tsconfig.json b/e2e/cloudflare-tanstack-start/tsconfig.json
new file mode 100644
index 0000000..5160f68
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "extends": "../../packages/tsconfig/solid-vite-app.json",
+ "include": ["src", "vite.config.ts"],
+ "exclude": [
+ "node_modules",
+ "dist",
+ ".output",
+ ".tanstack",
+ ".types",
+ ".wrangler"
+ ]
+}
diff --git a/e2e/cloudflare-tanstack-start/vite.config.ts b/e2e/cloudflare-tanstack-start/vite.config.ts
new file mode 100644
index 0000000..80413f9
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/vite.config.ts
@@ -0,0 +1,12 @@
+import { cloudflare } from '@cloudflare/vite-plugin';
+import { tanstackStart } from '@tanstack/solid-start/plugin/vite';
+import { defineConfig } from 'vite';
+import solidPlugin from 'vite-plugin-solid';
+
+export default defineConfig({
+ plugins: [
+ cloudflare({ viteEnvironment: { name: 'ssr' } }),
+ tanstackStart(),
+ solidPlugin({ ssr: true }),
+ ],
+});
diff --git a/e2e/cloudflare-tanstack-start/wrangler.jsonc b/e2e/cloudflare-tanstack-start/wrangler.jsonc
new file mode 100644
index 0000000..0b85389
--- /dev/null
+++ b/e2e/cloudflare-tanstack-start/wrangler.jsonc
@@ -0,0 +1,10 @@
+{
+ "$schema": "node_modules/wrangler/config-schema.json",
+ "name": "solid-email-cloudflare-tanstack-start-e2e",
+ "compatibility_date": "2026-07-04",
+ "compatibility_flags": ["nodejs_compat"],
+ "main": "@tanstack/solid-start/server-entry",
+ "observability": {
+ "enabled": true
+ }
+}
diff --git a/e2e/integrations.e2e.spec.ts b/e2e/integrations.e2e.spec.ts
index 1df6e83..9370b20 100644
--- a/e2e/integrations.e2e.spec.ts
+++ b/e2e/integrations.e2e.spec.ts
@@ -1,7 +1,9 @@
-import type { ExecFileException } from 'node:child_process';
-import { execFile } from 'node:child_process';
+import type { ChildProcess, ExecFileException } from 'node:child_process';
+import { execFile, spawn } from 'node:child_process';
import { mkdir, readFile, rm } from 'node:fs/promises';
+import { createServer } from 'node:net';
import path from 'node:path';
+import { setTimeout as delay } from 'node:timers/promises';
import { promisify } from 'node:util';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -140,40 +142,46 @@ const renderRequireProbe = `${renderExportSmoke}
process.exit(1);
});`;
+// These probes use dynamic import because the tests intentionally exercise
+// package export resolution under Node's condition flags.
const solidEmailBrowserImportProbe = `
const resolved = import.meta.resolve('@akin01/solid-email');
const mod = await import('@akin01/solid-email');
-for (const name of ['render', 'Section', 'Row', 'Heading']) {
+for (const name of ['Body', 'Button', 'Container', 'Heading', 'Preview', 'Row', 'Section', 'Text']) {
if (typeof mod[name] !== 'function') {
- throw new Error(\`missing solid-email export: \${name}\`);
+ throw new Error(\`missing browser root export: \${name}\`);
}
}
-const html = await mod.render(() =>
- mod.Section({
- style: { padding: '12px' },
- children: mod.Row({
- children: mod.Heading({
- as: 'h2',
- style: { color: 'blue' },
- children: 'Solid Email browser condition smoke',
- }),
- }),
- }),
-);
+for (const name of ['render', 'renderSync', 'compile', 'compileSync', 'toPlainText', 'pretty', 'Tailwind']) {
+ if (name in mod) {
+ throw new Error(\`unexpected server-only browser root export: \${name}\`);
+ }
+}
-if (!html.includes(' process.exit(0));`;
+
+const solidEmailBrowserRequireProbe = `
+const resolved = require.resolve('@akin01/solid-email');
+const mod = require('@akin01/solid-email');
+
+for (const name of ['Body', 'Button', 'Container', 'Heading', 'Preview', 'Row', 'Section', 'Text']) {
+ if (typeof mod[name] !== 'function') {
+ throw new Error(\`missing browser root require export: \${name}\`);
+ }
}
-if (!html.includes('style="width:100%"') || !html.includes('color:blue')) {
- throw new Error(\`missing server-rendered style output: \${html}\`);
+
+for (const name of ['render', 'renderSync', 'compile', 'compileSync', 'toPlainText', 'pretty', 'Tailwind']) {
+ if (name in mod) {
+ throw new Error(\`unexpected server-only browser root require export: \${name}\`);
+ }
}
process.stdout.write(resolved.replaceAll('\\\\', '/') + '\\n', () => process.exit(0));`;
-const solidEmailClientDomProbe = `
-const resolved = import.meta.resolve('@akin01/solid-email/client');
+const solidEmailBrowserRootDomProbe = `
+const resolved = import.meta.resolve('@akin01/solid-email');
const { JSDOM } = await import('jsdom');
const dom = new JSDOM('', {
url: 'https://solid.email/preview',
@@ -184,16 +192,16 @@ globalThis.Node = dom.window.Node;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.Element = dom.window.Element;
-const mod = await import('@akin01/solid-email/client');
+const mod = await import('@akin01/solid-email');
for (const name of ['render', 'compile', 'Tailwind']) {
if (name in mod) {
- throw new Error(\`unexpected client export: \${name}\`);
+ throw new Error(\`unexpected browser root export: \${name}\`);
}
}
for (const name of ['Container', 'Heading', 'Text', 'Preview']) {
if (typeof mod[name] !== 'function') {
- throw new Error(\`missing client export: \${name}\`);
+ throw new Error(\`missing browser root export: \${name}\`);
}
}
@@ -206,15 +214,15 @@ const dispose = mount(
children: mod.Heading({
as: 'h2',
style: { color: 'purple' },
- children: mod.Text({ children: 'Client preview mounted' }),
+ children: mod.Text({ children: 'Browser root preview mounted' }),
}),
}),
root,
);
const heading = root.querySelector('h2');
-if (!heading || heading.textContent !== 'Client preview mounted') {
- throw new Error(\`missing mounted client heading: \${root.innerHTML}\`);
+if (!heading || heading.textContent !== 'Browser root preview mounted') {
+ throw new Error(\`missing mounted browser root heading: \${root.innerHTML}\`);
}
if (!root.querySelector('table')) {
throw new Error(\`missing mounted email layout table: \${root.innerHTML}\`);
@@ -223,6 +231,52 @@ if (!root.querySelector('table')) {
dispose();
process.stdout.write(resolved.replaceAll('\\\\', '/') + '\\n', () => process.exit(0));`;
+const solidEmailServerRootSmoke = `
+async function assertSolidEmailServerRoot(mod, expectedText) {
+ for (const name of ['render', 'Tailwind', 'Text']) {
+ if (typeof mod[name] !== 'function') {
+ throw new Error(\`missing server root export: \${name}\`);
+ }
+ }
+
+ const html = await mod.render(() =>
+ mod.Tailwind({
+ children: mod.Text({
+ class: 'text-blue-600',
+ children: expectedText,
+ }),
+ }),
+ );
+
+ if (!html.includes(expectedText)) {
+ throw new Error(\`missing rendered Tailwind text: \${html}\`);
+ }
+ if (html.includes('text-blue-600')) {
+ throw new Error(\`Tailwind class was not removed: \${html}\`);
+ }
+ if (!html.includes('color:rgb(21,93,252)')) {
+ throw new Error(\`Tailwind class was not inlined: \${html}\`);
+ }
+}
+`;
+
+const solidEmailServerImportProbe = `${solidEmailServerRootSmoke}
+const resolved = import.meta.resolve('@akin01/solid-email');
+const mod = await import('@akin01/solid-email');
+await assertSolidEmailServerRoot(mod, 'Solid Email ESM Tailwind safe');
+process.stdout.write(resolved.replaceAll('\\\\', '/') + '\\n', () => process.exit(0));`;
+
+const solidEmailServerRequireProbe = `${solidEmailServerRootSmoke}
+(async () => {
+ const resolved = require.resolve('@akin01/solid-email');
+ const mod = require('@akin01/solid-email');
+ await assertSolidEmailServerRoot(mod, 'Solid Email CJS Tailwind safe');
+ process.stdout.write(resolved.replaceAll('\\\\', '/') + '\\n', () => process.exit(0));
+})().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});`;
+
type PnpmError = ExecFileException & {
stderr?: string;
stdout?: string;
@@ -232,7 +286,15 @@ function isPnpmError(error: unknown): error is PnpmError {
return error instanceof Error;
}
-async function cleanFixture(fixture: 'vite' | 'tanstack-start'): Promise {
+type FixtureName = 'vite' | 'tanstack-start' | 'cloudflare-tanstack-start';
+
+type PreviewServer = {
+ output: () => string;
+ process: ChildProcess;
+ stop: () => Promise;
+};
+
+async function cleanFixture(fixture: FixtureName): Promise {
const fixtureRoot = path.join(e2eRoot, fixture);
await rm(path.join(fixtureRoot, 'node_modules'), {
recursive: true,
@@ -246,6 +308,10 @@ async function cleanFixture(fixture: 'vite' | 'tanstack-start'): Promise {
recursive: true,
force: true,
});
+ await rm(path.join(fixtureRoot, '.wrangler'), {
+ recursive: true,
+ force: true,
+ });
await rm(path.join(fixtureRoot, 'src/routeTree.gen.ts'), {
force: true,
});
@@ -299,7 +365,7 @@ async function preparePackedPackages(): Promise {
]);
}
-async function installAndBuildFixture(fixture: 'vite' | 'tanstack-start') {
+async function installAndBuildFixture(fixture: FixtureName) {
const fixtureRoot = path.join(e2eRoot, fixture);
await cleanFixture(fixture);
@@ -322,6 +388,85 @@ async function installAndBuildFixture(fixture: 'vite' | 'tanstack-start') {
return fixtureRoot;
}
+async function getAvailablePort(): Promise {
+ const server = createServer();
+ await new Promise((resolve, reject) => {
+ server.once('error', reject);
+ server.listen(0, '127.0.0.1', () => resolve());
+ });
+ const address = server.address();
+ await new Promise((resolve, reject) => {
+ server.close((error) => (error ? reject(error) : resolve()));
+ });
+
+ if (typeof address === 'object' && address !== null) {
+ return address.port;
+ }
+
+ throw new Error('Unable to allocate a preview port');
+}
+
+function startFixturePreview(fixtureRoot: string, port: number): PreviewServer {
+ const child = spawn(
+ 'pnpm',
+ ['run', 'preview', '--port', String(port), '--strictPort'],
+ {
+ cwd: fixtureRoot,
+ env: { ...process.env, CI: '1' },
+ stdio: ['ignore', 'pipe', 'pipe'],
+ },
+ );
+ let output = '';
+ child.stdout?.on('data', (chunk) => {
+ output += chunk.toString();
+ });
+ child.stderr?.on('data', (chunk) => {
+ output += chunk.toString();
+ });
+
+ return {
+ output: () => output,
+ process: child,
+ stop: async () => {
+ if (child.exitCode !== null) {
+ return;
+ }
+
+ child.kill('SIGTERM');
+ await new Promise((resolve) => {
+ child.once('exit', () => resolve());
+ });
+ },
+ };
+}
+
+async function fetchWhenReady(
+ url: string,
+ server: PreviewServer,
+): Promise {
+ const deadline = Date.now() + 60_000;
+ let lastError: unknown;
+
+ while (Date.now() < deadline) {
+ if (server.process.exitCode !== null) {
+ throw new Error(
+ `Preview exited before serving ${url}.\n${server.output()}`,
+ );
+ }
+
+ try {
+ return await fetch(url);
+ } catch (error) {
+ lastError = error;
+ await delay(250);
+ }
+ }
+
+ throw new Error(
+ `Timed out waiting for ${url}: ${String(lastError)}\n${server.output()}`,
+ );
+}
+
async function resolveRenderImport(entry: RenderExportCase): Promise {
const { stdout } = await execFileAsync(
'node',
@@ -348,7 +493,7 @@ function expectAllComponentsHtml(html: string, label: string): void {
expect(html).toContain('padding:1rem');
expect(html).toContain('font-size:0.875rem');
expect(html).toContain('color:rgb(21,93,252)');
- expect(html).not.toContain('class="mx-auto bg-white p-4"');
+ expect(html).not.toContain('mx-auto bg-white p-4');
expect(html).toContain(' {
});
afterAll(async () => {
- await Promise.all([cleanFixture('vite'), cleanFixture('tanstack-start')]);
+ await Promise.all([
+ cleanFixture('vite'),
+ cleanFixture('tanstack-start'),
+ cleanFixture('cloudflare-tanstack-start'),
+ ]);
await rm(path.join(e2eRoot, '.tmp'), { recursive: true, force: true });
});
@@ -401,7 +550,27 @@ describe('published package integration fixtures', () => {
}
});
- it('loads solid-email under browser import conditions', async () => {
+ it('loads solid-email root server entry in ESM and CJS', async () => {
+ const esm = await execFileAsync(
+ 'node',
+ ['--input-type=module', '--eval', solidEmailServerImportProbe],
+ { cwd: root },
+ );
+ const cjs = await execFileAsync(
+ 'node',
+ ['--eval', solidEmailServerRequireProbe],
+ { cwd: root },
+ );
+
+ expect(esm.stdout.trim().replaceAll('\\', '/')).toContain(
+ '/packages/solid-email/dist/index.mjs',
+ );
+ expect(cjs.stdout.trim().replaceAll('\\', '/')).toContain(
+ '/packages/solid-email/dist/index.cjs',
+ );
+ });
+
+ it('loads solid-email root under browser import conditions', async () => {
const { stdout } = await execFileAsync(
'node',
[
@@ -414,18 +583,30 @@ describe('published package integration fixtures', () => {
);
expect(stdout.trim().replaceAll('\\', '/')).toContain(
- '/packages/solid-email/dist/index.mjs',
+ '/packages/solid-email/dist/client/index.mjs',
+ );
+ });
+
+ it('requires solid-email root under browser conditions', async () => {
+ const { stdout } = await execFileAsync(
+ 'node',
+ ['--conditions=browser', '--eval', solidEmailBrowserRequireProbe],
+ { cwd: root },
+ );
+
+ expect(stdout.trim().replaceAll('\\', '/')).toContain(
+ '/packages/solid-email/dist/client/index.cjs',
);
});
- it('mounts the client entrypoint in a Solid DOM preview', async () => {
+ it('mounts the browser root condition in a Solid DOM preview', async () => {
const { stdout } = await execFileAsync(
'node',
[
'--conditions=browser',
'--input-type=module',
'--eval',
- solidEmailClientDomProbe,
+ solidEmailBrowserRootDomProbe,
],
{ cwd: root },
);
@@ -434,6 +615,7 @@ describe('published package integration fixtures', () => {
'/packages/solid-email/dist/client/index.mjs',
);
});
+
it('builds and renders in a Solid Vite SSR fixture', async () => {
const fixtureRoot = await installAndBuildFixture('vite');
const html = await execFileAsync('node', ['dist/entry-server.mjs'], {
@@ -472,4 +654,46 @@ describe('published package integration fixtures', () => {
);
expect(componentTypes).toContain('AllComponentsEmail');
});
+
+ it('renders solid-email through a Cloudflare TanStack Start Worker route', async () => {
+ const fixtureRoot = await installAndBuildFixture(
+ 'cloudflare-tanstack-start',
+ );
+ const routeTree = await readFile(
+ path.join(fixtureRoot, 'src/routeTree.gen.ts'),
+ 'utf8',
+ );
+ expect(routeTree).toContain('/');
+
+ const port = await getAvailablePort();
+ const preview = startFixturePreview(fixtureRoot, port);
+
+ try {
+ const response = await fetchWhenReady(
+ `http://127.0.0.1:${port}/`,
+ preview,
+ );
+ const html = await response.text();
+ const decodedHtml = html
+ .replaceAll('"', '"')
+ .replaceAll('<', '<')
+ .replaceAll('>', '>')
+ .replaceAll('&', '&');
+
+ expect(response.status, html).toBe(200);
+ expect(html).toContain('data-email-status="rendered"');
+ expect(html).toContain('Cloudflare TanStack Start Solid route loaded');
+ expectAllComponentsHtml(
+ decodedHtml,
+ 'Cloudflare TanStack Solid email rendered',
+ );
+ expect(html).toContain('Cloudflare TanStack Solid email rendered');
+ expect(html).toContain('Hello Cloudflare TanStack Worker!');
+ expect(html).toContain('Solid Start route graph');
+ expect(html).toContain('href="https://example.com/action"');
+ expect(html).toContain('@solid-email/render in Workerd');
+ } finally {
+ await preview.stop();
+ }
+ });
});
diff --git a/packages/render/src/shared/compile.ts b/packages/render/src/shared/compile.ts
index 7ee9f1a..6a69c8e 100644
--- a/packages/render/src/shared/compile.ts
+++ b/packages/render/src/shared/compile.ts
@@ -14,6 +14,7 @@ import {
removeSolidResourceScripts,
renderOutput,
renderSyncOutput,
+ solidRenderOptions,
} from './render';
import {
buildMarkerRegex,
@@ -137,7 +138,7 @@ export async function compile<
options?: CompileOptions,
): Promise> {
const html = removeSolidResourceScripts(
- await renderToStringAsync(normalizeRenderable(node)),
+ await renderToStringAsync(normalizeRenderable(node), solidRenderOptions),
);
return new CompiledTemplate(html, options);
}
@@ -149,7 +150,7 @@ export function compileSync<
throw new Error('compileSync does not support pretty output; use compile.');
}
const html = removeSolidResourceScripts(
- renderToString(normalizeRenderable(node)),
+ renderToString(normalizeRenderable(node), solidRenderOptions),
);
return new CompiledTemplate(html, options);
}
diff --git a/packages/render/src/shared/render.ts b/packages/render/src/shared/render.ts
index d53b099..adfa5aa 100644
--- a/packages/render/src/shared/render.ts
+++ b/packages/render/src/shared/render.ts
@@ -11,6 +11,7 @@ export type Renderable = JSX.Element | (() => JSX.Element);
const doctype =
'';
+export const solidRenderOptions = { renderId: 'solid-email' } as const;
export function normalizeRenderable(node: Renderable) {
return typeof node === 'function' ? (node as () => JSX.Element) : () => node;
@@ -81,7 +82,7 @@ export async function render(
options?: Options,
): Promise {
const html = removeSolidResourceScripts(
- await renderToStringAsync(normalizeRenderable(node)),
+ await renderToStringAsync(normalizeRenderable(node), solidRenderOptions),
);
return renderOutput(html, options);
@@ -96,7 +97,7 @@ export function renderSync(
}
const html = removeSolidResourceScripts(
- renderToString(normalizeRenderable(node)),
+ renderToString(normalizeRenderable(node), solidRenderOptions),
);
return renderSyncOutput(html, options);
diff --git a/packages/solid-email/README.md b/packages/solid-email/README.md
index dfb68e0..806e31b 100644
--- a/packages/solid-email/README.md
+++ b/packages/solid-email/README.md
@@ -32,13 +32,14 @@ const html = await render(() => );
## Entrypoints
-Use `@akin01/solid-email` for SSR/email HTML string rendering. It exports
-`render`, `compile`, and the full email component set, including `Tailwind`.
-
-Use `@akin01/solid-email/client` only for DOM/CSR preview mounting. It exports
-DOM-safe preview components and intentionally excludes `render`, `compile`, and
+`@akin01/solid-email` is conditionally exported. Server, Workerd, and default
+imports expose `render`, `compile`, and the full email component set, including
`Tailwind`.
+Browser-condition imports of the same package root resolve to the DOM/CSR
+preview build. That build exports DOM-safe preview components and intentionally
+excludes `render`, `compile`, and `Tailwind`.
+
## Components
Includes email-safe primitives such as `Html`, `Head`, `Preview`, `Body`, `Container`, `Section`, `Row`, `Column`, `Text`, `Heading`, `Button`, `Link`, `Img`, `Hr`, `Markdown`, `CodeInline`, `CodeBlock`, and `Tailwind`.
diff --git a/packages/solid-email/package.json b/packages/solid-email/package.json
index b38f1ac..db12714 100644
--- a/packages/solid-email/package.json
+++ b/packages/solid-email/package.json
@@ -15,6 +15,29 @@
],
"exports": {
".": {
+ "workerd": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "worker": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "edge-light": {
+ "types": "./dist/index.d.mts",
+ "default": "./dist/index.mjs"
+ },
+ "browser": {
+ "import": {
+ "types": "./dist/client/index.d.mts",
+ "default": "./dist/client/index.mjs"
+ },
+ "require": {
+ "types": "./dist/client/index.d.cts",
+ "default": "./dist/client/index.cjs"
+ },
+ "default": "./dist/client/index.mjs"
+ },
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
@@ -23,16 +46,6 @@
"types": "./dist/index.d.cts",
"default": "./dist/index.cjs"
}
- },
- "./client": {
- "import": {
- "types": "./dist/client/index.d.mts",
- "default": "./dist/client/index.mjs"
- },
- "require": {
- "types": "./dist/client/index.d.cts",
- "default": "./dist/client/index.cjs"
- }
}
},
"scripts": {
diff --git a/packages/solid-email/src/components/code-block/prism.ts b/packages/solid-email/src/components/code-block/prism.ts
index fa0ab04..1f3c072 100644
--- a/packages/solid-email/src/components/code-block/prism.ts
+++ b/packages/solid-email/src/components/code-block/prism.ts
@@ -1,17 +1,19 @@
import * as PrismImport from 'prismjs';
-import loadLanguages from 'prismjs/components/index.js';
+import 'prismjs/components/prism-python.js';
+import 'prismjs/components/prism-typescript.js';
+
+type PrismModule = typeof PrismImport;
// Prism ships as CommonJS. Depending on the bundler/runtime, the module can
// appear either as a namespace object or under `default`; normalize that once
// so the rest of CodeBlock can use the Prism API directly.
-const PrismImportWithDefault = PrismImport as typeof PrismImport & {
- default?: typeof PrismImport;
+const PrismImportWithDefault = PrismImport as PrismModule & {
+ default?: PrismModule;
};
-const Prism: typeof import('prismjs') =
- PrismImportWithDefault.default ?? PrismImport;
+const Prism: PrismModule = PrismImportWithDefault.default ?? PrismImport;
-// Register Prism component grammars into `Prism.languages`. CodeBlock looks up
-// grammars by the `language` prop before calling `Prism.tokenize(...)`.
-loadLanguages();
+// Register the grammars covered by CodeBlock's e2e/test surface without using
+// prismjs/components/index.js. That loader calls require.resolve at runtime,
+// which is not available in Workerd.
export { Prism };
diff --git a/packages/solid-email/src/components/tailwind/inline-styles.ts b/packages/solid-email/src/components/tailwind/inline-styles.ts
index 515a8b7..c6ec4c5 100644
--- a/packages/solid-email/src/components/tailwind/inline-styles.ts
+++ b/packages/solid-email/src/components/tailwind/inline-styles.ts
@@ -1,4 +1,4 @@
-import type { StyleSheet } from 'css-tree';
+import type { StyleSheet } from 'css-tree/dist/csstree.esm';
import { extractRulesPerClass } from './utils/css/extract-rules-per-class';
import { getCustomProperties } from './utils/css/get-custom-properties';
import { makeInlineStylesFor } from './utils/css/make-inline-styles-for';
diff --git a/packages/solid-email/src/components/tailwind/sanitize-stylesheet.ts b/packages/solid-email/src/components/tailwind/sanitize-stylesheet.ts
index ee046bb..ff57c2a 100644
--- a/packages/solid-email/src/components/tailwind/sanitize-stylesheet.ts
+++ b/packages/solid-email/src/components/tailwind/sanitize-stylesheet.ts
@@ -1,4 +1,4 @@
-import type { StyleSheet } from 'css-tree';
+import type { StyleSheet } from 'css-tree/dist/csstree.esm';
import { resolveAllCssVariables } from './utils/css/resolve-all-css-variables';
import { resolveCalcExpressions } from './utils/css/resolve-calc-expressions';
import { sanitizeDeclarations } from './utils/css/sanitize-declarations';
diff --git a/packages/solid-email/src/components/tailwind/tailwind.tsx b/packages/solid-email/src/components/tailwind/tailwind.tsx
index af82fbb..cd27027 100644
--- a/packages/solid-email/src/components/tailwind/tailwind.tsx
+++ b/packages/solid-email/src/components/tailwind/tailwind.tsx
@@ -1,5 +1,11 @@
-import { type CssNode, generate, List, type StyleSheet } from 'css-tree';
-import { createResource, type JSX, Suspense } from 'solid-js';
+import {
+ type CssNode,
+ generate,
+ List,
+ type StyleSheet,
+} from 'css-tree/dist/csstree.esm';
+import type { JSX } from 'solid-js';
+import { createResource, Suspense } from 'solid-js/dist/server.js';
import { ssr } from 'solid-js/web/dist/server.js';
import type { Config } from 'tailwindcss';
import type { SolidStyle } from '../shared';
diff --git a/packages/solid-email/src/components/tailwind/utils/css/downlevel-for-email-clients.ts b/packages/solid-email/src/components/tailwind/utils/css/downlevel-for-email-clients.ts
index cdf8d34..77b2be6 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/downlevel-for-email-clients.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/downlevel-for-email-clients.ts
@@ -25,7 +25,7 @@ import {
type Rule,
type StyleSheet,
walk,
-} from 'css-tree';
+} from 'css-tree/dist/csstree.esm';
/**
* css-tree 3.x introduced new AST node types for query-related at-rules that
diff --git a/packages/solid-email/src/components/tailwind/utils/css/extract-rules-per-class.ts b/packages/solid-email/src/components/tailwind/utils/css/extract-rules-per-class.ts
index 6b587f7..cc1c444 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/extract-rules-per-class.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/extract-rules-per-class.ts
@@ -1,4 +1,9 @@
-import { type CssNode, type Rule, string, walk } from 'css-tree';
+import {
+ type CssNode,
+ type Rule,
+ string,
+ walk,
+} from 'css-tree/dist/csstree.esm';
import { isRuleInlinable } from './is-rule-inlinable';
import { splitMixedRule } from './split-mixed-rule';
diff --git a/packages/solid-email/src/components/tailwind/utils/css/get-custom-properties.ts b/packages/solid-email/src/components/tailwind/utils/css/get-custom-properties.ts
index 1ba184d..c0e4af1 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/get-custom-properties.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/get-custom-properties.ts
@@ -1,4 +1,9 @@
-import { type CssNode, type Declaration, generate, walk } from 'css-tree';
+import {
+ type CssNode,
+ type Declaration,
+ generate,
+ walk,
+} from 'css-tree/dist/csstree.esm';
export interface CustomProperty {
syntax?: Declaration;
diff --git a/packages/solid-email/src/components/tailwind/utils/css/is-part-inlinable.ts b/packages/solid-email/src/components/tailwind/utils/css/is-part-inlinable.ts
index 1399cc1..2d0de0e 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/is-part-inlinable.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/is-part-inlinable.ts
@@ -1,4 +1,4 @@
-import { type CssNode, find } from 'css-tree';
+import { type CssNode, find } from 'css-tree/dist/csstree.esm';
export function isPartInlinable(part: CssNode): boolean {
const hasAtRuleInside = find(part, (node) => node.type === 'Atrule') !== null;
diff --git a/packages/solid-email/src/components/tailwind/utils/css/is-rule-inlinable.ts b/packages/solid-email/src/components/tailwind/utils/css/is-rule-inlinable.ts
index 9df47ab..2ee61e5 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/is-rule-inlinable.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/is-rule-inlinable.ts
@@ -1,4 +1,4 @@
-import { find, type Rule } from 'css-tree';
+import { find, type Rule } from 'css-tree/dist/csstree.esm';
export function isRuleInlinable(rule: Rule): boolean {
const hasAtRuleInside = find(rule, (node) => node.type === 'Atrule') !== null;
diff --git a/packages/solid-email/src/components/tailwind/utils/css/make-inline-styles-for.ts b/packages/solid-email/src/components/tailwind/utils/css/make-inline-styles-for.ts
index c0181c7..622e4a6 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/make-inline-styles-for.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/make-inline-styles-for.ts
@@ -1,4 +1,9 @@
-import { type CssNode, type Declaration, generate, walk } from 'css-tree';
+import {
+ type CssNode,
+ type Declaration,
+ generate,
+ walk,
+} from 'css-tree/dist/csstree.esm';
import { getStyleProperty } from '../compatibility/get-style-property';
import type { CustomProperties } from './get-custom-properties';
import { stripEmptyTailwindVars } from './strip-empty-tailwind-vars';
diff --git a/packages/solid-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts b/packages/solid-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts
index ad64561..df5aad5 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/resolve-all-css-variables.ts
@@ -9,7 +9,7 @@ import {
type SelectorList,
type Value,
walk,
-} from 'css-tree';
+} from 'css-tree/dist/csstree.esm';
interface VariableUse {
declaration: Declaration;
diff --git a/packages/solid-email/src/components/tailwind/utils/css/resolve-calc-expressions.ts b/packages/solid-email/src/components/tailwind/utils/css/resolve-calc-expressions.ts
index 13b2417..081031d 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/resolve-calc-expressions.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/resolve-calc-expressions.ts
@@ -1,4 +1,4 @@
-import { type CssNode, walk } from 'css-tree';
+import { type CssNode, walk } from 'css-tree/dist/csstree.esm';
/**
* Intentionally only resolves `*` and `/` operations without dealing with parenthesis, because this is the only thing required to run Tailwind v4
diff --git a/packages/solid-email/src/components/tailwind/utils/css/sanitize-declarations.ts b/packages/solid-email/src/components/tailwind/utils/css/sanitize-declarations.ts
index 3ffb64c..d079ea4 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/sanitize-declarations.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/sanitize-declarations.ts
@@ -8,7 +8,7 @@ import {
type Raw,
type Value,
walk,
-} from 'css-tree';
+} from 'css-tree/dist/csstree.esm';
function rgbNode(
r: number,
diff --git a/packages/solid-email/src/components/tailwind/utils/css/sanitize-non-inlinable-rules.ts b/packages/solid-email/src/components/tailwind/utils/css/sanitize-non-inlinable-rules.ts
index fbb30a3..de9faa5 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/sanitize-non-inlinable-rules.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/sanitize-non-inlinable-rules.ts
@@ -1,4 +1,4 @@
-import { type CssNode, string, walk } from 'css-tree';
+import { type CssNode, string, walk } from 'css-tree/dist/csstree.esm';
import { sanitizeClassName } from '../compatibility/sanitize-class-name';
import { isRuleInlinable } from './is-rule-inlinable';
import { stripEmptyTailwindVars } from './strip-empty-tailwind-vars';
diff --git a/packages/solid-email/src/components/tailwind/utils/css/split-mixed-rule.ts b/packages/solid-email/src/components/tailwind/utils/css/split-mixed-rule.ts
index 03b0d98..cb7a1e0 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/split-mixed-rule.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/split-mixed-rule.ts
@@ -1,4 +1,10 @@
-import { type CssNode, clone, find, List, type Rule } from 'css-tree';
+import {
+ type CssNode,
+ clone,
+ find,
+ List,
+ type Rule,
+} from 'css-tree/dist/csstree.esm';
import { isPartInlinable } from './is-part-inlinable';
/**
diff --git a/packages/solid-email/src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts b/packages/solid-email/src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts
index b7399c4..70c7edc 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/strip-empty-tailwind-vars.ts
@@ -1,4 +1,4 @@
-import { type CssNode, generate, walk } from 'css-tree';
+import { type CssNode, generate, walk } from 'css-tree/dist/csstree.esm';
/**
* Tailwind v4 emits variant-stacking idioms like
diff --git a/packages/solid-email/src/components/tailwind/utils/css/unwrap-value.ts b/packages/solid-email/src/components/tailwind/utils/css/unwrap-value.ts
index 81fc7ff..83821e6 100644
--- a/packages/solid-email/src/components/tailwind/utils/css/unwrap-value.ts
+++ b/packages/solid-email/src/components/tailwind/utils/css/unwrap-value.ts
@@ -1,4 +1,4 @@
-import type { Raw, Value } from 'css-tree';
+import type { Raw, Value } from 'css-tree/dist/csstree.esm';
export function unwrapValue(value: Value | Raw) {
if (value.type === 'Value' && value.children.size === 1) {
diff --git a/packages/solid-email/src/components/tailwind/utils/tailwindcss/setup-tailwind.ts b/packages/solid-email/src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
index 948fed0..9c15652 100644
--- a/packages/solid-email/src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
+++ b/packages/solid-email/src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
@@ -1,4 +1,4 @@
-import { parse, type StyleSheet } from 'css-tree';
+import { parse, type StyleSheet } from 'css-tree/dist/csstree.esm';
import { compile } from 'tailwindcss';
import type { TailwindConfig } from '../../tailwind';
import indexCss from './tailwind-stylesheets/index';
@@ -15,6 +15,8 @@ interface TailwindCompiler {
build(candidates: string[]): string;
}
+const virtualTailwindBase = 'solid-email://tailwind/';
+
async function compileTailwind(
baseCss: string,
config: TailwindConfig | undefined,
@@ -24,8 +26,8 @@ async function compileTailwind(
async loadModule(id, base, resourceHint) {
if (resourceHint === 'config') {
return {
- path: id,
- base: base,
+ path: id ?? 'solid-email-tailwind.config.js',
+ base: base ?? virtualTailwindBase,
module: config ?? {},
};
}
@@ -36,9 +38,11 @@ async function compileTailwind(
},
polyfills: 0, // All
async loadStylesheet(id, base) {
+ const stylesheetBase = base ?? virtualTailwindBase;
+
if (id === 'tailwindcss') {
return {
- base,
+ base: stylesheetBase,
path: 'tailwindcss/index.css',
content: indexCss,
};
@@ -46,7 +50,7 @@ async function compileTailwind(
if (id === 'tailwindcss/preflight.css') {
return {
- base,
+ base: stylesheetBase,
path: id,
content: preflightCss,
};
@@ -54,7 +58,7 @@ async function compileTailwind(
if (id === 'tailwindcss/theme.css') {
return {
- base,
+ base: stylesheetBase,
path: id,
content: themeCss,
};
@@ -62,7 +66,7 @@ async function compileTailwind(
if (id === 'tailwindcss/utilities.css') {
return {
- base,
+ base: stylesheetBase,
path: id,
content: utilitiesCss,
};
@@ -70,7 +74,7 @@ async function compileTailwind(
if (id === 'custom-theme.css') {
return {
- base,
+ base: stylesheetBase,
path: id,
content: cssConfigs?.theme ?? '',
};
@@ -78,7 +82,7 @@ async function compileTailwind(
if (id === 'custom-utilities.css') {
return {
- base,
+ base: stylesheetBase,
path: id,
content: cssConfigs?.utility ?? '',
};
diff --git a/packages/solid-email/src/index.spec.tsx b/packages/solid-email/src/index.spec.tsx
index 16a91d9..ddfd3ef 100644
--- a/packages/solid-email/src/index.spec.tsx
+++ b/packages/solid-email/src/index.spec.tsx
@@ -1,24 +1,24 @@
import { describe, expect, it } from 'vitest';
-import * as client from './client';
+import * as browserRoot from './client';
import {
- Body as ClientBody,
- Button as ClientButton,
- CodeBlock as ClientCodeBlock,
- CodeInline as ClientCodeInline,
- Column as ClientColumn,
- Container as ClientContainer,
- Font as ClientFont,
- Head as ClientHead,
- Heading as ClientHeading,
- Hr as ClientHr,
- Html as ClientHtml,
- Img as ClientImg,
- Link as ClientLink,
- Markdown as ClientMarkdown,
- Preview as ClientPreview,
- Row as ClientRow,
- Section as ClientSection,
- Text as ClientText,
+ Body as BrowserRootBody,
+ Button as BrowserRootButton,
+ CodeBlock as BrowserRootCodeBlock,
+ CodeInline as BrowserRootCodeInline,
+ Column as BrowserRootColumn,
+ Container as BrowserRootContainer,
+ Font as BrowserRootFont,
+ Head as BrowserRootHead,
+ Heading as BrowserRootHeading,
+ Hr as BrowserRootHr,
+ Html as BrowserRootHtml,
+ Img as BrowserRootImg,
+ Link as BrowserRootLink,
+ Markdown as BrowserRootMarkdown,
+ Preview as BrowserRootPreview,
+ Row as BrowserRootRow,
+ Section as BrowserRootSection,
+ Text as BrowserRootText,
} from './client';
import {
Body,
@@ -70,25 +70,25 @@ const componentExports = {
Text,
};
-const clientComponentExports = {
- Body: ClientBody,
- Button: ClientButton,
- CodeBlock: ClientCodeBlock,
- CodeInline: ClientCodeInline,
- Column: ClientColumn,
- Container: ClientContainer,
- Font: ClientFont,
- Head: ClientHead,
- Heading: ClientHeading,
- Hr: ClientHr,
- Html: ClientHtml,
- Img: ClientImg,
- Link: ClientLink,
- Markdown: ClientMarkdown,
- Preview: ClientPreview,
- Row: ClientRow,
- Section: ClientSection,
- Text: ClientText,
+const browserRootComponentExports = {
+ Body: BrowserRootBody,
+ Button: BrowserRootButton,
+ CodeBlock: BrowserRootCodeBlock,
+ CodeInline: BrowserRootCodeInline,
+ Column: BrowserRootColumn,
+ Container: BrowserRootContainer,
+ Font: BrowserRootFont,
+ Head: BrowserRootHead,
+ Heading: BrowserRootHeading,
+ Hr: BrowserRootHr,
+ Html: BrowserRootHtml,
+ Img: BrowserRootImg,
+ Link: BrowserRootLink,
+ Markdown: BrowserRootMarkdown,
+ Preview: BrowserRootPreview,
+ Row: BrowserRootRow,
+ Section: BrowserRootSection,
+ Text: BrowserRootText,
};
describe('public entrypoint', () => {
@@ -141,14 +141,16 @@ describe('public entrypoint', () => {
});
});
-describe('client entrypoint', () => {
+describe('browser root entrypoint', () => {
it('exports DOM-safe preview components without render utilities or Tailwind', () => {
- for (const [name, component] of Object.entries(clientComponentExports)) {
+ for (const [name, component] of Object.entries(
+ browserRootComponentExports,
+ )) {
expect(component, name).toBeTypeOf('function');
}
- expect('render' in client).toBe(false);
- expect('compile' in client).toBe(false);
- expect('Tailwind' in client).toBe(false);
+ expect('render' in browserRoot).toBe(false);
+ expect('compile' in browserRoot).toBe(false);
+ expect('Tailwind' in browserRoot).toBe(false);
});
});
diff --git a/packages/solid-email/src/render-shim.d.ts b/packages/solid-email/src/render-shim.d.ts
index 4b74509..0824def 100644
--- a/packages/solid-email/src/render-shim.d.ts
+++ b/packages/solid-email/src/render-shim.d.ts
@@ -95,3 +95,11 @@ declare module 'solid-js/web/dist/server.js' {
ssr,
} from 'solid-js/web';
}
+
+declare module 'solid-js/dist/server.js' {
+ export { createResource, Suspense } from 'solid-js';
+}
+
+declare module 'css-tree/dist/csstree.esm' {
+ export * from 'css-tree';
+}
diff --git a/packages/solid-email/tsdown.config.ts b/packages/solid-email/tsdown.config.ts
index cb77c89..ae76392 100644
--- a/packages/solid-email/tsdown.config.ts
+++ b/packages/solid-email/tsdown.config.ts
@@ -18,10 +18,11 @@ const base: Pick = {
export default defineConfig([
{
...base,
- // Package root stays SSR/email-rendering oriented so render(), compile(),
- // and every email component are safe under browser-like import conditions.
+ // Package root stays SSR/email-rendering oriented. Workerd/Worker package
+ // conditions point at this same ESM build instead of a second server bundle.
entry: ['./src/index.ts'],
outDir: './dist',
+ platform: 'browser',
plugins: [
solid({
solid: {
@@ -34,9 +35,9 @@ export default defineConfig([
},
{
...base,
- // Explicit ./client subpath is a DOM/CSR build for previews. It excludes
- // render/compile and server-only components instead of using browser
- // conditions, because browser users may still render email HTML strings.
+ // Browser condition uses this DOM/CSR build for previews. It excludes
+ // render/compile and Tailwind while keeping the public import specifier at
+ // the package root.
entry: ['./src/client/index.ts'],
outDir: './dist/client',
platform: 'browser',
diff --git a/skills/solid-email/README.md b/skills/solid-email/README.md
index 39439fc..1f01aa9 100644
--- a/skills/solid-email/README.md
+++ b/skills/solid-email/README.md
@@ -24,6 +24,8 @@ skills/
- Using Tailwind utility inlining for email clients.
- Writing email-client-safe markup and styles.
- Integrating rendering in Node, Vite, TanStack Start, worker, and edge-style runtimes.
+- Previewing components in the browser through the `@akin01/solid-email`
+ browser-condition root, which resolves to the DOM-safe Solid DOM build.
- Keeping template code type-safe.
## Usage
diff --git a/skills/solid-email/SKILL.md b/skills/solid-email/SKILL.md
index a06304d..c89dd6e 100644
--- a/skills/solid-email/SKILL.md
+++ b/skills/solid-email/SKILL.md
@@ -79,11 +79,13 @@ const html = renderSync(() => (
## DOM/CSR preview entrypoint
-Use `@akin01/solid-email/client` only when mounting email components into the browser DOM for previews. Pair it with Solid's DOM renderer from `solid-js/web`.
+Use the `@akin01/solid-email` package root when mounting email components into
+the browser DOM for previews. Browser-condition bundlers resolve the root to the
+DOM-safe Solid DOM build. Pair it with Solid's DOM renderer from `solid-js/web`.
```tsx
import { render as mount } from 'solid-js/web';
-import { Body, Container, Heading, Html, Text } from '@akin01/solid-email/client';
+import { Body, Container, Heading, Html, Text } from '@akin01/solid-email';
mount(
() => (
@@ -100,7 +102,9 @@ mount(
);
```
-Do not import `render`, `compile`, or `Tailwind` from the client subpath. Use the package root or `@solid-email/render` for server/email HTML string rendering.
+Browser-condition root imports intentionally omit `render`, `compile`, and
+`Tailwind`. Use default/server root imports or `@solid-email/render` for
+send-ready email HTML strings.
## Compile for repeated renders
diff --git a/skills/solid-email/references/RENDERING.md b/skills/solid-email/references/RENDERING.md
index 485e462..276f299 100644
--- a/skills/solid-email/references/RENDERING.md
+++ b/skills/solid-email/references/RENDERING.md
@@ -17,11 +17,14 @@ Use `render()` by default. It supports async Solid rendering and Suspense waitin
## DOM/CSR preview mounting
-Use the DOM-safe client subpath only for browser previews that mount components into a real DOM. It intentionally excludes `render`, `compile`, and `Tailwind`.
+Use the `@akin01/solid-email` package root for browser previews that mount
+components into a real DOM. Browser-condition bundlers resolve the root to the
+DOM-safe Solid DOM build, which intentionally excludes `render`, `compile`, and
+`Tailwind`.
```tsx
import { render as mount } from 'solid-js/web';
-import { Body, Container, Heading, Html, Text } from '@akin01/solid-email/client';
+import { Body, Container, Heading, Html, Text } from '@akin01/solid-email';
mount(
() => (
@@ -38,7 +41,8 @@ mount(
);
```
-Keep email HTML generation on the server with `@solid-email/render` or the `@akin01/solid-email` package root. The client subpath is for mounted previews, not send-ready HTML strings.
+Keep send-ready email HTML generation on the server with `@solid-email/render`
+or default/server imports from the `@akin01/solid-email` package root.
## Pretty HTML