Skip to content

Commit

Permalink
Introduce asset registration system (#9)
Browse files Browse the repository at this point in the history
This PR introduces an asset registration system. This system streamlines
synchronous asset URL resolution. This replaces the previous makeshift
system that does the same thing, and unlike the previous system,
supports arbitrary import nesting.

This PR also contains a refactor of the onboarding email, to demonstrate
this new system.
  • Loading branch information
ben-z authored Oct 13, 2024
1 parent 2d40a3d commit 839b6ac
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 62 deletions.
22 changes: 5 additions & 17 deletions cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { render } from '@react-email/components';
import { Command } from 'commander';
import fs from 'fs';
import { resolveAll } from '../utils/watcloud-uri';
import { waitForAssets } from '../utils/watcloud-uri';

const program = new Command();

Expand All @@ -19,14 +19,8 @@ program
.option('-p, --pretty', 'Pretty print the output')
.option('-t, --text', 'Generate plain text output')
.action(async (template_name, options) => {
const mod = require(`../emails/${template_name}`);
const template = mod.default;

const images = mod.images;
if (images) {
// Preload all images
await resolveAll(Object.values(images));
}
const template = require(`../emails/${template_name}`).default;
await waitForAssets();

let data = {};
if (options.data) {
Expand All @@ -51,14 +45,8 @@ program
.option('-d, --data <data>', 'Data to pass to the template. Should be a JSON array of objects')
.option('-o, --output <output>', 'Output file. A JSON array of HTML and text emails will be written to this file')
.action(async (template_name, options) => {
const mod = require(`../emails/${template_name}`);
const template = mod.default;

const images = mod.images;
if (images) {
// Preload all images
await resolveAll(Object.values(images));
}
const template = require(`../emails/${template_name}`).default;
await waitForAssets();

let data = [];
if (options.data) {
Expand Down
33 changes: 33 additions & 0 deletions emails/_common/watcloud-email.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Body,
Container,
Head,
Html,
Img,
Preview
} from "@react-email/components";
import { getAsset, registerAsset, WATcloudURI } from "../../utils/watcloud-uri";

registerAsset('watcloud-logo', new WATcloudURI("watcloud://v1/sha256:393767e36d5387815c15d11c506c3c820de5db41723ffc062751673621dedb15?name=1024x512%20black%401x.png"))

// Wrapper for WATcloud-themed emails
export function WATcloudEmail({
previewText,
children,
}: {
previewText: string,
children: React.ReactNode
}) {
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={{ backgroundColor: "#ffffff", margin: "auto", fontFamily: "sans-serif" }}>
<Container style={{ border: "1px solid #eaeaea", borderRadius: "5px", margin: "40px auto", padding: "20px", maxWidth: "600px" }}>
<Img src={getAsset('watcloud-logo').resolveFromCache()} alt="WATcloud Logo" style={{ display: "block", margin: "0 auto" }} height="100" />
{children}
</Container>
</Body>
</Html>
)
}
77 changes: 33 additions & 44 deletions emails/onboarding.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@ import {
} from "@react-email/components";
import dedent from "dedent-js";
import { z } from "zod";
import { WATcloudURI } from "../utils/watcloud-uri";

export const images = {
'watcloud-logo': new WATcloudURI("watcloud://v1/sha256:393767e36d5387815c15d11c506c3c820de5db41723ffc062751673621dedb15?name=1024x512%20black%401x.png")
}
import { WATcloudEmail } from "./_common/watcloud-email";

const WATcloudOnboardingEmailProps = z.object({
name: z.string(),
Expand All @@ -37,51 +33,44 @@ export const WATcloudOnboardingEmail = (props: WATcloudOnboardingEmailProps) =>
)

return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Body style={{ backgroundColor: "#ffffff", margin: "auto", fontFamily: "sans-serif" }}>
<Container style={{ border: "1px solid #eaeaea", borderRadius: "5px", margin: "40px auto", padding: "20px", maxWidth: "600px" }}>
<Img src={images['watcloud-logo'].resolveFromCache()} alt="WATcloud Logo" style={{ display: "block", margin: "0 auto" }} height="100" />
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px" }}>
Hi {name},
</Text>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
Welcome to WATcloud, WATonomous's compute cluster and infrastructure. We are excited to have you on board!
</Text>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
This email confirms that your access to WATcloud has been successfully updated.
</Text>
<Section style={{ marginTop: "20px", marginBottom: "20px" }}>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px" }}>
Here's a list of services that you have access to:
</Text>
{accessInstructions}
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
Access instructions for each service can be found in the <Link href="https://cloud.watonomous.ca/docs/services" style={{ color: "#1e90ff", textDecoration: "none" }}>Services</Link> documentation.
</Text>
</Section>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
If you have any questions, please reach out to your <Link href="https://cloud.watonomous.ca/docs/services#watcloud-contact" style={{ color: "#1e90ff", textDecoration: "none" }}>WATcloud contact</Link> or the WATcloud team at <Link href={`mailto:[email protected]`} style={{ color: "#1e90ff", textDecoration: "none" }}>[email protected]</Link>.
</Text>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "20px" }}>
Vroom vroom,
</Text>
<pre style={{ fontFamily: "Courier New, monospace" }}>
{dedent(String.raw`
<WATcloudEmail previewText={previewText}>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px" }}>
Hi {name},
</Text>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
Welcome to WATcloud, WATonomous's compute cluster and infrastructure. We are excited to have you on board!
</Text>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
This email confirms that your access to WATcloud has been successfully updated.
</Text>
<Section style={{ marginTop: "20px", marginBottom: "20px" }}>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px" }}>
Here's a list of services that you have access to:
</Text>
{accessInstructions}
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
Access instructions for each service can be found in the <Link href="https://cloud.watonomous.ca/docs/services" style={{ color: "#1e90ff", textDecoration: "none" }}>Services</Link> documentation.
</Text>
</Section>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
If you have any questions, please reach out to your <Link href="https://cloud.watonomous.ca/docs/services#watcloud-contact" style={{ color: "#1e90ff", textDecoration: "none" }}>WATcloud contact</Link> or the WATcloud team at <Link href={`mailto:[email protected]`} style={{ color: "#1e90ff", textDecoration: "none" }}>[email protected]</Link>.
</Text>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "20px" }}>
Vroom vroom,
</Text>
<pre style={{ fontFamily: "Courier New, monospace" }}>
{dedent(String.raw`
_∩_
__|_|_
/|__|__\____
| |
${"`"}.(o)-----(o).'
`)}
</pre>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
WATcloud Onboarding Bot 🤖
</Text>
</Container>
</Body>
</Html>
</pre>
<Text style={{ color: "#000", fontSize: "14px", lineHeight: "24px", marginTop: "10px" }}>
WATcloud Onboarding Bot 🤖
</Text>
</WATcloudEmail>
);
};

Expand Down
38 changes: 37 additions & 1 deletion utils/watcloud-uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,45 @@ export class WATcloudURI extends URL {

return ret
}

equals(other: WATcloudURI) {
return this.toString() === other.toString();
}
}

// Resolve all assets. This can also be used to prewarm the cache.
export function resolveAll(assets: WATcloudURI[]) {
return Promise.all(assets.map((asset) => asset.resolveToURL()));
}
}

// MARK: Asset registration
// This section contains code for the asset registration system.
// This system allows for assets to be resolved synchronously.
// Usage:
// 1. Register assets using `registerAsset(name, uri)`.
// 2. Wait for all assets to resolve using `await waitForAssets()`.
// 3. Get an asset using `getAsset(name)`.

const assets = new Map<string, WATcloudURI>();
const assetResolverPromises = new Map<string, Promise<string>>();

// Register an asset with a name and a URI. This will also start resolving the asset.
// To wait for all assets to resolve, `await waitForAssets()` after all assets have been registered.
export function registerAsset(name: string, uri: WATcloudURI) {
if (assets.has(name) && !assets.get(name)!.equals(uri)) {
throw new Error(`Asset ${name} is already registered with a different URI (${assets.get(name)!.toString()}). Cannot add asset with URI ${uri.toString()}.`);
}
assets.set(name, uri);
assetResolverPromises.set(name, uri.resolveToURL());
}

export function getAsset(name: string) {
if (!assets.has(name)) {
throw new Error(`Asset ${name} is not registered.`);
}
return assets.get(name)!;
}

export function waitForAssets() {
return Promise.all(assetResolverPromises.values());
}

0 comments on commit 839b6ac

Please sign in to comment.