diff --git a/README.md b/README.md
index 7374f06..0629521 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,16 @@ import { WelcomeEmail } from './welcome-email';
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
+`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/integrations.e2e.spec.ts b/e2e/integrations.e2e.spec.ts
index 2def7d9..1df6e83 100644
--- a/e2e/integrations.e2e.spec.ts
+++ b/e2e/integrations.e2e.spec.ts
@@ -140,6 +140,89 @@ const renderRequireProbe = `${renderExportSmoke}
process.exit(1);
});`;
+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']) {
+ if (typeof mod[name] !== 'function') {
+ throw new Error(\`missing solid-email 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',
+ }),
+ }),
+ }),
+);
+
+if (!html.includes('
process.exit(0));`;
+
+const solidEmailClientDomProbe = `
+const resolved = import.meta.resolve('@akin01/solid-email/client');
+const { JSDOM } = await import('jsdom');
+const dom = new JSDOM('', {
+ url: 'https://solid.email/preview',
+});
+globalThis.window = dom.window;
+globalThis.document = dom.window.document;
+globalThis.Node = dom.window.Node;
+globalThis.HTMLElement = dom.window.HTMLElement;
+globalThis.Element = dom.window.Element;
+
+const mod = await import('@akin01/solid-email/client');
+
+for (const name of ['render', 'compile', 'Tailwind']) {
+ if (name in mod) {
+ throw new Error(\`unexpected client export: \${name}\`);
+ }
+}
+for (const name of ['Container', 'Heading', 'Text', 'Preview']) {
+ if (typeof mod[name] !== 'function') {
+ throw new Error(\`missing client export: \${name}\`);
+ }
+}
+
+const { render: mount } = await import('solid-js/web');
+const root = document.getElementById('root');
+const dispose = mount(
+ () =>
+ mod.Container({
+ style: { padding: '12px' },
+ children: mod.Heading({
+ as: 'h2',
+ style: { color: 'purple' },
+ children: mod.Text({ children: 'Client 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 (!root.querySelector('table')) {
+ throw new Error(\`missing mounted email layout table: \${root.innerHTML}\`);
+}
+
+dispose();
+process.stdout.write(resolved.replaceAll('\\\\', '/') + '\\n', () => process.exit(0));`;
+
type PnpmError = ExecFileException & {
stderr?: string;
stdout?: string;
@@ -318,6 +401,39 @@ describe('published package integration fixtures', () => {
}
});
+ it('loads solid-email under browser import conditions', async () => {
+ const { stdout } = await execFileAsync(
+ 'node',
+ [
+ '--conditions=browser',
+ '--input-type=module',
+ '--eval',
+ solidEmailBrowserImportProbe,
+ ],
+ { cwd: root },
+ );
+
+ expect(stdout.trim().replaceAll('\\', '/')).toContain(
+ '/packages/solid-email/dist/index.mjs',
+ );
+ });
+
+ it('mounts the client entrypoint in a Solid DOM preview', async () => {
+ const { stdout } = await execFileAsync(
+ 'node',
+ [
+ '--conditions=browser',
+ '--input-type=module',
+ '--eval',
+ solidEmailClientDomProbe,
+ ],
+ { cwd: root },
+ );
+
+ expect(stdout.trim().replaceAll('\\', '/')).toContain(
+ '/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'], {
diff --git a/package.json b/package.json
index 4fc2488..a337286 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
"@biomejs/biome": "catalog:",
"@solid-email/render": "workspace:*",
"@types/node": "catalog:",
+ "jsdom": "catalog:",
"solid-js": "catalog:",
"tsx": "catalog:",
"typescript": "catalog:",
diff --git a/packages/render/src/shared/slots.ts b/packages/render/src/shared/slots.ts
index 24bd1c7..35d9e04 100644
--- a/packages/render/src/shared/slots.ts
+++ b/packages/render/src/shared/slots.ts
@@ -1,5 +1,5 @@
import type { JSX } from 'solid-js';
-import { ssr } from 'solid-js/web';
+import { ssr } from 'solid-js/web/dist/server.js';
const MARKER_PREFIX = '__SM_';
const CONTENT_START = `${MARKER_PREFIX}CNT_`;
diff --git a/packages/render/src/shared/solid-js-web-server.d.ts b/packages/render/src/shared/solid-js-web-server.d.ts
index a09f553..505743d 100644
--- a/packages/render/src/shared/solid-js-web-server.d.ts
+++ b/packages/render/src/shared/solid-js-web-server.d.ts
@@ -1,3 +1,8 @@
declare module 'solid-js/web/dist/server.js' {
- export { renderToString, renderToStringAsync } from 'solid-js/web';
+ export {
+ Dynamic,
+ renderToString,
+ renderToStringAsync,
+ ssr,
+ } from 'solid-js/web';
}
diff --git a/packages/solid-email/README.md b/packages/solid-email/README.md
index 37facda..dfb68e0 100644
--- a/packages/solid-email/README.md
+++ b/packages/solid-email/README.md
@@ -30,6 +30,15 @@ function WelcomeEmail() {
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
+`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 d4e0950..cb4a450 100644
--- a/packages/solid-email/package.json
+++ b/packages/solid-email/package.json
@@ -23,6 +23,16 @@
"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/client/index.ts b/packages/solid-email/src/client/index.ts
new file mode 100644
index 0000000..ed37b36
--- /dev/null
+++ b/packages/solid-email/src/client/index.ts
@@ -0,0 +1 @@
+export * from '../components/client';
diff --git a/packages/solid-email/src/components/client.ts b/packages/solid-email/src/components/client.ts
new file mode 100644
index 0000000..6184458
--- /dev/null
+++ b/packages/solid-email/src/components/client.ts
@@ -0,0 +1,18 @@
+export * from './body/index';
+export * from './button/index';
+export * from './code-block/index';
+export * from './code-inline/index';
+export * from './column/index';
+export * from './container/index';
+export * from './font/index';
+export * from './head/index';
+export * from './heading/heading.client';
+export * from './hr/index';
+export * from './html/index';
+export * from './img/index';
+export * from './link/index';
+export * from './markdown/index';
+export * from './preview/index';
+export * from './row/index';
+export * from './section/index';
+export * from './text/index';
diff --git a/packages/solid-email/src/components/heading/heading.client.tsx b/packages/solid-email/src/components/heading/heading.client.tsx
new file mode 100644
index 0000000..963ab2e
--- /dev/null
+++ b/packages/solid-email/src/components/heading/heading.client.tsx
@@ -0,0 +1,45 @@
+import { splitProps } from 'solid-js';
+import { Dynamic } from 'solid-js/web';
+import {
+ cls,
+ type IntrinsicProps,
+ normalizeStyle,
+ styleObject,
+ withoutClass,
+} from '../shared';
+import type { As } from './utils/as';
+import type { Margin } from './utils/spaces';
+import { withMargin } from './utils/spaces';
+
+export type HeadingAs = As<'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'>;
+export type HeadingProps = Readonly & HeadingAs & Margin>;
+
+export function Heading(props: HeadingProps) {
+ const [local, rest] = splitProps(props, [
+ 'as',
+ 'children',
+ 'style',
+ 'm',
+ 'mx',
+ 'my',
+ 'mt',
+ 'mr',
+ 'mb',
+ 'ml',
+ 'class',
+ 'className',
+ ]);
+ return (
+
+ {local.children}
+
+ );
+}
diff --git a/packages/solid-email/src/components/heading/heading.tsx b/packages/solid-email/src/components/heading/heading.tsx
index db4f6eb..bc84627 100644
--- a/packages/solid-email/src/components/heading/heading.tsx
+++ b/packages/solid-email/src/components/heading/heading.tsx
@@ -1,5 +1,5 @@
import { splitProps } from 'solid-js';
-import { Dynamic } from 'solid-js/web';
+import { Dynamic } from 'solid-js/web/dist/server.js';
import {
cls,
type IntrinsicProps,
diff --git a/packages/solid-email/src/components/tailwind/tailwind.tsx b/packages/solid-email/src/components/tailwind/tailwind.tsx
index 9e898dc..af82fbb 100644
--- a/packages/solid-email/src/components/tailwind/tailwind.tsx
+++ b/packages/solid-email/src/components/tailwind/tailwind.tsx
@@ -1,6 +1,6 @@
import { type CssNode, generate, List, type StyleSheet } from 'css-tree';
import { createResource, type JSX, Suspense } from 'solid-js';
-import { ssr } from 'solid-js/web';
+import { ssr } from 'solid-js/web/dist/server.js';
import type { Config } from 'tailwindcss';
import type { SolidStyle } from '../shared';
import { sanitizeStyleSheet } from './sanitize-stylesheet';
diff --git a/packages/solid-email/src/index.spec.tsx b/packages/solid-email/src/index.spec.tsx
index c5d782f..16a91d9 100644
--- a/packages/solid-email/src/index.spec.tsx
+++ b/packages/solid-email/src/index.spec.tsx
@@ -1,4 +1,25 @@
import { describe, expect, it } from 'vitest';
+import * as client 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,
+} from './client';
import {
Body,
Button,
@@ -49,6 +70,27 @@ 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,
+};
+
describe('public entrypoint', () => {
it('exports every public component from the package root', () => {
for (const [name, component] of Object.entries(componentExports)) {
@@ -98,3 +140,15 @@ describe('public entrypoint', () => {
expect(syncHtml).toContain('Sync template');
});
});
+
+describe('client entrypoint', () => {
+ it('exports DOM-safe preview components without render utilities or Tailwind', () => {
+ for (const [name, component] of Object.entries(clientComponentExports)) {
+ expect(component, name).toBeTypeOf('function');
+ }
+
+ expect('render' in client).toBe(false);
+ expect('compile' in client).toBe(false);
+ expect('Tailwind' in client).toBe(false);
+ });
+});
diff --git a/packages/solid-email/src/render-shim.d.ts b/packages/solid-email/src/render-shim.d.ts
index a9fce7e..4b74509 100644
--- a/packages/solid-email/src/render-shim.d.ts
+++ b/packages/solid-email/src/render-shim.d.ts
@@ -86,3 +86,12 @@ declare module '@solid-email/render' {
options?: CompileSyncOptions,
): CompiledTemplate;
}
+
+declare module 'solid-js/web/dist/server.js' {
+ export {
+ Dynamic,
+ renderToString,
+ renderToStringAsync,
+ ssr,
+ } from 'solid-js/web';
+}
diff --git a/packages/solid-email/tsdown.config.ts b/packages/solid-email/tsdown.config.ts
index cebeb80..cb77c89 100644
--- a/packages/solid-email/tsdown.config.ts
+++ b/packages/solid-email/tsdown.config.ts
@@ -1,11 +1,52 @@
-import { defineConfig } from 'tsdown';
+import { defineConfig, type UserConfig } from 'tsdown';
import solid from 'vite-plugin-solid';
-export default defineConfig({
+const base: Pick = {
dts: true,
- entry: ['./src/index.ts'],
- deps: { neverBundle: ['solid-js', 'solid-js/web', '@solid-email/render'] },
+ deps: {
+ neverBundle: [
+ 'solid-js',
+ 'solid-js/web',
+ 'solid-js/web/dist/server.js',
+ '@solid-email/render',
+ ],
+ },
format: ['cjs', 'esm'],
- outDir: './dist',
- plugins: [solid({ solid: { generate: 'ssr', hydratable: false } })],
-});
+ fixedExtension: true,
+};
+
+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.
+ entry: ['./src/index.ts'],
+ outDir: './dist',
+ plugins: [
+ solid({
+ solid: {
+ generate: 'ssr',
+ hydratable: false,
+ moduleName: 'solid-js/web/dist/server.js',
+ },
+ }),
+ ],
+ },
+ {
+ ...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.
+ entry: ['./src/client/index.ts'],
+ outDir: './dist/client',
+ platform: 'browser',
+ plugins: [
+ solid({
+ solid: {
+ generate: 'dom',
+ hydratable: false,
+ },
+ }),
+ ],
+ },
+]);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3f9380c..e0c6186 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -30,6 +30,9 @@ catalogs:
css-tree:
specifier: 3.2.1
version: 3.2.1
+ jsdom:
+ specifier: 29.1.1
+ version: 29.1.1
marked:
specifier: 18.0.5
version: 18.0.5
@@ -80,6 +83,9 @@ importers:
'@types/node':
specifier: 'catalog:'
version: 25.0.2
+ jsdom:
+ specifier: 'catalog:'
+ version: 29.1.1
solid-js:
specifier: 'catalog:'
version: 1.9.13
@@ -2475,7 +2481,6 @@ snapshots:
'@csstools/css-color-parser': 4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- optional: true
'@asamuzakjp/dom-selector@7.1.1':
dependencies:
@@ -2484,13 +2489,10 @@ snapshots:
bidi-js: 1.0.3
css-tree: 3.2.1
is-potential-custom-element-name: 1.0.1
- optional: true
- '@asamuzakjp/generational-cache@1.0.1':
- optional: true
+ '@asamuzakjp/generational-cache@1.0.1': {}
- '@asamuzakjp/nwsapi@2.3.9':
- optional: true
+ '@asamuzakjp/nwsapi@2.3.9': {}
'@babel/code-frame@7.27.1':
dependencies:
@@ -2685,16 +2687,13 @@ snapshots:
'@bramus/specificity@2.4.2':
dependencies:
css-tree: 3.2.1
- optional: true
- '@csstools/color-helpers@6.0.2':
- optional: true
+ '@csstools/color-helpers@6.0.2': {}
'@csstools/css-calc@3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- optional: true
'@csstools/css-color-parser@4.1.8(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)':
dependencies:
@@ -2702,20 +2701,16 @@ snapshots:
'@csstools/css-calc': 3.2.1(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)
'@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0)
'@csstools/css-tokenizer': 4.0.0
- optional: true
'@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)':
dependencies:
'@csstools/css-tokenizer': 4.0.0
- optional: true
'@csstools/css-syntax-patches-for-csstree@1.1.5(css-tree@3.2.1)':
optionalDependencies:
css-tree: 3.2.1
- optional: true
- '@csstools/css-tokenizer@4.0.0':
- optional: true
+ '@csstools/css-tokenizer@4.0.0': {}
'@emnapi/core@1.11.1':
dependencies:
@@ -2889,8 +2884,7 @@ snapshots:
'@esbuild/win32-x64@0.28.1':
optional: true
- '@exodus/bytes@1.15.1':
- optional: true
+ '@exodus/bytes@1.15.1': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
@@ -3423,7 +3417,6 @@ snapshots:
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
- optional: true
birpc@4.0.0: {}
@@ -3493,7 +3486,6 @@ snapshots:
whatwg-url: 16.0.1
transitivePeerDependencies:
- '@noble/hashes'
- optional: true
debounce-fn@6.0.0:
dependencies:
@@ -3505,8 +3497,7 @@ snapshots:
dependencies:
ms: 2.1.3
- decimal.js@10.6.0:
- optional: true
+ decimal.js@10.6.0: {}
deepmerge-ts@7.1.5: {}
@@ -3571,8 +3562,7 @@ snapshots:
entities@7.0.1: {}
- entities@8.0.0:
- optional: true
+ entities@8.0.0: {}
env-paths@3.0.0: {}
@@ -3689,7 +3679,6 @@ snapshots:
'@exodus/bytes': 1.15.1
transitivePeerDependencies:
- '@noble/hashes'
- optional: true
html-entities@2.3.3: {}
@@ -3717,8 +3706,7 @@ snapshots:
import-without-cache@0.4.0: {}
- is-potential-custom-element-name@1.0.1:
- optional: true
+ is-potential-custom-element-name@1.0.1: {}
is-unicode-supported@2.1.0: {}
@@ -3761,7 +3749,6 @@ snapshots:
xml-name-validator: 5.0.0
transitivePeerDependencies:
- '@noble/hashes'
- optional: true
jsesc@3.1.0: {}
@@ -3900,7 +3887,6 @@ snapshots:
parse5@8.0.1:
dependencies:
entities: 8.0.0
- optional: true
parseley@0.12.1:
dependencies:
@@ -3944,8 +3930,7 @@ snapshots:
kleur: 3.0.3
sisteransi: 1.0.5
- punycode@2.3.1:
- optional: true
+ punycode@2.3.1: {}
quansync@1.0.0: {}
@@ -4068,7 +4053,6 @@ snapshots:
saxes@6.0.0:
dependencies:
xmlchars: 2.2.0
- optional: true
scheduler@0.27.0: {}
@@ -4157,8 +4141,7 @@ snapshots:
stubborn-utils@1.0.2: {}
- symbol-tree@3.2.4:
- optional: true
+ symbol-tree@3.2.4: {}
tagged-tag@1.0.0: {}
@@ -4175,23 +4158,19 @@ snapshots:
tinyrainbow@3.1.0: {}
- tldts-core@7.4.3:
- optional: true
+ tldts-core@7.4.3: {}
tldts@7.4.3:
dependencies:
tldts-core: 7.4.3
- optional: true
tough-cookie@6.0.1:
dependencies:
tldts: 7.4.3
- optional: true
tr46@6.0.0:
dependencies:
punycode: 2.3.1
- optional: true
tree-kill@1.2.2: {}
@@ -4254,8 +4233,7 @@ snapshots:
undici-types@7.16.0: {}
- undici@7.28.0:
- optional: true
+ undici@7.28.0: {}
unplugin@3.0.0:
dependencies:
@@ -4334,15 +4312,12 @@ snapshots:
w3c-xmlserializer@5.0.0:
dependencies:
xml-name-validator: 5.0.0
- optional: true
- webidl-conversions@8.0.1:
- optional: true
+ webidl-conversions@8.0.1: {}
webpack-virtual-modules@0.6.2: {}
- whatwg-mimetype@5.0.0:
- optional: true
+ whatwg-mimetype@5.0.0: {}
whatwg-url@16.0.1:
dependencies:
@@ -4351,7 +4326,6 @@ snapshots:
webidl-conversions: 8.0.1
transitivePeerDependencies:
- '@noble/hashes'
- optional: true
when-exit@2.1.5: {}
@@ -4362,8 +4336,7 @@ snapshots:
ws@8.21.0: {}
- xml-name-validator@5.0.0:
- optional: true
+ xml-name-validator@5.0.0: {}
xmlbuilder2@4.0.3:
dependencies:
@@ -4372,8 +4345,7 @@ snapshots:
'@oozcitak/util': 10.0.0
js-yaml: 4.2.0
- xmlchars@2.2.0:
- optional: true
+ xmlchars@2.2.0: {}
yallist@3.1.1: {}