react-email 6.1.4 → 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,35 @@
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
+
27
+ ## 6.1.5
28
+
29
+ ### Patch Changes
30
+
31
+ - 1a61cb0: Avoid OOM when running `email export` on projects with many templates. esbuild builds now run in batches of 10 entry points, and the render phase runs each batch of 25 templates inside a `worker_threads` worker so V8 isolate memory is reclaimed between batches.
32
+
3
33
  ## 6.1.4
4
34
 
5
35
  ### Patch Changes
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { spawn } from "node:child_process";
4
4
  import { Option, program } from "commander";
5
5
  import * as fs$1 from "node:fs";
6
- import fs, { existsSync, promises, statSync, unlinkSync, writeFileSync } from "node:fs";
6
+ import fs, { existsSync, promises, statSync } from "node:fs";
7
7
  import * as path$2 from "node:path";
8
8
  import path from "node:path";
9
9
  import url, { fileURLToPath } from "node:url";
@@ -31,7 +31,8 @@ import os from "node:os";
31
31
  import Conf from "conf";
32
32
  import * as nodeUtil from "node:util";
33
33
  import { lookup } from "mime-types";
34
- import { build } from "esbuild";
34
+ import { Worker } from "node:worker_threads";
35
+ import { build, stop } from "esbuild";
35
36
  import { glob } from "glob";
36
37
  import normalize$1 from "normalize-path";
37
38
  //#region \0rolldown/runtime.js
