Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 apps/backend/.env
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ STACK_SETUP_ADMIN_GITHUB_ID=# enter the account ID of the admin user here, and a
OTEL_EXPORTER_OTLP_ENDPOINT=# enter the OpenTelemetry endpoint here. Optional, default is `http://localhost:8131`
STACK_INTEGRATION_CLIENTS_CONFIG=# a list of oidc-provider clients for integrations. If not provided, disables integrations
STACK_FREESTYLE_API_KEY=# enter your freestyle.sh api key
STACK_VERCEL_SANDBOX_PROJECT_ID=# enter the project id for the vercel project that the vercel engine will use
STACK_VERCEL_SANDBOX_TEAM_ID=# enter the team id for the vercel project that the vercel engine will use
STACK_VERCEL_SANDBOX_TOKEN=# enter the token for the vercel project that the vercel engine will use
STACK_OPENAI_API_KEY=# enter your openai api key
STACK_FEATUREBASE_API_KEY=# enter your featurebase api key
STACK_STRIPE_SECRET_KEY=# enter your stripe api key
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,12 +77,13 @@
"@upstash/qstash": "^2.8.2",
"@vercel/functions": "^2.0.0",
"@vercel/otel": "^1.10.4",
"@vercel/sandbox": "^1.1.2",
"ai": "^4.3.17",
"bcrypt": "^5.1.1",
"chokidar-cli": "^3.0.0",
"dotenv": "^16.4.5",
"dotenv-cli": "^7.3.0",
"freestyle-sandboxes": "^0.0.92",
"freestyle-sandboxes": "^0.1.5",
"jose": "^5.2.2",
"json-diff": "^1.0.6",
"next": "16.1.1",
Expand Down
252 changes: 71 additions & 181 deletions apps/backend/src/lib/email-rendering.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Freestyle } from '@/lib/freestyle';
import { executeJavascript } from '@/lib/js-execution';
import { emptyEmailTheme } from '@stackframe/stack-shared/dist/helpers/emails';
import { getEnvVariable } from '@stackframe/stack-shared/dist/utils/env';
import { getNodeEnvironment } from '@stackframe/stack-shared/dist/utils/env';
import { captureError, StackAssertionError } from '@stackframe/stack-shared/dist/utils/errors';
import { bundleJavaScript } from '@stackframe/stack-shared/dist/utils/esbuild';
import { get, has } from '@stackframe/stack-shared/dist/utils/objects';
Expand Down Expand Up @@ -47,6 +47,68 @@ export function createTemplateComponentFromHtml(html: string) {
`;
}

const nodeModules = {
"react-dom": "19.1.1",
"react": "19.1.1",
"@react-email/components": "0.1.1",
"arktype": "2.1.20",
};

const entryJs = deindent`
export default async () => {
try {
const { renderAll } = await import("./render.tsx");
const result = await renderAll();
return { status: "ok", data: result };
} catch (e) {
return { status: "error", error: String(e) };
}
};
`;

type EmailRenderResult = { html: string, text: string, subject?: string, notificationCategory?: string };
type ExecuteResult =
| { status: "ok", data: unknown }
| { status: "error", error: string };

async function bundleAndExecute<T>(
files: Record<string, string> & { '/entry.js': string }
): Promise<Result<T, string>> {
const bundle = await bundleJavaScript(files, {
keepAsImports: ['arktype', 'react', 'react/jsx-runtime', '@react-email/components'],
externalPackages: { '@stackframe/emails': stackframeEmailsPackage },
format: 'esm',
sourcemap: false,
});
if (bundle.status === "error") {
return Result.error(bundle.error);
}

if (["development", "test"].includes(getNodeEnvironment())) {
const executeResult = await executeJavascript(bundle.data, { nodeModules, engine: 'freestyle' }) as ExecuteResult;
if (executeResult.status === "error") {
return Result.error(executeResult.error);
}
return Result.ok(executeResult.data as T);
}

const executeResult = await executeJavascript(bundle.data, { nodeModules }) as ExecuteResult;

if (executeResult.status === "error") {
const vercelResult = await executeJavascript(bundle.data, { nodeModules, engine: 'vercel-sandbox' }) as ExecuteResult;
if (vercelResult.status === "error") {
return Result.error(executeResult.error);
}
captureError("email-rendering-freestyle-runtime-error", new StackAssertionError(
"Email rendering failed with freestyle but succeeded with vercel-sandbox",
{ freestyleError: executeResult.error }
));
return Result.ok(vercelResult.data as T);
}

return Result.ok(executeResult.data as T);
}

export async function renderEmailWithTemplate(
templateOrDraftComponent: string,
themeComponent: string,
Expand All @@ -65,7 +127,7 @@ export async function renderEmailWithTemplate(
},
previewMode?: boolean,
},
): Promise<Result<{ html: string, text: string, subject?: string, notificationCategory?: string }, string>> {
): Promise<Result<EmailRenderResult, string>> {
const variables = options.variables ?? {};
const previewMode = options.previewMode ?? false;
const user = (previewMode && !options.user) ? { displayName: "John Doe" } : options.user;
Expand All @@ -77,7 +139,7 @@ export async function renderEmailWithTemplate(
throw new StackAssertionError("Project is required when not in preview mode", { user, project, variables });
}

const result = await bundleJavaScript({
const files = {
"/utils.tsx": findComponentValueUtil,
"/theme.tsx": themeComponent,
"/template.tsx": templateOrDraftComponent,
Expand Down Expand Up @@ -115,146 +177,10 @@ export async function renderEmailWithTemplate(
};
}
`,
"/entry.js": deindent`
import { renderAll } from "./render.tsx";
export default renderAll;
`,
}, {
keepAsImports: ['arktype', 'react', 'react/jsx-runtime', '@react-email/components'],
externalPackages: { '@stackframe/emails': stackframeEmailsPackage },
format: 'esm',
sourcemap: false,
});
if (result.status === "error") {
return Result.error(result.error);
}

