Skip to content

feat(root): Visual Studio Code extension #1835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 43 commits into
base: 4.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1ceea70
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
beb4871
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
945ea9c
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
907f5b4
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
8cbfa6c
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
d5abd6c
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
98d50c9
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
7c3bc14
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
c460afc
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
d040544
chore(deps): bump next from 14.2.3 to 15.0.4 (#1810)
dependabot[bot] Dec 9, 2024
0805b2b
fix (vs-code-ext): buuild the emails with a ts config tailored to bui…
gabrielmfern Nov 13, 2023
120e1fe
feat (vs-code-ext): build async and save the previews on the os's tem…
gabrielmfern Nov 13, 2023
80b8238
chore: format
gabrielmfern Nov 13, 2023
15e604d
chore(vs-code-ext): Upgrade esbuild
gabrielmfern Jan 27, 2024
98d9338
feat(vs-code-ext): Use jsx: "automatic" instead of pre-defined tsconfig
gabrielmfern Jan 27, 2024
b97fb4f
chore(vs-code-ext): Rename file properly
gabrielmfern Jan 27, 2024
40c33db
chore(vs-code-ext): Upgrade @vscode/vsce to 2.23.0
gabrielmfern Jan 27, 2024
a63e0c3
feat(vs-code-ext): Use `vm` to render email previews instead of creat…
gabrielmfern Jan 27, 2024
1c3013e
chore(vs-code-ext): Add 0.0.3 to changelog
gabrielmfern Jan 27, 2024
458c64d
chore(vs-code-ext): Bump to 0.0.4
gabrielmfern Jan 27, 2024
177a25e
chore(vs-code-ext): Update lock
gabrielmfern Jan 27, 2024
081134e
chore(vs-code-ext): Format
gabrielmfern Jan 27, 2024
a90c969
fix(react-email): Linting problem
gabrielmfern Jan 27, 2024
38e3ad2
move extension into packages
gabrielmfern Dec 18, 2024
cff8943
add missing files
gabrielmfern Dec 18, 2024
a46b32a
update package
gabrielmfern Dec 19, 2024
2b254f2
create a separate preview-utils private package for reutilziing code …
gabrielmfern Dec 19, 2024
8d024d7
fix name
gabrielmfern Dec 19, 2024
b14bc72
add npmrc with hoisted linker
gabrielmfern Dec 19, 2024
70451ba
ignore vsix package
gabrielmfern Dec 19, 2024
3c49039
add vscode debugging
gabrielmfern Dec 19, 2024
0762039
rmeove hoisted npmrc that is now unecessary
gabrielmfern Dec 19, 2024
7ec33d7
ignore somethings when packaging
gabrielmfern Dec 19, 2024
fdb1edd
add prebuild script
gabrielmfern Dec 19, 2024
b4bb887
move isFileAnEmail to the preview-utils
gabrielmfern Dec 19, 2024
19fc636
use the preview-utils to render in the VS Code extension
gabrielmfern Dec 19, 2024
9eb56d7
remove console.log
gabrielmfern Dec 21, 2024
5b57e15
format
gabrielmfern Dec 23, 2024
bd1e6e1
add build on package script
gabrielmfern Dec 23, 2024
8b2a443
fix build for demo
gabrielmfern Dec 23, 2024
803497f
format
gabrielmfern Dec 23, 2024
14209cd
fix linting
gabrielmfern Dec 24, 2024
500b0bd
format
gabrielmfern Dec 24, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/react-email/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ module.exports = {

return config;
},
eslint: {
ignoreDuringBuilds: true,
},
// Noticed an issue with typescript transpilation when going from Next 14.1.1 to 14.1.2
// and I narrowed that down into this PR https://github.com/vercel/next.js/pull/62005
//
Expand Down
15 changes: 15 additions & 0 deletions packages/react-email/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@
"bin": {
"email": "./dist/cli/index.js"
},
"main": "./dist/package/index.js",
"module": "./dist/package/index.mjs",
"types": "./dist/package/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/package/index.d.mts",
"default": "./dist/package/index.mjs"
},
"require": {
"types": "./dist/package/index.d.ts",
"default": "./dist/package/index.js"
}
}
},
"scripts": {
"build": "tsup-node && node build-preview-server.mjs",
"dev": "tsup-node --watch",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,7 @@
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import fs from 'node:fs';
import path from 'node:path';

const isFileAnEmail = (fullPath: string): boolean => {
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) return false;

const { ext } = path.parse(fullPath);

if (!['.js', '.tsx', '.jsx'].includes(ext)) return false;

// This is to avoid a possible race condition where the file doesn't exist anymore
// once we are checking if it is an actual email, this couuld cause issues that
// would be very hard to debug and find out the why of it happening.
if (!fs.existsSync(fullPath)) {
return false;
}

// check with a heuristic to see if the file has at least
// a default export
const fileContents = fs.readFileSync(fullPath, 'utf8');

return /\bexport\s+default\b/gm.test(fileContents);
};
import { isFileAnEmail } from '../package';

