Skip to content
This repository has been archived by the owner on Oct 21, 2024. It is now read-only.

feat(aws/static-site): support routes arg #320

Closed
wants to merge 4 commits into from
Closed
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
177 changes: 121 additions & 56 deletions pkg/platform/src/components/aws/static-site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "@pulumi/pulumi";
import { Cdn, CdnArgs } from "./cdn.js";
import { Bucket, BucketArgs } from "./bucket.js";
import { Router, RouterArgs } from "./router.js";
import { Component, Prettify, Transform, transform } from "../component.js";
import { Link } from "../link.js";
import { Input } from "../input.js";
Expand Down Expand Up @@ -185,6 +186,11 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs {
paths?: Input<"all" | string[]>;
}
>;
/**
* Refer to [Router's routes arg](/docs/components/aws/router/#routes).
* Note that `/*` is reserved for the static site.
*/
routes?: RouterArgs['routes']
/**
* [Transform](/docs/components#transform) how this component creates its underlying
* resources.
Expand All @@ -194,6 +200,11 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs {
* Transform the Bucket resource used for uploading the assets.
*/
assets?: Transform<BucketArgs>;
/**
* Transform the Cache Policy that's attached to each CloudFront behavior.
* Refer to [Router's transform cachePolicy](/docs/components/aws/router/#transform).
*/
cachePolicy?: Transform<aws.cloudfront.CachePolicyArgs>;
/**
* Transform the CloudFront CDN resource.
*/
Expand Down Expand Up @@ -346,8 +357,9 @@ export interface StaticSiteArgs extends BaseStaticSiteArgs {
* ```
*/
export class StaticSite extends Component implements Link.Linkable {
private cdn: Cdn;
private assets: Bucket;
private router: Router;
private cdn: Cdn;

constructor(
name: string,
Expand All @@ -357,14 +369,21 @@ export class StaticSite extends Component implements Link.Linkable {
super(__pulumiType, name, args, opts);

const parent = this;

const parsedArgs = parseArgs();

validateRoutes();

const { sitePath, environment, indexPage } = prepare(args);
const outputPath = buildApp(name, args.build, sitePath, environment);
const access = createCloudFrontOriginAccessIdentity();
const bucket = createS3Bucket();
const bucketFile = uploadAssets();
const distribution = createDistribution();
const router = createRouter();
const distribution = router.nodes.cdn;
createDistributionInvalidation();
this.assets = bucket;
this.router = router;
this.cdn = distribution;

this.registerOutputs({
Expand All @@ -376,6 +395,30 @@ export class StaticSite extends Component implements Link.Linkable {
},
});

function parseArgs() {
if (!args.routes)
args.routes = {};
return args as StaticSiteArgs & { routes: NonNullable<StaticSiteArgs['routes']> };
}

function validateRoutes() {
output(parsedArgs.routes).apply((routes) => {
Object.keys(routes).map((path) => {
if (path === "/*") {
throw new Error(
`In "${name}" StaticSite's routes, "/*" is reserved for the static deploy`,
);
}

if (!path.startsWith("/")) {
throw new Error(
`In "${name}" StaticSite's routes, the route path "${path}" must start with a "/"`,
);
}
});
});
}

