diff --git a/apps/preview/app/src/components/sidebar.tsx b/apps/preview/app/src/components/sidebar.tsx
index ca2a38b9..b500a221 100644
--- a/apps/preview/app/src/components/sidebar.tsx
+++ b/apps/preview/app/src/components/sidebar.tsx
@@ -9,7 +9,7 @@ import { Link, useLocation } from 'react-router-dom';
import { useAppStore } from '../composables/useAppStore';
import type { TemplatePart } from '../lib/types';
-import { Logo } from './Logo';
+import { Logo } from './logo';
import { Separator } from './ui/Separator';
interface DirectoryTreeProps {
diff --git a/apps/preview/app/src/layouts/Shell.tsx b/apps/preview/app/src/layouts/Shell.tsx
index 95f91406..585bc3ca 100644
--- a/apps/preview/app/src/layouts/Shell.tsx
+++ b/apps/preview/app/src/layouts/Shell.tsx
@@ -1,6 +1,6 @@
import { Outlet } from 'react-router-dom';
-import { Header, Sidebar } from '../components/Sidebar';
+import { Header, Sidebar } from '../components/sidebar';
export const Shell = () => (
<>
diff --git a/docs/components/graph.md b/docs/components/graph.md
index 0c2eea36..c88a928b 100644
--- a/docs/components/graph.md
+++ b/docs/components/graph.md
@@ -18,7 +18,7 @@ This component is wrapper around [QuickChart API](https://quickchart.io/) for ge
Add the graph component to your email template.
```jsx
-import { Html, Body, Section, Graph } from 'jsx-email';
+import { Body, Graph, Html, Section } from 'jsx-email';
const Email = () => {
return (
diff --git a/docs/core/compile.md b/docs/core/compile.md
index c2c38bbd..65298c21 100644
--- a/docs/core/compile.md
+++ b/docs/core/compile.md
@@ -28,10 +28,10 @@ const compiledFiles = await compile({ files: [templatePath], hashFiles: false, o
Once compiled into a bundle, the file can be imported and passed to render such like:
```jsx
-import { Template } from './.compiled/batman.js';
-
import { render } from 'jsx-email';
+import { Template } from './.compiled/batman.js';
+
const html = render();
```
diff --git a/docs/core/plugins.md b/docs/core/plugins.md
index 343fb725..959f5d7e 100644
--- a/docs/core/plugins.md
+++ b/docs/core/plugins.md
@@ -33,6 +33,7 @@ To instruct a render to use plugins, utilize a [Configuration File](/docs/core/c
```js
import { defineConfig } from 'jsx-email/config';
+
import { somePlugin } from './plugins/some-plugin';
export const config = defineConfig({
diff --git a/packages/create-mail/generators/package.json.mustache b/packages/create-mail/generators/package.json.mustache
index 543fe6e3..9c95408a 100644
--- a/packages/create-mail/generators/package.json.mustache
+++ b/packages/create-mail/generators/package.json.mustache
@@ -12,6 +12,7 @@
"jsx-email": "^2.0.0"
},
"devDependencies": {
- "react": "^19.1.0"{{{ typeDep }}}
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0"{{{ typeDep }}}
}
}
diff --git a/packages/jsx-email/src/cli/commands/preview.ts b/packages/jsx-email/src/cli/commands/preview.ts
index 1655ddea..8a6c61fb 100644
--- a/packages/jsx-email/src/cli/commands/preview.ts
+++ b/packages/jsx-email/src/cli/commands/preview.ts
@@ -1,7 +1,9 @@
/* eslint-disable no-use-before-define */
+import { AssertionError } from 'node:assert';
import { existsSync } from 'node:fs';
import { mkdir, rmdir } from 'node:fs/promises';
-import { join, resolve } from 'node:path';
+import os from 'node:os';
+import { isAbsolute, join, resolve, win32 } from 'node:path';
import react from '@vitejs/plugin-react';
import chalk from 'chalk-template';
@@ -11,7 +13,7 @@ import { parse as assert } from 'valibot';
import { type InlineConfig, createServer, build as viteBuild } from 'vite';
import { log } from '../../log.js';
-import { buildForPreview, writePreviewDataFiles } from '../helpers.js';
+import { buildForPreview, originalCwd, writePreviewDataFiles } from '../helpers.js';
import { reloadPlugin } from '../vite-reload.js';
import { staticPlugin } from '../vite-static.js';
import { watch } from '../watcher.js';
@@ -37,7 +39,7 @@ Starts the preview server for a directory of email templates
$ email preview [...options]
{underline Options}
- --build-path An absolute path. When set, builds the preview as a deployable app and saves to disk
+ --build-path When set, builds the preview as a deployable app and saves to disk
--exclude A micromatch glob pattern that specifies files to exclude from the preview
--host Allow thew preview server to listen on all addresses (0.0.0.0)
--no-open Do not open a browser tab when the preview server starts
@@ -45,6 +47,7 @@ Starts the preview server for a directory of email templates
{underline Examples}
$ email preview ./src/templates --port 55420
+ $ email preview ./src/templates --build-path ./.deploy
$ email preview ./src/templates --build-path /tmp/email-preview
`;
@@ -55,19 +58,19 @@ const buildDeployable = async ({ argv, targetPath }: PreviewCommonParams) => {
);
}
- const { basePath = './', buildPath } = argv;
+ const { basePath = './', buildPath = './.deploy' } = argv;
const common = { argv, targetPath };
await prepareBuild(common);
const config = await getConfig(common);
+ const outDir = isAbsolute(buildPath) ? buildPath : resolve(join(originalCwd, buildPath));
await viteBuild({
...config,
base: basePath,
build: {
minify: false,
- outDir: buildPath,
+ outDir,
rollupOptions: {
- external: ['react/jsx-runtime'],
output: {
manualChunks: {}
}
@@ -94,6 +97,22 @@ const getConfig = async ({ argv, targetPath }: PreviewCommonParams) => {
log.debug(`Vite Root: ${root}`);
+ // On Windows, Vite's import.meta.glob cannot cross drive letters. If the
+ // preview app root and the temporary build directory are on different
+ // drives (e.g., D: vs C:), fail fast with a helpful error.
+ if (os.platform() === 'win32') {
+ const rootDrive = getDriveLetter(root);
+ const buildDrive = getDriveLetter(buildPath);
+ if (rootDrive && buildDrive && rootDrive !== buildDrive) {
+ log.error(
+ `jsx-email preview cannot run on Windows when the application root directory and the system temporary directory are on different drive letters. Please consider using WSL`
+ );
+ throw new AssertionError({
+ message: `Temporary directory drive letter different than root directory drive letter`
+ });
+ }
+ }
+
newline();
log.info(chalk`{blue Starting build...}`);
@@ -127,6 +146,11 @@ const getConfig = async ({ argv, targetPath }: PreviewCommonParams) => {
return config;
};
+const getDriveLetter = (path: string) => {
+ if (os.platform() !== 'win32') return null;
+ return win32.parse(path).root.slice(0, 2).toUpperCase();
+};
+
const prepareBuild = async ({ targetPath, argv }: PreviewCommonParams) => {
const buildPath = await getTempPath('preview');
const { exclude } = argv;
diff --git a/packages/jsx-email/src/cli/vite-static.ts b/packages/jsx-email/src/cli/vite-static.ts
index bb0ff288..81bd0c46 100644
--- a/packages/jsx-email/src/cli/vite-static.ts
+++ b/packages/jsx-email/src/cli/vite-static.ts
@@ -3,7 +3,7 @@ import { extname } from 'node:path';
import { globby } from 'globby';
import mime from 'mime-types';
-import type { PluginOption, ViteDevServer } from 'vite';
+import { type PluginOption, type ViteDevServer, normalizePath } from 'vite';
interface ViteStaticOptions {
paths: string[];
@@ -50,7 +50,7 @@ interface MiddlwareParams {
const middleware = async (params: MiddlwareParams) => {
const { options, server } = params;
const { paths } = options;
- const files = await globby(paths);
+ const files = await globby(paths.map((path) => normalizePath(path)));
return () => {
server.middlewares.use(async (req, res, next) => {
diff --git a/test/cli/create-mail.test.ts b/test/cli/create-mail.test.ts
index 8ece17ca..20dd340b 100644
--- a/test/cli/create-mail.test.ts
+++ b/test/cli/create-mail.test.ts
@@ -21,6 +21,23 @@ describe('create-mail', async () => {
expect(plain).toMatchSnapshot();
+ type PackageJson = {
+ dependencies?: Record;
+ devDependencies?: Record;
+ };
+
+ const packageJson = JSON.parse(
+ await readFile(join(__dirname, '.test/new/package.json'), 'utf8')
+ ) as PackageJson;
+
+ expect(packageJson.devDependencies).toMatchObject({
+ react: expect.any(String),
+ 'react-dom': expect.any(String)
+ });
+
+ expect(packageJson.dependencies ?? {}).not.toHaveProperty('react');
+ expect(packageJson.dependencies ?? {}).not.toHaveProperty('react-dom');
+
const contents = await readFile(join(__dirname, '.test/new/templates/email.tsx'), 'utf8');
expect(contents).toMatchSnapshot();
diff --git a/test/cli/preview-build-path.test.ts b/test/cli/preview-build-path.test.ts
new file mode 100644
index 00000000..6c68390a
--- /dev/null
+++ b/test/cli/preview-build-path.test.ts
@@ -0,0 +1,51 @@
+import { access, readFile, rm } from 'node:fs/promises';
+import os from 'node:os';
+import { join, resolve } from 'node:path';
+
+import { execa } from 'execa';
+
+describe('cli: preview --build-path', async () => {
+ const outRelational = './.test/build-path';
+ const outAbsolute = resolve(__dirname, outRelational);
+ const templatePath = './.test/.deploy/emails';
+ const isWindows = os.platform() === 'win32';
+
+ beforeAll(async () => {
+ await rm(outAbsolute, { force: true, recursive: true });
+ });
+
+ afterAll(async () => {
+ await rm(outAbsolute, { force: true, recursive: true });
+ });
+
+ if (!isWindows) {
+ test('relative build path writes to an absolute path derived from the original cwd', async () => {
+ await execa({ cwd: __dirname, shell: true })`email create BatmanEmail --out ${templatePath} `;
+ const { stdout } = await execa({
+ cwd: __dirname,
+ shell: true
+ })`email preview ${templatePath} --build-path ${outRelational}`;
+
+ console.log(stdout);
+
+ await access(join(outAbsolute, 'index.html'));
+
+ const html = await readFile(join(outAbsolute, 'index.html'), 'utf8');
+ expect(html).toContain('');
+ }, 60e3);
+ }
+
+ if (isWindows) {
+ test('errors when Vite root and temp build path are on different drives (Windows)', async () => {
+ await execa({ cwd: __dirname, shell: true })`email create BatmanEmail --out ${templatePath} `;
+ await expect(
+ execa({
+ cwd: __dirname,
+ shell: true
+ })`email preview ${templatePath} --build-path ${outRelational}`
+ ).rejects.toThrow(
+ /Temporary directory drive letter different than root directory drive letter/
+ );
+ }, 60e3);
+ }
+});