@@ -6522,7 +6523,7 @@ const getEmailsDirectoryMetadata = async (absolutePathToEmailsDirectory, keepFil
6522
6523
  //#region package.json
6523
6524
  var package_default = {
6524
6525
  name: "react-email",
6525
- version: "6.1.4",
6526
+ version: "6.2.0",
6526
6527
  description: "A live preview of your emails right in your browser.",
6527
6528
  bin: { "email": "./dist/cli/index.mjs" },
6528
6529
  type: "module",
@@ -6863,7 +6864,8 @@ const build$1 = async ({ dir: emailsDirRelativePath, packageManager }) => {
6863
6864
  //#region src/cli/utils/preview/hot-reloading/get-imported-modules.ts
6864
6865
  const traverse = typeof traverseModule === "function" ? traverseModule : traverseModule.default;
6865
6866
  const getImportedModules = (contents) => {
6866
- const importedPaths = [];
6867
+ const staticImports = [];
6868
+ const dynamicImportPrefixes = [];
6867
6869
  traverse(parse(contents, {
6868
6870
  sourceType: "unambiguous",
6869
6871
  strictMode: false,
@@ -6875,27 +6877,49 @@ const getImportedModules = (contents) => {
6875
6877
  ]
6876
6878
  }), {
6877
6879
  ImportDeclaration({ node }) {
6878
- importedPaths.push(node.source.value);
6880
+ staticImports.push(node.source.value);
6879
6881
  },
6880
6882
  ExportAllDeclaration({ node }) {
6881
- importedPaths.push(node.source.value);
6883
+ staticImports.push(node.source.value);
6882
6884
  },
6883
6885
  ExportNamedDeclaration({ node }) {
6884
- if (node.source) importedPaths.push(node.source.value);
6886
+ if (node.source) staticImports.push(node.source.value);
6885
6887
  },
6886
6888
  TSExternalModuleReference({ node }) {
6887
- importedPaths.push(node.expression.value);
6889
+ staticImports.push(node.expression.value);
6888
6890
  },
6889
6891
  CallExpression({ node }) {
6890
6892
  if ("name" in node.callee && node.callee.name === "require") {
6891
6893
  if (node.arguments.length === 1) {
6892
6894
  const importPathNode = node.arguments[0];
6893
- 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);
6894
6915
  }
6895
6916
  }
6896
6917
  }
6897
6918
  });
6898
- return importedPaths;
6919
+ return {
6920
+ staticImports,
6921
+ dynamicImportPrefixes
6922
+ };
6899
6923
  };
6900
6924
  //#endregion
6901
6925
  //#region src/cli/utils/preview/hot-reloading/resolve-path-aliases.ts
@@ -6918,6 +6942,25 @@ const resolvePathAliases = (importPaths, projectPath) => {
6918
6942
  }
6919
6943
  return importPaths;
6920
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
+ };
6921
6964
  //#endregion
6922
6965
  //#region src/cli/utils/preview/hot-reloading/create-dependency-graph.ts
6923
6966
  const readAllFilesInsideDirectory = async (directory) => {
@@ -6950,6 +6993,34 @@ const checkFileExtensionsUntilItExists = (pathWithoutExtension) => {
6950
6993
  if (existsSync(`${pathWithoutExtension}.mjs`)) return `${pathWithoutExtension}.mjs`;
6951
6994
  if (existsSync(`${pathWithoutExtension}.cjs`)) return `${pathWithoutExtension}.cjs`;
6952
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
+ };
6953
7024
  /**
6954
7025
  * Creates a stateful dependency graph that is structured in a way that you can get
6955
7026
  * the dependents of a module from its path.
@@ -6964,11 +7035,17 @@ const createDependencyGraph = async (directory) => {
6964
7035
  path,
6965
7036
  dependencyPaths: [],
6966
7037
  dependentPaths: [],
7038
+ dynamicDependencyDirectories: [],
6967
7039
  moduleDependencies: []
6968
7040
  }]));
6969
7041
  const getDependencyPaths = async (filePath) => {
6970
7042
  const contents = await promises.readFile(filePath, "utf8");
6971
- 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];
6972
7049
  if (!dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)) return dependencyPath;
6973
7050
  let pathToDependencyFromDirectory = path.resolve(path.dirname(filePath), dependencyPath);
6974
7051
  let isDirectory = false;
@@ -6993,7 +7070,8 @@ const createDependencyGraph = async (directory) => {
6993
7070
  const moduleDependencies = importedPathsRelativeToDirectory.filter((dependencyPath) => !dependencyPath.startsWith(".") && !path.isAbsolute(dependencyPath));
6994
7071
  return {
6995
7072
  dependencyPaths: importedPathsRelativeToDirectory.filter((dependencyPath) => dependencyPath.startsWith(".") || path.isAbsolute(dependencyPath)),
6996
- moduleDependencies
7073
+ moduleDependencies,
7074
+ dynamicDependencyDirectories: Array.from(new Set(imports.dynamicImportPrefixes.map((prefix) => resolveDynamicImportDirectory(prefix, filePath)).filter((d) => typeof d === "string")))
6997
7075
  };
6998
7076
  };
6999
7077
  const updateModuleDependenciesInGraph = async (moduleFilePath) => {
@@ -7001,10 +7079,12 @@ const createDependencyGraph = async (directory) => {
7001
7079
  path: moduleFilePath,
7002
7080
  dependencyPaths: [],
7003
7081
  dependentPaths: [],
7082
+ dynamicDependencyDirectories: [],
7004
7083
  moduleDependencies: []
7005
7084
  };
7006
- const { moduleDependencies, dependencyPaths: newDependencyPaths } = await getDependencyPaths(moduleFilePath);
7085
+ const { moduleDependencies, dependencyPaths: newDependencyPaths, dynamicDependencyDirectories: newDynamicDependencyDirectories } = await getDependencyPaths(moduleFilePath);
7007
7086
  graph[moduleFilePath].moduleDependencies = moduleDependencies;
7087
+ graph[moduleFilePath].dynamicDependencyDirectories = newDynamicDependencyDirectories;
7008
7088
  for (const dependencyPath of graph[moduleFilePath].dependencyPaths) {
7009
7089
  if (newDependencyPaths.includes(dependencyPath)) continue;
7010
7090
  const dependencyModule = graph[dependencyPath];
@@ -7054,6 +7134,10 @@ const createDependencyGraph = async (directory) => {
7054
7134
  { resolveDependentsOf: function resolveDependentsOf(pathToModule) {
7055
7135
  const dependentPaths = /* @__PURE__ */ new Set();
7056
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
+ }
7057
7141
  while (stack.length > 0) {
7058
7142
  const moduleEntry = graph[stack.pop()];
7059
7143
  if (!moduleEntry) continue;
@@ -7093,6 +7177,13 @@ const setupHotreloading = async (devServer, emailDirRelativePath) => {
7093
7177
  const getFilesOutsideEmailsDirectory = () => Object.keys(dependencyGraph).filter((p) => path.relative(absolutePathToEmailsDirectory, p).startsWith(".."));
7094
7178
  let filesOutsideEmailsDirectory = getFilesOutsideEmailsDirectory();
7095
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);
7096
7187
  const exit = async () => {
7097
7188
  await watcher.close();
7098
7189
  };
@@ -7106,6 +7197,10 @@ const setupHotreloading = async (devServer, emailDirRelativePath) => {
7106
7197
  for (const p of filesOutsideEmailsDirectory) if (!newFilesOutsideEmailsDirectory.includes(p)) watcher.unwatch(p);
7107
7198
  for (const p of newFilesOutsideEmailsDirectory) if (!filesOutsideEmailsDirectory.includes(p)) watcher.add(p);
7108
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;
7109
7204
  changes.push({
7110
7205
  event,
7111
7206
  filename: relativePathToChangeTarget
@@ -7351,6 +7446,33 @@ const dev = async ({ dir: emailsDirRelativePath, port }) => {
7351
7446
  }
7352
7447
  };
7353
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
7354
7476
  //#region src/cli/utils/esbuild/escape-string-for-regex.ts
7355
7477
  function escapeStringForRegex(string) {
7356
7478
  return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
@@ -7403,7 +7525,40 @@ const getEmailTemplatesFromDirectory = (emailDirectory) => {
7403
7525
  for (const directory of emailDirectory.subDirectories) templatePaths.push(...getEmailTemplatesFromDirectory(directory));
7404
7526
  return templatePaths;
7405
7527
  };
7406
- const require$1 = createRequire(url.fileURLToPath(import.meta.url));
7528
+ const BUILD_BATCH_SIZE = 10;
7529
+ const RENDER_BATCH_SIZE = 25;
7530
+ const renderWorkerSource = `
7531
+ const { unlinkSync, writeFileSync } = require('node:fs');
7532
+ const { parentPort, workerData } = require('node:worker_threads');
7533
+
7534
+ const { templates, options } = workerData;
7535
+
7536
+ (async () => {
7537
+ for (const template of templates) {
7538
+ try {
7539
+ const emailModule = require(template);
7540
+ const rendered = await emailModule.render(
7541
+ emailModule.reactEmailCreateReactElement(emailModule.default, {}),
7542
+ options,
7543
+ );
7544
+ const htmlPath = template.replace(
7545
+ '.cjs',
7546
+ options.plainText ? '.txt' : '.html',
7547
+ );
7548
+ writeFileSync(htmlPath, rendered);
7549
+ unlinkSync(template);
7550
+ parentPort.postMessage({ type: 'progress', template });
7551
+ } catch (exception) {
7552
+ parentPort.postMessage({
7553
+ type: 'error',
7554
+ template,
7555
+ message: exception && exception.stack ? exception.stack : String(exception),
7556
+ });
7557
+ process.exit(1);
7558
+ }
7559
+ }
7560
+ })();
7561
+ `;
7407
7562
  const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirectoryPath, options) => {
7408
7563
  let spinner;
7409
7564
  if (!options.silent) {
@@ -7423,20 +7578,24 @@ const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirec
7423
7578
  if (fs.existsSync(pathToWhereEmailMarkupShouldBeDumped)) fs.rmSync(pathToWhereEmailMarkupShouldBeDumped, { recursive: true });
7424
7579
  const allTemplates = getEmailTemplatesFromDirectory(emailsDirectoryMetadata);
7425
7580
  try {
7426
- await build({
7427
- bundle: true,
7428
- entryPoints: allTemplates,
7429
- external: ["css-tree"],
7430
- format: "cjs",
7431
- jsx: "automatic",
7432
- loader: { ".js": "jsx" },
7433
- logLevel: "silent",
7434
- outExtension: { ".js": ".cjs" },
7435
- outdir: pathToWhereEmailMarkupShouldBeDumped,
7436
- platform: "node",
7437
- plugins: [renderingUtilitiesExporter(allTemplates)],
7438
- write: true
7439
- });
7581
+ for (let i = 0; i < allTemplates.length; i += BUILD_BATCH_SIZE) {
7582
+ const batch = allTemplates.slice(i, i + BUILD_BATCH_SIZE);
7583
+ await build({
7584
+ bundle: true,
7585
+ entryPoints: batch,
7586
+ external: ["css-tree"],
7587
+ format: "cjs",
7588
+ jsx: "automatic",
7589
+ loader: { ".js": "jsx" },
7590
+ logLevel: "silent",
7591
+ outExtension: { ".js": ".cjs" },
7592
+ outdir: pathToWhereEmailMarkupShouldBeDumped,
7593
+ platform: "node",
7594
+ plugins: [inlineCssLoader(), renderingUtilitiesExporter(batch)],
7595
+ write: true
7596
+ });
7597
+ await stop();
7598
+ }
7440
7599
  } catch (exception) {
7441
7600
  if (spinner) stopSpinnerAndPersist(spinner, {
7442
7601
  symbol: logSymbols.error,
@@ -7451,20 +7610,41 @@ const exportTemplates = async (pathToWhereEmailMarkupShouldBeDumped, emailsDirec
7451
7610
  spinner.setText(`rendering ${allBuiltTemplates[0]?.split("/").pop()}`);
7452
7611
  spinner.start();
7453
7612
  }
7454
- for await (const template of allBuiltTemplates) try {
7455
- if (spinner) spinner.setText(`rendering ${template.split("/").pop()}`);
7456
- delete require$1.cache[template];
7457
- const emailModule = require$1(template);
7458
- const rendered = await emailModule.render(emailModule.reactEmailCreateReactElement(emailModule.default, {}), options);
7459
- writeFileSync(template.replace(".cjs", options.plainText ? ".txt" : ".html"), rendered);
7460
- unlinkSync(template);
7461
- } catch (exception) {
7462
- if (spinner) stopSpinnerAndPersist(spinner, {
7463
- symbol: logSymbols.error,
7464
- text: `failed when rendering ${template.split("/").pop()}`
7465
- });
7466
- console.error(exception);
7467
- process.exit(1);
7613
+ for (let i = 0; i < allBuiltTemplates.length; i += RENDER_BATCH_SIZE) {
7614
+ const batch = allBuiltTemplates.slice(i, i + RENDER_BATCH_SIZE);
7615
+ let failedTemplate;
7616
+ let failureMessage;
7617
+ try {
7618
+ await new Promise((resolve, reject) => {
7619
+ const worker = new Worker(renderWorkerSource, {
7620
+ eval: true,
7621
+ workerData: {
7622
+ templates: batch,
7623
+ options
7624
+ }
7625
+ });
7626
+ worker.on("message", (msg) => {
7627
+ if (msg.type === "progress") {
7628
+ if (spinner) spinner.setText(`rendering ${msg.template.split("/").pop()}`);
7629
+ } else if (msg.type === "error") {
7630
+ failedTemplate = msg.template;
7631
+ failureMessage = msg.message;
7632
+ }
7633
+ });
7634
+ worker.on("error", reject);
7635
+ worker.on("exit", (code) => {
7636
+ if (code !== 0) reject(new Error(failureMessage ?? `Render worker exited with code ${code}`));
7637
+ else resolve();
7638
+ });
7639
+ });
7640
+ } catch (exception) {
7641
+ if (spinner) stopSpinnerAndPersist(spinner, {
7642
+ symbol: logSymbols.error,
7643
+ text: failedTemplate ? `failed when rendering ${failedTemplate.split("/").pop()}` : "failed when rendering"
7644
+ });
7645
+ console.error(exception);
7646
+ process.exit(1);
7647
+ }
7468
7648
  }
7469
7649
  if (spinner) {
7470
7650
  spinner.succeed("Rendered all files");
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.4",
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
  );