react-email 4.0.0-alpha.4 → 4.0.0-alpha.6
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 +12 -0
- package/dist/cli/index.js +1175 -2658
- package/dist/cli/index.mjs +18 -12
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +31 -34
- package/dist/preview/.next/app-path-routes-manifest.json +6 -1
- package/dist/preview/.next/build-manifest.json +14 -14
- package/dist/preview/.next/cache/.rscinfo +1 -1
- package/dist/preview/.next/cache/webpack/client-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/client-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/0.pack +0 -0
- package/dist/preview/.next/cache/webpack/server-production/index.pack +0 -0
- package/dist/preview/.next/diagnostics/framework.json +1 -1
- package/dist/preview/.next/export-marker.json +6 -1
- package/dist/preview/.next/images-manifest.json +57 -1
- package/dist/preview/.next/next-minimal-server.js.nft.json +1 -1
- package/dist/preview/.next/next-server.js.nft.json +1 -1
- package/dist/preview/.next/prerender-manifest.json +41 -1
- package/dist/preview/.next/required-server-files.json +310 -1
- package/dist/preview/.next/routes-manifest.json +64 -1
- package/dist/preview/.next/server/app/_not-found/page.js +1 -1
- package/dist/preview/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js +1 -1
- package/dist/preview/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page.js +1 -1
- package/dist/preview/.next/server/app/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page.js +47 -10
- package/dist/preview/.next/server/app/preview/[...slug]/page.js.nft.json +1 -1
- package/dist/preview/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -1
- package/dist/preview/.next/server/app-paths-manifest.json +1 -1
- package/dist/preview/.next/server/chunks/171.js +14 -0
- package/dist/preview/.next/server/chunks/446.js +6 -0
- package/dist/preview/.next/server/chunks/600.js +8 -0
- package/dist/preview/.next/server/chunks/811.js +13 -0
- package/dist/preview/.next/server/chunks/833.js +1 -0
- package/dist/preview/.next/server/functions-config-manifest.json +4 -1
- package/dist/preview/.next/server/middleware-build-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.js +1 -1
- package/dist/preview/.next/server/next-font-manifest.json +1 -1
- package/dist/preview/.next/server/pages/500.html +1 -1
- package/dist/preview/.next/server/pages/_app.js +1 -1
- package/dist/preview/.next/server/pages/_app.js.nft.json +1 -1
- package/dist/preview/.next/server/pages/_document.js +1 -1
- package/dist/preview/.next/server/pages/_document.js.nft.json +1 -1
- package/dist/preview/.next/server/pages/_error.js +1 -1
- package/dist/preview/.next/server/pages/_error.js.nft.json +1 -1
- package/dist/preview/.next/server/pages-manifest.json +5 -1
- package/dist/preview/.next/server/server-reference-manifest.js +1 -1
- package/dist/preview/.next/server/server-reference-manifest.json +1 -1
- package/dist/preview/.next/server/webpack-runtime.js +1 -1
- package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +1 -0
- package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +1 -0
- package/dist/preview/.next/static/chunks/744-79730358b37b2212.js +1 -0
- package/dist/preview/.next/static/chunks/781-5f16c6bc9d9d4cc1.js +1 -0
- package/dist/preview/.next/static/chunks/832ad4be-cb988facfb8f955f.js +1 -0
- package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +1 -0
- package/dist/preview/.next/static/chunks/{afa401a5-9ebf2515b1397993.js → afa401a5-3e949a1cfd317dd3.js} +3 -3
- package/dist/preview/.next/static/chunks/app/_not-found/page-09d694081cc9d4dc.js +1 -0
- package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +1 -0
- package/dist/preview/.next/static/chunks/framework-c2bd6d936e3077bc.js +1 -0
- package/dist/preview/.next/static/chunks/main-44463a8301435b64.js +1 -0
- package/dist/preview/.next/static/chunks/main-app-c2e686acf8d370d7.js +1 -0
- package/dist/preview/.next/static/chunks/pages/_app-f3011d3f00bb8dba.js +1 -0
- package/dist/preview/.next/static/chunks/pages/_error-39a87dee2e97a2a3.js +1 -0
- package/dist/preview/.next/static/chunks/{webpack-9255716c9496e606.js → webpack-41e2667c9f086a4f.js} +1 -1
- package/dist/preview/.next/static/css/d7df9cfc3e182163.css +3 -0
- package/dist/preview/.next/static/gFk9UfWL8joM4iD7-wlKF/_buildManifest.js +1 -0
- package/dist/preview/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
- package/dist/preview/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
- package/dist/preview/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
- package/dist/preview/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
- package/dist/preview/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
- package/dist/preview/.next/static/media/e4051546b3043204-s.p.otf +0 -0
- package/dist/preview/.next/trace +26 -22
- package/dist/preview/.next/types/cache-life.d.ts +3 -3
- package/package.json +17 -11
- package/scripts/build-preview-server.mjs +32 -0
- package/scripts/fill-caniemail-data.mjs +36 -0
- package/src/actions/email-validation/caniemail-data.ts +85993 -0
- package/src/actions/email-validation/check-compatibility.ts +322 -0
- package/src/actions/email-validation/check-images.spec.tsx +21 -12
- package/src/actions/email-validation/check-images.ts +88 -86
- package/src/actions/email-validation/check-links.spec.tsx +24 -14
- package/src/actions/email-validation/check-links.ts +59 -56
- package/src/actions/get-email-path-from-slug.ts +1 -1
- package/src/actions/render-email-by-path.tsx +2 -1
- package/src/{utils/emails-directory-absolute-path.ts → app/env.ts} +2 -0
- package/src/app/fonts/SFMono/SFMonoBold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoBoldItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavy.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoHeavyItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLight.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoLightItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMedium.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoMediumItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegular.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoRegularItalic.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemibold.otf +0 -0
- package/src/app/fonts/SFMono/SFMonoSemiboldItalic.otf +0 -0
- package/src/app/fonts.ts +39 -0
- package/src/app/layout.tsx +6 -3
- package/src/app/page.tsx +4 -4
- package/src/app/preview/[...slug]/page.tsx +73 -16
- package/src/app/preview/[...slug]/preview.tsx +49 -77
- package/src/components/code.tsx +0 -1
- package/src/components/icons/icon-base.tsx +4 -2
- package/src/components/icons/icon-reload.tsx +19 -0
- package/src/components/icons/icon-scanner.tsx +19 -0
- package/src/components/icons/icon-scissors.tsx +19 -0
- package/src/components/icons/icon-warning.tsx +31 -0
- package/src/components/send.tsx +1 -2
- package/src/components/shell.tsx +52 -88
- package/src/components/sidebar/file-tree-directory-children.tsx +1 -1
- package/src/components/sidebar/file-tree.tsx +1 -1
- package/src/components/sidebar/sidebar.tsx +23 -378
- package/src/components/toolbar/linter.tsx +310 -0
- package/src/components/toolbar/results-table.tsx +0 -0
- package/src/components/toolbar/results.tsx +48 -0
- package/src/components/toolbar/spam-assassin.tsx +144 -0
- package/src/components/toolbar/toolbar-button.tsx +50 -0
- package/src/components/toolbar/use-cached-state.ts +33 -0
- package/src/components/toolbar.tsx +197 -0
- package/src/components/tooltip-content.tsx +1 -2
- package/src/components/topbar/view-size-controls.tsx +1 -0
- package/src/components/topbar.tsx +29 -48
- package/src/contexts/emails.tsx +2 -1
- package/src/contexts/preview.tsx +81 -0
- package/src/hooks/use-email-rendering-result.ts +2 -1
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- package/src/utils/caniemail/all-css-properties.ts +358 -0
- package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
- package/src/utils/caniemail/ast/get-used-style-properties.ts +91 -0
- package/src/utils/caniemail/get-compatibility-stats-for-entry.ts +118 -0
- package/src/utils/caniemail/get-css-functions.ts +25 -0
- package/src/utils/caniemail/get-css-property-names.ts +32 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +14 -0
- package/src/utils/caniemail/get-css-unit.ts +3 -0
- package/src/utils/caniemail/get-element-attributes.ts +7 -0
- package/src/utils/caniemail/get-element-names.ts +20 -0
- package/src/utils/caniemail/tailwind/generate-tailwind-rules.ts +30 -0
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +205 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.spec.ts +25 -0
- package/src/utils/caniemail/tailwind/get-tailwind-metadata.ts +45 -0
- package/src/utils/caniemail/tailwind/setup-tailwind-context.ts +15 -0
- package/src/utils/get-email-component.ts +34 -67
- package/src/utils/linting.ts +85 -0
- package/src/utils/result.ts +49 -0
- package/src/utils/run-bundled-code.ts +64 -0
- package/tailwind-internals.d.ts +133 -0
- package/tailwind.config.ts +1 -0
- package/tsconfig.json +9 -3
- package/build-preview-server.mjs +0 -25
- package/dist/preview/.next/server/chunks/196.js +0 -5
- package/dist/preview/.next/server/chunks/300.js +0 -13
- package/dist/preview/.next/server/chunks/631.js +0 -6
- package/dist/preview/.next/server/chunks/644.js +0 -1
- package/dist/preview/.next/server/chunks/734.js +0 -15
- package/dist/preview/.next/static/Pt6wqIrWnQxbiyqaKNFOx/_buildManifest.js +0 -1
- package/dist/preview/.next/static/chunks/285-dbf6306a0d45c33d.js +0 -1
- package/dist/preview/.next/static/chunks/447-886131c35ca42b91.js +0 -1
- package/dist/preview/.next/static/chunks/490-d5745684930d49e0.js +0 -1
- package/dist/preview/.next/static/chunks/5fec7a0a-5179023f3f5a9421.js +0 -1
- package/dist/preview/.next/static/chunks/603-36207c8905355e23.js +0 -1
- package/dist/preview/.next/static/chunks/797-46f6c20952f0a280.js +0 -2
- package/dist/preview/.next/static/chunks/app/_not-found/page-96d3eac723be3ee2.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-d06046b8a368df3b.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-ef1c23b954fbd0b5.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-ea8e1ae2b5a4a0ec.js +0 -1
- package/dist/preview/.next/static/chunks/framework-e7cae9cecd5c9ba2.js +0 -1
- package/dist/preview/.next/static/chunks/main-app-9f2fb5ea26e2765b.js +0 -1
- package/dist/preview/.next/static/chunks/main-df761fde212f9cda.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_app-203a61b355820ccf.js +0 -1
- package/dist/preview/.next/static/chunks/pages/_error-1764ca54938748c8.js +0 -1
- package/dist/preview/.next/static/css/e4822d5ba3082a95.css +0 -3
- package/dist/preview/.next/static/css/ec5d7e66bd3b6cb8.css +0 -1
- package/src/app/inter.ts +0 -7
- package/src/components/icons/icon-circle-check.tsx +0 -21
- package/src/components/icons/icon-circle-close.tsx +0 -17
- package/src/components/icons/icon-circle-warning.tsx +0 -17
- package/src/components/sidebar/image-checker.tsx +0 -162
- package/src/components/sidebar/link-checker.tsx +0 -151
- package/src/components/sidebar/spam-assassin.tsx +0 -158
- /package/dist/preview/.next/static/{Pt6wqIrWnQxbiyqaKNFOx → gFk9UfWL8joM4iD7-wlKF}/_ssgManifest.js +0 -0
- /package/src/components/{sidebar → toolbar}/checking-results.tsx +0 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export const getElementNames = (title: string, keywords: string | null) => {
|
|
2
|
+
const match = /<(?<elementName>[^>]*)> element/.exec(title);
|
|
3
|
+
if (match) {
|
|
4
|
+
const [_full, elementName] = match;
|
|
5
|
+
|
|
6
|
+
if (elementName) {
|
|
7
|
+
return [elementName];
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (keywords !== null && keywords.length > 0) {
|
|
12
|
+
return keywords.split(/\s*,\s*/).map((piece) => piece.trim());
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (title.split(',').length > 1) {
|
|
16
|
+
return title.split(/\s*,\s*/).map((piece) => piece.trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return [];
|
|
20
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Root, Rule } from 'postcss';
|
|
2
|
+
import postcss from 'postcss';
|
|
3
|
+
import evaluateTailwindFunctions from 'tailwindcss/lib/lib/evaluateTailwindFunctions';
|
|
4
|
+
import { generateRules as rawGenerateRules } from 'tailwindcss/lib/lib/generateRules';
|
|
5
|
+
import type { JitContext } from 'tailwindcss/lib/lib/setupContextUtils';
|
|
6
|
+
|
|
7
|
+
export const generateTailwindCssRules = (
|
|
8
|
+
classNames: string[],
|
|
9
|
+
tailwindContext: JitContext,
|
|
10
|
+
): { root: Root; rules: Rule[] } => {
|
|
11
|
+
const bigIntRuleTuples: [bigint, Rule][] = rawGenerateRules(
|
|
12
|
+
new Set(classNames),
|
|
13
|
+
tailwindContext,
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
const root = postcss.root({
|
|
17
|
+
nodes: bigIntRuleTuples.map(([, rule]) => rule),
|
|
18
|
+
});
|
|
19
|
+
evaluateTailwindFunctions(tailwindContext)(root);
|
|
20
|
+
|
|
21
|
+
const actualRules: Rule[] = [];
|
|
22
|
+
root.walkRules((rule) => {
|
|
23
|
+
actualRules.push(rule);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
root,
|
|
28
|
+
rules: actualRules,
|
|
29
|
+
};
|
|
30
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { Node } from '@babel/traverse';
|
|
4
|
+
import traverse from '@babel/traverse';
|
|
5
|
+
import * as esbuild from 'esbuild';
|
|
6
|
+
import type { Config as TailwindOriginalConfig } from 'tailwindcss';
|
|
7
|
+
import type { AST } from '../../../actions/email-validation/check-compatibility';
|
|
8
|
+
import { isErr } from '../../result';
|
|
9
|
+
import { runBundledCode } from '../../run-bundled-code';
|
|
10
|
+
|
|
11
|
+
export type TailwindConfig = Pick<
|
|
12
|
+
TailwindOriginalConfig,
|
|
13
|
+
| 'important'
|
|
14
|
+
| 'prefix'
|
|
15
|
+
| 'separator'
|
|
16
|
+
| 'safelist'
|
|
17
|
+
| 'blocklist'
|
|
18
|
+
| 'presets'
|
|
19
|
+
| 'future'
|
|
20
|
+
| 'experimental'
|
|
21
|
+
| 'darkMode'
|
|
22
|
+
| 'theme'
|
|
23
|
+
| 'corePlugins'
|
|
24
|
+
| 'plugins'
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
const getFirstExistingFilepath = (filePaths: string[]) => {
|
|
28
|
+
for (const filePath of filePaths) {
|
|
29
|
+
if (fs.existsSync(filePath)) {
|
|
30
|
+
return filePath;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
type ImportDeclaration = Node & { type: 'ImportDeclaration' };
|
|
36
|
+
|
|
37
|
+
export const getTailwindConfig = async (
|
|
38
|
+
sourceCode: string,
|
|
39
|
+
ast: AST,
|
|
40
|
+
sourcePath: string,
|
|
41
|
+
): Promise<TailwindConfig> => {
|
|
42
|
+
const configAttribute = getTailwindConfigNode(ast);
|
|
43
|
+
|
|
44
|
+
if (configAttribute) {
|
|
45
|
+
const configIdentifierName =
|
|
46
|
+
configAttribute.value?.type === 'JSXExpressionContainer' &&
|
|
47
|
+
configAttribute.value.expression.type === 'Identifier'
|
|
48
|
+
? configAttribute.value.expression.name
|
|
49
|
+
: undefined;
|
|
50
|
+
if (configIdentifierName) {
|
|
51
|
+
const tailwindConfigImport = getImportWithGivenDefaultSpecifier(
|
|
52
|
+
ast,
|
|
53
|
+
configIdentifierName,
|
|
54
|
+
);
|
|
55
|
+
if (tailwindConfigImport) {
|
|
56
|
+
return getConfigFromImport(tailwindConfigImport, sourcePath);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const configObjectExpression =
|
|
61
|
+
configAttribute.value?.type === 'JSXExpressionContainer' &&
|
|
62
|
+
configAttribute.value.expression.type === 'ObjectExpression'
|
|
63
|
+
? configAttribute.value.expression
|
|
64
|
+
: undefined;
|
|
65
|
+
if (configObjectExpression?.start && configObjectExpression.end) {
|
|
66
|
+
const configObjectSourceCode = sourceCode.slice(
|
|
67
|
+
configObjectExpression.start,
|
|
68
|
+
configObjectExpression.end,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const getConfig = new Function(`return ${configObjectSourceCode}`);
|
|
73
|
+
return getConfig() as TailwindConfig;
|
|
74
|
+
} catch (exception) {
|
|
75
|
+
console.warn(exception);
|
|
76
|
+
console.warn(
|
|
77
|
+
`Tried reading the config defined directly in the Tailwind component but was unable to, probably because it isn't a valid javascript object by itself.`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {};
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getConfigFromImport = async (
|
|
87
|
+
tailwindConfigImport: ImportDeclaration,
|
|
88
|
+
sourcePath: string,
|
|
89
|
+
): Promise<TailwindConfig> => {
|
|
90
|
+
const configRelativePath = tailwindConfigImport.source.value;
|
|
91
|
+
const configImportPath = path.resolve(
|
|
92
|
+
path.dirname(sourcePath),
|
|
93
|
+
configRelativePath,
|
|
94
|
+
);
|
|
95
|
+
const configFilepath = getFirstExistingFilepath([
|
|
96
|
+
configImportPath,
|
|
97
|
+
`${configImportPath}.ts`,
|
|
98
|
+
`${configImportPath}.js`,
|
|
99
|
+
`${configImportPath}.mjs`,
|
|
100
|
+
`${configImportPath}.cjs`,
|
|
101
|
+
]);
|
|
102
|
+
|
|
103
|
+
if (configFilepath === undefined) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Could not find Tailwind config by inferring it's extension type (tried .ts, .js, .mjs and .cjs).`,
|
|
106
|
+
{
|
|
107
|
+
cause: {
|
|
108
|
+
configPath: configImportPath,
|
|
109
|
+
sourcePath,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const configBuildResult = await esbuild.build({
|
|
116
|
+
bundle: false,
|
|
117
|
+
entryPoints: [configFilepath],
|
|
118
|
+
platform: 'node',
|
|
119
|
+
write: false,
|
|
120
|
+
format: 'cjs',
|
|
121
|
+
logLevel: 'silent',
|
|
122
|
+
});
|
|
123
|
+
const configFile = configBuildResult.outputFiles[0];
|
|
124
|
+
if (configFile === undefined) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
'Could not build config file as it was found as undefined, this is a bug please open an issue.',
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const configModule = runBundledCode(configFile.text, configFilepath);
|
|
130
|
+
if (isErr(configModule)) {
|
|
131
|
+
throw new Error('Error when trying to run the config file', {
|
|
132
|
+
cause: configModule.error,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
console.log(configModule);
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
typeof configModule.value === 'object' &&
|
|
140
|
+
configModule.value !== null &&
|
|
141
|
+
'default' in configModule.value
|
|
142
|
+
) {
|
|
143
|
+
return configModule.value.default as TailwindConfig;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
throw new Error(
|
|
147
|
+
`Could not read Tailwind config at ${configFilepath} because it doesn't have a default export in it.`,
|
|
148
|
+
{
|
|
149
|
+
cause: {
|
|
150
|
+
configModule,
|
|
151
|
+
configFilepath,
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const getImportWithGivenDefaultSpecifier = (
|
|
158
|
+
ast: AST,
|
|
159
|
+
specifierName: string,
|
|
160
|
+
) => {
|
|
161
|
+
let importNode: ImportDeclaration | undefined;
|
|
162
|
+
traverse(ast, {
|
|
163
|
+
ImportDeclaration(nodePath) {
|
|
164
|
+
if (
|
|
165
|
+
nodePath.node.specifiers.some(
|
|
166
|
+
(specifier) =>
|
|
167
|
+
specifier.type === 'ImportDefaultSpecifier' &&
|
|
168
|
+
specifier.local.name === specifierName,
|
|
169
|
+
)
|
|
170
|
+
) {
|
|
171
|
+
importNode = nodePath.node;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return importNode;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
type JSXAttribute = Node & { type: 'JSXAttribute' };
|
|
179
|
+
|
|
180
|
+
const getTailwindConfigNode = (ast: AST) => {
|
|
181
|
+
let tailwindConfigNode: JSXAttribute | undefined;
|
|
182
|
+
traverse(ast, {
|
|
183
|
+
JSXOpeningElement(nodePath) {
|
|
184
|
+
if (
|
|
185
|
+
nodePath.node.name.type === 'JSXIdentifier' &&
|
|
186
|
+
nodePath.node.name.name === 'Tailwind'
|
|
187
|
+
) {
|
|
188
|
+
const configAttribute = nodePath.node.attributes.find(
|
|
189
|
+
(
|
|
190
|
+
attribute,
|
|
191
|
+
): attribute is Node & {
|
|
192
|
+
type: 'JSXAttribute';
|
|
193
|
+
} =>
|
|
194
|
+
attribute.type === 'JSXAttribute' &&
|
|
195
|
+
attribute.name.type === 'JSXIdentifier' &&
|
|
196
|
+
attribute.name.name === 'config',
|
|
197
|
+
);
|
|
198
|
+
if (configAttribute) {
|
|
199
|
+
tailwindConfigNode = configAttribute;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return tailwindConfigNode;
|
|
205
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { parse } from '@babel/parser';
|
|
4
|
+
import { getTailwindMetadata } from './get-tailwind-metadata';
|
|
5
|
+
|
|
6
|
+
describe('getTailwindMetadata()', () => {
|
|
7
|
+
test('with the netlify-welcome demo email', async () => {
|
|
8
|
+
const emailPath = path.resolve(
|
|
9
|
+
__dirname,
|
|
10
|
+
'../../../../../../apps/demo/emails/welcome/netlify-welcome.tsx',
|
|
11
|
+
);
|
|
12
|
+
const reactCode = await fs.readFile(emailPath, 'utf8');
|
|
13
|
+
const ast = parse(reactCode, {
|
|
14
|
+
strictMode: false,
|
|
15
|
+
errorRecovery: true,
|
|
16
|
+
sourceType: 'unambiguous',
|
|
17
|
+
plugins: ['jsx', 'typescript', 'decorators'],
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const tailwindMetadata = getTailwindMetadata(ast, reactCode, emailPath);
|
|
21
|
+
|
|
22
|
+
expect(tailwindMetadata).toBeDefined();
|
|
23
|
+
// console.log(tailwindMetadata);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import traverse from '@babel/traverse';
|
|
2
|
+
import type { JitContext } from 'tailwindcss/lib/lib/setupContextUtils';
|
|
3
|
+
import type { AST } from '../../../actions/email-validation/check-compatibility';
|
|
4
|
+
import { type TailwindConfig, getTailwindConfig } from './get-tailwind-config';
|
|
5
|
+
import { setupTailwindContext } from './setup-tailwind-context';
|
|
6
|
+
|
|
7
|
+
export const getTailwindMetadata = async (
|
|
8
|
+
ast: AST,
|
|
9
|
+
sourceCode: string,
|
|
10
|
+
sourcePath: string,
|
|
11
|
+
): Promise<
|
|
12
|
+
| {
|
|
13
|
+
hasTailwind: false;
|
|
14
|
+
}
|
|
15
|
+
| {
|
|
16
|
+
hasTailwind: true;
|
|
17
|
+
config: TailwindConfig;
|
|
18
|
+
context: JitContext;
|
|
19
|
+
}
|
|
20
|
+
> => {
|
|
21
|
+
let hasTailwind = false as boolean;
|
|
22
|
+
traverse(ast, {
|
|
23
|
+
JSXOpeningElement(path) {
|
|
24
|
+
if (
|
|
25
|
+
path.node.name.type === 'JSXIdentifier' &&
|
|
26
|
+
path.node.name.name === 'Tailwind'
|
|
27
|
+
) {
|
|
28
|
+
hasTailwind = true;
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!hasTailwind) {
|
|
34
|
+
return { hasTailwind: false };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const config = await getTailwindConfig(sourceCode, ast, sourcePath);
|
|
38
|
+
const context = setupTailwindContext(config);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
hasTailwind: true,
|
|
42
|
+
config,
|
|
43
|
+
context,
|
|
44
|
+
};
|
|
45
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { createContext } from 'tailwindcss/lib/lib/setupContextUtils';
|
|
2
|
+
import resolveConfig from 'tailwindcss/resolveConfig';
|
|
3
|
+
import type { TailwindConfig } from './get-tailwind-config';
|
|
4
|
+
|
|
5
|
+
export const setupTailwindContext = (config: TailwindConfig) => {
|
|
6
|
+
return createContext(
|
|
7
|
+
resolveConfig({
|
|
8
|
+
...config,
|
|
9
|
+
content: [],
|
|
10
|
+
corePlugins: {
|
|
11
|
+
preflight: false,
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
);
|
|
15
|
+
};
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import vm from 'node:vm';
|
|
4
2
|
import type { render } from '@react-email/render';
|
|
5
3
|
import { type BuildFailure, type OutputFile, build } from 'esbuild';
|
|
6
4
|
import type React from 'react';
|
|
7
5
|
import type { RawSourceMap } from 'source-map-js';
|
|
6
|
+
import { z } from 'zod';
|
|
8
7
|
import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
|
|
9
8
|
import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
|
|
10
|
-
import {
|
|
9
|
+
import { isErr } from './result';
|
|
10
|
+
import { runBundledCode } from './run-bundled-code';
|
|
11
11
|
import type { EmailTemplate as EmailComponent } from './types/email-template';
|
|
12
12
|
import type { ErrorObject } from './types/error-object';
|
|
13
13
|
|
|
14
|
+
const EmailComponentModule = z.object({
|
|
15
|
+
default: z.function(),
|
|
16
|
+
render: z.function(),
|
|
17
|
+
reactEmailCreateReactElement: z.function(),
|
|
18
|
+
});
|
|
19
|
+
|
|
14
20
|
export const getEmailComponent = async (
|
|
15
21
|
emailPath: string,
|
|
16
22
|
): Promise<
|
|
@@ -61,79 +67,38 @@ export const getEmailComponent = async (
|
|
|
61
67
|
const bundledEmailFile = outputFiles[1]!;
|
|
62
68
|
const builtEmailCode = bundledEmailFile.text;
|
|
63
69
|
|
|
64
|
-
const fakeContext = {
|
|
65
|
-
...global,
|
|
66
|
-
console,
|
|
67
|
-
Buffer,
|
|
68
|
-
AbortSignal,
|
|
69
|
-
Event,
|
|
70
|
-
EventTarget,
|
|
71
|
-
TextDecoder,
|
|
72
|
-
Request,
|
|
73
|
-
Response,
|
|
74
|
-
TextDecoderStream,
|
|
75
|
-
TextEncoder,
|
|
76
|
-
TextEncoderStream,
|
|
77
|
-
ReadableStream,
|
|
78
|
-
URL,
|
|
79
|
-
URLSearchParams,
|
|
80
|
-
Headers,
|
|
81
|
-
module: {
|
|
82
|
-
exports: {
|
|
83
|
-
default: undefined as unknown,
|
|
84
|
-
render: undefined as unknown,
|
|
85
|
-
reactEmailCreateReactElement: undefined as unknown,
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
__filename: emailPath,
|
|
89
|
-
__dirname: path.dirname(emailPath),
|
|
90
|
-
require: (specifiedModule: string) => {
|
|
91
|
-
let m = specifiedModule;
|
|
92
|
-
if (specifiedModule.startsWith('node:')) {
|
|
93
|
-
m = m.split(':')[1]!;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (m in staticNodeModulesForVM) {
|
|
97
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
98
|
-
return staticNodeModulesForVM[m];
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-useless-template-literals
|
|
102
|
-
return require(`${specifiedModule}`) as unknown;
|
|
103
|
-
// this stupid string templating was necessary to not have
|
|
104
|
-
// webpack warnings like:
|
|
105
|
-
//
|
|
106
|
-
// Import trace for requested module:
|
|
107
|
-
// ./src/utils/get-email-component.tsx
|
|
108
|
-
// ./src/app/page.tsx
|
|
109
|
-
// ⚠ ./src/utils/get-email-component.tsx
|
|
110
|
-
// Critical dependency: the request of a dependency is an expression
|
|
111
|
-
},
|
|
112
|
-
process,
|
|
113
|
-
};
|
|
114
70
|
const sourceMapToEmail = JSON.parse(sourceMapFile.text) as RawSourceMap;
|
|
115
71
|
// because it will have a path like <tsconfigLocation>/stdout/email.js.map
|
|
116
72
|
sourceMapToEmail.sourceRoot = path.resolve(sourceMapFile.path, '../..');
|
|
117
73
|
sourceMapToEmail.sources = sourceMapToEmail.sources.map((source) =>
|
|
118
74
|
path.resolve(sourceMapFile.path, '..', source),
|
|
119
75
|
);
|
|
120
|
-
try {
|
|
121
|
-
vm.runInNewContext(builtEmailCode, fakeContext, { filename: emailPath });
|
|
122
|
-
} catch (exception) {
|
|
123
|
-
const error = exception as Error;
|
|
124
76
|
|
|
125
|
-
|
|
77
|
+
const runningResult = runBundledCode(builtEmailCode, emailPath);
|
|
126
78
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
79
|
+
if (isErr(runningResult)) {
|
|
80
|
+
const { error } = runningResult;
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
error.stack &&= error.stack.split('at Script.runInContext (node:vm')[0];
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
error: improveErrorWithSourceMap(error, emailPath, sourceMapToEmail),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
throw error;
|
|
130
90
|
}
|
|
131
91
|
|
|
132
|
-
|
|
92
|
+
const parseResult = EmailComponentModule.safeParse(runningResult.value);
|
|
93
|
+
|
|
94
|
+
if (parseResult.error) {
|
|
133
95
|
return {
|
|
134
96
|
error: improveErrorWithSourceMap(
|
|
135
97
|
new Error(
|
|
136
|
-
`The email component at ${emailPath} does not contain
|
|
98
|
+
`The email component at ${emailPath} does not contain the expected exports`,
|
|
99
|
+
{
|
|
100
|
+
cause: parseResult.error,
|
|
101
|
+
},
|
|
137
102
|
),
|
|
138
103
|
emailPath,
|
|
139
104
|
sourceMapToEmail,
|
|
@@ -141,11 +106,13 @@ export const getEmailComponent = async (
|
|
|
141
106
|
};
|
|
142
107
|
}
|
|
143
108
|
|
|
109
|
+
const { data: componentModule } = parseResult;
|
|
110
|
+
|
|
144
111
|
return {
|
|
145
|
-
emailComponent:
|
|
146
|
-
render:
|
|
147
|
-
createElement:
|
|
148
|
-
.reactEmailCreateReactElement as typeof React.createElement,
|
|
112
|
+
emailComponent: componentModule.default as EmailComponent,
|
|
113
|
+
render: componentModule.render as typeof render,
|
|
114
|
+
createElement:
|
|
115
|
+
componentModule.reactEmailCreateReactElement as typeof React.createElement,
|
|
149
116
|
|
|
150
117
|
sourceMapToOriginalFile: sourceMapToEmail,
|
|
151
118
|
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { checkCompatibility } from '../actions/email-validation/check-compatibility';
|
|
2
|
+
import { checkImages } from '../actions/email-validation/check-images';
|
|
3
|
+
import { checkLinks } from '../actions/email-validation/check-links';
|
|
4
|
+
import type { LintingRow } from '../components/toolbar/linter';
|
|
5
|
+
|
|
6
|
+
export interface LintingSource<T> {
|
|
7
|
+
getStream(): Promise<ReadableStream<T>>;
|
|
8
|
+
mapValue(value: NoInfer<T>): LintingRow | undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function createSource<T>(source: LintingSource<T>): LintingSource<T> {
|
|
12
|
+
return source;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getLintingSources(
|
|
16
|
+
markup: string,
|
|
17
|
+
reactMarkup: string,
|
|
18
|
+
emailPath: string,
|
|
19
|
+
|
|
20
|
+
urlBase: string,
|
|
21
|
+
): LintingSource<unknown>[] {
|
|
22
|
+
return [
|
|
23
|
+
createSource({
|
|
24
|
+
getStream() {
|
|
25
|
+
return checkImages(markup, urlBase);
|
|
26
|
+
},
|
|
27
|
+
mapValue(result) {
|
|
28
|
+
if (result && result.status !== 'success') {
|
|
29
|
+
return {
|
|
30
|
+
result: result,
|
|
31
|
+
source: 'image',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
createSource({
|
|
37
|
+
getStream() {
|
|
38
|
+
return checkLinks(markup);
|
|
39
|
+
},
|
|
40
|
+
mapValue(result) {
|
|
41
|
+
if (result && result.status !== 'success') {
|
|
42
|
+
return {
|
|
43
|
+
result: result,
|
|
44
|
+
source: 'link',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
}),
|
|
49
|
+
createSource({
|
|
50
|
+
getStream() {
|
|
51
|
+
return checkCompatibility(reactMarkup, emailPath);
|
|
52
|
+
},
|
|
53
|
+
mapValue(value) {
|
|
54
|
+
if (value && value.status !== 'success') {
|
|
55
|
+
return {
|
|
56
|
+
result: value,
|
|
57
|
+
source: 'compatibility',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function* loadLintingRowsFrom(sources: LintingSource<unknown>[]) {
|
|
66
|
+
for await (const source of sources) {
|
|
67
|
+
const stream = await source.getStream();
|
|
68
|
+
const reader = stream.getReader();
|
|
69
|
+
try {
|
|
70
|
+
while (true) {
|
|
71
|
+
const { value, done } = await reader.read();
|
|
72
|
+
if (done) {
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const row = source.mapValue(value);
|
|
77
|
+
if (row) {
|
|
78
|
+
yield row;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} finally {
|
|
82
|
+
reader.releaseLock();
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
type Ok<T, _E> = {
|
|
2
|
+
value: T;
|
|
3
|
+
};
|
|
4
|
+
type Error<_T, E> = {
|
|
5
|
+
error: E;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Do not destructure this object, it is meant to have all fields together
|
|
10
|
+
* in the same object
|
|
11
|
+
*/
|
|
12
|
+
export type Result<T, E> = Ok<T, E> | Error<T, E>;
|
|
13
|
+
|
|
14
|
+
export function isErr<T, E>(result: Result<T, E>): result is Error<T, E> {
|
|
15
|
+
return 'error' in result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isOk<T, E>(result: Result<T, E>): result is Ok<T, E> {
|
|
19
|
+
return 'value' in result && !('error' in result);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function mapResult<T, E, B>(
|
|
23
|
+
result: Result<T, E>,
|
|
24
|
+
callback: (value: T) => B,
|
|
25
|
+
): Result<B, E> {
|
|
26
|
+
if (isOk(result)) {
|
|
27
|
+
return ok(callback(result.value));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function ok<T, E>(value: NoInfer<T>): Ok<T, E>;
|
|
34
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: This is required for void return types on functions that can still error
|
|
35
|
+
export function ok<T extends void = void, E = never>(value: void): Ok<void, E>;
|
|
36
|
+
export function ok<T, E>(value: NoInfer<T>): Ok<T, E> {
|
|
37
|
+
return {
|
|
38
|
+
value,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function err<T, E>(error: NoInfer<E>): Error<T, E>;
|
|
43
|
+
// biome-ignore lint/suspicious/noConfusingVoidType: This is required for void return types on functions that can still error
|
|
44
|
+
export function err<T, E extends void = void>(error: void): Error<T, void>;
|
|
45
|
+
export function err<T, E>(error: NoInfer<E>): Error<T, E> {
|
|
46
|
+
return {
|
|
47
|
+
error,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import vm from 'node:vm';
|
|
3
|
+
import { type Result, err, ok } from './result';
|
|
4
|
+
import { staticNodeModulesForVM } from './static-node-modules-for-vm';
|
|
5
|
+
|
|
6
|
+
export const runBundledCode = (
|
|
7
|
+
code: string,
|
|
8
|
+
filename: string,
|
|
9
|
+
): Result<unknown, unknown> => {
|
|
10
|
+
const fakeContext = {
|
|
11
|
+
...global,
|
|
12
|
+
console,
|
|
13
|
+
Buffer,
|
|
14
|
+
AbortSignal,
|
|
15
|
+
Event,
|
|
16
|
+
EventTarget,
|
|
17
|
+
TextDecoder,
|
|
18
|
+
Request,
|
|
19
|
+
Response,
|
|
20
|
+
TextDecoderStream,
|
|
21
|
+
TextEncoder,
|
|
22
|
+
TextEncoderStream,
|
|
23
|
+
ReadableStream,
|
|
24
|
+
URL,
|
|
25
|
+
URLSearchParams,
|
|
26
|
+
Headers,
|
|
27
|
+
module: {
|
|
28
|
+
exports: {},
|
|
29
|
+
},
|
|
30
|
+
__filename: filename,
|
|
31
|
+
__dirname: path.dirname(filename),
|
|
32
|
+
require: (specifiedModule: string) => {
|
|
33
|
+
let m = specifiedModule;
|
|
34
|
+
if (specifiedModule.startsWith('node:')) {
|
|
35
|
+
m = m.split(':')[1]!;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (m in staticNodeModulesForVM) {
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
40
|
+
return staticNodeModulesForVM[m];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-useless-template-literals
|
|
44
|
+
return require(`${specifiedModule}`) as unknown;
|
|
45
|
+
// this stupid string templating was necessary to not have
|
|
46
|
+
// webpack warnings like:
|
|
47
|
+
//
|
|
48
|
+
// Import trace for requested module:
|
|
49
|
+
// ./src/utils/get-email-component.tsx
|
|
50
|
+
// ./src/app/page.tsx
|
|
51
|
+
// ⚠ ./src/utils/get-email-component.tsx
|
|
52
|
+
// Critical dependency: the request of a dependency is an expression
|
|
53
|
+
},
|
|
54
|
+
process,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
vm.runInNewContext(code, fakeContext, { filename });
|
|
59
|
+
} catch (exception) {
|
|
60
|
+
return err(exception);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return ok(fakeContext.module.exports as unknown);
|
|
64
|
+
};
|