diff --git a/lib/cdn-site-hosting/cdn-site-hosting-construct.ts b/lib/cdn-site-hosting/cdn-site-hosting-construct.ts index ce995ed4..0efce377 100644 --- a/lib/cdn-site-hosting/cdn-site-hosting-construct.ts +++ b/lib/cdn-site-hosting/cdn-site-hosting-construct.ts @@ -31,10 +31,6 @@ export class CdnSiteHostingConstruct extends cdk.Construct { ) { super(scope, id); - if (props.websiteIndexDocument.startsWith("/")) { - throw Error("leading slashes are not allowed in websiteIndexDocument"); - } - validateProps(props); const siteDomain = getSiteDomain(props); @@ -109,7 +105,7 @@ export class CdnSiteHostingConstruct extends cdk.Construct { props.sourcesWithDeploymentOptions.length === 1; // multiple sources with granular cache and invalidation control - props.sourcesWithDeploymentOptions.forEach( + const deployments = props.sourcesWithDeploymentOptions.map( ( { name, sources, distributionPathsToInvalidate, cacheControl }, index @@ -120,20 +116,30 @@ export class CdnSiteHostingConstruct extends cdk.Construct { const nameOrIndex = name ? name : `${index}`; - new s3deploy.BucketDeployment(this, `CustomDeploy${nameOrIndex}`, { - cacheControl, - sources: sources, - prune: isSingleDeploymentStep, - destinationBucket: this.s3Bucket, - distribution: isInvalidationRequired - ? this.cloudfrontWebDistribution - : undefined, - distributionPaths: isInvalidationRequired - ? distributionPathsToInvalidate - : undefined, - }); + return new s3deploy.BucketDeployment( + this, + `CustomDeploy${nameOrIndex}`, + { + cacheControl, + sources: sources, + prune: isSingleDeploymentStep, + destinationBucket: this.s3Bucket, + distribution: isInvalidationRequired + ? this.cloudfrontWebDistribution + : undefined, + distributionPaths: isInvalidationRequired + ? distributionPathsToInvalidate + : undefined, + } + ); } ); + + deployments.forEach((deployment, deploymentIndex) => { + if (deploymentIndex > 0) { + deployment.node.addDependency(deployments[deploymentIndex - 1]); + } + }); } else if (props.sources) { // multiple sources, with default cache-control and wholesale invalidation new s3deploy.BucketDeployment(this, "DeployAndInvalidate", { @@ -147,9 +153,9 @@ export class CdnSiteHostingConstruct extends cdk.Construct { } function validateProps(props: CdnSiteHostingConstructProps): void { - const { sources, sourcesWithDeploymentOptions } = props; + const { sources, sourcesWithDeploymentOptions, websiteIndexDocument } = props; - // validate source specfications + // validate source specifications if (!sources && !sourcesWithDeploymentOptions) { throw new Error( "Either `sources` or `sourcesWithDeploymentOptions` must be specified" @@ -173,4 +179,8 @@ function validateProps(props: CdnSiteHostingConstructProps): void { } else if (sources && sources.length === 0) { throw new Error("If specified, `sources` cannot be empty"); } + + if (websiteIndexDocument.startsWith("/")) { + throw Error("leading slashes are not allowed in websiteIndexDocument"); + } } diff --git a/test/infra/cdn-site-hosting/cdn-site-hosting-construct.test.ts b/test/infra/cdn-site-hosting/cdn-site-hosting-construct.test.ts index 2a4be220..90254aec 100644 --- a/test/infra/cdn-site-hosting/cdn-site-hosting-construct.test.ts +++ b/test/infra/cdn-site-hosting/cdn-site-hosting-construct.test.ts @@ -8,6 +8,7 @@ import * as cdk from "@aws-cdk/core"; import { Environment, RemovalPolicy, Stack } from "@aws-cdk/core"; import * as s3deploy from "@aws-cdk/aws-s3-deployment"; import { CdnSiteHostingConstruct } from "../../../lib/cdn-site-hosting"; +import { Template } from "@aws-cdk/assertions"; // hosted-zone requires an environment be attached to the Stack const testEnv: Environment = { @@ -251,4 +252,59 @@ describe("CdnSiteHostingConstruct", () => { ); }); }); + + describe("When sourcesWithDeploymentOptions is provided", () => { + let stack: Stack; + + beforeAll(() => { + const app = new cdk.App(); + stack = new cdk.Stack(app, "TestRoutedSPAStack", { env: testEnv }); + new CdnSiteHostingConstruct(stack, "MyTestConstruct", { + certificateArn: fakeCertificateArn, + siteSubDomain: fakeSiteSubDomain, + domainName: fakeDomain, + removalPolicy: RemovalPolicy.DESTROY, + isRoutedSpa: true, + sourcesWithDeploymentOptions: [ + { + name: "source1", + sources: [s3deploy.Source.asset("./", { exclude: ["index.html"] })], + }, + { + name: "source2", + sources: [ + s3deploy.Source.asset("./", { exclude: ["*", "!index.html"] }), + ], + }, + ], + websiteIndexDocument: "index.html", + }); + }); + + test("provisions a single S3 bucket with website hosting configured", () => { + expectCDK(stack).to(countResources("AWS::S3::Bucket", 1)); + expectCDK(stack).to( + haveResource("AWS::S3::Bucket", { + BucketName: fakeFqdn, + WebsiteConfiguration: { + IndexDocument: "index.html", + ErrorDocument: "index.html", + }, + }) + ); + }); + + test("configures all S3 deployments sequentially, with each deployment depending on the previous one", () => { + const template = Template.fromStack(stack); + const deployments = Object.entries( + template.findResources("Custom::CDKBucketDeployment") + ); + expect(deployments.length).toBe(2); + const [[firstDeploymentId, firstDeployment], [, secondDeployment]] = + deployments; + expect(firstDeployment.DependsOn).toBeUndefined(); + expect(secondDeployment.DependsOn).toBeDefined(); + expect(secondDeployment.DependsOn).toContain(firstDeploymentId); + }); + }); });