export interface EmailsDirectory {
absolutePath: string;
Expand Down
98 changes: 23 additions & 75 deletions packages/react-email/src/actions/render-email-by-path.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,20 @@
'use server';
import fs from 'node:fs';
import path from 'node:path';
import ora from 'ora';
import logSymbols from 'log-symbols';
import chalk from 'chalk';
import { getEmailComponent } from '../utils/get-email-component';
import type { ErrorObject } from '../utils/types/error-object';
import { improveErrorWithSourceMap } from '../utils/improve-error-with-sourcemap';
import type { RenderedEmailMetadata } from '../package';
import { renderEmailByPath } from '../package';
import { fromError, type ErrorObject } from '../utils/types/error-object';
import { registerSpinnerAutostopping } from '../utils/register-spinner-autostopping';

export interface RenderedEmailMetadata {
markup: string;
plainText: string;
reactMarkup: string;
}

export type EmailRenderingResult =
export type ActionResult =
| RenderedEmailMetadata
| {
error: ErrorObject;
};

export const renderEmailByPath = async (
emailPath: string,
): Promise<EmailRenderingResult> => {
const action = async (emailPath: string): Promise<ActionResult> => {
const timeBeforeEmailRendered = performance.now();

const emailFilename = path.basename(emailPath);
Expand All @@ -36,74 +27,31 @@ export const renderEmailByPath = async (
registerSpinnerAutostopping(spinner);
}

const result = await getEmailComponent(emailPath);
const result = await renderEmailByPath(emailPath);

if ('error' in result) {
spinner?.stopAndPersist({
symbol: logSymbols.error,
text: `Failed while rendering ${emailFilename}`,
});
return { error: result.error };
return { error: fromError(result.error) };
}

const {
emailComponent: Email,
createElement,
render,
sourceMapToOriginalFile,
} = result;

const previewProps = Email.PreviewProps || {};
const EmailComponent = Email as React.FC;
try {
const markup = await render(createElement(EmailComponent, previewProps), {
pretty: true,
});
const plainText = await render(
createElement(EmailComponent, previewProps),
{
plainText: true,
},
);

const reactMarkup = await fs.promises.readFile(emailPath, 'utf-8');

const milisecondsToRendered = performance.now() - timeBeforeEmailRendered;
let timeForConsole = `${milisecondsToRendered.toFixed(0)}ms`;
if (milisecondsToRendered <= 450) {
timeForConsole = chalk.green(timeForConsole);
} else if (milisecondsToRendered <= 1000) {
timeForConsole = chalk.yellow(timeForConsole);
} else {
timeForConsole = chalk.red(timeForConsole);
}
spinner?.stopAndPersist({
symbol: logSymbols.success,
text: `Successfully rendered ${emailFilename} in ${timeForConsole}`,
});

return {
// This ensures that no null byte character ends up in the rendered
// markup making users suspect of any issues. These null byte characters
// only seem to happen with React 18, as it has no similar incident with React 19.
markup: markup.replaceAll('\0', ''),
plainText,
reactMarkup,
};
} catch (exception) {
const error = exception as Error;

spinner?.stopAndPersist({
symbol: logSymbols.error,
text: `Failed while rendering ${emailFilename}`,
});

return {
error: improveErrorWithSourceMap(
error,
emailPath,
sourceMapToOriginalFile,
),
};
const milisecondsToRendered = performance.now() - timeBeforeEmailRendered;
let timeForConsole = `${milisecondsToRendered.toFixed(0)}ms`;
if (milisecondsToRendered <= 450) {
timeForConsole = chalk.green(timeForConsole);
} else if (milisecondsToRendered <= 1000) {
timeForConsole = chalk.yellow(timeForConsole);
} else {
timeForConsole = chalk.red(timeForConsole);
}
spinner?.stopAndPersist({
symbol: logSymbols.success,
text: `Successfully rendered ${emailFilename} in ${timeForConsole}`,
});

return result;
};

export { action as renderEmailByPath };
4 changes: 2 additions & 2 deletions packages/react-email/src/app/preview/[...slug]/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation';
import React from 'react';
import { Toaster } from 'sonner';
import { useHotreload } from '../../../hooks/use-hot-reload';
import type { EmailRenderingResult } from '../../../actions/render-email-by-path';
import type { ActionResult } from '../../../actions/render-email-by-path';
import { CodeContainer } from '../../../components/code-container';
import { Shell } from '../../../components/shell';
import { Tooltip } from '../../../components/tooltip';
Expand All @@ -16,7 +16,7 @@ interface PreviewProps {
slug: string;
emailPath: string;
pathSeparator: string;
renderingResult: EmailRenderingResult;
renderingResult: ActionResult;
}

const Preview = ({
Expand Down
2 changes: 1 addition & 1 deletion packages/react-email/src/cli/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
EmailsDirectory,
getEmailsDirectoryMetadata,
} from '../../actions/get-emails-directory-metadata';
import { renderingUtilitiesExporter } from '../../utils/esbuild/renderring-utilities-exporter';
import { renderingUtilitiesExporter } from '../../package';

const getEmailTemplatesFromDirectory = (emailDirectory: EmailsDirectory) => {
const templatePaths = [] as string[];
Expand Down
8 changes: 4 additions & 4 deletions packages/react-email/src/contexts/emails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { useHotreload } from '../hooks/use-hot-reload';
import {
renderEmailByPath,
type EmailRenderingResult,
type ActionResult,
} from '../actions/render-email-by-path';
import { getEmailPathFromSlug } from '../actions/get-email-path-from-slug';

Expand All @@ -19,8 +19,8 @@ const EmailsContext = createContext<
*/
useEmailRenderingResult: (
emailPath: string,
serverEmailRenderedResult: EmailRenderingResult,
) => EmailRenderingResult;
serverEmailRenderedResult: ActionResult,
) => ActionResult;
}
| undefined
>(undefined);
Expand All @@ -45,7 +45,7 @@ export const EmailsProvider = (props: {
useState<EmailsDirectory>(props.initialEmailsDirectoryMetadata);

const [renderingResultPerEmailPath, setRenderingResultPerEmailPath] =
useState<Record<string, EmailRenderingResult>>({});
useState<Record<string, ActionResult>>({});

if (process.env.NEXT_PUBLIC_IS_BUILDING !== 'true') {
// this will not change on runtime so it doesn't violate
Expand Down
10 changes: 4 additions & 6 deletions packages/react-email/src/hooks/use-rendering-metadata.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { useEffect } from 'react';
import type {
EmailRenderingResult,
RenderedEmailMetadata,
} from '../actions/render-email-by-path';
import type { RenderedEmailMetadata } from '../package';
import type { ActionResult } from '../actions/render-email-by-path';

const lastRenderingMetadataPerEmailPath = {} as Record<
string,
Expand All @@ -15,8 +13,8 @@ const lastRenderingMetadataPerEmailPath = {} as Record<
*/
export const useRenderingMetadata = (
emailPath: string,
renderingResult: EmailRenderingResult,
initialRenderingMetadata?: EmailRenderingResult,
renderingResult: ActionResult,
initialRenderingMetadata?: ActionResult,
): RenderedEmailMetadata | undefined => {
useEffect(() => {
if ('markup' in renderingResult) {
Expand Down
1 change: 1 addition & 0 deletions packages/react-email/src/package/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './preview-utils';
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import type React from 'react';
import { type RawSourceMap } from 'source-map-js';
import { type OutputFile, build, type BuildFailure } from 'esbuild';
import type { render } from '@react-email/render';
import type { EmailTemplate as EmailComponent } from './types/email-template';
import type { ErrorObject } from './types/error-object';
import { improveErrorWithSourceMap } from './improve-error-with-sourcemap';
import { staticNodeModulesForVM } from './static-node-modules-for-vm';
import { renderingUtilitiesExporter } from './esbuild/renderring-utilities-exporter';
import type { EmailTemplate as EmailComponent } from './utils/email-template';
import { improveErrorWithSourceMap } from './utils/improve-error-with-source-map';
import { staticNodeModulesForVM } from './utils/static-node-modules-for-vm';
import { renderingUtilitiesExporter } from './utils/rendering-utilities-exporter';

export const getEmailComponent = async (
emailPath: string,
Expand All @@ -23,7 +22,7 @@ export const getEmailComponent = async (

sourceMapToOriginalFile: RawSourceMap;
}
| { error: ErrorObject }
| { error: Error }
> => {
let outputFiles: OutputFile[];
try {
Expand Down Expand Up @@ -95,13 +94,12 @@ export const getEmailComponent = async (

if (m in staticNodeModulesForVM) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return staticNodeModulesForVM[m];
return staticNodeModulesForVM[m as keyof typeof staticNodeModulesForVM];
}

// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-useless-template-literals
return require(`${specifiedModule}`) as unknown;
// this stupid string templating was necessary to not have
// webpack warnings like:
// this string templating was necessary to not have webpack warnings like:
//
// Import trace for requested module:
// ./src/utils/get-email-component.tsx
Expand Down
5 changes: 5 additions & 0 deletions packages/react-email/src/package/preview-utils/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from './render-email-by-path';
export * from './get-email-component';
export * from './utils/email-template';
export * from './utils/rendering-utilities-exporter';
export * from './is-file-an-email';
25 changes: 25 additions & 0 deletions packages/react-email/src/package/preview-utils/is-file-an-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import fs from 'node:fs';
import path from 'node:path';

export const isFileAnEmail = (fullPath: string): boolean => {
const stat = fs.statSync(fullPath);

if (stat.isDirectory()) return false;

const { ext } = path.parse(fullPath);

if (!['.js', '.tsx', '.jsx'].includes(ext)) return false;

// This is to avoid a possible race condition where the file doesn't exist anymore
// once we are checking if it is an actual email, this couuld cause issues that
// would be very hard to debug and find out the why of it happening.
if (!fs.existsSync(fullPath)) {
return false;
}

// check with a heuristic to see if the file has at least
// a default export
const fileContents = fs.readFileSync(fullPath, 'utf8');

return /\bexport\s+default\b/gm.test(fileContents);
};
Loading
Loading