const freestyle = new Freestyle();
const nodeModules = {
"react-dom": "19.1.1",
"react": "19.1.1",
"@react-email/components": "0.1.1",
"arktype": "2.1.20",
"/entry.js": entryJs,
};
const executeResult = await freestyle.executeScript(result.data, { nodeModules });
if (executeResult.status === "error") {
return Result.error(`${executeResult.error}`);
}
if (!executeResult.data.result) {
const noResultError = new StackAssertionError("No result from Freestyle", {
executeResult,
templateOrDraftComponent,
themeComponent,
options,
});
captureError("freestyle-no-result", noResultError);
throw noResultError;
}
return Result.ok(executeResult.data.result as { html: string, text: string, subject: string, notificationCategory: string });
}

// unused, but kept for reference & in case we need it again
export async function renderEmailsWithTemplateBatched(
templateOrDraftComponent: string,
themeComponent: string,
inputs: Array<{
user: { displayName: string | null },
project: { displayName: string },
variables?: Record<string, any>,
unsubscribeLink?: string,
themeProps?: {
projectLogos: {
logoUrl?: string,
logoFullUrl?: string,
logoDarkModeUrl?: string,
logoFullDarkModeUrl?: string,
},
},
}>,
): Promise<Result<Array<{ html: string, text: string, subject?: string, notificationCategory?: string }>, string>> {
const apiKey = getEnvVariable("STACK_FREESTYLE_API_KEY");

const serializedInputs = JSON.stringify(inputs);

const result = await bundleJavaScript({
"/utils.tsx": findComponentValueUtil,
"/theme.tsx": themeComponent,
"/template.tsx": templateOrDraftComponent,
"/render.tsx": deindent`
import { configure } from "arktype/config"
configure({ onUndeclaredKey: "delete" })
import React from 'react';
import { render } from '@react-email/components';
import { type } from "arktype";
import { findComponentValue } from "./utils.tsx";
import * as TemplateModule from "./template.tsx";
const { variablesSchema, EmailTemplate } = TemplateModule;
import { EmailTheme } from "./theme.tsx";

export const renderAll = async () => {
const inputs = ${serializedInputs}
const renderOne = async (input: any) => {
const variables = variablesSchema ? variablesSchema({
...(input.variables || {}),
}) : {};
if (variables instanceof type.errors) {
throw new Error(variables.summary)
}
const themeProps = {
...{ projectLogos: input.themeProps?.projectLogos ?? {} },
unsubscribeLink: input.unsubscribeLink,
}
const EmailTemplateWithProps = <EmailTemplate variables={variables} user={input.user} project={input.project} />;
const Email = <EmailTheme {...themeProps}>
{ EmailTemplateWithProps }
</EmailTheme>;
return {
html: await render(Email),
text: await render(Email, { plainText: true }),
subject: findComponentValue(EmailTemplateWithProps, "Subject"),
notificationCategory: findComponentValue(EmailTemplateWithProps, "NotificationCategory"),
};
};

return await Promise.all(inputs.map(renderOne));
}
`,
"/entry.js": deindent`
import { renderAll } from "./render.tsx";
export default renderAll;
`,
}, {
keepAsImports: ['arktype', 'react', 'react/jsx-runtime', '@react-email/components'],
externalPackages: { '@stackframe/emails': stackframeEmailsPackage },
format: 'esm',
sourcemap: false,
});
if (result.status === "error") {
return Result.error(result.error);
}

const freestyle = new Freestyle({ apiKey });
const nodeModules = {
"react-dom": "19.1.1",
"react": "19.1.1",
"@react-email/components": "0.1.1",
"arktype": "2.1.20",
};
const executeResult = await freestyle.executeScript(result.data, { nodeModules });
if (executeResult.status === "error") {
return Result.error(executeResult.error);
}
if (!executeResult.data.result) {
const noResultError = new StackAssertionError("No result from Freestyle", {
executeResult,
templateOrDraftComponent,
themeComponent,
inputs,
});
captureError("freestyle-no-result", noResultError);
throw noResultError;
}
return Result.ok(executeResult.data.result as Array<{ html: string, text: string, subject?: string, notificationCategory?: string }>);
return await bundleAndExecute<EmailRenderResult>(files);
}

