chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:32:57 +08:00
commit cd420f9332
4811 changed files with 884702 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
.react-email
+17
View File
@@ -0,0 +1,17 @@
# Emails
## Getting Started
1. First, install the dependencies:
```sh
pnpm install --filter emails
```
2. Then, run the development server:
```sh
pnpm run dev --filter emails
```
Open [localhost:3080](http://localhost:3080) with your browser to see the result.
@@ -0,0 +1,98 @@
import {
Body,
CodeBlock,
Container,
Head,
Html,
Link,
Preview,
Text,
dracula,
} from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, container, h1, main, paragraphLight, paragraphTight } from "./components/styles";
export const AlertAttemptEmailSchema = z.object({
email: z.literal("alert-attempt"),
taskIdentifier: z.string(),
fileName: z.string(),
exportName: z.string(),
version: z.string(),
environment: z.string(),
error: z.object({
message: z.string(),
name: z.string().optional(),
stackTrace: z.string().optional(),
}),
attemptLink: z.string().url(),
organization: z.string(),
});
const previewDefaults = {
taskIdentifier: "my-task",
fileName: "other.ts",
exportName: "myTask",
version: "20240101.1",
environment: "prod",
error: {
message: "Error message",
name: "Error name",
stackTrace: "Error stack trace",
},
attemptLink: "https://trigger.dev",
};
export default function Email(props: z.infer<typeof AlertAttemptEmailSchema>) {
const {
taskIdentifier,
fileName,
exportName,
version,
environment,
error,
attemptLink,
organization,
} = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>{`${organization}: [${version}.${environment} ${taskIdentifier}] ${error.message}`}</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>There's been an error on `{taskIdentifier}`</Text>
<Text style={paragraphTight}>Task ID: {taskIdentifier}</Text>
<Text style={paragraphTight}>Filename: {fileName}</Text>
<Text style={paragraphTight}>Function: {exportName}()</Text>
<Text style={paragraphTight}>Version: {version}</Text>
<Text style={paragraphTight}>Environment: {environment}</Text>
<Text style={paragraphTight}>Organization: {organization}</Text>
<Text style={paragraphLight}>{error.message}</Text>
{error.stackTrace && (
<CodeBlock code={error.stackTrace} theme={dracula} lineNumbers language="log" />
)}
<Link
href={attemptLink}
target="_blank"
style={{
...anchor,
display: "block",
marginBottom: "50px",
}}
>
Investigate this error
</Link>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,114 @@
import {
Body,
CodeBlock,
Container,
Head,
Html,
Link,
Preview,
Text,
dracula,
} from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, container, h1, main, paragraphLight, paragraphTight } from "./components/styles";
import React from "react";
export const AlertErrorGroupEmailSchema = z.object({
email: z.literal("alert-error-group"),
classification: z.enum(["new_issue", "regression", "unignored"]),
taskIdentifier: z.string(),
environment: z.string(),
error: z.object({
message: z.string(),
type: z.string().optional(),
stackTrace: z.string().optional(),
}),
occurrenceCount: z.number(),
errorLink: z.string().url(),
organization: z.string(),
project: z.string(),
});
type AlertErrorGroupEmailProps = z.infer<typeof AlertErrorGroupEmailSchema>;
const classificationLabels: Record<string, string> = {
new_issue: "New error",
regression: "Regression",
unignored: "Error resurfaced",
};
const previewDefaults: AlertErrorGroupEmailProps = {
email: "alert-error-group",
classification: "new_issue",
taskIdentifier: "my-task",
environment: "Production",
error: {
message: "Cannot read property 'foo' of undefined",
type: "TypeError",
stackTrace: "TypeError: Cannot read property 'foo' of undefined\n at Object.<anonymous>",
},
occurrenceCount: 42,
errorLink: "https://trigger.dev",
organization: "my-organization",
project: "my-project",
};
export default function Email(props: AlertErrorGroupEmailProps) {
const {
classification,
taskIdentifier,
environment,
error,
occurrenceCount,
errorLink,
organization,
project,
} = {
...previewDefaults,
...props,
};
const label = classificationLabels[classification] ?? "Error alert";
return (
<Html>
<Head />
<Preview>
{`${organization}: [${label}] ${error.type ?? "Error"} in ${taskIdentifier} (${environment})`}
</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>
{label}: {error.type ?? "Error"} in {taskIdentifier}
</Text>
<Text style={paragraphTight}>Organization: {organization}</Text>
<Text style={paragraphTight}>Project: {project}</Text>
<Text style={paragraphTight}>Task: {taskIdentifier}</Text>
<Text style={paragraphTight}>Environment: {environment}</Text>
<Text style={paragraphTight}>Occurrences: {occurrenceCount}</Text>
<Text style={paragraphLight}>{error.message}</Text>
{error.stackTrace && (
<CodeBlock code={error.stackTrace} theme={dracula} lineNumbers language="log" />
)}
<Link
href={errorLink}
target="_blank"
style={{
...anchor,
display: "block",
marginBottom: "50px",
}}
>
Investigate this error
</Link>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,106 @@
import {
Body,
CodeBlock,
Container,
Head,
Html,
Link,
Preview,
Text,
dracula,
} from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, container, h1, main, paragraphLight, paragraphTight } from "./components/styles";
import React from "react";
export const AlertRunEmailSchema = z.object({
email: z.literal("alert-run"),
runId: z.string(),
project: z.string(),
taskIdentifier: z.string(),
fileName: z.string(),
version: z.string(),
environment: z.string(),
error: z.object({
message: z.string(),
name: z.string().optional(),
stackTrace: z.string().optional(),
}),
runLink: z.string().url(),
organization: z.string(),
});
type AlertRunEmailProps = z.infer<typeof AlertRunEmailSchema>;
const previewDefaults: AlertRunEmailProps = {
email: "alert-run",
runId: "run_12345678",
project: "my-project",
taskIdentifier: "my-task",
fileName: "other.ts",
version: "20240101.1",
environment: "prod",
error: {
message: "Error message",
name: "Error name",
stackTrace: "Error stack trace",
},
runLink: "https://trigger.dev",
organization: "my-organization",
};
export default function Email(props: AlertRunEmailProps) {
const {
runId,
project,
taskIdentifier,
fileName,
version,
environment,
error,
runLink,
organization,
} = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>{`${organization}: [${version}.${environment} ${taskIdentifier}] ${error.message}`}</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>Run `{runId}` failed</Text>
<Text style={paragraphTight}>Organization: {organization}</Text>
<Text style={paragraphTight}>Project: {project}</Text>
<Text style={paragraphTight}>Task ID: {taskIdentifier}</Text>
<Text style={paragraphTight}>Filename: {fileName}</Text>
<Text style={paragraphTight}>Version: {version}</Text>
<Text style={paragraphTight}>Environment: {environment}</Text>
<Text style={paragraphLight}>{error.message}</Text>
{error.stackTrace && (
<CodeBlock code={error.stackTrace} theme={dracula} lineNumbers language="log" />
)}
<Link
href={runLink}
target="_blank"
style={{
...anchor,
display: "block",
marginBottom: "50px",
}}
>
Investigate this error
</Link>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,122 @@
import {
Body,
Button,
Column,
Container,
Head,
Html,
Preview,
Row,
Text,
} from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { bullets, container, grey, h1, main, paragraphLight, sans } from "./components/styles";
export const BulkActionCompletedEmailSchema = z.object({
email: z.literal("bulk-action-completed"),
bulkActionId: z.string(),
url: z.string().url(),
totalCount: z.number(),
successCount: z.number(),
failureCount: z.number(),
createdAt: z.string(),
completedAt: z.string(),
type: z.enum(["CANCEL", "REPLAY"]),
});
type BulkActionCompletedEmailProps = z.infer<typeof BulkActionCompletedEmailSchema>;
const previewDefaults: BulkActionCompletedEmailProps = {
email: "bulk-action-completed",
bulkActionId: "bulk_cmcxgmhjn0001cw7ct7g936uz",
url: "http://localhost:3000/bulk-actions/123",
totalCount: 100,
successCount: 90,
failureCount: 10,
type: "CANCEL",
createdAt: "10 Jul 2025, 15:04:04",
completedAt: "10 Jul 2025, 15:05:04",
};
export default function Email(props: BulkActionCompletedEmailProps) {
const {
bulkActionId,
url,
totalCount,
successCount,
failureCount,
type,
createdAt,
completedAt,
} = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>Bulk action {bulkActionId} finished.</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>Bulk action finished</Text>
<Text style={paragraphLight}>Here's a summary of your bulk action:</Text>
<Property label="ID" value={bulkActionId} />
<Property label="Started" value={`${createdAt} (UTC)`} />
<Property label="Completed" value={`${completedAt} (UTC)`} />
<Property label={`Successfully ${pastTense(type)}`} value={`${successCount} runs`} />
<Property
label={`Failed to ${type.toLocaleLowerCase()}`}
value={`${failureCount} runs`}
/>
<Property label="Total runs" value={`${totalCount} runs`} />
<Button
style={{
...sans,
boxSizing: "border-box",
padding: "12px 24px",
borderRadius: "8px",
backgroundColor: "#615FFF",
textAlign: "center",
fontWeight: "600",
color: "#ffffff",
marginTop: "24px",
marginBottom: "32px",
}}
href={url}
>
View bulk action
</Button>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
function Property({ label, value }: { label: string; value: string | number }) {
return (
<Row>
<Column align="left" style={{ ...bullets, width: "33%" }}>
{label}
</Column>
<Column align="left" style={{ ...bullets, ...grey, width: "66%" }}>
{value}
</Column>
</Row>
);
}
function pastTense(type: "CANCEL" | "REPLAY") {
switch (type) {
case "CANCEL":
return "canceled";
case "REPLAY":
return "replayed";
}
}
@@ -0,0 +1,10 @@
// Use a global variable to store the base path
let globalBasePath: string = "http://localhost:3000";
export function setGlobalBasePath(basePath: string) {
globalBasePath = basePath;
}
export function getGlobalBasePath() {
return globalBasePath;
}
@@ -0,0 +1,17 @@
import { Hr, Link, Text } from "@react-email/components";
import React from "react";
import { footer, footerAnchor, hr } from "./styles";
export function Footer() {
return (
<>
<Hr style={hr} />
<Text style={footer}>
©Trigger.dev, 1111B S Governors Ave STE 6433, Dover, DE 19904 |{" "}
<Link style={footerAnchor} href="https://trigger.dev/">
Trigger.dev
</Link>
</Text>
</>
);
}
@@ -0,0 +1,13 @@
import { Img } from "@react-email/components";
import * as React from "react";
import { getGlobalBasePath } from "./BasePath";
type ImageProps = Omit<Parameters<typeof Img>[0], "src"> & {
path: string;
};
export function Image({ path, ...props }: ImageProps) {
const basePath = getGlobalBasePath();
return <Img src={`${basePath}${path}`} {...props} />;
}
@@ -0,0 +1,122 @@
export const h1 = {
color: "#D7D9DD",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: "24px",
fontWeight: "bold",
margin: "40px 0",
padding: "0",
};
export const main = {
backgroundColor: "#15171A",
padding: "0 20px",
};
export const container = {
backgroundColor: "#15171A",
margin: "0 auto",
padding: "20px 0 48px",
marginBottom: "64px",
};
export const box = {
padding: "0 48px",
};
export const hr = {
borderColor: "#272A2E",
margin: "20px 0",
};
export const sans = {
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
};
export const paragraph = {
color: "#878C99",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};
export const paragraphLight = {
color: "#D7D9DD",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
};
export const paragraphTight = {
color: "#D7D9DD",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px",
lineHeight: "16px",
textAlign: "left" as const,
};
export const bullets = {
color: "#D7D9DD",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px",
lineHeight: "24px",
textAlign: "left" as const,
margin: "0",
};
export const grey = {
color: "#878C99",
};
export const anchor = {
color: "#826DFF",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: "16px",
textDecoration: "underline",
};
export const button = {
backgroundColor: "#826DFF",
borderRadius: "5px",
color: "#D7D9DD",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "16px",
fontWeight: "bold",
textDecoration: "none",
textAlign: "center" as const,
display: "block",
};
export const footer = {
color: "#878C99",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "12px",
lineHeight: "16px",
};
export const footerItalic = {
color: "#878C99",
fontStyle: "italic",
fontFamily:
'-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Ubuntu,sans-serif',
fontSize: "12px",
lineHeight: "16px",
};
export const footerAnchor = {
color: "#878C99",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
fontSize: "12px",
textDecoration: "underline",
};
@@ -0,0 +1,164 @@
import {
Body,
CodeBlock,
Column,
Container,
Head,
Html,
Link,
Preview,
Row,
Text,
dracula,
} from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, bullets, container, grey, h1, main, paragraphLight } from "./components/styles";
export const AlertDeploymentFailureEmailSchema = z.object({
email: z.literal("alert-deployment-failure"),
version: z.string(),
environment: z.string(),
organization: z.string(),
shortCode: z.string(),
failedAt: z.date(),
error: z.object({
name: z.string(),
message: z.string(),
stack: z.string().optional(),
}),
deploymentLink: z.string().url(),
git: z
.object({
branchName: z.string(),
shortSha: z.string(),
commitMessage: z.string(),
commitUrl: z.string(),
branchUrl: z.string(),
pullRequestNumber: z.number().optional(),
pullRequestTitle: z.string().optional(),
pullRequestUrl: z.string().optional(),
})
.optional(),
vercelDeploymentUrl: z.string().url().optional(),
});
const previewDefaults = {
version: "v1",
environment: "production",
organization: "My Organization",
shortCode: "abc123",
failedAt: new Date().toISOString(),
error: {
name: "Error",
stack: "Error: Something went wrong\n at main.ts:12:34",
},
deploymentLink: "https://trigger.dev",
git: {
branchName: "feat/new-feature",
shortSha: "abc1234",
commitMessage: "Add new background task for processing uploads",
commitUrl: "https://github.com/acme/app/commit/abc1234",
branchUrl: "https://github.com/acme/app/tree/feat/new-feature",
pullRequestNumber: 42,
pullRequestTitle: "Add upload processing",
pullRequestUrl: "https://github.com/acme/app/pull/42",
},
vercelDeploymentUrl: "https://vercel.com/acme/app/abc1234",
};
export default function Email(props: z.infer<typeof AlertDeploymentFailureEmailSchema>) {
const {
version,
environment,
organization,
shortCode: _shortCode,
failedAt: _failedAt,
error,
deploymentLink,
git,
vercelDeploymentUrl,
} = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>{`[${organization}] Deployment ${version} [${environment}] failed: ${error.name}`}</Preview>
<Body style={main}>
<Container style={container}>
<Text
style={h1}
>{`An error occurred deploying ${version} in ${environment} in your ${organization} organization`}</Text>
<Text style={paragraphLight}>
{error.name} {error.message}
</Text>
{error.stack && (
<CodeBlock code={error.stack} theme={dracula} lineNumbers language="log" />
)}
<Link
href={deploymentLink}
target="_blank"
style={{
...anchor,
display: "block",
marginBottom: "50px",
}}
>
Investigate this error
</Link>
{git && (
<>
<Row>
<Column style={{ ...bullets, width: "33%" }}>Branch</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={git.branchUrl} style={anchor}>
{git.branchName}
</Link>
</Column>
</Row>
<Row>
<Column style={{ ...bullets, width: "33%" }}>Commit</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={git.commitUrl} style={anchor}>
{git.shortSha}
</Link>{" "}
{git.commitMessage}
</Column>
</Row>
{git.pullRequestNumber && git.pullRequestUrl && (
<Row>
<Column style={{ ...bullets, width: "33%" }}>Pull Request</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={git.pullRequestUrl} style={anchor}>
#{git.pullRequestNumber}
</Link>
{git.pullRequestTitle ? ` ${git.pullRequestTitle}` : ""}
</Column>
</Row>
)}
</>
)}
{vercelDeploymentUrl && (
<Row>
<Column style={{ ...bullets, width: "33%" }}>Vercel</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={vercelDeploymentUrl} style={anchor}>
View Vercel Deployment
</Link>
</Column>
</Row>
)}
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,149 @@
import {
Body,
Column,
Container,
Head,
Html,
Link,
Preview,
Row,
Text,
} from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, bullets, container, grey, h1, main } from "./components/styles";
export const AlertDeploymentSuccessEmailSchema = z.object({
email: z.literal("alert-deployment-success"),
version: z.string(),
environment: z.string(),
organization: z.string(),
shortCode: z.string(),
deployedAt: z.date(),
taskCount: z.number(),
deploymentLink: z.string().url(),
git: z
.object({
branchName: z.string(),
shortSha: z.string(),
commitMessage: z.string(),
commitUrl: z.string(),
branchUrl: z.string(),
pullRequestNumber: z.number().optional(),
pullRequestTitle: z.string().optional(),
pullRequestUrl: z.string().optional(),
})
.optional(),
vercelDeploymentUrl: z.string().url().optional(),
});
const previewDefaults = {
version: "v1",
environment: "production",
organization: "My Organization",
shortCode: "abc123",
deployedAt: new Date().toISOString(),
taskCount: 3,
deploymentLink: "https://trigger.dev",
git: {
branchName: "feat/new-feature",
shortSha: "abc1234",
commitMessage: "Add new background task for processing uploads",
commitUrl: "https://github.com/acme/app/commit/abc1234",
branchUrl: "https://github.com/acme/app/tree/feat/new-feature",
pullRequestNumber: 42,
pullRequestTitle: "Add upload processing",
pullRequestUrl: "https://github.com/acme/app/pull/42",
},
vercelDeploymentUrl: "https://vercel.com/acme/app/abc1234",
};
export default function Email(props: z.infer<typeof AlertDeploymentSuccessEmailSchema>) {
const {
version,
environment,
organization,
shortCode: _shortCode,
deployedAt: _deployedAt,
taskCount,
deploymentLink,
git,
vercelDeploymentUrl,
} = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>{`[${organization}] Deployment ${version} [${environment}] succeeded`}</Preview>
<Body style={main}>
<Container style={container}>
<Text
style={h1}
>{`Version ${version} successfully deployed ${taskCount} tasks in ${environment} in your ${organization} organization`}</Text>
<Link
href={deploymentLink}
target="_blank"
style={{
...anchor,
display: "block",
marginBottom: "50px",
}}
>
View Deployment
</Link>
{git && (
<>
<Row>
<Column style={{ ...bullets, width: "33%" }}>Branch</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={git.branchUrl} style={anchor}>
{git.branchName}
</Link>
</Column>
</Row>
<Row>
<Column style={{ ...bullets, width: "33%" }}>Commit</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={git.commitUrl} style={anchor}>
{git.shortSha}
</Link>{" "}
{git.commitMessage}
</Column>
</Row>
{git.pullRequestNumber && git.pullRequestUrl && (
<Row>
<Column style={{ ...bullets, width: "33%" }}>Pull Request</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={git.pullRequestUrl} style={anchor}>
#{git.pullRequestNumber}
</Link>
{git.pullRequestTitle ? ` ${git.pullRequestTitle}` : ""}
</Column>
</Row>
)}
</>
)}
{vercelDeploymentUrl && (
<Row>
<Column style={{ ...bullets, width: "33%" }}>Vercel</Column>
<Column style={{ ...bullets, ...grey, width: "66%" }}>
<Link href={vercelDeploymentUrl} style={anchor}>
View Vercel Deployment
</Link>
</Column>
</Row>
)}
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,49 @@
import { Body, Container, Head, Html, Link, Preview, Text } from "@react-email/components";
import { z } from "zod";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, container, h1, main, paragraphLight } from "./components/styles";
export const InviteEmailSchema = z.object({
email: z.literal("invite"),
orgName: z.string(),
inviterName: z.string().optional(),
inviterEmail: z.string(),
inviteLink: z.string().url(),
});
export default function Email({
orgName,
inviterName,
inviterEmail,
inviteLink,
}: z.infer<typeof InviteEmailSchema>) {
return (
<Html>
<Head />
<Preview>{`You've been invited to ${orgName}`}</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>{`You've been invited to ${orgName}`}</Text>
<Text style={paragraphLight}>
{inviterName ?? inviterEmail} has invited you to join their organization on Trigger.dev.
</Text>
<Link
href={inviteLink}
target="_blank"
style={{
...anchor,
display: "block",
marginBottom: "50px",
}}
>
Click here to view the invitation
</Link>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,39 @@
import { Body, Container, Head, Html, Link, Preview, Text } from "@react-email/components";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { anchor, container, h1, main, paragraphLight } from "./components/styles";
export default function Email({ magicLink }: { magicLink: string }) {
return (
<Html>
<Head />
<Preview>Log in with this magic link 🪄</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>Log in to Trigger.dev</Text>
<Link
href={magicLink}
target="_blank"
style={{
...anchor,
display: "block",
}}
>
Click here to log in with this magic link
</Link>
<Text
style={{
...paragraphLight,
display: "block",
marginBottom: "50px",
}}
>
If you didn&apos;t try to log in, you can safely ignore this email.
</Text>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,48 @@
import { Body, Container, Head, Html, Preview, Text } from "@react-email/components";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { container, h1, main, paragraphLight } from "./components/styles";
import { z } from "zod";
export const MfaDisabledEmailSchema = z.object({
email: z.literal("mfa-disabled"),
userEmail: z.string(),
});
type MfaDisabledEmailProps = z.infer<typeof MfaDisabledEmailSchema>;
const previewDefaults: MfaDisabledEmailProps = {
email: "mfa-disabled",
userEmail: "user@example.com",
};
export default function Email(props: MfaDisabledEmailProps) {
const { userEmail } = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>Multi-factor authentication disabled</Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>Multi-factor authentication disabled</Text>
<Text style={paragraphLight}>Hi there,</Text>
<Text style={paragraphLight}>
You have successfully disabled multi-factor authentication (MFA) for your Trigger.dev
account ({userEmail}). Your account no longer has the additional security layer provided
by MFA.
</Text>
<Text style={paragraphLight}>
You can re-enable MFA at any time from your account security page. If you didn't disable
MFA, please contact our support team immediately.
</Text>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,61 @@
import { Body, Container, Head, Html, Preview, Text } from "@react-email/components";
import { Footer } from "./components/Footer";
import { Image } from "./components/Image";
import { container, h1, main, paragraphLight } from "./components/styles";
import { z } from "zod";
export const MfaEnabledEmailSchema = z.object({
email: z.literal("mfa-enabled"),
userEmail: z.string(),
});
type MfaEnabledEmailProps = z.infer<typeof MfaEnabledEmailSchema>;
const previewDefaults: MfaEnabledEmailProps = {
email: "mfa-enabled",
userEmail: "user@example.com",
};
export default function Email(props: MfaEnabledEmailProps) {
const { userEmail } = {
...previewDefaults,
...props,
};
return (
<Html>
<Head />
<Preview>Multi-factor authentication enabled </Preview>
<Body style={main}>
<Container style={container}>
<Text style={h1}>Multi-factor authentication enabled</Text>
<Text style={paragraphLight}>Hi there,</Text>
<Text style={paragraphLight}>
Multi-factor authentication was successfully enabled for your Trigger.dev account (
{userEmail}). If you did not make this change, contact our support team immediately.
</Text>
<Text style={paragraphLight}>
<strong>Staying secure:</strong>
</Text>
<Text style={paragraphLight}>
Keep your authenticator app safe and secured
<br /> Never share your MFA codes with anyone
<br /> Store your recovery codes in a secure location
</Text>
<Text
style={{
...paragraphLight,
display: "block",
marginBottom: "50px",
}}
>
Your account now has an additional layer of protection and you'll need to enter a code
from your authenticator app when logging in.
</Text>
<Image path="/emails/logo-mono.png" width="120" height="22" alt="Trigger.dev" />
<Footer />
</Container>
</Body>
</Html>
);
}
@@ -0,0 +1,51 @@
import { Body, Head, Html, Link, Preview, Text } from "@react-email/components";
import { Footer } from "./components/Footer";
import { anchor, bullets, footerItalic, main, paragraphLight } from "./components/styles";
export default function Email({ name }: { name?: string }) {
return (
<Html>
<Head />
<Preview>Power up your workflows</Preview>
<Body style={main}>
<Text style={paragraphLight}>Hey {name ?? "there"},</Text>
<Text style={paragraphLight}>Im Matt, CEO of Trigger.dev.</Text>
<Text style={paragraphLight}>
Our goal is to give developers like you the ability to effortlessly create powerful
workflows in code.
</Text>
<Text style={paragraphLight}>
We recommend{" "}
<Link style={anchor} href="https://app.trigger.dev/templates">
getting started with one of our templates
</Link>{" "}
to get familiar with how Trigger.dev works, and then moving on to create your own
workflows.
</Text>
<Text style={paragraphLight}>
Feel free to reply to me if you have any questions. You can also{" "}
<Link style={anchor} href="https://cal.com/team/triggerdotdev/call">
schedule a call
</Link>{" "}
, or join our{" "}
<Link style={anchor} href="https://discord.gg/JtBAxBr2m3">
Discord server
</Link>{" "}
to connect with the community and our team.
</Text>
<Text style={paragraphLight}>We hope you enjoy using Trigger.dev!</Text>
<Text style={bullets}>Best,</Text>
<Text style={bullets}>Matt</Text>
<Text style={paragraphLight}>CEO, Trigger.dev</Text>
<Text style={footerItalic}>
If you dont want me to contact you again, please just let me know and Ill update your
preferences.
</Text>
<Footer />
</Body>
</Html>
);
}
+32
View File
@@ -0,0 +1,32 @@
{
"private": true,
"name": "emails",
"version": "1.0.0",
"description": "Send emails",
"main": "./src/index.tsx",
"types": "./src/index.tsx",
"scripts": {
"dev": "PORT=3080 email dev",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@aws-sdk/client-sesv2": "^3.716.0",
"@react-email/components": "1.0.12",
"@react-email/render": "^2.0.8",
"nodemailer": "^8.0.6",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"resend": "^3.2.0",
"tiny-invariant": "^1.2.0",
"zod": "3.25.76"
},
"devDependencies": {
"@types/nodemailer": "^8.0.0",
"@types/react": "18.2.69",
"@types/react-dom": "18.2.7",
"react-email": "^6.5.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+173
View File
@@ -0,0 +1,173 @@
import type { ReactElement } from "react";
import { z } from "zod";
import AlertAttemptFailureEmail, { AlertAttemptEmailSchema } from "../emails/alert-attempt-failure";
import AlertErrorGroupEmail, { AlertErrorGroupEmailSchema } from "../emails/alert-error-group";
import AlertRunFailureEmail, { AlertRunEmailSchema } from "../emails/alert-run-failure";
import { setGlobalBasePath } from "../emails/components/BasePath";
import AlertDeploymentFailureEmail, {
AlertDeploymentFailureEmailSchema,
} from "../emails/deployment-failure";
import AlertDeploymentSuccessEmail, {
AlertDeploymentSuccessEmailSchema,
} from "../emails/deployment-success";
import InviteEmail, { InviteEmailSchema } from "../emails/invite";
import MagicLinkEmail from "../emails/magic-link";
import type { MailTransport, MailTransportOptions } from "./transports";
import { constructMailTransport } from "./transports";
import MfaEnabledEmail, { MfaEnabledEmailSchema } from "../emails/mfa-enabled";
import MfaDisabledEmail, { MfaDisabledEmailSchema } from "../emails/mfa-disabled";
import BulkActionCompletedEmail, {
BulkActionCompletedEmailSchema,
} from "../emails/bulk-action-complete";
export { type MailTransportOptions };
export const DeliverEmailSchema = z
.discriminatedUnion("email", [
z.object({
email: z.literal("magic_link"),
magicLink: z.string().url(),
}),
InviteEmailSchema,
AlertRunEmailSchema,
AlertAttemptEmailSchema,
AlertErrorGroupEmailSchema,
AlertDeploymentFailureEmailSchema,
AlertDeploymentSuccessEmailSchema,
MfaEnabledEmailSchema,
MfaDisabledEmailSchema,
BulkActionCompletedEmailSchema,
])
.and(z.object({ to: z.string() }));
export type DeliverEmail = z.infer<typeof DeliverEmailSchema>;
export type SendPlainTextOptions = { to: string; subject: string; text: string };
export class EmailClient {
#transport: MailTransport;
#imagesBaseUrl: string;
#from: string;
#replyTo: string;
constructor(config: {
transport?: MailTransportOptions;
imagesBaseUrl: string;
from: string;
replyTo: string;
}) {
this.#transport = constructMailTransport(config.transport ?? { type: undefined });
this.#imagesBaseUrl = config.imagesBaseUrl;
this.#from = config.from;
this.#replyTo = config.replyTo;
}
async send(data: DeliverEmail) {
const { subject, component } = this.#getTemplate(data);
setGlobalBasePath(this.#imagesBaseUrl);
return await this.#transport.send({
to: data.to,
subject,
react: component,
from: this.#from,
replyTo: this.#replyTo,
});
}
async sendPlainText(options: SendPlainTextOptions) {
await this.#transport.sendPlainText({
...options,
from: this.#from,
replyTo: this.#replyTo,
});
}
#getTemplate(data: DeliverEmail): {
subject: string;
component: ReactElement;
} {
switch (data.email) {
case "magic_link":
return {
subject: "Magic sign-in link for Trigger.dev",
component: <MagicLinkEmail magicLink={data.magicLink} />,
};
case "invite":
return {
subject: `You've been invited to join ${data.orgName} on Trigger.dev`,
component: <InviteEmail {...data} />,
};
case "alert-attempt": {
return {
subject: `[${data.organization}] Error on ${data.taskIdentifier} [${data.version}.${data.environment}] ${data.error.message}`,
component: <AlertAttemptFailureEmail {...data} />,
};
}
case "alert-run": {
return {
subject: `[${data.organization}] Run ${data.runId} failed for ${data.taskIdentifier} [${
data.version
}.${data.environment}] ${formatErrorMessageForSubject(data.error.message)}`,
component: <AlertRunFailureEmail {...data} />,
};
}
case "alert-error-group": {
const classLabel =
data.classification === "new_issue"
? "New error"
: data.classification === "regression"
? "Regression"
: "Error resurfaced";
return {
subject: `[${data.organization}] ${classLabel}: ${data.error.type ?? "Error"} in ${data.taskIdentifier} [${data.environment}]`,
component: <AlertErrorGroupEmail {...data} />,
};
}
case "alert-deployment-failure": {
return {
subject: `[${data.organization}] Deployment ${data.version} [${data.environment}] failed: ${data.error.name}`,
component: <AlertDeploymentFailureEmail {...data} />,
};
}
case "alert-deployment-success": {
return {
subject: `[${data.organization}] Deployment ${data.version} [${data.environment}] succeeded`,
component: <AlertDeploymentSuccessEmail {...data} />,
};
}
case "mfa-enabled": {
return {
subject: `Multi-factor authentication enabled on your Trigger.dev account`,
component: <MfaEnabledEmail {...data} />,
};
}
case "mfa-disabled": {
return {
subject: `Multi-factor authentication disabled on your Trigger.dev account`,
component: <MfaDisabledEmail {...data} />,
};
}
case "bulk-action-completed": {
return {
subject: `Bulk action finished`,
component: <BulkActionCompletedEmail {...data} />,
};
}
}
}
}
function formatErrorMessageForSubject(message?: string) {
if (!message) {
return "";
}
const singleLine = message.replace(/[\r\n]+/g, " ");
return singleLine.length > 30 ? singleLine.substring(0, 27) + "..." : singleLine;
}
@@ -0,0 +1,63 @@
import { render } from "@react-email/render";
import type { MailMessage, MailTransport, PlainTextMailMessage } from "./index";
import { EmailError } from "./index";
import nodemailer from "nodemailer";
import { SESv2Client, SendEmailCommand } from "@aws-sdk/client-sesv2";
export type AwsSesMailTransportOptions = {
type: "aws-ses";
};
export class AwsSesMailTransport implements MailTransport {
#client: nodemailer.Transporter;
constructor(options: AwsSesMailTransportOptions) {
const sesClient = new SESv2Client();
this.#client = nodemailer.createTransport({
SES: { sesClient, SendEmailCommand },
});
}
async send({ to, from, replyTo, subject, react }: MailMessage): Promise<void> {
try {
await this.#client.sendMail({
from: from,
to,
replyTo: replyTo,
subject,
html: await render(react),
});
} catch (error) {
if (error instanceof Error) {
console.error(
`Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}`
);
throw new EmailError(error);
} else {
throw error;
}
}
}
async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise<void> {
try {
await this.#client.sendMail({
from: from,
to,
replyTo: replyTo,
subject,
text: text,
});
} catch (error) {
if (error instanceof Error) {
console.error(
`Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}`
);
throw new EmailError(error);
} else {
throw error;
}
}
}
}
@@ -0,0 +1,56 @@
import type { ReactElement } from "react";
import type { AwsSesMailTransportOptions } from "./aws-ses";
import { AwsSesMailTransport } from "./aws-ses";
import type { NullMailTransportOptions } from "./null";
import { NullMailTransport } from "./null";
import type { ResendMailTransportOptions } from "./resend";
import { ResendMailTransport } from "./resend";
import type { SmtpMailTransportOptions } from "./smtp";
import { SmtpMailTransport } from "./smtp";
export type MailMessage = {
to: string;
from: string;
replyTo: string;
subject: string;
react: ReactElement;
};
export type PlainTextMailMessage = {
to: string;
from: string;
replyTo: string;
subject: string;
text: string;
};
export interface MailTransport {
send(message: MailMessage): Promise<void>;
sendPlainText(message: PlainTextMailMessage): Promise<void>;
}
export class EmailError extends Error {
constructor({ name, message }: { name: string; message: string }) {
super(message);
this.name = name;
}
}
export type MailTransportOptions =
| AwsSesMailTransportOptions
| ResendMailTransportOptions
| NullMailTransportOptions
| SmtpMailTransportOptions;
export function constructMailTransport(options: MailTransportOptions): MailTransport {
switch (options.type) {
case "aws-ses":
return new AwsSesMailTransport(options);
case "resend":
return new ResendMailTransport(options);
case "smtp":
return new SmtpMailTransport(options);
case undefined:
return new NullMailTransport(options);
}
}
@@ -0,0 +1,30 @@
import { render } from "@react-email/render";
import type { MailMessage, MailTransport, PlainTextMailMessage } from "./index";
export type NullMailTransportOptions = {
type: undefined;
};
export class NullMailTransport implements MailTransport {
constructor(options: NullMailTransportOptions) {}
async send({ to, subject, react }: MailMessage): Promise<void> {
const plainText = await render(react, {
plainText: true,
});
console.log(`
##### sendEmail to ${to}, subject: ${subject}
${plainText}
`);
}
async sendPlainText({ to, subject, text }: PlainTextMailMessage): Promise<void> {
console.log(`
##### sendEmail to ${to}, subject: ${subject}
${text}
`);
}
}
@@ -0,0 +1,52 @@
import type { MailMessage, MailTransport, PlainTextMailMessage } from "./index";
import { EmailError } from "./index";
import { Resend } from "resend";
export type ResendMailTransportOptions = {
type: "resend";
config: {
apiKey?: string;
};
};
export class ResendMailTransport implements MailTransport {
#client: Resend;
constructor(options: ResendMailTransportOptions) {
this.#client = new Resend(options.config.apiKey);
}
async send({ to, from, replyTo, subject, react }: MailMessage): Promise<void> {
const result = await this.#client.emails.send({
from: from,
to,
reply_to: replyTo,
subject,
react,
});
if (result.error) {
console.error(
`Failed to send email to ${to}, ${subject}. Error ${result.error.name}: ${result.error.message}`
);
throw new EmailError(result.error);
}
}
async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise<void> {
const result = await this.#client.emails.send({
from: from,
to,
reply_to: replyTo,
subject,
text,
});
if (result.error) {
console.error(
`Failed to send email to ${to}, ${subject}. Error ${result.error.name}: ${result.error.message}`
);
throw new EmailError(result.error);
}
}
}
@@ -0,0 +1,67 @@
import { render } from "@react-email/render";
import nodemailer from "nodemailer";
import type { MailMessage, MailTransport, PlainTextMailMessage } from "./index";
import { EmailError } from "./index";
export type SmtpMailTransportOptions = {
type: "smtp";
config: {
host?: string;
port?: number;
secure?: boolean;
auth?: {
user?: string;
pass?: string;
};
};
};
export class SmtpMailTransport implements MailTransport {
#client: nodemailer.Transporter;
constructor(options: SmtpMailTransportOptions) {
this.#client = nodemailer.createTransport(options.config);
}
async send({ to, from, replyTo, subject, react }: MailMessage): Promise<void> {
try {
await this.#client.sendMail({
from: from,
to,
replyTo: replyTo,
subject,
html: await render(react),
});
} catch (error) {
if (error instanceof Error) {
console.error(
`Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}`
);
throw new EmailError(error);
} else {
throw error;
}
}
}
async sendPlainText({ to, from, replyTo, subject, text }: PlainTextMailMessage): Promise<void> {
try {
await this.#client.sendMail({
from: from,
to,
replyTo: replyTo,
subject,
text: text,
});
} catch (error) {
if (error instanceof Error) {
console.error(
`Failed to send email to ${to}, ${subject}. Error ${error.name}: ${error.message}`
);
throw new EmailError(error);
} else {
throw error;
}
}
}
}
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"incremental": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"target": "ES2015"
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}