react-email 4.0.0-alpha.6 → 4.0.0-alpha.8
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 +13 -0
- package/dist/cli/index.js +18 -13
- package/dist/cli/index.mjs +26 -21
- package/dist/preview/.next/BUILD_ID +1 -1
- package/dist/preview/.next/app-build-manifest.json +14 -13
- package/dist/preview/.next/build-manifest.json +3 -3
- 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/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 +3 -3
- package/dist/preview/.next/required-server-files.json +3 -3
- package/dist/preview/.next/server/app/_not-found/page.js +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/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 +133 -25
- 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/chunks/42.js +1 -0
- package/dist/preview/.next/server/chunks/600.js +3 -3
- package/dist/preview/.next/server/chunks/{171.js → 816.js} +6 -6
- 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-manifest.json +1 -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/static/chunks/287-7864b805e6bdc854.js +1 -0
- package/dist/preview/.next/static/chunks/412-31817e53b50a3e73.js +1 -0
- package/dist/preview/.next/static/chunks/683-b769e5d91bdf9a82.js +1 -0
- package/dist/preview/.next/static/chunks/880-9c0b721328117b8b.js +1 -0
- package/dist/preview/.next/static/chunks/app/layout-7dee682873546401.js +1 -0
- package/dist/preview/.next/static/chunks/app/page-9ea0bd45cd6294b0.js +1 -0
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-a610d641c64448cc.js +1 -0
- package/dist/preview/.next/static/chunks/{main-app-c2e686acf8d370d7.js → main-app-256b213b179a95cc.js} +1 -1
- package/dist/preview/.next/static/css/e68ebc9bb8f7b3f4.css +3 -0
- package/dist/preview/.next/trace +26 -26
- package/dist/preview/.next/types/app/layout.ts +1 -1
- package/dist/preview/.next/types/app/page.ts +84 -0
- package/dist/preview/.next/types/app/preview/[...slug]/page.ts +1 -1
- package/package.json +1 -1
- package/src/actions/email-validation/check-compatibility.ts +16 -5
- package/src/actions/email-validation/check-images.spec.tsx +13 -11
- package/src/actions/email-validation/check-images.ts +6 -0
- package/src/actions/email-validation/check-links.spec.tsx +23 -11
- package/src/actions/email-validation/check-links.ts +6 -0
- package/src/actions/email-validation/get-code-location-from-ast-element.ts +18 -0
- package/src/actions/render-email-by-path.tsx +2 -2
- package/src/app/env.ts +3 -0
- package/src/app/preview/[...slug]/page.tsx +24 -11
- package/src/app/preview/[...slug]/preview.tsx +15 -12
- package/src/components/code-container.tsx +90 -71
- package/src/components/code.tsx +106 -42
- package/src/components/icons/icon-info.tsx +18 -0
- package/src/components/icons/icon-reload.tsx +13 -14
- package/src/components/logo.tsx +3 -2
- package/src/components/resizable-wrapper.tsx +1 -4
- package/src/components/sidebar/file-tree-directory-children.tsx +13 -2
- package/src/components/sidebar/file-tree-directory.tsx +26 -18
- package/src/components/sidebar/file-tree.tsx +2 -2
- package/src/components/sidebar/sidebar.tsx +16 -19
- 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 +69 -111
- package/src/components/toolbar/results.tsx +5 -2
- package/src/components/toolbar/spam-assassin.tsx +31 -20
- package/src/components/toolbar/toolbar-button.tsx +4 -2
- package/src/components/toolbar/use-cached-state.ts +2 -2
- package/src/components/toolbar.tsx +152 -30
- package/src/components/tooltip-content.tsx +1 -1
- package/src/components/topbar/view-size-controls.tsx +1 -2
- package/src/components/topbar.tsx +1 -20
- package/src/contexts/fragment-identifier.tsx +46 -0
- package/src/hooks/use-fragment-identifier.ts +14 -0
- package/src/utils/__snapshots__/get-email-component.spec.ts.snap +1 -1
- 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-used-style-properties.spec.ts +23 -0
- package/src/utils/caniemail/get-css-property-with-value.ts +2 -2
- package/src/utils/caniemail/tailwind/get-tailwind-config.ts +0 -2
- package/src/utils/get-email-component.ts +1 -1
- 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/index.ts +1 -0
- package/src/utils/linting.ts +5 -30
- package/src/utils/load-stream.ts +15 -0
- package/src/utils/sanitize.ts +6 -0
- package/dist/preview/.next/server/chunks/833.js +0 -1
- package/dist/preview/.next/static/chunks/416-56f79fc7e689f06f.js +0 -1
- package/dist/preview/.next/static/chunks/683-8bbfd191e5105f01.js +0 -1
- package/dist/preview/.next/static/chunks/87-38e35f08507de015.js +0 -1
- package/dist/preview/.next/static/chunks/app/layout-a6640e62690d8fd6.js +0 -1
- package/dist/preview/.next/static/chunks/app/page-ba68f50b287e7478.js +0 -1
- package/dist/preview/.next/static/chunks/app/preview/[...slug]/page-4a5b026ab543e27f.js +0 -1
- package/dist/preview/.next/static/css/d7df9cfc3e182163.css +0 -3
- package/src/actions/email-validation/get-line-and-column-from-index.spec.ts +0 -22
- package/src/actions/email-validation/get-line-and-column-from-index.ts +0 -43
- package/src/components/icons/icon-scanner.tsx +0 -19
- package/src/components/icons/icon-scissors.tsx +0 -19
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_buildManifest.js +0 -0
- /package/dist/preview/.next/static/{gFk9UfWL8joM4iD7-wlKF → SoPVDfPAp9R983pBBriVn}/_ssgManifest.js +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// File: /home/gabriel/Projects/Resend/react-email
|
|
1
|
+
// File: /home/gabriel/Projects/Resend/react-email/packages/react-email/src/app/layout.tsx
|
|
2
2
|
import * as entry from '../../../src/app/layout.js'
|
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
|
4
4
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// File: /home/gabriel/Projects/Resend/react-email/packages/react-email/src/app/page.tsx
|
|
2
|
+
import * as entry from '../../../src/app/page.js'
|
|
3
|
+
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
|
4
|
+
|
|
5
|
+
type TEntry = typeof import('../../../src/app/page.js')
|
|
6
|
+
|
|
7
|
+
type SegmentParams<T extends Object = any> = T extends Record<string, any>
|
|
8
|
+
? { [K in keyof T]: T[K] extends string ? string | string[] | undefined : never }
|
|
9
|
+
: T
|
|
10
|
+
|
|
11
|
+
// Check that the entry is a valid entry
|
|
12
|
+
checkFields<Diff<{
|
|
13
|
+
default: Function
|
|
14
|
+
config?: {}
|
|
15
|
+
generateStaticParams?: Function
|
|
16
|
+
revalidate?: RevalidateRange<TEntry> | false
|
|
17
|
+
dynamic?: 'auto' | 'force-dynamic' | 'error' | 'force-static'
|
|
18
|
+
dynamicParams?: boolean
|
|
19
|
+
fetchCache?: 'auto' | 'force-no-store' | 'only-no-store' | 'default-no-store' | 'default-cache' | 'only-cache' | 'force-cache'
|
|
20
|
+
preferredRegion?: 'auto' | 'global' | 'home' | string | string[]
|
|
21
|
+
runtime?: 'nodejs' | 'experimental-edge' | 'edge'
|
|
22
|
+
maxDuration?: number
|
|
23
|
+
|
|
24
|
+
metadata?: any
|
|
25
|
+
generateMetadata?: Function
|
|
26
|
+
viewport?: any
|
|
27
|
+
generateViewport?: Function
|
|
28
|
+
experimental_ppr?: boolean
|
|
29
|
+
|
|
30
|
+
}, TEntry, ''>>()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
// Check the prop type of the entry function
|
|
34
|
+
checkFields<Diff<PageProps, FirstArg<TEntry['default']>, 'default'>>()
|
|
35
|
+
|
|
36
|
+
// Check the arguments and return type of the generateMetadata function
|
|
37
|
+
if ('generateMetadata' in entry) {
|
|
38
|
+
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
|
|
39
|
+
checkFields<Diff<ResolvingMetadata, SecondArg<MaybeField<TEntry, 'generateMetadata'>>, 'generateMetadata'>>()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check the arguments and return type of the generateViewport function
|
|
43
|
+
if ('generateViewport' in entry) {
|
|
44
|
+
checkFields<Diff<PageProps, FirstArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
|
|
45
|
+
checkFields<Diff<ResolvingViewport, SecondArg<MaybeField<TEntry, 'generateViewport'>>, 'generateViewport'>>()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check the arguments and return type of the generateStaticParams function
|
|
49
|
+
if ('generateStaticParams' in entry) {
|
|
50
|
+
checkFields<Diff<{ params: SegmentParams }, FirstArg<MaybeField<TEntry, 'generateStaticParams'>>, 'generateStaticParams'>>()
|
|
51
|
+
checkFields<Diff<{ __tag__: 'generateStaticParams', __return_type__: any[] | Promise<any[]> }, { __tag__: 'generateStaticParams', __return_type__: ReturnType<MaybeField<TEntry, 'generateStaticParams'>> }>>()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PageProps {
|
|
55
|
+
params?: Promise<SegmentParams>
|
|
56
|
+
searchParams?: Promise<any>
|
|
57
|
+
}
|
|
58
|
+
export interface LayoutProps {
|
|
59
|
+
children?: React.ReactNode
|
|
60
|
+
|
|
61
|
+
params?: Promise<SegmentParams>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============
|
|
65
|
+
// Utility types
|
|
66
|
+
type RevalidateRange<T> = T extends { revalidate: any } ? NonNegative<T['revalidate']> : never
|
|
67
|
+
|
|
68
|
+
// If T is unknown or any, it will be an empty {} type. Otherwise, it will be the same as Omit<T, keyof Base>.
|
|
69
|
+
type OmitWithTag<T, K extends keyof any, _M> = Omit<T, K>
|
|
70
|
+
type Diff<Base, T extends Base, Message extends string = ''> = 0 extends (1 & T) ? {} : OmitWithTag<T, keyof Base, Message>
|
|
71
|
+
|
|
72
|
+
type FirstArg<T extends Function> = T extends (...args: [infer T, any]) => any ? unknown extends T ? any : T : never
|
|
73
|
+
type SecondArg<T extends Function> = T extends (...args: [any, infer T]) => any ? unknown extends T ? any : T : never
|
|
74
|
+
type MaybeField<T, K extends string> = T extends { [k in K]: infer G } ? G extends Function ? G : never : never
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
function checkFields<_ extends { [k in keyof any]: never }>() {}
|
|
79
|
+
|
|
80
|
+
// https://github.com/sindresorhus/type-fest
|
|
81
|
+
type Numeric = number | bigint
|
|
82
|
+
type Zero = 0 | 0n
|
|
83
|
+
type Negative<T extends Numeric> = T extends Zero ? never : `${T}` extends `-${string}` ? T : never
|
|
84
|
+
type NonNegative<T extends Numeric> = T extends Zero ? T : Negative<T> extends never ? T : '__invalid_negative_number__'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// File: /home/gabriel/Projects/Resend/react-email
|
|
1
|
+
// File: /home/gabriel/Projects/Resend/react-email/packages/react-email/src/app/preview/[...slug]/page.tsx
|
|
2
2
|
import * as entry from '../../../../../src/app/preview/[...slug]/page.js'
|
|
3
3
|
import type { ResolvingMetadata, ResolvingViewport } from 'next/dist/lib/metadata/types/metadata-interface.js'
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -112,7 +112,6 @@ export interface SupportEntry {
|
|
|
112
112
|
const relevantEmailClients: EmailClient[] = [
|
|
113
113
|
'gmail',
|
|
114
114
|
'apple-mail',
|
|
115
|
-
'hey',
|
|
116
115
|
'outlook',
|
|
117
116
|
'yahoo',
|
|
118
117
|
];
|
|
@@ -155,7 +154,11 @@ export const checkCompatibility = async (
|
|
|
155
154
|
);
|
|
156
155
|
if (Object.keys(compatibilityStats.perEmailClient).length === 0)
|
|
157
156
|
continue;
|
|
158
|
-
if (
|
|
157
|
+
if (
|
|
158
|
+
compatibilityStats.status === 'success' ||
|
|
159
|
+
compatibilityStats.status === 'warning'
|
|
160
|
+
)
|
|
161
|
+
continue;
|
|
159
162
|
|
|
160
163
|
if (entry.category === 'html') {
|
|
161
164
|
const entryElements = getElementNames(entry.title, entry.keywords);
|
|
@@ -275,8 +278,9 @@ export const checkCompatibility = async (
|
|
|
275
278
|
|
|
276
279
|
if (cssEntryType === 'full property') {
|
|
277
280
|
if (
|
|
278
|
-
property.name ===
|
|
279
|
-
|
|
281
|
+
snakeToCamel(property.name) ===
|
|
282
|
+
snakeToCamel(entryFullProperty!.name) &&
|
|
283
|
+
property.value === entryFullProperty!.value
|
|
280
284
|
) {
|
|
281
285
|
addToInsights(property);
|
|
282
286
|
break;
|
|
@@ -303,7 +307,8 @@ export const checkCompatibility = async (
|
|
|
303
307
|
}
|
|
304
308
|
} else if (
|
|
305
309
|
entryProperties.some(
|
|
306
|
-
(propertyName) =>
|
|
310
|
+
(propertyName) =>
|
|
311
|
+
snakeToCamel(property.name) === snakeToCamel(propertyName),
|
|
307
312
|
)
|
|
308
313
|
) {
|
|
309
314
|
addToInsights(property);
|
|
@@ -319,4 +324,10 @@ export const checkCompatibility = async (
|
|
|
319
324
|
return readableStream;
|
|
320
325
|
};
|
|
321
326
|
|
|
327
|
+
const snakeToCamel = (snakeStr: string) => {
|
|
328
|
+
return snakeStr
|
|
329
|
+
.toLowerCase()
|
|
330
|
+
.replace(/-+([a-z])/g, (_match, letter) => letter.toUpperCase());
|
|
331
|
+
};
|
|
332
|
+
|
|
322
333
|
export type AST = ReturnType<typeof parse>;
|
|
@@ -1,18 +1,12 @@
|
|
|
1
|
-
import { render } from '@react-email/render';
|
|
2
1
|
import { type ImageCheckingResult, checkImages } from './check-images';
|
|
3
2
|
|
|
4
3
|
test('checkImages()', async () => {
|
|
5
4
|
const results: ImageCheckingResult[] = [];
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
<img src="/static/codepen-challengers.png" alt="codepen challenges" />,
|
|
12
|
-
</div>,
|
|
13
|
-
),
|
|
14
|
-
'https://demo.react.email',
|
|
15
|
-
);
|
|
5
|
+
const html = `<div>
|
|
6
|
+
<img src="https://resend.com/static/brand/resend-icon-white.png" />,
|
|
7
|
+
<img src="/static/codepen-challengers.png" alt="codepen challenges" />,
|
|
8
|
+
</div>`;
|
|
9
|
+
const stream = await checkImages(html, 'https://demo.react.email');
|
|
16
10
|
const reader = stream.getReader();
|
|
17
11
|
while (true) {
|
|
18
12
|
const { done, value } = await reader.read();
|
|
@@ -26,6 +20,10 @@ test('checkImages()', async () => {
|
|
|
26
20
|
expect(results).toEqual([
|
|
27
21
|
{
|
|
28
22
|
source: 'https://resend.com/static/brand/resend-icon-white.png',
|
|
23
|
+
codeLocation: {
|
|
24
|
+
line: 2,
|
|
25
|
+
column: 3,
|
|
26
|
+
},
|
|
29
27
|
checks: [
|
|
30
28
|
{
|
|
31
29
|
passed: false,
|
|
@@ -60,6 +58,10 @@ test('checkImages()', async () => {
|
|
|
60
58
|
status: 'warning',
|
|
61
59
|
},
|
|
62
60
|
{
|
|
61
|
+
codeLocation: {
|
|
62
|
+
line: 3,
|
|
63
|
+
column: 3,
|
|
64
|
+
},
|
|
63
65
|
checks: [
|
|
64
66
|
{
|
|
65
67
|
metadata: {
|
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
import type { IncomingMessage } from 'node:http';
|
|
4
4
|
import { parse } from 'node-html-parser';
|
|
5
|
+
import {
|
|
6
|
+
type CodeLocation,
|
|
7
|
+
getCodeLocationFromAstElement,
|
|
8
|
+
} from './get-code-location-from-ast-element';
|
|
5
9
|
import { quickFetch } from './quick-fetch';
|
|
6
10
|
|
|
7
11
|
export type ImageCheck = { passed: boolean } & (
|
|
@@ -34,6 +38,7 @@ export type ImageCheck = { passed: boolean } & (
|
|
|
34
38
|
export interface ImageCheckingResult {
|
|
35
39
|
status: 'success' | 'warning' | 'error';
|
|
36
40
|
source: string;
|
|
41
|
+
codeLocation: CodeLocation;
|
|
37
42
|
checks: ImageCheck[];
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -61,6 +66,7 @@ export const checkImages = async (code: string, base: string) => {
|
|
|
61
66
|
|
|
62
67
|
const result: ImageCheckingResult = {
|
|
63
68
|
source: rawSource,
|
|
69
|
+
codeLocation: getCodeLocationFromAstElement(image, code),
|
|
64
70
|
status: 'success',
|
|
65
71
|
checks: [],
|
|
66
72
|
};
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
import { render } from '@react-email/render';
|
|
2
1
|
import { type LinkCheckingResult, checkLinks } from './check-links';
|
|
3
2
|
|
|
4
3
|
test('checkLinks()', async () => {
|
|
5
4
|
const results: LinkCheckingResult[] = [];
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</div>,
|
|
14
|
-
),
|
|
15
|
-
);
|
|
5
|
+
const html = `<div>
|
|
6
|
+
<a href="/">Root</a>
|
|
7
|
+
<a href="https://resend.com">Resend</a>
|
|
8
|
+
<a href="https://notion.so">Notion</a>
|
|
9
|
+
<a href="http://react.email">React Email unsafe</a>
|
|
10
|
+
</div>`;
|
|
11
|
+
const stream = await checkLinks(html);
|
|
16
12
|
const reader = stream.getReader();
|
|
17
13
|
while (true) {
|
|
18
14
|
const { done, value } = await reader.read();
|
|
@@ -26,6 +22,10 @@ test('checkLinks()', async () => {
|
|
|
26
22
|
expect(results).toEqual([
|
|
27
23
|
{
|
|
28
24
|
status: 'error',
|
|
25
|
+
codeLocation: {
|
|
26
|
+
line: 2,
|
|
27
|
+
column: 3,
|
|
28
|
+
},
|
|
29
29
|
checks: [
|
|
30
30
|
{
|
|
31
31
|
type: 'syntax',
|
|
@@ -36,6 +36,10 @@ test('checkLinks()', async () => {
|
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
status: 'success',
|
|
39
|
+
codeLocation: {
|
|
40
|
+
line: 3,
|
|
41
|
+
column: 3,
|
|
42
|
+
},
|
|
39
43
|
checks: [
|
|
40
44
|
{
|
|
41
45
|
type: 'syntax',
|
|
@@ -57,6 +61,10 @@ test('checkLinks()', async () => {
|
|
|
57
61
|
},
|
|
58
62
|
{
|
|
59
63
|
status: 'warning',
|
|
64
|
+
codeLocation: {
|
|
65
|
+
line: 4,
|
|
66
|
+
column: 3,
|
|
67
|
+
},
|
|
60
68
|
checks: [
|
|
61
69
|
{
|
|
62
70
|
type: 'syntax',
|
|
@@ -78,6 +86,10 @@ test('checkLinks()', async () => {
|
|
|
78
86
|
},
|
|
79
87
|
{
|
|
80
88
|
status: 'warning',
|
|
89
|
+
codeLocation: {
|
|
90
|
+
line: 5,
|
|
91
|
+
column: 3,
|
|
92
|
+
},
|
|
81
93
|
checks: [
|
|
82
94
|
{
|
|
83
95
|
type: 'syntax',
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
'use server';
|
|
2
2
|
|
|
3
3
|
import { parse } from 'node-html-parser';
|
|
4
|
+
import {
|
|
5
|
+
type CodeLocation,
|
|
6
|
+
getCodeLocationFromAstElement,
|
|
7
|
+
} from './get-code-location-from-ast-element';
|
|
4
8
|
import { quickFetch } from './quick-fetch';
|
|
5
9
|
|
|
6
10
|
export type LinkCheck = { passed: boolean } & (
|
|
@@ -21,6 +25,7 @@ export type LinkCheck = { passed: boolean } & (
|
|
|
21
25
|
export interface LinkCheckingResult {
|
|
22
26
|
status: 'success' | 'warning' | 'error';
|
|
23
27
|
link: string;
|
|
28
|
+
codeLocation: CodeLocation;
|
|
24
29
|
checks: LinkCheck[];
|
|
25
30
|
}
|
|
26
31
|
|
|
@@ -37,6 +42,7 @@ export const checkLinks = async (code: string) => {
|
|
|
37
42
|
|
|
38
43
|
const result: LinkCheckingResult = {
|
|
39
44
|
link,
|
|
45
|
+
codeLocation: getCodeLocationFromAstElement(anchor, code),
|
|
40
46
|
status: 'success',
|
|
41
47
|
checks: [],
|
|
42
48
|
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { HTMLElement } from 'node-html-parser';
|
|
2
|
+
import { getLineAndColumnFromOffset } from '../../utils/get-line-and-column-from-offset';
|
|
3
|
+
|
|
4
|
+
export interface CodeLocation {
|
|
5
|
+
line: number;
|
|
6
|
+
column: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getCodeLocationFromAstElement = (
|
|
10
|
+
ast: HTMLElement,
|
|
11
|
+
html: string,
|
|
12
|
+
): CodeLocation => {
|
|
13
|
+
const [line, column] = getLineAndColumnFromOffset(ast.range[0], html);
|
|
14
|
+
return {
|
|
15
|
+
line,
|
|
16
|
+
column,
|
|
17
|
+
};
|
|
18
|
+
};
|
|
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
import logSymbols from 'log-symbols';
|
|
6
6
|
import ora from 'ora';
|
|
7
|
-
import { isBuilding } from '../app/env';
|
|
7
|
+
import { isBuilding, isPreviewDevelopment } from '../app/env';
|
|
8
8
|
import { getEmailComponent } from '../utils/get-email-component';
|
|
9
9
|
import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
|
|
10
10
|
import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping';
|
|
@@ -36,7 +36,7 @@ export const renderEmailByPath = async (
|
|
|
36
36
|
|
|
37
37
|
const emailFilename = path.basename(emailPath);
|
|
38
38
|
let spinner: ora.Ora | undefined;
|
|
39
|
-
if (!isBuilding) {
|
|
39
|
+
if (!isBuilding && !isPreviewDevelopment) {
|
|
40
40
|
spinner = ora({
|
|
41
41
|
text: `Rendering email template ${emailFilename}\n`,
|
|
42
42
|
prefixText: ' ',
|
package/src/app/env.ts
CHANGED
|
@@ -10,3 +10,6 @@ export const emailsDirectoryAbsolutePath =
|
|
|
10
10
|
process.env.EMAILS_DIR_ABSOLUTE_PATH!;
|
|
11
11
|
|
|
12
12
|
export const isBuilding = process.env.NEXT_PUBLIC_IS_BUILDING === 'true';
|
|
13
|
+
|
|
14
|
+
export const isPreviewDevelopment =
|
|
15
|
+
process.env.NEXT_PUBLIC_IS_PREVIEW_DEVELOPMENT === 'true';
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { redirect } from 'next/navigation';
|
|
3
3
|
import { Suspense } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
type CompatibilityCheckingResult,
|
|
6
|
+
checkCompatibility,
|
|
7
|
+
} from '../../../actions/email-validation/check-compatibility';
|
|
4
8
|
import { getEmailPathFromSlug } from '../../../actions/get-email-path-from-slug';
|
|
5
9
|
import { renderEmailByPath } from '../../../actions/render-email-by-path';
|
|
6
10
|
import { Shell } from '../../../components/shell';
|
|
@@ -10,8 +14,8 @@ import type { SpamCheckingResult } from '../../../components/toolbar/spam-assass
|
|
|
10
14
|
import { PreviewProvider } from '../../../contexts/preview';
|
|
11
15
|
import { getEmailsDirectoryMetadata } from '../../../utils/get-emails-directory-metadata';
|
|
12
16
|
import { getLintingSources, loadLintingRowsFrom } from '../../../utils/linting';
|
|
17
|
+
import { loadStream } from '../../../utils/load-stream';
|
|
13
18
|
import { emailsDirectoryAbsolutePath, isBuilding } from '../../env';
|
|
14
|
-
import Home from '../../page';
|
|
15
19
|
import Preview from './preview';
|
|
16
20
|
|
|
17
21
|
export const dynamicParams = true;
|
|
@@ -56,20 +60,19 @@ This is most likely not an issue with the preview server. Maybe there was a typo
|
|
|
56
60
|
|
|
57
61
|
const serverEmailRenderingResult = await renderEmailByPath(emailPath);
|
|
58
62
|
|
|
59
|
-
if (isBuilding && 'error' in serverEmailRenderingResult) {
|
|
60
|
-
throw new Error(serverEmailRenderingResult.error.message, {
|
|
61
|
-
cause: serverEmailRenderingResult.error,
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
63
|
let spamCheckingResult: SpamCheckingResult | undefined = undefined;
|
|
66
64
|
let lintingRows: LintingRow[] | undefined = undefined;
|
|
65
|
+
let compatibilityCheckingResults: CompatibilityCheckingResult[] | undefined =
|
|
66
|
+
undefined;
|
|
67
67
|
|
|
68
|
-
if (isBuilding
|
|
68
|
+
if (isBuilding) {
|
|
69
|
+
if ('error' in serverEmailRenderingResult) {
|
|
70
|
+
throw new Error(serverEmailRenderingResult.error.message, {
|
|
71
|
+
cause: serverEmailRenderingResult.error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
69
74
|
const lintingSources = getLintingSources(
|
|
70
75
|
serverEmailRenderingResult.markup,
|
|
71
|
-
serverEmailRenderingResult.reactMarkup,
|
|
72
|
-
emailPath,
|
|
73
76
|
'',
|
|
74
77
|
);
|
|
75
78
|
lintingRows = [];
|
|
@@ -87,6 +90,15 @@ This is most likely not an issue with the preview server. Maybe there was a typo
|
|
|
87
90
|
|
|
88
91
|
return 0;
|
|
89
92
|
});
|
|
93
|
+
compatibilityCheckingResults = [];
|
|
94
|
+
for await (const result of loadStream(
|
|
95
|
+
await checkCompatibility(
|
|
96
|
+
serverEmailRenderingResult.reactMarkup,
|
|
97
|
+
emailPath,
|
|
98
|
+
),
|
|
99
|
+
)) {
|
|
100
|
+
compatibilityCheckingResults.push(result);
|
|
101
|
+
}
|
|
90
102
|
|
|
91
103
|
const response = await fetch('https://react.email/api/check-spam', {
|
|
92
104
|
method: 'POST',
|
|
@@ -118,12 +130,13 @@ This is most likely not an issue with the preview server. Maybe there was a typo
|
|
|
118
130
|
{/* This suspense is so that this page doesn't throw warnings */}
|
|
119
131
|
{/* on the build of the preview server de-opting into */}
|
|
120
132
|
{/* client-side rendering on build */}
|
|
121
|
-
<Suspense
|
|
133
|
+
<Suspense>
|
|
122
134
|
<Preview emailTitle={path.basename(emailPath)} />
|
|
123
135
|
|
|
124
136
|
<Toolbar
|
|
125
137
|
serverLintingRows={lintingRows}
|
|
126
138
|
serverSpamCheckingResult={spamCheckingResult}
|
|
139
|
+
serverCompatibilityResults={compatibilityCheckingResults}
|
|
127
140
|
/>
|
|
128
141
|
</Suspense>
|
|
129
142
|
</Shell>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
4
|
-
import { use,
|
|
4
|
+
import { use, useRef } from 'react';
|
|
5
5
|
import { flushSync } from 'react-dom';
|
|
6
6
|
import { Toaster } from 'sonner';
|
|
7
7
|
import { useDebouncedCallback } from 'use-debounce';
|
|
@@ -37,21 +37,24 @@ const Preview = ({ emailTitle }: PreviewProps) => {
|
|
|
37
37
|
const handleViewChange = (view: string) => {
|
|
38
38
|
const params = new URLSearchParams(searchParams);
|
|
39
39
|
params.set('view', view);
|
|
40
|
-
router.push(`${pathname}?${params.toString()}`);
|
|
40
|
+
router.push(`${pathname}?${params.toString()}${location.hash}`);
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
const handleLangChange = (lang: string) => {
|
|
44
44
|
const params = new URLSearchParams(searchParams);
|
|
45
45
|
params.set('view', 'source');
|
|
46
46
|
params.set('lang', lang);
|
|
47
|
-
|
|
47
|
+
const isSameLang = searchParams.get('lang') === lang;
|
|
48
|
+
router.push(
|
|
49
|
+
`${pathname}?${params.toString()}${isSameLang ? location.hash : ''}`,
|
|
50
|
+
);
|
|
48
51
|
};
|
|
49
52
|
|
|
50
53
|
const hasRenderingMetadata = typeof renderedEmailMetadata !== 'undefined';
|
|
51
54
|
const hasErrors = 'error' in renderingResult;
|
|
52
55
|
|
|
53
|
-
const
|
|
54
|
-
const
|
|
56
|
+
const maxWidthRef = useRef(Number.POSITIVE_INFINITY);
|
|
57
|
+
const maxHeightRef = useRef(Number.POSITIVE_INFINITY);
|
|
55
58
|
const minWidth = 350;
|
|
56
59
|
const minHeight = 600;
|
|
57
60
|
const storedWidth = searchParams.get('width');
|
|
@@ -59,19 +62,19 @@ const Preview = ({ emailTitle }: PreviewProps) => {
|
|
|
59
62
|
const [width, setWidth] = useClampedState(
|
|
60
63
|
storedWidth ? Number.parseInt(storedWidth) : 600,
|
|
61
64
|
350,
|
|
62
|
-
|
|
65
|
+
maxWidthRef.current,
|
|
63
66
|
);
|
|
64
67
|
const [height, setHeight] = useClampedState(
|
|
65
68
|
storedHeight ? Number.parseInt(storedHeight) : 1024,
|
|
66
69
|
600,
|
|
67
|
-
|
|
70
|
+
maxHeightRef.current,
|
|
68
71
|
);
|
|
69
72
|
|
|
70
73
|
const handleSaveViewSize = useDebouncedCallback(() => {
|
|
71
74
|
const params = new URLSearchParams(searchParams);
|
|
72
75
|
params.set('width', width.toString());
|
|
73
76
|
params.set('height', height.toString());
|
|
74
|
-
router.push(`${pathname}?${params.toString()}`);
|
|
77
|
+
router.push(`${pathname}?${params.toString()}${location.hash}`);
|
|
75
78
|
}, 300);
|
|
76
79
|
|
|
77
80
|
return (
|
|
@@ -110,8 +113,8 @@ const Preview = ({ emailTitle }: PreviewProps) => {
|
|
|
110
113
|
const observer = new ResizeObserver((entry) => {
|
|
111
114
|
const [elementEntry] = entry;
|
|
112
115
|
if (elementEntry) {
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
maxWidthRef.current = elementEntry.contentRect.width - 80;
|
|
117
|
+
maxHeightRef.current = elementEntry.contentRect.height - 80;
|
|
115
118
|
}
|
|
116
119
|
});
|
|
117
120
|
|
|
@@ -132,8 +135,8 @@ const Preview = ({ emailTitle }: PreviewProps) => {
|
|
|
132
135
|
<ResizableWarpper
|
|
133
136
|
minHeight={minHeight}
|
|
134
137
|
minWidth={minWidth}
|
|
135
|
-
maxHeight={
|
|
136
|
-
maxWidth={
|
|
138
|
+
maxHeight={maxHeightRef.current}
|
|
139
|
+
maxWidth={maxWidthRef.current}
|
|
137
140
|
height={height}
|
|
138
141
|
onResizeEnd={() => {
|
|
139
142
|
handleSaveViewSize();
|