function createCloudFrontOriginAccessIdentity() {
return new aws.cloudfront.OriginAccessIdentity(
`${name}OriginAccessIdentity`,
Expand All @@ -387,7 +430,7 @@ export class StaticSite extends Component implements Link.Linkable {
function createS3Bucket() {
return new Bucket(
`${name}Assets`,
transform(args.transform?.assets, {
transform(parsedArgs.transform?.assets, {
transform: {
policy: (policyArgs) => {
const newPolicy = aws.iam.getPolicyDocumentOutput({
Expand Down Expand Up @@ -420,7 +463,7 @@ export class StaticSite extends Component implements Link.Linkable {
}

function uploadAssets() {
return all([outputPath, args.assets]).apply(
return all([outputPath, parsedArgs.assets]).apply(
async ([outputPath, assets]) => {
const bucketFiles: BucketFile[] = [];

Expand Down Expand Up @@ -529,64 +572,82 @@ export class StaticSite extends Component implements Link.Linkable {
return `${mime}${charset}`;
}

function createDistribution() {
return new Cdn(
`${name}Cdn`,
transform(args.transform?.cdn, {
comment: `${name} site`,
origins: [
{
originId: "s3",
domainName: bucket.nodes.bucket.bucketRegionalDomainName,
originPath: "",
s3OriginConfig: {
originAccessIdentity: access.cloudfrontAccessIdentityPath,
},
},
],
defaultRootObject: indexPage,
customErrorResponses: args.errorPage
? [
{
errorCode: 403,
responsePagePath: interpolate`/${args.errorPage}`,
},
{
errorCode: 404,
responsePagePath: interpolate`/${args.errorPage}`,
},
]
: [
{
errorCode: 403,
responsePagePath: interpolate`/${indexPage}`,
responseCode: 200,
},
{
errorCode: 404,
responsePagePath: interpolate`/${indexPage}`,
responseCode: 200,
},
],
defaultCacheBehavior: {
targetOriginId: "s3",
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["GET", "HEAD", "OPTIONS"],
cachedMethods: ["GET", "HEAD"],
compress: true,
// CloudFront's managed CachingOptimized policy
cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
function createRouter() {
return new Router(
`${name}Router`,
{
domain: parsedArgs.domain,
routes: {
'/*': 'https://will-be-replaced.dummy',
...parsedArgs.routes
},
domain: args.domain,
wait: !$dev,
}),
// create distribution after s3 upload finishes
transform: {
cachePolicy: parsedArgs.transform?.cachePolicy,
cdn(routerCdnArgs) {
const s3Origin = {
originId: "s3",
domainName: bucket.nodes.bucket.bucketRegionalDomainName,
originPath: "",
s3OriginConfig: {
originAccessIdentity: access.cloudfrontAccessIdentityPath,
},
};

const newCdnArgs: CdnArgs = transform(parsedArgs.transform?.cdn, {
...routerCdnArgs,
comment: `${name} site`,
origins: all([routerCdnArgs.origins, s3Origin]).apply(([origins, s3Origin]) => {
// Replace the hard-coded '/*' origin at index 0 to our s3Origin
origins[0] = s3Origin;
return origins;
}),
defaultCacheBehavior: {
targetOriginId: "s3",
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["GET", "HEAD", "OPTIONS"],
cachedMethods: ["GET", "HEAD"],
compress: true,
// CloudFront's managed CachingOptimized policy
cachePolicyId: "658327ea-f89d-4fab-a63d-7e88639e58f6",
},
defaultRootObject: indexPage,
customErrorResponses: parsedArgs.errorPage
? [
{
errorCode: 403,
responsePagePath: interpolate`/${parsedArgs.errorPage}`,
},
{
errorCode: 404,
responsePagePath: interpolate`/${parsedArgs.errorPage}`,
},
]
: [
{
errorCode: 403,
responsePagePath: interpolate`/${indexPage}`,
responseCode: 200,
},
{
errorCode: 404,
responsePagePath: interpolate`/${indexPage}`,
responseCode: 200,
},
],
wait: !$dev,
});

// @ts-expect-error I wish TS is smarter
Object.entries(newCdnArgs).forEach(([key, value]) => routerCdnArgs[key] = value);
NamesMT marked this conversation as resolved.
Show resolved Hide resolved
},
}
},
{ dependsOn: bucketFile, parent },
);
}

function createDistributionInvalidation() {
all([outputPath, args.invalidation]).apply(
all([outputPath, parsedArgs.invalidation]).apply(
([outputPath, invalidationRaw]) => {
// Normalize invalidation
if (invalidationRaw === false) return;
Expand Down Expand Up @@ -656,6 +717,10 @@ export class StaticSite extends Component implements Link.Linkable {
* The Amazon S3 Bucket that stores the assets.
*/
assets: this.assets,
/**
* SST [Router](/docs/component/aws/router).
*/
router: this.router,
/**
* The Amazon CloudFront CDN that serves the site.
*/
Expand Down