react-email 6.1.5 → 6.2.0

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,29 @@
1
1
  # react-email
2
2
 
3
+ ## 6.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 192d82a: Add `theme` and `utility` props to `<Tailwind>` for Tailwind v4 CSS-first configuration. Both accept a CSS string and can be combined with the existing `config` prop.
8
+
9
+ ```tsx
10
+ import themeCss from "./theme.css?inline";
11
+
12
+ <Tailwind theme={themeCss}>
13
+ <div className="bg-brand font-display">Custom themed content</div>
14
+ </Tailwind>;
15
+ ```
16
+
17
+ Empty strings are no-ops. The base Tailwind theme and utilities are still loaded — `theme` and `utility` layer on top.
18
+
19
+ The preview server, `email export`, and the caniemail compatibility check all understand the Vite-style `?inline` and `?raw` suffixes on CSS imports, so the pattern above works the same in your project and inside the preview UI. The compatibility check also extracts the `theme` and `utility` props (in addition to `config`) when analyzing your template, so any caniemail incompatibilities in CSS produced by those props will surface as warnings.
20
+
21
+ Internal note: the exported `setupTailwind` helper now takes `{ config, cssConfigs }` instead of a positional `TailwindConfig`. Calling it with the old shape throws with a migration hint.
22
+
23
+ ### Patch Changes
24
+
25
+ - 06f1d05: Watch directories targeted by dynamic `import()` template literals so changes to runtime-resolved files trigger preview reloads.
26
+
3
27
  ## 6.1.5
4
28
 
5
29
  ### Patch Changes
@@ -6523,7 +6523,7 @@ const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFil
6523
6523
  //#region package.json
