@react-email/preview-server 1.0.0-canary.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/.next/BUILD_ID +1 -0
- package/.next/app-build-manifest.json +44 -0
- package/.next/app-path-routes-manifest.json +6 -0
- package/.next/build-manifest.json +33 -0
- package/.next/diagnostics/build-diagnostics.json +6 -0
- package/.next/diagnostics/framework.json +1 -0
- package/.next/export-marker.json +6 -0
- package/.next/images-manifest.json +57 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +41 -0
- package/.next/react-loadable-manifest.json +1 -0
- package/.next/required-server-files.json +311 -0
- package/.next/routes-manifest.json +64 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/favicon.ico/route.js +1 -0
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -0
- package/.next/server/app/favicon.ico.body +0 -0
- package/.next/server/app/favicon.ico.meta +1 -0
- package/.next/server/app/page.js +1 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/preview/[...slug]/page.js +321 -0
- package/.next/server/app/preview/[...slug]/page.js.nft.json +1 -0
- package/.next/server/app/preview/[...slug]/page_client-reference-manifest.js +1 -0
- package/.next/server/app-paths-manifest.json +6 -0
- package/.next/server/chunks/134.js +6 -0
- package/.next/server/chunks/235.js +15 -0
- package/.next/server/chunks/278.js +1 -0
- package/.next/server/chunks/343.js +20 -0
- package/.next/server/chunks/428.js +14 -0
- package/.next/server/chunks/963.js +1 -0
- package/.next/server/chunks/999.js +1 -0
- package/.next/server/functions-config-manifest.json +4 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +6 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +5 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/VkyJa9K30jCrKBesOgrQT/_buildManifest.js +1 -0
- package/.next/static/VkyJa9K30jCrKBesOgrQT/_ssgManifest.js +1 -0
- package/.next/static/chunks/107-3043079e7cb8bcae.js +1 -0
- package/.next/static/chunks/293-67391ef0e44ffa4f.js +1 -0
- package/.next/static/chunks/3bd82e28-cda2c00a924937c5.js +1 -0
- package/.next/static/chunks/45-1021fac82f766268.js +1 -0
- package/.next/static/chunks/484-1969fe871b27074e.js +1 -0
- package/.next/static/chunks/484-e02de792ff5f9ea5.js +1 -0
- package/.next/static/chunks/589-817d8691661d370e.js +1 -0
- package/.next/static/chunks/902-c34acb56733e0ce1.js +1 -0
- package/.next/static/chunks/app/_not-found/page-4cbc7dce3ad33336.js +1 -0
- package/.next/static/chunks/app/layout-74628781c0b7e7bf.js +1 -0
- package/.next/static/chunks/app/layout-daeba68330ab58bb.js +1 -0
- package/.next/static/chunks/app/page-55cf199b7ca71958.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-07dd9a701d0b3e56.js +1 -0
- package/.next/static/chunks/app/preview/[...slug]/page-61b0ea70a8d72916.js +1 -0
- package/.next/static/chunks/f33a14d2-ec7c5f0b91818561.js +6 -0
- package/.next/static/chunks/framework-b887e9fc751a9906.js +1 -0
- package/.next/static/chunks/main-9a03e7ba8acb1900.js +1 -0
- package/.next/static/chunks/main-app-5bc2d814f500db60.js +1 -0
- package/.next/static/chunks/pages/_app-542a93a5a214e1c0.js +1 -0
- package/.next/static/chunks/pages/_error-d5fe1b1612642f76.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-31c45daa2bd82a7b.js +1 -0
- package/.next/static/css/35e8811589f0962b.css +3 -0
- package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_buildManifest.js +1 -0
- package/.next/static/j4oDiQwPKPQgK5jAoiUTk/_ssgManifest.js +1 -0
- package/.next/static/media/05613964ce6c782e-s.p.otf +0 -0
- package/.next/static/media/11c6126b9369e85e-s.p.otf +0 -0
- package/.next/static/media/26a46d62cd723877-s.woff2 +0 -0
- package/.next/static/media/26cb97734d8cb717-s.p.otf +0 -0
- package/.next/static/media/55c55f0601d81cf3-s.woff2 +0 -0
- package/.next/static/media/581909926a08bbc8-s.woff2 +0 -0
- package/.next/static/media/6d93bde91c0c2823-s.woff2 +0 -0
- package/.next/static/media/97e0cb1ae144a2a9-s.woff2 +0 -0
- package/.next/static/media/a34f9d1faa5f3315-s.p.woff2 +0 -0
- package/.next/static/media/bb6462617151f6b7-s.p.otf +0 -0
- package/.next/static/media/cf6daef822ab0142-s.p.otf +0 -0
- package/.next/static/media/df0a9ae256c0569c-s.woff2 +0 -0
- package/.next/static/media/e4051546b3043204-s.p.otf +0 -0
- package/.next/static/media/logo.2ce2a759.png +0 -0
- package/.next/trace +27 -0
- package/.next/types/app/layout.ts +84 -0
- package/.next/types/app/page.ts +84 -0
- package/.next/types/app/preview/[...slug]/page.ts +84 -0
- package/.next/types/cache-life.d.ts +141 -0
- package/.next/types/package.json +1 -0
- package/_index.js +4 -0
- package/license.md +7 -0
- package/module-punycode.d.ts +3 -0
- package/next-env.d.ts +5 -0
- package/next.config.js +22 -0
- package/package.json +82 -0
- package/postcss.config.js +8 -0
- package/scripts/build-preview-server.mjs +29 -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 +333 -0
- package/src/actions/email-validation/check-images.spec.tsx +100 -0
- package/src/actions/email-validation/check-images.ts +160 -0
- package/src/actions/email-validation/check-links.spec.tsx +113 -0
- package/src/actions/email-validation/check-links.ts +113 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/email-validation/quick-fetch.ts +14 -0
- package/src/actions/get-email-path-from-slug.ts +32 -0
- package/src/actions/get-emails-directory-metadata-action.ts +19 -0
- package/src/actions/render-email-by-path.tsx +121 -0
- package/src/animated-icons-data/help.json +1082 -0
- package/src/animated-icons-data/link.json +1309 -0
- package/src/animated-icons-data/load.json +443 -0
- package/src/animated-icons-data/mail.json +1320 -0
- package/src/app/env.ts +15 -0
- package/src/app/favicon.ico +0 -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/globals.css +15 -0
- package/src/app/layout.tsx +43 -0
- package/src/app/logo.png +0 -0
- package/src/app/page.tsx +46 -0
- package/src/app/preview/[...slug]/page.tsx +157 -0
- package/src/app/preview/[...slug]/preview.tsx +216 -0
- package/src/app/preview/[...slug]/rendering-error.tsx +40 -0
- package/src/components/button.tsx +101 -0
- package/src/components/code-container.tsx +164 -0
- package/src/components/code-snippet.tsx +9 -0
- package/src/components/code.tsx +184 -0
- package/src/components/heading.tsx +113 -0
- package/src/components/icons/icon-arrow-down.tsx +16 -0
- package/src/components/icons/icon-base.tsx +26 -0
- package/src/components/icons/icon-bug.tsx +19 -0
- package/src/components/icons/icon-button.tsx +23 -0
- package/src/components/icons/icon-check.tsx +19 -0
- package/src/components/icons/icon-clipboard.tsx +40 -0
- package/src/components/icons/icon-download.tsx +19 -0
- package/src/components/icons/icon-email.tsx +18 -0
- package/src/components/icons/icon-file.tsx +19 -0
- package/src/components/icons/icon-folder-open.tsx +19 -0
- package/src/components/icons/icon-folder.tsx +18 -0
- package/src/components/icons/icon-hide-sidebar.tsx +23 -0
- package/src/components/icons/icon-image.tsx +19 -0
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-link.tsx +14 -0
- package/src/components/icons/icon-monitor.tsx +19 -0
- package/src/components/icons/icon-phone.tsx +26 -0
- package/src/components/icons/icon-reload.tsx +18 -0
- package/src/components/icons/icon-source.tsx +19 -0
- package/src/components/icons/icon-stamp.tsx +14 -0
- package/src/components/icons/icon-warning.tsx +31 -0
- package/src/components/index.ts +7 -0
- package/src/components/logo.tsx +63 -0
- package/src/components/resizable-wrapper.tsx +173 -0
- package/src/components/send.tsx +134 -0
- package/src/components/shell.tsx +92 -0
- package/src/components/sidebar/file-tree-directory-children.tsx +139 -0
- package/src/components/sidebar/file-tree-directory.tsx +92 -0
- package/src/components/sidebar/file-tree.tsx +31 -0
- package/src/components/sidebar/index.ts +1 -0
- package/src/components/sidebar/sidebar.tsx +43 -0
- package/src/components/text.tsx +99 -0
- package/src/components/toolbar/checking-results.tsx +150 -0
- package/src/components/toolbar/code-preview-line-link.tsx +40 -0
- package/src/components/toolbar/compatibility.tsx +113 -0
- package/src/components/toolbar/linter.tsx +278 -0
- package/src/components/toolbar/results-table.tsx +0 -0
- package/src/components/toolbar/results.tsx +51 -0
- package/src/components/toolbar/spam-assassin.tsx +155 -0
- package/src/components/toolbar/toolbar-button.tsx +52 -0
- package/src/components/toolbar/use-cached-state.ts +33 -0
- package/src/components/toolbar.tsx +349 -0
- package/src/components/tooltip-content.tsx +31 -0
- package/src/components/tooltip.tsx +19 -0
- package/src/components/topbar/active-view-toggle-group.tsx +86 -0
- package/src/components/topbar/view-size-controls.tsx +247 -0
- package/src/components/topbar.tsx +59 -0
- package/src/contexts/emails.tsx +59 -0
- package/src/contexts/fragment-identifier.tsx +48 -0
- package/src/contexts/preview.tsx +79 -0
- package/src/hooks/use-clamped-state.ts +24 -0
- package/src/hooks/use-email-rendering-result.ts +58 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/hooks/use-hot-reload.ts +31 -0
- package/src/hooks/use-icon-animation.ts +41 -0
- package/src/hooks/use-rendering-metadata.ts +36 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +3 -0
- package/src/utils/caniemail/all-css-properties.ts +358 -0
- package/src/utils/caniemail/ast/__snapshots__/get-object-variables.spec.ts.snap +74 -0
- package/src/utils/caniemail/ast/__snapshots__/get-used-style-properties.spec.ts.snap +24 -0
- package/src/utils/caniemail/ast/get-object-variables.spec.ts +19 -0
- package/src/utils/caniemail/ast/get-object-variables.ts +61 -0
- package/src/utils/caniemail/ast/get-used-style-properties.spec.ts +23 -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 +187 -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/cn.ts +6 -0
- package/src/utils/constants.ts +6 -0
- package/src/utils/contains-email-template.spec.ts +124 -0
- package/src/utils/contains-email-template.ts +33 -0
- package/src/utils/copy-text-to-clipboard.ts +7 -0
- package/src/utils/esbuild/escape-string-for-regex.ts +3 -0
- package/src/utils/esbuild/renderring-utilities-exporter.ts +63 -0
- package/src/utils/get-email-component.spec.ts +41 -0
- package/src/utils/get-email-component.ts +134 -0
- package/src/utils/get-emails-directory-metadata.spec.ts +82 -0
- package/src/utils/get-emails-directory-metadata.ts +141 -0
- package/src/utils/get-line-and-column-from-offset.spec.ts +11 -0
- package/src/utils/get-line-and-column-from-offset.ts +11 -0
- package/src/utils/improve-error-with-sourcemap.ts +85 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/js-email-detection.spec.ts +24 -0
- package/src/utils/language-map.ts +7 -0
- package/src/utils/linting.ts +60 -0
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/register-spinner-autostopping.ts +28 -0
- package/src/utils/result.ts +49 -0
- package/src/utils/run-bundled-code.ts +64 -0
- package/src/utils/sanitize.ts +6 -0
- package/src/utils/static-node-modules-for-vm.ts +93 -0
- package/src/utils/testing/js-email-export-default.js +17 -0
- package/src/utils/testing/js-email-test.js +18 -0
- package/src/utils/testing/mdx-email-test.js +128 -0
- package/src/utils/testing/request-response-email.tsx +9 -0
- package/src/utils/types/as.ts +26 -0
- package/src/utils/types/email-template.ts +8 -0
- package/src/utils/types/error-object.ts +11 -0
- package/src/utils/types/hot-reload-change.ts +13 -0
- package/src/utils/unreachable.ts +8 -0
- package/tailwind-internals.d.ts +133 -0
- package/tailwind.config.ts +99 -0
- package/tsconfig.json +45 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import traverse from '@babel/traverse';
|
|
2
|
+
import type { AST } from '../../../actions/email-validation/check-compatibility';
|
|
3
|
+
import { generateTailwindCssRules } from '../tailwind/generate-tailwind-rules';
|
|
4
|
+
import { getTailwindMetadata } from '../tailwind/get-tailwind-metadata';
|
|
5
|
+
import type { ObjectVariables, SourceLocation } from './get-object-variables';
|
|
6
|
+
|
|
7
|
+
export interface StylePropertyUsage {
|
|
8
|
+
location: SourceLocation | undefined | null;
|
|
9
|
+
name: string;
|
|
10
|
+
value: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const doesPropertyHaveLocation = (
|
|
14
|
+
prop: StylePropertyUsage,
|
|
15
|
+
): prop is StylePropertyUsage & { location: SourceLocation } => {
|
|
16
|
+
return prop.location !== undefined && prop.location !== null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const getUsedStyleProperties = async (
|
|
20
|
+
ast: AST,
|
|
21
|
+
sourceCode: string,
|
|
22
|
+
sourcePath: string,
|
|
23
|
+
objectVariables: ObjectVariables,
|
|
24
|
+
) => {
|
|
25
|
+
const styleProperties: StylePropertyUsage[] = [];
|
|
26
|
+
const tailwindMetadata = await getTailwindMetadata(
|
|
27
|
+
ast,
|
|
28
|
+
sourceCode,
|
|
29
|
+
sourcePath,
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
if (tailwindMetadata.hasTailwind) {
|
|
33
|
+
traverse(ast, {
|
|
34
|
+
JSXAttribute(path) {
|
|
35
|
+
if (path.node.name.name === 'className') {
|
|
36
|
+
path.traverse({
|
|
37
|
+
StringLiteral(stringPath) {
|
|
38
|
+
const className = stringPath.node.value;
|
|
39
|
+
const { rules } = generateTailwindCssRules(
|
|
40
|
+
className.split(' '),
|
|
41
|
+
tailwindMetadata.context,
|
|
42
|
+
);
|
|
43
|
+
for (const rule of rules) {
|
|
44
|
+
rule.walkDecls((decl) => {
|
|
45
|
+
styleProperties.push({
|
|
46
|
+
location: stringPath.node.loc,
|
|
47
|
+
name: decl.prop,
|
|
48
|
+
value: decl.value,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
traverse(ast, {
|
|
60
|
+
JSXAttribute(path) {
|
|
61
|
+
if (
|
|
62
|
+
path.node.value?.type === 'JSXExpressionContainer' &&
|
|
63
|
+
path.node.value.expression.type === 'Identifier' &&
|
|
64
|
+
path.node.name.name === 'style'
|
|
65
|
+
) {
|
|
66
|
+
const styleVariable = objectVariables[path.node.value.expression.name];
|
|
67
|
+
if (styleVariable) {
|
|
68
|
+
for (const property of styleVariable) {
|
|
69
|
+
if (
|
|
70
|
+
(property.key.type === 'StringLiteral' ||
|
|
71
|
+
property.key.type === 'Identifier') &&
|
|
72
|
+
property.value.type === 'StringLiteral'
|
|
73
|
+
) {
|
|
74
|
+
const propertyName =
|
|
75
|
+
property.key.type === 'StringLiteral'
|
|
76
|
+
? property.key.value
|
|
77
|
+
: property.key.name;
|
|
78
|
+
styleProperties.push({
|
|
79
|
+
name: propertyName,
|
|
80
|
+
value: property.value.value,
|
|
81
|
+
location: property.loc,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return styleProperties;
|
|
91
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
import type {
|
|
3
|
+
EmailClient,
|
|
4
|
+
Platform,
|
|
5
|
+
SupportEntry,
|
|
6
|
+
} from '../../actions/email-validation/check-compatibility';
|
|
7
|
+
|
|
8
|
+
export type SupportStatus = DetailedSupportStatus['status'];
|
|
9
|
+
|
|
10
|
+
export type DetailedSupportStatus =
|
|
11
|
+
| {
|
|
12
|
+
status: 'success';
|
|
13
|
+
}
|
|
14
|
+
| {
|
|
15
|
+
status: 'error';
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
status: 'warning';
|
|
19
|
+
notes: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type EmailClientStats = {
|
|
23
|
+
status: SupportStatus;
|
|
24
|
+
perPlatform: Partial<Record<Platform, DetailedSupportStatus>>;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export type CompatibilityStats = {
|
|
28
|
+
status: SupportStatus;
|
|
29
|
+
perEmailClient: Partial<Record<EmailClient, EmailClientStats>>;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const noteNumbersRegex = /#(?<noteNumber>\d+)/g;
|
|
33
|
+
|
|
34
|
+
export const getCompatibilityStatsForEntry = (
|
|
35
|
+
entry: SupportEntry,
|
|
36
|
+
emailClients: EmailClient[],
|
|
37
|
+
) => {
|
|
38
|
+
const stats: CompatibilityStats = {
|
|
39
|
+
status: 'success',
|
|
40
|
+
perEmailClient: {},
|
|
41
|
+
};
|
|
42
|
+
for (const emailClient of emailClients) {
|
|
43
|
+
const rawStats = entry.stats[emailClient];
|
|
44
|
+
if (rawStats) {
|
|
45
|
+
const emailClientStats: EmailClientStats = {
|
|
46
|
+
status: 'success',
|
|
47
|
+
perPlatform: {},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
for (const [platform, statusPerVersion] of Object.entries(rawStats)) {
|
|
51
|
+
const latestStatus = statusPerVersion[statusPerVersion.length - 1];
|
|
52
|
+
if (latestStatus === undefined)
|
|
53
|
+
throw new Error(
|
|
54
|
+
'Cannot load in status because there are none recorded for this platform/email client',
|
|
55
|
+
{
|
|
56
|
+
cause: {
|
|
57
|
+
latestStatus,
|
|
58
|
+
statusPerVersion,
|
|
59
|
+
platform,
|
|
60
|
+
emailClient,
|
|
61
|
+
supportEntry: entry,
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
const statusString = latestStatus[Object.keys(latestStatus)[0]!]!;
|
|
66
|
+
if (statusString.startsWith('u')) continue;
|
|
67
|
+
if (statusString.startsWith('a')) {
|
|
68
|
+
const notes: string[] = [];
|
|
69
|
+
noteNumbersRegex.lastIndex = 0;
|
|
70
|
+
for (const match of statusString.matchAll(noteNumbersRegex)) {
|
|
71
|
+
if (match.groups?.noteNumber) {
|
|
72
|
+
const { noteNumber } = match.groups;
|
|
73
|
+
const note = entry.notes_by_num?.[Number.parseInt(noteNumber)];
|
|
74
|
+
if (note) {
|
|
75
|
+
notes.push(note);
|
|
76
|
+
}
|
|
77
|
+
// else if (isInternalDev) {
|
|
78
|
+
// console.warn(
|
|
79
|
+
// 'Could not get note by the number for a support entry',
|
|
80
|
+
// {
|
|
81
|
+
// platform,
|
|
82
|
+
// statusString,
|
|
83
|
+
// note,
|
|
84
|
+
// },
|
|
85
|
+
// );
|
|
86
|
+
// }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (emailClientStats.status === 'success')
|
|
90
|
+
emailClientStats.status = 'warning';
|
|
91
|
+
if (stats.status === 'success') stats.status = 'warning';
|
|
92
|
+
emailClientStats.perPlatform[platform as Platform] = {
|
|
93
|
+
status: 'warning',
|
|
94
|
+
notes:
|
|
95
|
+
notes.length === 1
|
|
96
|
+
? notes[0]!
|
|
97
|
+
: notes.map((note) => `- ${note}`).join('\n'),
|
|
98
|
+
};
|
|
99
|
+
} else if (statusString.startsWith('y')) {
|
|
100
|
+
emailClientStats.perPlatform[platform as Platform] = {
|
|
101
|
+
status: 'success',
|
|
102
|
+
};
|
|
103
|
+
} else if (statusString.startsWith('n')) {
|
|
104
|
+
if (emailClientStats.status !== 'error')
|
|
105
|
+
emailClientStats.status = 'error';
|
|
106
|
+
if (stats.status !== 'error') stats.status = 'error';
|
|
107
|
+
emailClientStats.perPlatform[platform as Platform] = {
|
|
108
|
+
status: 'error',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
stats.perEmailClient[emailClient] = emailClientStats;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return stats;
|
|
118
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function getCssFunctions(title: string) {
|
|
2
|
+
if (/^[a-zA-Z]\(\)$/.test(title.trim())) {
|
|
3
|
+
return [title.replace('()', '')];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// ex: lch(), oklch(), lab(), oklab()
|
|
7
|
+
// this regex avoids matching entries that are for CSS properties listed
|
|
8
|
+
// separated by commas as well
|
|
9
|
+
if (/^(?:[^(),]+?\(\),?)*$/.test(title.trim())) {
|
|
10
|
+
return title
|
|
11
|
+
.split(/\s*,\s*/)
|
|
12
|
+
.map((functionCallWithoutParameters) =>
|
|
13
|
+
functionCallWithoutParameters.replace('()', ''),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ex: CSS calc() function
|
|
18
|
+
if (/^CSS [a-z]+\(\) function$/.test(title.trim())) {
|
|
19
|
+
return [
|
|
20
|
+
title.replace('CSS ', '').replace(' function', '').replace('()', ''),
|
|
21
|
+
];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { allCssProperties } from './all-css-properties';
|
|
2
|
+
|
|
3
|
+
export const getCssPropertyNames = (title: string, keywords: string | null) => {
|
|
4
|
+
if (allCssProperties.includes(title.replace(' property', '')))
|
|
5
|
+
return [title.replace(' property', '')];
|
|
6
|
+
|
|
7
|
+
if (title.split('&').length > 1) {
|
|
8
|
+
return title
|
|
9
|
+
.split(/\s*&\s*/)
|
|
10
|
+
.map((piece) => piece.trim())
|
|
11
|
+
.filter((possiblePropertyName) =>
|
|
12
|
+
allCssProperties.includes(possiblePropertyName),
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (title.split(',').length > 1) {
|
|
17
|
+
return title
|
|
18
|
+
.split(/\s*,\s*/)
|
|
19
|
+
.map((piece) => piece.trim())
|
|
20
|
+
.filter((possiblePropertyName) =>
|
|
21
|
+
allCssProperties.includes(possiblePropertyName),
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (keywords) {
|
|
26
|
+
return keywords
|
|
27
|
+
.split(/\s*,\s*/)
|
|
28
|
+
.filter((keyword) => allCssProperties.includes(keyword));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [];
|
|
32
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const propertyRegex =
|
|
2
|
+
/(?<propertyName>[a-z-]+)\s*:\s*(?<propertyValue>[a-zA-Z\-0-9()+*/_ ]+)/;
|
|
3
|
+
|
|
4
|
+
export const getCssPropertyWithValue = (title: string) => {
|
|
5
|
+
const match = propertyRegex.exec(title.trim());
|
|
6
|
+
if (match) {
|
|
7
|
+
const [_full, propertyName, propertyValue] = match;
|
|
8
|
+
return {
|
|
9
|
+
name: propertyName!,
|
|
10
|
+
value: propertyValue!,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
return undefined;
|
|
14
|
+
};
|
|
@@ -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 postcss from 'postcss';
|
|
2
|
+
import type { Root, Rule } 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,187 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import traverse from '@babel/traverse';
|
|
4
|
+
import type { Node } 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 can't run 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 sourceDirpath = path.dirname(sourcePath);
|
|
92
|
+
const configFilepath = path.join(sourceDirpath, configRelativePath);
|
|
93
|
+
|
|
94
|
+
const configBuildResult = await esbuild.build({
|
|
95
|
+
bundle: true,
|
|
96
|
+
stdin: {
|
|
97
|
+
contents: `import tailwindConfig from "${configRelativePath}";
|
|
98
|
+
export { tailwindConfig };`,
|
|
99
|
+
loader: 'tsx',
|
|
100
|
+
resolveDir: path.dirname(sourcePath),
|
|
101
|
+
},
|
|
102
|
+
platform: 'node',
|
|
103
|
+
write: false,
|
|
104
|
+
format: 'cjs',
|
|
105
|
+
logLevel: 'silent',
|
|
106
|
+
});
|
|
107
|
+
const configFile = configBuildResult.outputFiles[0];
|
|
108
|
+
if (configFile === undefined) {
|
|
109
|
+
throw new Error(
|
|
110
|
+
'Could not build config file as it was found as undefined, this is most likely a bug, please open an issue.',
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
const configModule = runBundledCode(configFile.text, configFilepath);
|
|
114
|
+
if (isErr(configModule)) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Error when trying to run the config file: ${configModule.error}`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
typeof configModule.value === 'object' &&
|
|
122
|
+
configModule.value !== null &&
|
|
123
|
+
'tailwindConfig' in configModule.value
|
|
124
|
+
) {
|
|
125
|
+
return configModule.value.tailwindConfig as TailwindConfig;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
throw new Error(
|
|
129
|
+
`Could not read Tailwind config at ${configFilepath} because it doesn't have a default export in it.`,
|
|
130
|
+
{
|
|
131
|
+
cause: {
|
|
132
|
+
configModule,
|
|
133
|
+
configFilepath,
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const getImportWithGivenDefaultSpecifier = (
|
|
140
|
+
ast: AST,
|
|
141
|
+
specifierName: string,
|
|
142
|
+
) => {
|
|
143
|
+
let importNode: ImportDeclaration | undefined;
|
|
144
|
+
traverse(ast, {
|
|
145
|
+
ImportDeclaration(nodePath) {
|
|
146
|
+
if (
|
|
147
|
+
nodePath.node.specifiers.some(
|
|
148
|
+
(specifier) =>
|
|
149
|
+
specifier.type === 'ImportDefaultSpecifier' &&
|
|
150
|
+
specifier.local.name === specifierName,
|
|
151
|
+
)
|
|
152
|
+
) {
|
|
153
|
+
importNode = nodePath.node;
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
return importNode;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
type JSXAttribute = Node & { type: 'JSXAttribute' };
|
|
161
|
+
|
|
162
|
+
const getTailwindConfigNode = (ast: AST) => {
|
|
163
|
+
let tailwindConfigNode: JSXAttribute | undefined;
|
|
164
|
+
traverse(ast, {
|
|
165
|
+
JSXOpeningElement(nodePath) {
|
|
166
|
+
if (
|
|
167
|
+
nodePath.node.name.type === 'JSXIdentifier' &&
|
|
168
|
+
nodePath.node.name.name === 'Tailwind'
|
|
169
|
+
) {
|
|
170
|
+
const configAttribute = nodePath.node.attributes.find(
|
|
171
|
+
(
|
|
172
|
+
attribute,
|
|
173
|
+
): attribute is Node & {
|
|
174
|
+
type: 'JSXAttribute';
|
|
175
|
+
} =>
|
|
176
|
+
attribute.type === 'JSXAttribute' &&
|
|
177
|
+
attribute.name.type === 'JSXIdentifier' &&
|
|
178
|
+
attribute.name.name === 'config',
|
|
179
|
+
);
|
|
180
|
+
if (configAttribute) {
|
|
181
|
+
tailwindConfigNode = configAttribute;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
return tailwindConfigNode;
|
|
187
|
+
};
|
|
@@ -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 { getTailwindConfig, type TailwindConfig } 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
|
+
};
|
package/src/utils/cn.ts
ADDED