export type RenderEmailRequestForTenancy = {
Expand All @@ -276,12 +202,11 @@ export type RenderEmailRequestForTenancy = {
},
};

export async function renderEmailsForTenancyBatched(requests: RenderEmailRequestForTenancy[]): Promise<Result<Array<{ html: string, text: string, subject?: string, notificationCategory?: string }>, string>> {
export async function renderEmailsForTenancyBatched(requests: RenderEmailRequestForTenancy[]): Promise<Result<EmailRenderResult[], string>> {
if (requests.length === 0) {
return Result.ok([]);
}

const apiKey = getEnvVariable("STACK_FREESTYLE_API_KEY");
const files: Record<string, string> = {
"/utils.tsx": findComponentValueUtil,
};
Expand Down Expand Up @@ -338,44 +263,9 @@ export async function renderEmailsForTenancyBatched(requests: RenderEmailRequest
};
`;

files["/entry.js"] = deindent`
import { renderAll } from "./render.tsx";
export default renderAll;
`;

const bundle = await bundleJavaScript(files as Record<string, string> & { '/entry.js': string }, {
keepAsImports: ["arktype", "react", "react/jsx-runtime", "@react-email/components"],
externalPackages: { "@stackframe/emails": stackframeEmailsPackage },
format: "esm",
sourcemap: false,
});

if (bundle.status === "error") {
return Result.error(bundle.error);
}

const freestyle = new Freestyle({ apiKey });
const nodeModules = {
"react-dom": "19.1.1",
"react": "19.1.1",
"@react-email/components": "0.1.1",
"arktype": "2.1.20",
};

const execution = await freestyle.executeScript(bundle.data, { nodeModules });
if (execution.status === "error") {
return Result.error(execution.error);
}
if (!execution.data.result) {
const noResultError = new StackAssertionError("No result from Freestyle", {
execution,
requests,
});
captureError("freestyle-no-result", noResultError);
throw noResultError;
}
files["/entry.js"] = entryJs;

return Result.ok(execution.data.result as Array<{ html: string, text: string, subject?: string, notificationCategory?: string }>);
return await bundleAndExecute<EmailRenderResult[]>(files as Record<string, string> & { '/entry.js': string });
}

const findComponentValueUtil = `import React from 'react';
Expand Down
57 changes: 0 additions & 57 deletions apps/backend/src/lib/freestyle.tsx

This file was deleted.

Loading