6524
6524
  var package_default = {
6525
6525
  name: "react-email",
6526
- version: "6.1.5",
6526
+ version: "6.2.0",
6527
6527
  description: "A live preview of your emails right in your browser.",
6528
6528
  bin: { "email": "./dist/cli/index.mjs" },
6529
6529
  type: "module",
@@ -6864,7 +6864,8 @@ const build$1 = async ({ dir: emailsDirRelativePath, packageManager }) => {
6864
6864
  //#region src/cli/utils/preview/hot-reloading/get-imported-modules.ts
6865
6865
  const traverse = typeof traverseModule === "function" ? traverseModule : traverseModule.default;
6866
6866
  const getImportedModules = (contents) => {
6867
- const importedPaths = [];
6867
+ const staticImports = [];
6868
+ const dynamicImportPrefixes = [];
6868
6869
  traverse(parse(contents, {
6869
6870
  sourceType: "unambiguous",
6870
6871
  strictMode: false,
@@ -6876,27 +6877,49 @@ const getImportedModules = (contents) => {
6876
6877
  ]
6877
6878
  }), {
6878
6879
  ImportDeclaration({ node }) {
6879
- importedPaths.push(node.source.value);
6880
+ staticImports.push(node.source.value);
6880
6881
  },
6881
6882
  ExportAllDeclaration({ node }) {
6882
- importedPaths.push(node.source.value);
6883
+ staticImports.push(node.source.value);
6883
6884
  },
6884
6885
  ExportNamedDeclaration({ node }) {
6885
- if (node.source) importedPaths.push(node.source.value);
6886
+ if (node.source) staticImports.push(node.source.value);
6886
6887
  },
6887
6888
  TSExternalModuleReference({ node }) {
6888
- importedPaths.push(node.expression.value);
6889
+ staticImports.push(node.expression.value);
6889
6890
  },
6890
6891
  CallExpression({ node }) {
6891
6892
  if ("name" in node.callee && node.callee.name === "require") {
6892
6893
  if (node.arguments.length === 1) {
6893
6894
  const importPathNode = node.arguments[0];
6894
- if (importPathNode.type === "StringLiteral") importedPaths.push(importPathNode.value);
6895
+ if (importPathNode.type === "StringLiteral") staticImports.push(importPathNode.value);
6896
+ }
6897
+ return;
6898
+ }
6899
+ if (node.callee.type === "Import" && node.arguments.length === 1) {
6900
+ const argument = node.arguments[0];
6901
+ if (argument.type === "StringLiteral") {
6902
+ staticImports.push(argument.value);
6903
+ return;
6904
+ }
6905
+ if (argument.type === "TemplateLiteral" && argument.quasis.length > 0) {
6906
+ if (argument.expressions.length === 0) {
6907
+ const onlyQuasi = argument.quasis[0];
6908
+ const staticPath = onlyQuasi.value.cooked ?? onlyQuasi.value.raw;
6909
+ if (staticPath.length > 0) staticImports.push(staticPath);
6910
+ return;
6911
+ }
6912
+ const firstQuasi = argument.quasis[0];
6913
+ const leadingStatic = firstQuasi.value.cooked ?? firstQuasi.value.raw;
6914
+ if (leadingStatic.length > 0) dynamicImportPrefixes.push(leadingStatic);
6895
6915
  }
6896
6916
  }
6897
6917
  }
6898
6918
  });
6899
- return importedPaths;
6919
+ return {
6920
+ staticImports,
6921
+ dynamicImportPrefixes
6922
+ };
6900
6923
  };
6901
6924
  //#endregion
6902
6925
  //#region src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts
@@ -6919,6 +6942,25 @@ const resolvePathAliases = (importPaths, projectPath) => {
6919
6942
  }
6920
6943
  return importPaths;
6921
6944
  };
6945
+ const isExistingDirectory = (candidate) => {
6946
+ try {
6947
+ return statSync(candidate).isDirectory();
6948
+ } catch (_) {
6949
+ return false;
6950
+ }
6951
+ };
6952
+ const resolveAliasedDirectoryPrefix = (prefix, projectPath) => {
6953
+ const configLoadResult = loadConfig(projectPath);
6954
+ if (configLoadResult.resultType !== "success") return void 0;
6955
+ return createMatchPath(configLoadResult.absoluteBaseUrl, configLoadResult.paths)(prefix, void 0, isExistingDirectory, [
6956
+ ".tsx",
6957
+ ".ts",
6958
+ ".js",
6959
+ ".jsx",
6960
+ ".cjs",
6961
+ ".mjs"
6962
+ ]) ?? void 0;
6963
+ };
6922
6964
  //#endregion
6923
6965
  //#region src/cli/utils/preview/hot-reloading/create-dependency-graph.ts
6924
6966
  const readAllFilesInsideDirectory = async (directory) => {
@@ -6951,6 +6993,34 @@ const checkFileExtensionsUntilItExists = (pathWithoutExtension) => {
6951
6993
  if (existsSync(`${pathWithoutExtension}.mjs`)) return `${pathWithoutExtension}.mjs`;
6952
6994
  if (existsSync(`${pathWithoutExtension}.cjs`)) return `${pathWithoutExtension}.cjs`;
6953
6995
  };
6996
+ const isUnderDirectory = (filePath, directoryPath) => {
6997
+ if (filePath === directoryPath) return true;
6998
+ const prefix = directoryPath.endsWith(path.sep) ? directoryPath : directoryPath + path.sep;
6999
+ return filePath.startsWith(prefix);
7000
+ };
7001
+ const resolveDynamicImportDirectory = (prefix, filePath) => {
7002
+ const moduleDirectory = path.dirname(filePath);
7003
+ const normalizedPrefix = path.normalize(prefix);
7004
+ const endsWithSeparator = normalizedPrefix.endsWith(path.sep);
7005
+ const trimmed = endsWithSeparator ? normalizedPrefix.slice(0, -path.sep.length) : normalizedPrefix;
7006
+ let resolvedPrefix;
7007
+ if (prefix.startsWith(".") || path.isAbsolute(prefix)) resolvedPrefix = path.resolve(moduleDirectory, normalizedPrefix);
7008
+ else {
7009
+ if (trimmed.length === 0) return void 0;
7010
+ const aliased = resolveAliasedDirectoryPrefix(trimmed, moduleDirectory);
7011
+ if (aliased === void 0) return;
7012
+ resolvedPrefix = aliased;
7013
+ }
7014
+ const directory = endsWithSeparator ? resolvedPrefix : path.dirname(resolvedPrefix);
7015
+ if (isUnderDirectory(moduleDirectory, directory)) return void 0;
7016
+ if (!existsSync(directory)) return void 0;
7017
+ try {
7018
+ if (!statSync(directory).isDirectory()) return void 0;
7019
+ } catch (_) {
7020
+ return;
7021
+ }
7022
+ return directory;
7023
+ };
6954
7024
  /**
6955
7025
  * Creates a stateful dependency graph that is structured in a way that you can get
6956
7026
  * the dependents of a module from its path.
@@ -6965,11 +7035,17 @@ const createDependencyGraph = async (directory) => {
6965
7035
  path,
6966
7036
  dependencyPaths: [],
6967
7037
  dependentPaths: [],
7038
+ dynamicDependencyDirectories: [],
6968
7039
  moduleDependencies: []
6969
7040
  }]));
6970
7041
  const getDependencyPaths = async (filePath) => {
6971
7042
  const contents = await promises.readFile(filePath, "utf8");
6972
- const importedPathsRelativeToDirectory = (isJavascriptModule(filePath) ? resolvePathAliases(getImportedModules(contents), path.dirname(filePath)) : []).map((dependencyPath) => {
7043
+ const imports = isJavascriptModule(filePath) ? getImportedModules(contents) : {
7044
+ staticImports: [],
7045
+ dynamicImportPrefixes: []
7046
+ };
7047
+ const importedPathsRelativeToDirectory = (isJavascriptModule(filePath) ? resolvePathAliases(imports.staticImports, path.dirname(filePath)) : []).map((rawDependencyPath) => {
7048
+ const dependencyPath = rawDependencyPath.split("?")[0];
6973
7049
  if (!dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)) return dependencyPath;
6974
7050
  let pathToDependencyFromDirectory = path.resolve(path.dirname(filePath), dependencyPath);
6975
7051
  let isDirectory = false;
@@ -6994,7 +7070,8 @@ const createDependencyGraph = async (directory) => {
6994
7070
  const moduleDependencies = importedPathsRelativeToDirectory.filter((dependencyPath) => !dependencyPath.startsWith(".") && !path.isAbsolute(dependencyPath));
6995
7071
  return {
6996
7072
  dependencyPaths: importedPathsRelativeToDirectory.filter((dependencyPath) => dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)),
6997
- moduleDependencies
7073
+ moduleDependencies,
7074
+ dynamicDependencyDirectories: Array.from(new Set(imports.dynamicImportPrefixes.map((prefix) => resolveDynamicImportDirectory(prefix, filePath)).filter((d) => typeof d === "string")))
6998
7075
  };
6999
7076
  };
7000
7077
  const updateModuleDependenciesInGraph = async (moduleFilePath) => {
@@ -7002,10 +7079,12 @@ const createDependencyGraph = async (directory) => {
7002
7079
  path: moduleFilePath,
7003
7080
  dependencyPaths: [],
7004
7081
  dependentPaths: [],
7082
+ dynamicDependencyDirectories: [],
7005
7083
  moduleDependencies: []
7006
7084
  };
7007
- const { moduleDependencies, dependencyPaths: newDependencyPaths } = await getDependencyPaths(moduleFilePath);
7085
+ const { moduleDependencies, dependencyPaths: newDependencyPaths, dynamicDependencyDirectories: newDynamicDependencyDirectories } = await getDependencyPaths(moduleFilePath);
7008
7086
  graph[moduleFilePath].moduleDependencies = moduleDependencies;
7087
+ graph[moduleFilePath].dynamicDependencyDirectories = newDynamicDependencyDirectories;
7009
7088
  for (const dependencyPath of graph[moduleFilePath].dependencyPaths) {
7010
7089
  if (newDependencyPaths.includes(dependencyPath)) continue;
7011
7090
  const dependencyModule = graph[dependencyPath];
@@ -7055,6 +7134,10 @@ const createDependencyGraph = async (directory) => {
7055
7134
  { resolveDependentsOf: function resolveDependentsOf(pathToModule) {
7056
7135
  const dependentPaths = /* @__PURE__ */ new Set();
7057
7136
  const stack = [pathToModule];
7137
+ for (const module of Object.values(graph)) for (const directory of module.dynamicDependencyDirectories) if (isUnderDirectory(pathToModule, directory) && module.path !== pathToModule && !dependentPaths.has(module.path)) {
7138
+ dependentPaths.add(module.path);
7139
+ stack.push(module.path);
7140
+ }
7058
7141
  while (stack.length > 0) {
7059
7142
  const moduleEntry = graph[stack.pop()];
7060
7143
  if (!moduleEntry) continue;
@@ -7094,6 +7177,13 @@ const setupHotreloading = async (devServer, emailDirRelativePath) => {
7094
7177
  const getFilesOutsideEmailsDirectory = () => Object.keys(dependencyGraph).filter((p) => path.relative(absolutePathToEmailsDirectory, p).startsWith(".."));
7095
7178
  let filesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
7096
7179
  for (const p of filesOutsideEmailsDirectory) watcher.add(p);
7180
+ const getDynamicDependencyDirectories = () => {
7181
+ const directories = /* @__PURE__ */ new Set();
7182
+ for (const module of Object.values(dependencyGraph)) for (const directory of module.dynamicDependencyDirectories) directories.add(directory);
7183
+ return [...directories];
7184
+ };
7185
+ let dynamicDependencyDirectories = getDynamicDependencyDirectories();
7186
+ for (const directory of dynamicDependencyDirectories) watcher.add(directory);
7097
7187
  const exit = async () => {
7098
7188
  await watcher.close();
7099
7189
  };
@@ -7107,6 +7197,10 @@ const setupHotreloading = async (devServer, emailDirRelativePath) => {
7107
7197
  for (const p of filesOutsideEmailsDirectory) if (!newFilesOutsideEmailsDirectory.includes(p)) watcher.unwatch(p);
7108
7198
  for (const p of newFilesOutsideEmailsDirectory) if (!filesOutsideEmailsDirectory.includes(p)) watcher.add(p);
7109
7199
  filesOutsideEmailsDirectory = newFilesOutsideEmailsDirectory;
7200
+ const newDynamicDependencyDirectories = getDynamicDependencyDirectories();
7201
+ for (const directory of dynamicDependencyDirectories) if (!newDynamicDependencyDirectories.includes(directory)) watcher.unwatch(directory);
7202
+ for (const directory of newDynamicDependencyDirectories) if (!dynamicDependencyDirectories.includes(directory)) watcher.add(directory);
7203
+ dynamicDependencyDirectories = newDynamicDependencyDirectories;
7110
7204
  changes.push({
7111
7205
  event,
7112
7206
  filename: relativePathToChangeTarget
@@ -7352,6 +7446,33 @@ const dev = async ({ dir: emailsDirRelativePath, port }) => {
7352
7446
  }
7353
7447
  };
7354
7448
  //#endregion
7449
+ //#region src/cli/utils/esbuild/inline-css-loader.ts
7450
+ const inlineCssLoader = () => ({
7451
+ name: "inline-css-loader",
7452
+ setup(build) {
7453
+ const namespace = "inline-css";
7454
+ build.onResolve({ filter: /\?(inline|raw)(&|$)/ }, (args) => {
7455
+ const [pathWithoutSuffix] = args.path.split("?");
7456
+ if (!pathWithoutSuffix) return null;
7457
+ return {
7458
+ path: path.isAbsolute(pathWithoutSuffix) ? pathWithoutSuffix : path.resolve(args.resolveDir, pathWithoutSuffix),
7459
+ namespace
7460
+ };
7461
+ });
7462
+ build.onLoad({
7463
+ filter: /.*/,
7464
+ namespace
7465
+ }, async (args) => {
7466
+ const contents = await promises.readFile(args.path, "utf8");
7467
+ return {
7468
+ contents: `export default ${JSON.stringify(contents)};`,
7469
+ loader: "js",
7470
+ watchFiles: [args.path]
7471
+ };
7472
+ });
7473
+ }
7474
+ });
7475
+ //#endregion
7355
7476
  //#region src/cli/utils/esbuild/escape-string-for-regex.ts
7356
7477
  function escapeStringForRegex(string) {
7357
7478
  return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
@@ -7470,7 +7591,7 @@ const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirec
7470
7591
  outExtension: { ".js": ".cjs" },
7471
7592
  outdir: pathToWhereEmailMarkupShouldBeDumped,
7472
7593
  platform: "node",
7473
- plugins: [renderingUtilitiesExporter(batch)],
7594
+ plugins: [inlineCssLoader(), renderingUtilitiesExporter(batch)],
7474
7595
  write: true
7475
7596
  });
7476
7597
  await stop();
package/dist/index.cjs CHANGED
@@ -40434,11 +40434,17 @@ const css = `
40434
40434
  `;
40435
40435
  //#endregion
40436
40436
  //#region src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
40437
- async function setupTailwind(config) {
40437
+ const SETUP_TAILWIND_KEYS = new Set(["config", "cssConfigs"]);
40438
+ async function setupTailwind(props = {}) {
40439
+ const stray = Object.keys(props).filter((k) => !SETUP_TAILWIND_KEYS.has(k));
40440
+ if (stray.length > 0) throw new Error(`setupTailwind now takes { config, cssConfigs } — received unexpected keys: ${stray.join(", ")}. If you used to call setupTailwind(config), wrap it: setupTailwind({ config }).`);
40441
+ const { config, cssConfigs } = props;
40438
40442
  const baseCss = `
40439
40443
  @layer theme, base, components, utilities;
40440
40444
  @import "tailwindcss/theme.css" layer(theme);
40441
40445
  @import "tailwindcss/utilities.css" layer(utilities);
40446
+ ${cssConfigs?.theme ? "@import \"custom-theme.css\" layer(theme);" : ""}
40447
+ ${cssConfigs?.utility ? "@import \"custom-utilities.css\" layer(utilities);" : ""}
40442
40448
  @config;
40443
40449
  `;
40444
40450
  const compiler = await (0, tailwindcss.compile)(baseCss, {
@@ -40446,7 +40452,7 @@ async function setupTailwind(config) {
40446
40452
  if (resourceHint === "config") return {
40447
40453
  path: id,
40448
40454
  base,
40449
- module: config
40455
+ module: config ?? {}
40450
40456
  };
40451
40457
  throw new Error(`NO-OP: should we implement support for ${resourceHint}?`);
40452
40458
  },
@@ -40472,6 +40478,16 @@ async function setupTailwind(config) {
40472
40478
  path: id,
40473
40479
  content: css
40474
40480
  };
40481
+ if (id === "custom-theme.css") return {
40482
+ base,
40483
+ path: id,
40484
+ content: cssConfigs?.theme ?? ""
40485
+ };
40486
+ if (id === "custom-utilities.css") return {
40487
+ base,
40488
+ path: id,
40489
+ content: cssConfigs?.utility ?? ""
40490
+ };
40475
40491
  throw new Error("stylesheet not supported, you can only import the ones from tailwindcss");
40476
40492
  }
40477
40493
  });
@@ -40541,8 +40557,15 @@ const pixelBasedPreset = { theme: { extend: {
40541
40557
  96: "384px"
40542
40558
  }
40543
40559
  } } };
40544
- function Tailwind({ children, config }) {
40545
- const tailwindSetup = useSuspensedPromise(() => setupTailwind(config ?? {}), JSON.stringify(config, (_key, value) => typeof value === "function" ? value.toString() : value));
40560
+ function Tailwind({ children, config, theme, utility }) {
40561
+ const twConfigData = {
40562
+ config,
40563
+ cssConfigs: {
40564
+ theme,
40565
+ utility
40566
+ }
40567
+ };
40568
+ const tailwindSetup = useSuspensedPromise(() => setupTailwind(twConfigData), JSON.stringify(twConfigData, (_key, value) => typeof value === "function" ? value.toString() : value));
40546
40569
  let classesUsed = [];
40547
40570
  let mappedChildren = mapReactTree(children, (node) => {
40548
40571
  if (react.isValidElement(node)) {
package/dist/index.d.cts CHANGED
@@ -5085,24 +5085,36 @@ declare function sanitizeStyleSheet(styleSheet: StyleSheet): void;
5085
5085
  //#endregion
5086
5086
  //#region src/components/tailwind/tailwind.d.ts
5087
5087
  type TailwindConfig = Omit<Config, 'content'>;
5088
- interface TailwindProps {
5089
- children: React$2.ReactNode;
5090
- config?: TailwindConfig;
5091
- }
5092
5088
  interface EmailElementProps {
5093
5089
  children?: React$2.ReactNode;
5094
5090
  className?: string;
5095
5091
  style?: React$2.CSSProperties;
5096
5092
  }
5097
5093
  declare const pixelBasedPreset: TailwindConfig;
5094
+ interface TailwindProps {
5095
+ children: React$2.ReactNode;
5096
+ config?: TailwindConfig;
5097
+ theme?: string;
5098
+ utility?: string;
5099
+ }
5098
5100
  declare function Tailwind({
5099
5101
  children,
5100
- config
5102
+ config,
5103
+ theme,
5104
+ utility
5101
5105
  }: TailwindProps): React$2.ReactNode;
5102
5106
  //#endregion
5103
5107
  //#region src/components/tailwind/utils/tailwindcss/setup-tailwind.d.ts
5104
5108
  type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
5105
- declare function setupTailwind(config: TailwindConfig): Promise<{
5109
+ interface CSSConfigs {
5110
+ theme?: string;
5111
+ utility?: string;
5112
+ }
5113
+ interface SetupTailwindProps {
5114
+ config?: TailwindConfig;
5115
+ cssConfigs?: CSSConfigs;
5116
+ }
5117
+ declare function setupTailwind(props?: SetupTailwindProps): Promise<{
5106
5118
  addUtilities: (candidates: string[]) => void;
5107
5119
  getStyleSheet: () => StyleSheet;
5108
5120
  }>;
package/dist/index.d.mts CHANGED
@@ -5085,24 +5085,36 @@ declare function sanitizeStyleSheet(styleSheet: StyleSheet): void;
5085
5085
  //#endregion
5086
5086
  //#region src/components/tailwind/tailwind.d.ts
5087
5087
  type TailwindConfig = Omit<Config, 'content'>;
5088
- interface TailwindProps {
5089
- children: React$2.ReactNode;
5090
- config?: TailwindConfig;
5091
- }
5092
5088
  interface EmailElementProps {
5093
5089
  children?: React$2.ReactNode;
5094
5090
  className?: string;
5095
5091
  style?: React$2.CSSProperties;
5096
5092
  }
5097
5093
  declare const pixelBasedPreset: TailwindConfig;
5094
+ interface TailwindProps {
5095
+ children: React$2.ReactNode;
5096
+ config?: TailwindConfig;
5097
+ theme?: string;
5098
+ utility?: string;
5099
+ }
5098
5100
  declare function Tailwind({
5099
5101
  children,
5100
- config
5102
+ config,
5103
+ theme,
5104
+ utility
5101
5105
  }: TailwindProps): React$2.ReactNode;
5102
5106
  //#endregion
5103
5107
  //#region src/components/tailwind/utils/tailwindcss/setup-tailwind.d.ts
5104
5108
  type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
5105
- declare function setupTailwind(config: TailwindConfig): Promise<{
5109
+ interface CSSConfigs {
5110
+ theme?: string;
5111
+ utility?: string;
5112
+ }
5113
+ interface SetupTailwindProps {
5114
+ config?: TailwindConfig;
5115
+ cssConfigs?: CSSConfigs;
5116
+ }
5117
+ declare function setupTailwind(props?: SetupTailwindProps): Promise<{
5106
5118
  addUtilities: (candidates: string[]) => void;
5107
5119
  getStyleSheet: () => StyleSheet;
5108
5120
  }>;
package/dist/index.mjs CHANGED
@@ -40413,11 +40413,17 @@ const css = `
40413
40413
  `;
40414
40414
  //#endregion
40415
40415
  //#region src/components/tailwind/utils/tailwindcss/setup-tailwind.ts
40416
- async function setupTailwind(config) {
40416
+ const SETUP_TAILWIND_KEYS = new Set(["config", "cssConfigs"]);
40417
+ async function setupTailwind(props = {}) {
40418
+ const stray = Object.keys(props).filter((k) => !SETUP_TAILWIND_KEYS.has(k));
40419
+ if (stray.length > 0) throw new Error(`setupTailwind now takes { config, cssConfigs } — received unexpected keys: ${stray.join(", ")}. If you used to call setupTailwind(config), wrap it: setupTailwind({ config }).`);
40420
+ const { config, cssConfigs } = props;
40417
40421
  const baseCss = `
40418
40422
  @layer theme, base, components, utilities;
40419
40423
  @import "tailwindcss/theme.css" layer(theme);
40420
40424
  @import "tailwindcss/utilities.css" layer(utilities);
40425
+ ${cssConfigs?.theme ? "@import \"custom-theme.css\" layer(theme);" : ""}
40426
+ ${cssConfigs?.utility ? "@import \"custom-utilities.css\" layer(utilities);" : ""}
40421
40427
  @config;
40422
40428
  `;
40423
40429
  const compiler = await compile(baseCss, {
@@ -40425,7 +40431,7 @@ async function setupTailwind(config) {
40425
40431
  if (resourceHint === "config") return {
40426
40432
  path: id,
40427
40433
  base,
40428
- module: config
40434
+ module: config ?? {}
40429
40435
  };
40430
40436
  throw new Error(`NO-OP: should we implement support for ${resourceHint}?`);
40431
40437
  },
@@ -40451,6 +40457,16 @@ async function setupTailwind(config) {
40451
40457
  path: id,
40452
40458
  content: css
40453
40459
  };
40460
+ if (id === "custom-theme.css") return {
40461
+ base,
40462
+ path: id,
40463
+ content: cssConfigs?.theme ?? ""
40464
+ };
40465
+ if (id === "custom-utilities.css") return {
40466
+ base,
40467
+ path: id,
40468
+ content: cssConfigs?.utility ?? ""
40469
+ };
40454
40470
  throw new Error("stylesheet not supported, you can only import the ones from tailwindcss");
40455
40471
  }
40456
40472
  });
@@ -40520,8 +40536,15 @@ const pixelBasedPreset = { theme: { extend: {
40520
40536
  96: "384px"
40521
40537
  }
40522
40538
  } } };
40523
- function Tailwind({ children, config }) {
40524
- const tailwindSetup = useSuspensedPromise(() => setupTailwind(config ?? {}), JSON.stringify(config, (_key, value) => typeof value === "function" ? value.toString() : value));
40539
+ function Tailwind({ children, config, theme, utility }) {
40540
+ const twConfigData = {
40541
+ config,
40542
+ cssConfigs: {
40543
+ theme,
40544
+ utility
40545
+ }
40546
+ };
40547
+ const tailwindSetup = useSuspensedPromise(() => setupTailwind(twConfigData), JSON.stringify(twConfigData, (_key, value) => typeof value === "function" ? value.toString() : value));
40525
40548
  let classesUsed = [];
40526
40549
  let mappedChildren = mapReactTree(children, (node) => {
40527
40550
  if (React$1.isValidElement(node)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email",
3
- "version": "6.1.5",
3
+ "version": "6.2.0",
4
4
  "description": "A live preview of your emails right in your browser.",
5
5
  "bin": {
6
6
  "email": "./dist/cli/index.mjs"
@@ -899,6 +899,321 @@ describe('Tailwind component', () => {
899
899
  });
900
900
  });
901
901
 
902
+ describe('with css configuration', () => {
903
+ it('handles empty theme string', async () => {
904
+ const actualOutput = await render(
905
+ <Tailwind theme="">
906
+ <div className="bg-red-500 text-white">Default utilities</div>
907
+ </Tailwind>,
908
+ ).then(pretty);
909
+
910
+ expect(actualOutput).toMatchInlineSnapshot(`
911
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
912
+ <!--$-->
913
+ <div style="background-color:rgb(251,44,54);color:rgb(255,255,255)">
914
+ Default utilities
915
+ </div>
916
+ <!--/$-->
917
+ "
918
+ `);
919
+ });
920
+
921
+ it('supports custom colors', async () => {
922
+ const theme = `
923
+ @theme {
924
+ --color-custom: #1fb6ff;
925
+ }
926
+ `;
927
+
928
+ const actualOutput = await render(
929
+ <Tailwind theme={theme}>
930
+ <div className="bg-custom text-custom" />
931
+ </Tailwind>,
932
+ ).then(pretty);
933
+
934
+ expect(actualOutput).toMatchInlineSnapshot(`
935
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
936
+ <!--$-->
937
+ <div style="background-color:rgb(31,182,255);color:rgb(31,182,255)"></div>
938
+ <!--/$-->
939
+ "
940
+ `);
941
+ });
942
+
943
+ it('supports custom fonts', async () => {
944
+ const theme = `
945
+ @theme {
946
+ --font-sans: "Graphik", sans-serif;
947
+ --font-serif: "Merriweather", serif;
948
+ }
949
+ `;
950
+
951
+ const actualOutput = await render(
952
+ <Tailwind theme={theme}>
953
+ <div className="font-sans" />
954
+ <div className="font-serif" />
955
+ </Tailwind>,
956
+ ).then(pretty);
957
+
958
+ expect(actualOutput).toMatchInlineSnapshot(`
959
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
960
+ <!--$-->
961
+ <div style='font-family:"Graphik",sans-serif'></div>
962
+ <div style='font-family:"Merriweather",serif'></div>
963
+ <!--/$-->
964
+ "
965
+ `);
966
+ });
967
+
968
+ it('supports custom spacing', async () => {
969
+ const theme = `
970
+ @theme {
971
+ --spacing-8xl: 96rem;
972
+ }
973
+ `;
974
+
975
+ const actualOutput = await render(
976
+ <Tailwind theme={theme}>
977
+ <div className="m-8xl" />
978
+ </Tailwind>,
979
+ ).then(pretty);
980
+ expect(actualOutput).toMatchInlineSnapshot(`
981
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
982
+ <!--$-->
983
+ <div style="margin:96rem"></div>
984
+ <!--/$-->
985
+ "
986
+ `);
987
+ });
988
+
989
+ it('supports custom border radius', async () => {
990
+ const theme = `
991
+ @theme {
992
+ --border-radius-4xl: 2rem;
993
+ }
994
+ `;
995
+
996
+ const actualOutput = await render(
997
+ <Tailwind theme={theme}>
998
+ <div className="rounded-4xl" />
999
+ </Tailwind>,
1000
+ ).then(pretty);
1001
+ expect(actualOutput).toMatchInlineSnapshot(`
1002
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1003
+ <!--$-->
1004
+ <div style="border-radius:2rem"></div>
1005
+ <!--/$-->
1006
+ "
1007
+ `);
1008
+ });
1009
+
1010
+ it('supports custom text alignment', async () => {
1011
+ const theme = `
1012
+ @theme {
1013
+ --text-align-justify: justify;
1014
+ }
1015
+ `;
1016
+
1017
+ const actualOutput = await render(
1018
+ <Tailwind theme={theme}>
1019
+ <div className="text-justify" />
1020
+ </Tailwind>,
1021
+ ).then(pretty);
1022
+
1023
+ expect(actualOutput).toMatchInlineSnapshot(`
1024
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1025
+ <!--$-->
1026
+ <div style="text-align:justify"></div>
1027
+ <!--/$-->
1028
+ "
1029
+ `);
1030
+ });
1031
+
1032
+ it('supports both config and theme props together', async () => {
1033
+ const customConfig = {
1034
+ theme: {
1035
+ extend: {
1036
+ colors: {
1037
+ primary: '#ff0000',
1038
+ },
1039
+ },
1040
+ },
1041
+ } satisfies TailwindConfig;
1042
+
1043
+ const customTheme = `
1044
+ @theme {
1045
+ --color-secondary: #00ff00;
1046
+ }
1047
+ `;
1048
+
1049
+ const actualOutput = await render(
1050
+ <Tailwind config={customConfig} theme={customTheme}>
1051
+ <div className="bg-primary text-secondary">Both config and theme</div>
1052
+ </Tailwind>,
1053
+ ).then(pretty);
1054
+
1055
+ expect(actualOutput).toMatchInlineSnapshot(`
1056
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1057
+ <!--$-->
1058
+ <div style="background-color:rgb(255,0,0);color:rgb(0,255,0)">
1059
+ Both config and theme
1060
+ </div>
1061
+ <!--/$-->
1062
+ "
1063
+ `);
1064
+ });
1065
+ });
1066
+
1067
+ describe('with utilities', () => {
1068
+ it('handles empty utilities string', async () => {
1069
+ const actualOutput = await render(
1070
+ <Tailwind utility="">
1071
+ <div className="bg-red-500 text-white">Default utilities</div>
1072
+ </Tailwind>,
1073
+ ).then(pretty);
1074
+
1075
+ expect(actualOutput).toMatchInlineSnapshot(`
1076
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1077
+ <!--$-->
1078
+ <div style="background-color:rgb(251,44,54);color:rgb(255,255,255)">
1079
+ Default utilities
1080
+ </div>
1081
+ <!--/$-->
1082
+ "
1083
+ `);
1084
+ });
1085
+
1086
+ it('supports custom utilities', async () => {
1087
+ const utilities = `
1088
+ .custom-shadow {
1089
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
1090
+ border-radius: 8px;
1091
+ padding: 16px;
1092
+ }
1093
+ `;
1094
+
1095
+ const actualOutput = await render(
1096
+ <Tailwind utility={utilities}>
1097
+ <div className="custom-shadow" />
1098
+ </Tailwind>,
1099
+ ).then(pretty);
1100
+
1101
+ expect(actualOutput).toMatchInlineSnapshot(`
1102
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1103
+ <!--$-->
1104
+ <div
1105
+ style="box-shadow:0 4px 6px rgba(0,0,0,0.1);border-radius:8px;padding:16px"></div>
1106
+ <!--/$-->
1107
+ "
1108
+ `);
1109
+ });
1110
+
1111
+ it('supports animations', async () => {
1112
+ const utilities = `
1113
+ .pulse-animation {
1114
+ animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
1115
+ }
1116
+ `;
1117
+
1118
+ const actualOutput = await render(
1119
+ <Tailwind utility={utilities}>
1120
+ <div className="pulse-animation" />
1121
+ </Tailwind>,
1122
+ ).then(pretty);
1123
+
1124
+ expect(actualOutput).toMatchInlineSnapshot(`
1125
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1126
+ <!--$-->
1127
+ <div style="animation:pulse 2s cubic-bezier(0.4,0,0.6,1) infinite"></div>
1128
+ <!--/$-->
1129
+ "
1130
+ `);
1131
+ });
1132
+
1133
+ it('supports both config and utilities props together', async () => {
1134
+ const customConfig = {
1135
+ theme: {
1136
+ extend: {
1137
+ colors: {
1138
+ primary: '#ff0000',
1139
+ },
1140
+ },
1141
+ },
1142
+ } satisfies TailwindConfig;
1143
+
1144
+ const customUtilities = `
1145
+ .card-base {
1146
+ border: 1px solid #e5e7eb;
1147
+ padding: 20px;
1148
+ }
1149
+ `;
1150
+
1151
+ const actualOutput = await render(
1152
+ <Tailwind config={customConfig} utility={customUtilities}>
1153
+ <div className="bg-primary card-base">Config and utilities</div>
1154
+ </Tailwind>,
1155
+ ).then(pretty);
1156
+
1157
+ expect(actualOutput).toMatchInlineSnapshot(`
1158
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1159
+ <!--$-->
1160
+ <div
1161
+ style="background-color:rgb(255,0,0);border:1px solid rgb(229,231,235);padding:20px">
1162
+ Config and utilities
1163
+ </div>
1164
+ <!--/$-->
1165
+ "
1166
+ `);
1167
+ });
1168
+
1169
+ it('supports config, theme, and utilities together', async () => {
1170
+ const customConfig = {
1171
+ theme: {
1172
+ extend: {
1173
+ colors: {
1174
+ primary: '#ff0000',
1175
+ },
1176
+ },
1177
+ },
1178
+ } satisfies TailwindConfig;
1179
+
1180
+ const customTheme = `
1181
+ @theme {
1182
+ --color-secondary: #00ff00;
1183
+ }
1184
+ `;
1185
+
1186
+ const customUtilities = `
1187
+ .special-border {
1188
+ border: 2px dashed #0000ff;
1189
+ }
1190
+ `;
1191
+
1192
+ const actualOutput = await render(
1193
+ <Tailwind
1194
+ config={customConfig}
1195
+ theme={customTheme}
1196
+ utility={customUtilities}
1197
+ >
1198
+ <div className="bg-primary text-secondary special-border">
1199
+ All three props
1200
+ </div>
1201
+ </Tailwind>,
1202
+ ).then(pretty);
1203
+
1204
+ expect(actualOutput).toMatchInlineSnapshot(`
1205
+ "<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
1206
+ <!--$-->
1207
+ <div
1208
+ style="background-color:rgb(255,0,0);color:rgb(0,255,0);border:2px dashed rgb(0,0,255)">
1209
+ All three props
1210
+ </div>
1211
+ <!--/$-->
1212
+ "
1213
+ `);
1214
+ });
1215
+ });
1216
+
902
1217
  describe('with custom plugins config', () => {
903
1218
  const config = {
904
1219
  plugins: [
@@ -13,11 +13,6 @@ import { setupTailwind } from './utils/tailwindcss/setup-tailwind.js';
13
13
 
14
14
  export type TailwindConfig = Omit<Config, 'content'>;
15
15
 
16
- export interface TailwindProps {
17
- children: React.ReactNode;
18
- config?: TailwindConfig;
19
- }
20
-
21
16
  export interface EmailElementProps {
22
17
  children?: React.ReactNode;
23
18
  className?: string;
@@ -83,15 +78,29 @@ export const pixelBasedPreset: TailwindConfig = {
83
78
  },
84
79
  };
85
80
 
86
- export function Tailwind({ children, config }: TailwindProps) {
81
+ export interface TailwindProps {
82
+ children: React.ReactNode;
83
+ config?: TailwindConfig;
84
+ theme?: string;
85
+ utility?: string;
86
+ }
87
+
88
+ export function Tailwind({ children, config, theme, utility }: TailwindProps) {
89
+ const twConfigData = {
90
+ config,
91
+ cssConfigs: {
92
+ theme,
93
+ utility,
94
+ },
95
+ };
87
96
  const tailwindSetup = useSuspensedPromise(
88
- () => setupTailwind(config ?? {}),
89
- JSON.stringify(config, (_key, value) =>
97
+ () => setupTailwind(twConfigData),
98
+ JSON.stringify(twConfigData, (_key, value) =>
90
99
  typeof value === 'function' ? value.toString() : value,
91
100
  ),
92
101
  );
93
- let classesUsed: string[] = [];
94
102
 
103
+ let classesUsed: string[] = [];
95
104
  let mappedChildren: React.ReactNode = mapReactTree(children, (node) => {
96
105
  if (React.isValidElement<EmailElementProps>(node)) {
97
106
  if (node.props.className) {
@@ -80,17 +80,19 @@ describe('extractRulesPerClass()', async () => {
80
80
 
81
81
  it('splits mixed rules (base + media) into inlinable base and non-inlinable media', async () => {
82
82
  const tailwind = await setupTailwind({
83
- plugins: [
84
- {
85
- handler: (api) => {
86
- api.addUtilities({
87
- '.text-body': {
88
- '@apply text-[green] sm:text-[darkgreen]': {},
89
- },
90
- });
83
+ config: {
84
+ plugins: [
85
+ {
86
+ handler: (api) => {
87
+ api.addUtilities({
88
+ '.text-body': {
89
+ '@apply text-[green] sm:text-[darkgreen]': {},
90
+ },
91
+ });
92
+ },
91
93
  },
92
- },
93
- ],
94
+ ],
95
+ },
94
96
  });
95
97
  const classes = ['text-body'];
96
98
  tailwind.addUtilities(classes);
@@ -115,17 +117,19 @@ describe('extractRulesPerClass()', async () => {
115
117
 
116
118
  it('treats rules with pseudo-selectors as fully non-inlinable', async () => {
117
119
  const tailwind = await setupTailwind({
118
- plugins: [
119
- {
120
- handler: (api) => {
121
- api.addUtilities({
122
- '.btn:hover': {
123
- color: 'red',
124
- },
125
- });
120
+ config: {
121
+ plugins: [
122
+ {
123
+ handler: (api) => {
124
+ api.addUtilities({
125
+ '.btn:hover': {
126
+ color: 'red',
127
+ },
128
+ });
129
+ },
126
130
  },
127
- },
128
- ],
131
+ ],
132
+ },
129
133
  });
130
134
  const classes = ['btn'];
131
135
  tailwind.addUtilities(classes);
@@ -16,3 +16,10 @@ test('setupTailwind() and addUtilities()', async () => {
16
16
  `"/*! tailwindcss v4.1.18 | MIT License | https://tailwindcss.com */@layer theme,base,components,utilities;@layer theme{:root,:host{--color-red-100: oklch(93.6% 0.032 17.717);--color-red-500: oklch(63.7% 0.237 25.331);--color-blue-300: oklch(80.9% 0.105 251.813);--color-slate-900: oklch(20.8% 0.042 265.755)}}@layer utilities{.bg-red-100{background-color:var(--color-red-100)}.bg-slate-900{background-color:var(--color-slate-900)}.text-red-500{color:var(--color-red-500)}.sm\\:bg-blue-300{@media (width>=40rem){background-color:var(--color-blue-300)}}}"`,
17
17
  );
18
18
  });
19
+
20
+ test('setupTailwind() throws on legacy positional config shape', async () => {
21
+ await expect(
22
+ // @ts-expect-error — intentionally calling with the old shape to verify the guard
23
+ setupTailwind({ plugins: [], theme: { extend: {} } }),
24
+ ).rejects.toThrowError(/setupTailwind now takes \{ config, cssConfigs \}/);
25
+ });
@@ -8,20 +8,43 @@ import utilitiesCss from './tailwind-stylesheets/utilities.js';
8
8
 
9
9
  export type TailwindSetup = Awaited<ReturnType<typeof setupTailwind>>;
10
10
 
11
- export async function setupTailwind(config: TailwindConfig) {
11
+ interface CSSConfigs {
12
+ theme?: string;
13
+ utility?: string;
14
+ }
15
+
16
+ interface SetupTailwindProps {
17
+ config?: TailwindConfig;
18
+ cssConfigs?: CSSConfigs;
19
+ }
20
+
21
+ const SETUP_TAILWIND_KEYS = new Set(['config', 'cssConfigs']);
22
+
23
+ export async function setupTailwind(props: SetupTailwindProps = {}) {
24
+ const stray = Object.keys(props).filter((k) => !SETUP_TAILWIND_KEYS.has(k));
25
+ if (stray.length > 0) {
26
+ throw new Error(
27
+ `setupTailwind now takes { config, cssConfigs } — received unexpected keys: ${stray.join(', ')}. ` +
28
+ 'If you used to call setupTailwind(config), wrap it: setupTailwind({ config }).',
29
+ );
30
+ }
31
+ const { config, cssConfigs } = props;
12
32
  const baseCss = `
13
33
  @layer theme, base, components, utilities;
14
34
  @import "tailwindcss/theme.css" layer(theme);
15
35
  @import "tailwindcss/utilities.css" layer(utilities);
36
+ ${cssConfigs?.theme ? '@import "custom-theme.css" layer(theme);' : ''}
37
+ ${cssConfigs?.utility ? '@import "custom-utilities.css" layer(utilities);' : ''}
16
38
  @config;
17
39
  `;
40
+
18
41
  const compiler = await compile(baseCss, {
19
42
  async loadModule(id, base, resourceHint) {
20
43
  if (resourceHint === 'config') {
21
44
  return {
22
45
  path: id,
23
46
  base: base,
24
- module: config,
47
+ module: config ?? {},
25
48
  };
26
49
  }
27
50
 
@@ -63,6 +86,22 @@ export async function setupTailwind(config: TailwindConfig) {
63
86
  };
64
87
  }
65
88
 
89
+ if (id === 'custom-theme.css') {
90
+ return {
91
+ base,
92
+ path: id,
93
+ content: cssConfigs?.theme ?? '',
94
+ };
95
+ }
96
+
97
+ if (id === 'custom-utilities.css') {
98
+ return {
99
+ base,
100
+ path: id,
101
+ content: cssConfigs?.utility ?? '',
102
+ };
103
+ }
104
+
66
105
  throw new Error(
67
106
  'stylesheet not supported, you can only import the ones from tailwindcss',
68
107
  );