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 +30 -0
- package/dist/cli/index.mjs +222 -42
- package/dist/index.cjs +27 -4
- package/dist/index.d.cts +18 -6
- package/dist/index.d.mts +18 -6
- package/dist/index.mjs +27 -4
- package/package.json +1 -1
- package/src/components/tailwind/tailwind.spec.tsx +315 -0
- package/src/components/tailwind/tailwind.tsx +18 -9
- package/src/components/tailwind/utils/css/extract-rules-per-class.spec.ts +24 -20
- package/src/components/tailwind/utils/tailwindcss/setup-tailwind.spec.ts +7 -0
- package/src/components/tailwind/utils/tailwindcss/setup-tailwind.ts +41 -2
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
|
package/dist/cli/index.mjs
CHANGED
|
@@ -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
|
|
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 {
|
|
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.
|
|
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
|
|
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
|
-
|
|
6880
|
+
staticImports.push(node.source.value);
|
|
6879
6881
|
},
|
|
6880
6882
|
ExportAllDeclaration({ node }) {
|
|
6881
|
-
|
|
6883
|
+
staticImports.push(node.source.value);
|
|
6882
6884
|
},
|
|
6883
6885
|
ExportNamedDeclaration({ node }) {
|
|
6884
|
-
if (node.source)
|
|
6886
|
+
if (node.source) staticImports.push(node.source.value);
|
|
6885
6887
|
},
|
|
6886
6888
|
TSExternalModuleReference({ node }) {
|
|
6887
|
-
|
|
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")
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7433
|
-
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
|
|
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
|
|
7455
|
-
|
|
7456
|
-
|
|
7457
|
-
|
|
7458
|
-
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
|
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(
|
|
89
|
-
JSON.stringify(
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
api
|
|
87
|
-
|
|
88
|
-
'
|
|
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
api
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
);
|