From acecac655be4b3598b4afd75d52019fb54b8e635 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Tue, 17 Oct 2023 21:05:07 -0300 Subject: [PATCH 01/10] feat: basePath and multiple nextjs sites on same distribution --- .gitignore | 1 + .projenrc.ts | 1 + API.md | 19 +- examples/multiple-sites/.gitignore | 8 + examples/multiple-sites/cdk.json | 60 +++ examples/multiple-sites/package.json | 21 + examples/multiple-sites/pnpm-lock.yaml | 623 +++++++++++++++++++++++++ examples/multiple-sites/src/app.ts | 6 + examples/multiple-sites/src/stack.ts | 43 ++ examples/multiple-sites/tsconfig.json | 31 ++ src/NextjsDistribution.ts | 69 ++- 11 files changed, 860 insertions(+), 22 deletions(-) create mode 100644 examples/multiple-sites/.gitignore create mode 100644 examples/multiple-sites/cdk.json create mode 100644 examples/multiple-sites/package.json create mode 100644 examples/multiple-sites/pnpm-lock.yaml create mode 100644 examples/multiple-sites/src/app.ts create mode 100644 examples/multiple-sites/src/stack.ts create mode 100644 examples/multiple-sites/tsconfig.json diff --git a/.gitignore b/.gitignore index 092ff775..5b32173c 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ jspm_packages/ *.tgz .yarn-integrity .cache +.idea !/.projenrc.js /test-reports/ junit.xml diff --git a/.projenrc.ts b/.projenrc.ts index 3b27a579..358411ce 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -46,6 +46,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ projenrcTs: true, tsconfig: { compilerOptions: { ...commonTscOptions } }, tsconfigDev: { compilerOptions: { ...commonTscOptions } }, + gitignore: ['.idea'], // dependency config jsiiVersion: '~5.0.0', cdkVersion: '2.93.0', diff --git a/API.md b/API.md index af062417..65760ff4 100644 --- a/API.md +++ b/API.md @@ -3165,17 +3165,17 @@ const nextjsDistributionCdkProps: NextjsDistributionCdkProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | -| distribution | aws-cdk-lib.aws_cloudfront.DistributionProps | Pass in a value to override the default settings this construct uses to create the CloudFront `Distribution` internally. | +| distribution | aws-cdk-lib.aws_cloudfront.DistributionProps \| aws-cdk-lib.aws_cloudfront.Distribution | Pass in a value to override the default settings this construct uses to create the CloudFront `Distribution` internally. | --- ##### `distribution`Optional ```typescript -public readonly distribution: DistributionProps; +public readonly distribution: DistributionProps | Distribution; ``` -- *Type:* aws-cdk-lib.aws_cloudfront.DistributionProps +- *Type:* aws-cdk-lib.aws_cloudfront.DistributionProps | aws-cdk-lib.aws_cloudfront.Distribution Pass in a value to override the default settings this construct uses to create the CloudFront `Distribution` internally. @@ -3208,6 +3208,7 @@ const nextjsDistributionProps: NextjsDistributionProps = { ... } | nextBuild | NextjsBuild | Built NextJS app. | | serverFunction | aws-cdk-lib.aws_lambda.IFunction | Lambda function to route all non-static requests to. | | staticAssetsBucket | aws-cdk-lib.aws_s3.IBucket | Bucket containing static assets. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | cachePolicies | NextjsCachePolicyProps | Override the default CloudFront cache policies created internally. | | cdk | NextjsDistributionCdkProps | Overrides for created CDK resources. | | customDomain | string \| NextjsDomainProps | The customDomain for this website. Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. | @@ -3396,6 +3397,18 @@ Must be provided if you want to serve static files. --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `cachePolicies`Optional ```typescript diff --git a/examples/multiple-sites/.gitignore b/examples/multiple-sites/.gitignore new file mode 100644 index 00000000..ff4194ab --- /dev/null +++ b/examples/multiple-sites/.gitignore @@ -0,0 +1,8 @@ +*.d.ts +node_modules + +# CDK asset staging directory +.cdk.staging +cdk.out + +!tsconfig.json \ No newline at end of file diff --git a/examples/multiple-sites/cdk.json b/examples/multiple-sites/cdk.json new file mode 100644 index 00000000..1b4b4ebe --- /dev/null +++ b/examples/multiple-sites/cdk.json @@ -0,0 +1,60 @@ +{ + "app": "pnpm tsx src/app.ts", + "watch": { + "include": [ + "**" + ], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": [ + "aws", + "aws-cn" + ], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true + } +} diff --git a/examples/multiple-sites/package.json b/examples/multiple-sites/package.json new file mode 100644 index 00000000..bcf80662 --- /dev/null +++ b/examples/multiple-sites/package.json @@ -0,0 +1,21 @@ +{ + "name": "cdk-nextjs-standalone-example-multiple-sites", + "version": "0.1.0", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "cdk": "cdk" + }, + "devDependencies": { + "@types/node": "20.5.7", + "aws-cdk": "2.94.0", + "esbuild": "^0.19.3", + "tsx": "^3.12.10", + "typescript": "~5.2.2" + }, + "dependencies": { + "aws-cdk-lib": "2.94.0", + "cdk-nextjs-standalone": "link:../..", + "constructs": "^10.0.0" + } +} diff --git a/examples/multiple-sites/pnpm-lock.yaml b/examples/multiple-sites/pnpm-lock.yaml new file mode 100644 index 00000000..87a3094f --- /dev/null +++ b/examples/multiple-sites/pnpm-lock.yaml @@ -0,0 +1,623 @@ +lockfileVersion: '6.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +dependencies: + aws-cdk-lib: + specifier: 2.94.0 + version: 2.94.0(constructs@10.2.70) + cdk-nextjs-standalone: + specifier: link:../.. + version: link:../.. + constructs: + specifier: ^10.0.0 + version: 10.2.70 + +devDependencies: + '@types/node': + specifier: 20.5.7 + version: 20.5.7 + aws-cdk: + specifier: 2.94.0 + version: 2.94.0 + esbuild: + specifier: ^0.19.3 + version: 0.19.3 + tsx: + specifier: ^3.12.10 + version: 3.12.10 + typescript: + specifier: ~5.2.2 + version: 5.2.2 + +packages: + + /@aws-cdk/asset-awscli-v1@2.2.200: + resolution: {integrity: sha512-Kf5J8DfJK4wZFWT2Myca0lhwke7LwHcHBo+4TvWOGJrFVVKVuuiLCkzPPRBQQVDj0Vtn2NBokZAz8pfMpAqAKg==} + dev: false + + /@aws-cdk/asset-kubectl-v20@2.1.2: + resolution: {integrity: sha512-3M2tELJOxQv0apCIiuKQ4pAbncz9GuLwnKFqxifWfe77wuMxyTRPmxssYHs42ePqzap1LT6GDcPygGs+hHstLg==} + dev: false + + /@aws-cdk/asset-node-proxy-agent-v6@2.0.1: + resolution: {integrity: sha512-DDt4SLdLOwWCjGtltH4VCST7hpOI5DzieuhGZsBpZ+AgJdSI2GCjklCXm0GCTwJG/SolkL5dtQXyUKgg9luBDg==} + dev: false + + /@esbuild-kit/cjs-loader@2.4.4: + resolution: {integrity: sha512-NfsJX4PdzhwSkfJukczyUiZGc7zNNWZcEAyqeISpDnn0PTfzMJR1aR8xAIPskBejIxBJbIgCCMzbaYa9SXepIg==} + dependencies: + '@esbuild-kit/core-utils': 3.3.1 + get-tsconfig: 4.7.0 + dev: true + + /@esbuild-kit/core-utils@3.3.1: + resolution: {integrity: sha512-zg2aeGLgbZ/U8AnHRD6y085BkRqlw7jOsqpI/AFaQg6FhcCRycAe+aFLibs9okVVYTMqWANDC76UVSzd3qBoOw==} + dependencies: + esbuild: 0.18.20 + source-map-support: 0.5.21 + dev: true + + /@esbuild-kit/esm-loader@2.6.4: + resolution: {integrity: sha512-xcbyhN97xFFFEdDw6IC4EuzX9Ali3aV3cj2FIYragOQpbPM4X6QA2R5qaP3h7Tr0tuyI6dmJJdMw7oBHxBSXQA==} + dependencies: + '@esbuild-kit/core-utils': 3.3.1 + get-tsconfig: 4.7.0 + dev: true + + /@esbuild/android-arm64@0.18.20: + resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm64@0.19.3: + resolution: {integrity: sha512-w+Akc0vv5leog550kjJV9Ru+MXMR2VuMrui3C61mnysim0gkFCPOUTAfzTP0qX+HpN9Syu3YA3p1hf3EPqObRw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.18.20: + resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-arm@0.19.3: + resolution: {integrity: sha512-Lemgw4io4VZl9GHJmjiBGzQ7ONXRfRPHcUEerndjwiSkbxzrpq0Uggku5MxxrXdwJ+pTj1qyw4jwTu7hkPsgIA==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.18.20: + resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/android-x64@0.19.3: + resolution: {integrity: sha512-FKQJKkK5MXcBHoNZMDNUAg1+WcZlV/cuXrWCoGF/TvdRiYS4znA0m5Il5idUwfxrE20bG/vU1Cr5e1AD6IEIjQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.18.20: + resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-arm64@0.19.3: + resolution: {integrity: sha512-kw7e3FXU+VsJSSSl2nMKvACYlwtvZB8RUIeVShIEY6PVnuZ3c9+L9lWB2nWeeKWNNYDdtL19foCQ0ZyUL7nqGw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.18.20: + resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/darwin-x64@0.19.3: + resolution: {integrity: sha512-tPfZiwF9rO0jW6Jh9ipi58N5ZLoSjdxXeSrAYypy4psA2Yl1dAMhM71KxVfmjZhJmxRjSnb29YlRXXhh3GqzYw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.18.20: + resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-arm64@0.19.3: + resolution: {integrity: sha512-ERDyjOgYeKe0Vrlr1iLrqTByB026YLPzTytDTz1DRCYM+JI92Dw2dbpRHYmdqn6VBnQ9Bor6J8ZlNwdZdxjlSg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.18.20: + resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/freebsd-x64@0.19.3: + resolution: {integrity: sha512-nXesBZ2Ad1qL+Rm3crN7NmEVJ5uvfLFPLJev3x1j3feCQXfAhoYrojC681RhpdOph8NsvKBBwpYZHR7W0ifTTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.18.20: + resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm64@0.19.3: + resolution: {integrity: sha512-qXvYKmXj8GcJgWq3aGvxL/JG1ZM3UR272SdPU4QSTzD0eymrM7leiZH77pvY3UetCy0k1xuXZ+VPvoJNdtrsWQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.18.20: + resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-arm@0.19.3: + resolution: {integrity: sha512-zr48Cg/8zkzZCzDHNxXO/89bf9e+r4HtzNUPoz4GmgAkF1gFAFmfgOdCbR8zMbzFDGb1FqBBhdXUpcTQRYS1cQ==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.18.20: + resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ia32@0.19.3: + resolution: {integrity: sha512-7XlCKCA0nWcbvYpusARWkFjRQNWNGlt45S+Q18UeS///K6Aw8bB2FKYe9mhVWy/XLShvCweOLZPrnMswIaDXQA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.18.20: + resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64@0.19.3: + resolution: {integrity: sha512-qGTgjweER5xqweiWtUIDl9OKz338EQqCwbS9c2Bh5jgEH19xQ1yhgGPNesugmDFq+UUSDtWgZ264st26b3de8A==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.18.20: + resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-mips64el@0.19.3: + resolution: {integrity: sha512-gy1bFskwEyxVMFRNYSvBauDIWNggD6pyxUksc0MV9UOBD138dKTzr8XnM2R4mBsHwVzeuIH8X5JhmNs2Pzrx+A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.18.20: + resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-ppc64@0.19.3: + resolution: {integrity: sha512-UrYLFu62x1MmmIe85rpR3qou92wB9lEXluwMB/STDzPF9k8mi/9UvNsG07Tt9AqwPQXluMQ6bZbTzYt01+Ue5g==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.18.20: + resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-riscv64@0.19.3: + resolution: {integrity: sha512-9E73TfyMCbE+1AwFOg3glnzZ5fBAFK4aawssvuMgCRqCYzE0ylVxxzjEfut8xjmKkR320BEoMui4o/t9KA96gA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.18.20: + resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-s390x@0.19.3: + resolution: {integrity: sha512-LlmsbuBdm1/D66TJ3HW6URY8wO6IlYHf+ChOUz8SUAjVTuaisfuwCOAgcxo3Zsu3BZGxmI7yt//yGOxV+lHcEA==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.18.20: + resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-x64@0.19.3: + resolution: {integrity: sha512-ogV0+GwEmvwg/8ZbsyfkYGaLACBQWDvO0Kkh8LKBGKj9Ru8VM39zssrnu9Sxn1wbapA2qNS6BiLdwJZGouyCwQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.18.20: + resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/netbsd-x64@0.19.3: + resolution: {integrity: sha512-o1jLNe4uzQv2DKXMlmEzf66Wd8MoIhLNO2nlQBHLtWyh2MitDG7sMpfCO3NTcoTMuqHjfufgUQDFRI5C+xsXQw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.18.20: + resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/openbsd-x64@0.19.3: + resolution: {integrity: sha512-AZJCnr5CZgZOdhouLcfRdnk9Zv6HbaBxjcyhq0StNcvAdVZJSKIdOiPB9az2zc06ywl0ePYJz60CjdKsQacp5Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.18.20: + resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/sunos-x64@0.19.3: + resolution: {integrity: sha512-Acsujgeqg9InR4glTRvLKGZ+1HMtDm94ehTIHKhJjFpgVzZG9/pIcWW/HA/DoMfEyXmANLDuDZ2sNrWcjq1lxw==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.18.20: + resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-arm64@0.19.3: + resolution: {integrity: sha512-FSrAfjVVy7TifFgYgliiJOyYynhQmqgPj15pzLyJk8BUsnlWNwP/IAy6GAiB1LqtoivowRgidZsfpoYLZH586A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.18.20: + resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-ia32@0.19.3: + resolution: {integrity: sha512-xTScXYi12xLOWZ/sc5RBmMN99BcXp/eEf7scUC0oeiRoiT5Vvo9AycuqCp+xdpDyAU+LkrCqEpUS9fCSZF8J3Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.18.20: + resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@esbuild/win32-x64@0.19.3: + resolution: {integrity: sha512-FbUN+0ZRXsypPyWE2IwIkVjDkDnJoMJARWOcFZn4KPPli+QnKqF0z1anvfaYe3ev5HFCpRDLLBDHyOALLppWHw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /@types/node@20.5.7: + resolution: {integrity: sha512-dP7f3LdZIysZnmvP3ANJYTSwg+wLLl8p7RqniVlV7j+oXSXAbt9h0WIBFmJy5inWZoX9wZN6eXx+YXd9Rh3RBA==} + dev: true + + /aws-cdk-lib@2.94.0(constructs@10.2.70): + resolution: {integrity: sha512-pB/UzKeM+p/wY9WuFYkEewOFUh2r8qwaML63is4vUChXY2G2Bj3pGyfJ97Xir2Q5KIhgJPJz5igdouI4+F9A+g==} + engines: {node: '>= 14.15.0'} + peerDependencies: + constructs: ^10.0.0 + dependencies: + '@aws-cdk/asset-awscli-v1': 2.2.200 + '@aws-cdk/asset-kubectl-v20': 2.1.2 + '@aws-cdk/asset-node-proxy-agent-v6': 2.0.1 + constructs: 10.2.70 + dev: false + bundledDependencies: + - '@balena/dockerignore' + - case + - fs-extra + - ignore + - jsonschema + - minimatch + - punycode + - semver + - table + - yaml + + /aws-cdk@2.94.0: + resolution: {integrity: sha512-9bJkzxFDYZDwPDfZi/DSUODn4HFRzuXWPhpFgIIgRykfT18P+iAIJ1AEhaaCmlqrrog5yQgN+2iYd9BwDsiBeg==} + engines: {node: '>= 14.15.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + + /constructs@10.2.70: + resolution: {integrity: sha512-z6zr1E8K/9tzJbCQzY0UGX0/oVKPFKu9C/mzEnghCG6TAJINnvlq0CMKm63XqqeMleadZYm5T3sZGJKcxJS/Pg==} + engines: {node: '>= 16.14.0'} + dev: false + + /esbuild@0.18.20: + resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.18.20 + '@esbuild/android-arm64': 0.18.20 + '@esbuild/android-x64': 0.18.20 + '@esbuild/darwin-arm64': 0.18.20 + '@esbuild/darwin-x64': 0.18.20 + '@esbuild/freebsd-arm64': 0.18.20 + '@esbuild/freebsd-x64': 0.18.20 + '@esbuild/linux-arm': 0.18.20 + '@esbuild/linux-arm64': 0.18.20 + '@esbuild/linux-ia32': 0.18.20 + '@esbuild/linux-loong64': 0.18.20 + '@esbuild/linux-mips64el': 0.18.20 + '@esbuild/linux-ppc64': 0.18.20 + '@esbuild/linux-riscv64': 0.18.20 + '@esbuild/linux-s390x': 0.18.20 + '@esbuild/linux-x64': 0.18.20 + '@esbuild/netbsd-x64': 0.18.20 + '@esbuild/openbsd-x64': 0.18.20 + '@esbuild/sunos-x64': 0.18.20 + '@esbuild/win32-arm64': 0.18.20 + '@esbuild/win32-ia32': 0.18.20 + '@esbuild/win32-x64': 0.18.20 + dev: true + + /esbuild@0.19.3: + resolution: {integrity: sha512-UlJ1qUUA2jL2nNib1JTSkifQTcYTroFqRjwCFW4QYEKEsixXD5Tik9xML7zh2gTxkYTBKGHNH9y7txMwVyPbjw==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.19.3 + '@esbuild/android-arm64': 0.19.3 + '@esbuild/android-x64': 0.19.3 + '@esbuild/darwin-arm64': 0.19.3 + '@esbuild/darwin-x64': 0.19.3 + '@esbuild/freebsd-arm64': 0.19.3 + '@esbuild/freebsd-x64': 0.19.3 + '@esbuild/linux-arm': 0.19.3 + '@esbuild/linux-arm64': 0.19.3 + '@esbuild/linux-ia32': 0.19.3 + '@esbuild/linux-loong64': 0.19.3 + '@esbuild/linux-mips64el': 0.19.3 + '@esbuild/linux-ppc64': 0.19.3 + '@esbuild/linux-riscv64': 0.19.3 + '@esbuild/linux-s390x': 0.19.3 + '@esbuild/linux-x64': 0.19.3 + '@esbuild/netbsd-x64': 0.19.3 + '@esbuild/openbsd-x64': 0.19.3 + '@esbuild/sunos-x64': 0.19.3 + '@esbuild/win32-arm64': 0.19.3 + '@esbuild/win32-ia32': 0.19.3 + '@esbuild/win32-x64': 0.19.3 + dev: true + + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /get-tsconfig@4.7.0: + resolution: {integrity: sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + + /tsx@3.12.10: + resolution: {integrity: sha512-2+46h4xvUt1aLDNvk5YBT8Uzw+b7BolGbn7iSMucYqCXZiDc+1IMghLVdw8kKjING32JFOeO+Am9posvjkeclA==} + hasBin: true + dependencies: + '@esbuild-kit/cjs-loader': 2.4.4 + '@esbuild-kit/core-utils': 3.3.1 + '@esbuild-kit/esm-loader': 2.6.4 + optionalDependencies: + fsevents: 2.3.3 + dev: true + + /typescript@5.2.2: + resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} + engines: {node: '>=14.17'} + hasBin: true + dev: true diff --git a/examples/multiple-sites/src/app.ts b/examples/multiple-sites/src/app.ts new file mode 100644 index 00000000..4c4a4acd --- /dev/null +++ b/examples/multiple-sites/src/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { PagesRouterStack } from './stack'; + +const app = new cdk.App(); +new PagesRouterStack(app, 'pr'); // pr = pages router diff --git a/examples/multiple-sites/src/stack.ts b/examples/multiple-sites/src/stack.ts new file mode 100644 index 00000000..aa6a336a --- /dev/null +++ b/examples/multiple-sites/src/stack.ts @@ -0,0 +1,43 @@ +import { CfnOutput, Stack, StackProps, Token } from 'aws-cdk-lib'; +import { Distribution, OriginProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; +import { HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; +import { Construct } from 'constructs'; +import { Nextjs } from 'cdk-nextjs-standalone'; + +export class PagesRouterStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const distribution = new Distribution(this, 'distribution', { + defaultBehavior: { + origin: new HttpOrigin('constructs.dev', { + protocolPolicy: OriginProtocolPolicy.MATCH_VIEWER, + }), + }, + }) + + const app1 = new Nextjs(this, 'app-router', { + nextjsPath: '../../open-next/examples/app-router', + defaults: { + distribution: { + basePath: 'app-router', + cdk: { distribution } + } + } + }); + + const app2 = new Nextjs(this, 'pages-router', { + nextjsPath: '../../open-next/examples/pages-router', + defaults: { + distribution: { + basePath: 'pages-router', + cdk: { distribution } + } + } + }); + + new CfnOutput(this, "CloudFrontDistributionDomain", { + value: distribution.distributionDomainName, + }); + } +} diff --git a/examples/multiple-sites/tsconfig.json b/examples/multiple-sites/tsconfig.json new file mode 100644 index 00000000..aaa7dc51 --- /dev/null +++ b/examples/multiple-sites/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": [ + "es2020", + "dom" + ], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": false, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "cdk.out" + ] +} diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index cae3ee2e..360e3c1b 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -26,7 +26,7 @@ export interface NextjsDistributionCdkProps { * Pass in a value to override the default settings this construct uses to * create the CloudFront `Distribution` internally. */ - readonly distribution?: NextjsDistributionCdkOverrideProps; + readonly distribution?: NextjsDistributionCdkOverrideProps | Distribution; } export interface NextjsCachePolicyProps { @@ -107,6 +107,12 @@ export interface NextjsDistributionProps extends NextjsBaseProps { */ readonly customDomain?: string | NextjsDomainProps; + /** + * Optional value to prefix the Next.js site under a /prefix path on CloudFront. + * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + */ + readonly basePath?: string; + /** * Include the name of your deployment stage if present. * Used to name the edge functions stack. @@ -404,16 +410,43 @@ export class NextjsDistribution extends Construct { private createCloudFrontDistribution(): cloudfront.Distribution { const { cdk: cdkProps } = this.props; - const cfDistributionProps = cdkProps?.distribution; + const cfDistributionProps = cdkProps?.distribution ?? ({} as NextjsDistributionCdkOverrideProps); - // build domainNames - const domainNames = this.buildDistributionDomainNames(); + const distribution = + 'node' in cfDistributionProps // if cdkProps.distribution is a cdk.cloudfront.Distribution + ? cfDistributionProps + : this.createDefaultCloudFrontDistribution(cfDistributionProps); // if we don't have a static file called index.html then we should // redirect to the lambda handler const hasIndexHtml = this.props.nextBuild.readPublicFileList().includes('index.html'); - const distribution = new cloudfront.Distribution(this, 'Distribution', { + const additionalBehaviors = { + // is index.html static or dynamic? + ...(hasIndexHtml ? {} : { '/': this.serverBehaviorOptions }), + // known dynamic routes + 'api/*': this.serverBehaviorOptions, + '_next/data/*': this.serverBehaviorOptions, + // dynamic images go to lambda + '_next/image*': this.imageBehaviorOptions, + }; + + // add additional behaviors + for (const [pathPattern, behaviorOptions] of Object.entries(additionalBehaviors)) { + const finalPathPattern = this.getPathPattern(pathPattern); + const { origin, ...options } = behaviorOptions; + distribution.addBehavior(finalPathPattern, origin, options); + } + + return distribution; + } + + private createDefaultCloudFrontDistribution(cfDistributionProps?: NextjsDistributionCdkOverrideProps) { + console.log('createDefaultCloudFrontDistribution', this.props.cdk?.distribution); + // build domainNames + const domainNames = this.buildDistributionDomainNames(); + + return new cloudfront.Distribution(this, 'Distribution', { // defaultRootObject: "index.html", defaultRootObject: '', minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, @@ -425,20 +458,7 @@ export class NextjsDistribution extends Construct { domainNames, certificate: this.certificate, defaultBehavior: this.serverBehaviorOptions, - - additionalBehaviors: { - // is index.html static or dynamic? - ...(hasIndexHtml ? {} : { '/': this.serverBehaviorOptions }), - - // known dynamic routes - 'api/*': this.serverBehaviorOptions, - '_next/data/*': this.serverBehaviorOptions, - - // dynamic images go to lambda - '_next/image*': this.imageBehaviorOptions, - }, }); - return distribution; } private addStaticBehaviorsToDistribution() { @@ -457,8 +477,19 @@ export class NextjsDistribution extends Construct { `Invalid CloudFront Distribution Cache Behavior Path Pattern: ${pathPattern}. Please see documentation here: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesPathPattern` ); } - this.distribution.addBehavior(pathPattern, this.s3Origin, this.staticBehaviorOptions); + const finalPathPattern = this.getPathPattern(pathPattern); + this.distribution.addBehavior(finalPathPattern, this.s3Origin, this.staticBehaviorOptions); + } + } + + private getPathPattern(pathPattern: string) { + if (this.props.basePath) { + // when basePath is set, we emulate the "default behavior" (*) for the / as `/base-path/*` + if (pathPattern === '/') pathPattern = '*'; + return `${this.props.basePath}/${pathPattern}`; } + + return pathPattern; } private buildDistributionDomainNames(): string[] { From 2cfac1f4170e8cf809d6ce41b5a1251e560f4997 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Tue, 17 Oct 2023 21:06:19 -0300 Subject: [PATCH 02/10] drop console.log --- src/NextjsDistribution.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index 360e3c1b..0b13097b 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -442,7 +442,6 @@ export class NextjsDistribution extends Construct { } private createDefaultCloudFrontDistribution(cfDistributionProps?: NextjsDistributionCdkOverrideProps) { - console.log('createDefaultCloudFrontDistribution', this.props.cdk?.distribution); // build domainNames const domainNames = this.buildDistributionDomainNames(); From 0f638648d509cb8c23b37d160756fe3aeb7311e1 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Tue, 17 Oct 2023 22:02:15 -0300 Subject: [PATCH 03/10] fixes on default behavior emulation and s3 assets basePath --- API.md | 255 ++++++++++++++++++++++++++++++++++---- src/Nextjs.ts | 1 + src/NextjsBase.ts | 6 + src/NextjsDistribution.ts | 40 +++--- src/NextjsStaticAssets.ts | 14 ++- 5 files changed, 273 insertions(+), 43 deletions(-) diff --git a/API.md b/API.md index 65760ff4..885fc26b 100644 --- a/API.md +++ b/API.md @@ -2588,6 +2588,7 @@ const nextjsBaseProps: NextjsBaseProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -2613,6 +2614,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -2876,6 +2889,7 @@ const nextjsBuildProps: NextjsBuildProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -2902,6 +2916,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -3196,6 +3222,7 @@ const nextjsDistributionProps: NextjsDistributionProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -3208,7 +3235,6 @@ const nextjsDistributionProps: NextjsDistributionProps = { ... } | nextBuild | NextjsBuild | Built NextJS app. | | serverFunction | aws-cdk-lib.aws_lambda.IFunction | Lambda function to route all non-static requests to. | | staticAssetsBucket | aws-cdk-lib.aws_s3.IBucket | Bucket containing static assets. | -| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | cachePolicies | NextjsCachePolicyProps | Override the default CloudFront cache policies created internally. | | cdk | NextjsDistributionCdkProps | Overrides for created CDK resources. | | customDomain | string \| NextjsDomainProps | The customDomain for this website. Supports domains that are hosted either on [Route 53](https://aws.amazon.com/route53/) or externally. | @@ -3233,6 +3259,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -3397,18 +3435,6 @@ Must be provided if you want to serve static files. --- -##### `basePath`Optional - -```typescript -public readonly basePath: string; -``` - -- *Type:* string - -Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. - ---- - ##### `cachePolicies`Optional ```typescript @@ -3639,6 +3665,7 @@ const nextjsImageProps: NextjsImageProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -3667,6 +3694,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -3912,6 +3951,7 @@ const nextjsProps: NextjsProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -3940,6 +3980,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -4102,6 +4154,7 @@ const nextjsRevalidationProps: NextjsRevalidationProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -4130,6 +4183,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -4291,6 +4356,7 @@ const nextjsServerProps: NextjsServerProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | | nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | | buildCommand | string | Optional value used to install NextJS node dependencies. | | buildPath | string | The directory to execute `npm run build` from. | | environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | @@ -4319,6 +4385,18 @@ Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- +##### `basePath`Optional + +```typescript +public readonly basePath: string; +``` + +- *Type:* string + +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + ##### `buildCommand`Optional ```typescript @@ -4481,33 +4559,73 @@ const nextjsStaticAssetsProps: NextjsStaticAssetsProps = { ... } | **Name** | **Type** | **Description** | | --- | --- | --- | +| nextjsPath | string | Relative path to the directory where the NextJS project is located. | +| basePath | string | Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. | +| buildCommand | string | Optional value used to install NextJS node dependencies. | +| buildPath | string | The directory to execute `npm run build` from. | +| environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | +| projectRoot | string | Root of your project, if different from `nextjsPath`. | +| quiet | boolean | Less build output. | +| sharpLayerArn | string | Optional arn for the sharp lambda layer. | +| skipFullInvalidation | boolean | By default all CloudFront cache will be invalidated on deployment. | +| tempBuildDir | string | Directory to store temporary build files in. | | nextBuild | NextjsBuild | The `NextjsBuild` instance representing the built Nextjs application. | | bucket | aws-cdk-lib.aws_s3.IBucket | Define your own bucket to store static assets. | -| environment | {[ key: string ]: string} | Custom environment variables to pass to the NextJS build and runtime. | --- -##### `nextBuild`Required +##### `nextjsPath`Required ```typescript -public readonly nextBuild: NextjsBuild; +public readonly nextjsPath: string; ``` -- *Type:* NextjsBuild +- *Type:* string -The `NextjsBuild` instance representing the built Nextjs application. +Relative path to the directory where the NextJS project is located. + +Can be the root of your project (`.`) or a subdirectory (`packages/web`). --- -##### `bucket`Optional +##### `basePath`Optional ```typescript -public readonly bucket: IBucket; +public readonly basePath: string; ``` -- *Type:* aws-cdk-lib.aws_s3.IBucket +- *Type:* string -Define your own bucket to store static assets. +Optional value to prefix the Next.js site under a /prefix path on CloudFront. Usually used when you deploy multiple Next.js sites on same domain using /sub-path. + +--- + +##### `buildCommand`Optional + +```typescript +public readonly buildCommand: string; +``` + +- *Type:* string +- *Default:* 'npx --yes open-next@2 build' + +Optional value used to install NextJS node dependencies. + +--- + +##### `buildPath`Optional + +```typescript +public readonly buildPath: string; +``` + +- *Type:* string + +The directory to execute `npm run build` from. + +By default, it is `nextjsPath`. +Can be overridden, particularly useful for monorepos where `build` is expected to run +at the root of the project. --- @@ -4523,5 +4641,98 @@ Custom environment variables to pass to the NextJS build and runtime. --- +##### `projectRoot`Optional + +```typescript +public readonly projectRoot: string; +``` + +- *Type:* string + +Root of your project, if different from `nextjsPath`. + +Defaults to current working directory. + +--- + +##### `quiet`Optional + +```typescript +public readonly quiet: boolean; +``` + +- *Type:* boolean + +Less build output. + +--- + +##### `sharpLayerArn`Optional + +```typescript +public readonly sharpLayerArn: string; +``` + +- *Type:* string + +Optional arn for the sharp lambda layer. + +If omitted, the layer will be created. + +--- + +##### `skipFullInvalidation`Optional + +```typescript +public readonly skipFullInvalidation: boolean; +``` + +- *Type:* boolean + +By default all CloudFront cache will be invalidated on deployment. + +This can be set to true to skip the full cache invalidation, which +could be important for some users. + +--- + +##### `tempBuildDir`Optional + +```typescript +public readonly tempBuildDir: string; +``` + +- *Type:* string + +Directory to store temporary build files in. + +Defaults to os.tmpdir(). + +--- + +##### `nextBuild`Required + +```typescript +public readonly nextBuild: NextjsBuild; +``` + +- *Type:* NextjsBuild + +The `NextjsBuild` instance representing the built Nextjs application. + +--- + +##### `bucket`Optional + +```typescript +public readonly bucket: IBucket; +``` + +- *Type:* aws-cdk-lib.aws_s3.IBucket + +Define your own bucket to store static assets. + +--- + diff --git a/src/Nextjs.ts b/src/Nextjs.ts index 498475de..9fc2f0d5 100644 --- a/src/Nextjs.ts +++ b/src/Nextjs.ts @@ -124,6 +124,7 @@ export class Nextjs extends Construct { // deploy nextjs static assets to s3 this.staticAssets = new NextjsStaticAssets(this, 'StaticAssets', { + ...props, bucket: props.defaults?.assetDeployment?.bucket, environment: props.environment, nextBuild: this.nextBuild, diff --git a/src/NextjsBase.ts b/src/NextjsBase.ts index d8bdaff9..7e579fcb 100644 --- a/src/NextjsBase.ts +++ b/src/NextjsBase.ts @@ -58,6 +58,12 @@ export interface NextjsBaseProps { * could be important for some users. */ readonly skipFullInvalidation?: boolean; + + /** + * Optional value to prefix the Next.js site under a /prefix path on CloudFront. + * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + */ + readonly basePath?: string; } ///// stuff below taken from https://github.com/serverless-stack/sst/blob/8d377e941467ced81d8cc31ee67d5a06550f04d4/packages/resources/src/BaseSite.ts diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index 0b13097b..9bae69f2 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -1,5 +1,3 @@ -import * as fs from 'node:fs'; -import * as path from 'path'; import { Duration, Fn, RemovalPolicy } from 'aws-cdk-lib'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; @@ -13,6 +11,8 @@ import * as route53Patterns from 'aws-cdk-lib/aws-route53-patterns'; import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; +import * as fs from 'node:fs'; +import * as path from 'path'; import { DEFAULT_STATIC_MAX_AGE, NEXTJS_BUILD_DIR, NEXTJS_STATIC_DIR } from './constants'; import { BaseSiteDomainProps, NextjsBaseProps } from './NextjsBase'; import { NextjsBuild } from './NextjsBuild'; @@ -107,12 +107,6 @@ export interface NextjsDistributionProps extends NextjsBaseProps { */ readonly customDomain?: string | NextjsDomainProps; - /** - * Optional value to prefix the Next.js site under a /prefix path on CloudFront. - * Usually used when you deploy multiple Next.js sites on same domain using /sub-path - */ - readonly basePath?: string; - /** * Include the name of your deployment stage if present. * Used to name the edge functions stack. @@ -228,6 +222,7 @@ export class NextjsDistribution extends Construct { // Create CloudFront Distribution this.distribution = this.createCloudFrontDistribution(); this.addStaticBehaviorsToDistribution(); + this.addRootPathBehavior(); // Connect Custom Domain to CloudFront Distribution this.createRoute53Records(); @@ -417,13 +412,8 @@ export class NextjsDistribution extends Construct { ? cfDistributionProps : this.createDefaultCloudFrontDistribution(cfDistributionProps); - // if we don't have a static file called index.html then we should - // redirect to the lambda handler - const hasIndexHtml = this.props.nextBuild.readPublicFileList().includes('index.html'); const additionalBehaviors = { - // is index.html static or dynamic? - ...(hasIndexHtml ? {} : { '/': this.serverBehaviorOptions }), // known dynamic routes 'api/*': this.serverBehaviorOptions, '_next/data/*': this.serverBehaviorOptions, @@ -460,6 +450,26 @@ export class NextjsDistribution extends Construct { }); } + /** + * this needs to be added last so that it doesn't override any other behaviors + * when basePath is set, we emulate the "default behavior" (*) and / as `/base-path/*` + * @private + */ + private addRootPathBehavior() { + // if we don't have a static file called index.html then we should + // redirect to the lambda handler + const hasIndexHtml = this.props.nextBuild.readPublicFileList().includes('index.html'); + if (hasIndexHtml) return // don't add root path behavior + + const { origin, ...options } = this.serverBehaviorOptions; + this.distribution.addBehavior(this.getPathPattern('/'), origin, options); + + // when basePath is set, we emulate the "default behavior" (*) for the site as `/base-path/*` + if (this.props.basePath) { + this.distribution.addBehavior(this.getPathPattern('/*'), origin, options); + } + } + private addStaticBehaviorsToDistribution() { const publicFiles = fs.readdirSync(path.join(this.props.nextjsPath, NEXTJS_BUILD_DIR, NEXTJS_STATIC_DIR), { withFileTypes: true, @@ -483,8 +493,8 @@ export class NextjsDistribution extends Construct { private getPathPattern(pathPattern: string) { if (this.props.basePath) { - // when basePath is set, we emulate the "default behavior" (*) for the / as `/base-path/*` - if (pathPattern === '/') pathPattern = '*'; + // remove leading slash to avoid double slash in pathPattern /base-path// + if (pathPattern.startsWith('/')) pathPattern = pathPattern.slice(1); return `${this.props.basePath}/${pathPattern}`; } diff --git a/src/NextjsStaticAssets.ts b/src/NextjsStaticAssets.ts index 0e83c86f..36378363 100644 --- a/src/NextjsStaticAssets.ts +++ b/src/NextjsStaticAssets.ts @@ -1,15 +1,17 @@ import * as fs from 'node:fs'; import { tmpdir } from 'node:os'; +import * as path from 'node:path'; import { resolve } from 'node:path'; import { RemovalPolicy } from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; import { Construct } from 'constructs'; import { NEXTJS_CACHE_DIR } from './constants'; +import { NextjsBaseProps } from './NextjsBase'; import { NextjsBucketDeployment } from './NextjsBucketDeployment'; import { NextjsBuild } from './NextjsBuild'; -export interface NextjsStaticAssetsProps { +export interface NextjsStaticAssetsProps extends NextjsBaseProps { /** * Define your own bucket to store static assets. */ @@ -18,10 +20,6 @@ export interface NextjsStaticAssetsProps { * The `NextjsBuild` instance representing the built Nextjs application. */ readonly nextBuild: NextjsBuild; - /** - * Custom environment variables to pass to the NextJS build and runtime. - */ - readonly environment?: Record; } /** @@ -72,7 +70,11 @@ export class NextjsStaticAssets extends Construct { private createAsset(): Asset { // create temporary directory to join open-next's static output with cache output - const tmpAssetsDir = fs.mkdtempSync(resolve(tmpdir(), 'cdk-nextjs-assets-')); + const tmpBaseDir = fs.mkdtempSync(resolve(tmpdir(), 'cdk-nextjs-assets-')); + // if basePath is defined, copy static assets to a subdirectory using the basePath + const tmpAssetsDir = this.props.basePath ? path.join(tmpBaseDir, this.props.basePath) : tmpBaseDir; + fs.mkdirSync(tmpAssetsDir, { recursive: true }) + fs.cpSync(this.props.nextBuild.nextStaticDir, tmpAssetsDir, { recursive: true }); fs.cpSync(this.props.nextBuild.nextCacheDir, resolve(tmpAssetsDir, NEXTJS_CACHE_DIR), { recursive: true }); const asset = new Asset(this, 'Asset', { From 0e0eb1306e197df82d5ba06fcf60ac13b89344f5 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Tue, 17 Oct 2023 22:58:33 -0300 Subject: [PATCH 04/10] use destinationKeyPrefix for basePath prefix on deployment bucket --- src/NextjsDistribution.ts | 17 +++++++++-------- src/NextjsStaticAssets.ts | 16 ++++++++-------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index 9bae69f2..63dcc63f 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -1,3 +1,5 @@ +import * as fs from 'node:fs'; +import * as path from 'path'; import { Duration, Fn, RemovalPolicy } from 'aws-cdk-lib'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; @@ -11,8 +13,6 @@ import * as route53Patterns from 'aws-cdk-lib/aws-route53-patterns'; import * as route53Targets from 'aws-cdk-lib/aws-route53-targets'; import * as s3 from 'aws-cdk-lib/aws-s3'; import { Construct } from 'constructs'; -import * as fs from 'node:fs'; -import * as path from 'path'; import { DEFAULT_STATIC_MAX_AGE, NEXTJS_BUILD_DIR, NEXTJS_STATIC_DIR } from './constants'; import { BaseSiteDomainProps, NextjsBaseProps } from './NextjsBase'; import { NextjsBuild } from './NextjsBuild'; @@ -412,7 +412,6 @@ export class NextjsDistribution extends Construct { ? cfDistributionProps : this.createDefaultCloudFrontDistribution(cfDistributionProps); - const additionalBehaviors = { // known dynamic routes 'api/*': this.serverBehaviorOptions, @@ -459,14 +458,16 @@ export class NextjsDistribution extends Construct { // if we don't have a static file called index.html then we should // redirect to the lambda handler const hasIndexHtml = this.props.nextBuild.readPublicFileList().includes('index.html'); - if (hasIndexHtml) return // don't add root path behavior + if (hasIndexHtml) return; // don't add root path behavior const { origin, ...options } = this.serverBehaviorOptions; - this.distribution.addBehavior(this.getPathPattern('/'), origin, options); // when basePath is set, we emulate the "default behavior" (*) for the site as `/base-path/*` if (this.props.basePath) { - this.distribution.addBehavior(this.getPathPattern('/*'), origin, options); + this.distribution.addBehavior(this.getPathPattern(''), origin, options); + this.distribution.addBehavior(this.getPathPattern('*'), origin, options); + } else { + this.distribution.addBehavior(this.getPathPattern('/'), origin, options); } } @@ -493,8 +494,8 @@ export class NextjsDistribution extends Construct { private getPathPattern(pathPattern: string) { if (this.props.basePath) { - // remove leading slash to avoid double slash in pathPattern /base-path// - if (pathPattern.startsWith('/')) pathPattern = pathPattern.slice(1); + // because we already have a basePath we don't use / instead we use /base-path + if (pathPattern === '') return this.props.basePath; return `${this.props.basePath}/${pathPattern}`; } diff --git a/src/NextjsStaticAssets.ts b/src/NextjsStaticAssets.ts index 36378363..3da1bbc4 100644 --- a/src/NextjsStaticAssets.ts +++ b/src/NextjsStaticAssets.ts @@ -1,6 +1,5 @@ import * as fs from 'node:fs'; import { tmpdir } from 'node:os'; -import * as path from 'node:path'; import { resolve } from 'node:path'; import { RemovalPolicy } from 'aws-cdk-lib'; import * as s3 from 'aws-cdk-lib/aws-s3'; @@ -70,11 +69,7 @@ export class NextjsStaticAssets extends Construct { private createAsset(): Asset { // create temporary directory to join open-next's static output with cache output - const tmpBaseDir = fs.mkdtempSync(resolve(tmpdir(), 'cdk-nextjs-assets-')); - // if basePath is defined, copy static assets to a subdirectory using the basePath - const tmpAssetsDir = this.props.basePath ? path.join(tmpBaseDir, this.props.basePath) : tmpBaseDir; - fs.mkdirSync(tmpAssetsDir, { recursive: true }) - + const tmpAssetsDir = fs.mkdtempSync(resolve(tmpdir(), 'cdk-nextjs-assets-')); fs.cpSync(this.props.nextBuild.nextStaticDir, tmpAssetsDir, { recursive: true }); fs.cpSync(this.props.nextBuild.nextCacheDir, resolve(tmpAssetsDir, NEXTJS_CACHE_DIR), { recursive: true }); const asset = new Asset(this, 'Asset', { @@ -85,19 +80,24 @@ export class NextjsStaticAssets extends Construct { } private createBucketDeployment(asset: Asset) { + const basePath = this.props.basePath?.replace(/^\//, ''); // remove leading slash (if present) + const allFiles = basePath ? `${basePath}/**/*` : '**/*'; + const staticFiles = basePath ? `${basePath}/_next/static/**/*'` : '_next/static/**/*'; + return new NextjsBucketDeployment(this, 'BucketDeployment', { asset, destinationBucket: this.bucket, + destinationKeyPrefix: basePath, debug: true, // only put env vars that are placeholders in custom resource properties // to be replaced. other env vars were injected at build time. substitutionConfig: NextjsBucketDeployment.getSubstitutionConfig(this.buildEnvVars), prune: true, putConfig: { - '**/*': { + [allFiles]: { CacheControl: 'public, max-age=0, must-revalidate', }, - '_next/static/**/*': { + [staticFiles]: { CacheControl: 'public, max-age=31536000, immutable', }, }, From b15618217cefac640ff9cbbd48502983ab9c3e79 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Tue, 17 Oct 2023 23:17:11 -0300 Subject: [PATCH 05/10] fix example code --- examples/multiple-sites/src/stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/multiple-sites/src/stack.ts b/examples/multiple-sites/src/stack.ts index aa6a336a..4068ac1f 100644 --- a/examples/multiple-sites/src/stack.ts +++ b/examples/multiple-sites/src/stack.ts @@ -18,9 +18,9 @@ export class PagesRouterStack extends Stack { const app1 = new Nextjs(this, 'app-router', { nextjsPath: '../../open-next/examples/app-router', + basePath: '/app-router', defaults: { distribution: { - basePath: 'app-router', cdk: { distribution } } } @@ -28,9 +28,9 @@ export class PagesRouterStack extends Stack { const app2 = new Nextjs(this, 'pages-router', { nextjsPath: '../../open-next/examples/pages-router', + basePath: '/pages-router', defaults: { distribution: { - basePath: 'pages-router', cdk: { distribution } } } From 951443eb9963e6eaec003f182d5b2c144e55c1e9 Mon Sep 17 00:00:00 2001 From: Ben Stickley Date: Fri, 20 Oct 2023 07:41:52 -0400 Subject: [PATCH 06/10] feat: simplify getCloudFrontDistribution; move props location; update READMEs; rename example stack --- README.md | 42 ++++----------- examples/README.md | 2 +- examples/multiple-sites/README.md | 5 ++ examples/multiple-sites/src/app.ts | 4 +- examples/multiple-sites/src/stack.ts | 10 +++- src/Nextjs.ts | 14 ++++- src/NextjsBase.ts | 6 --- src/NextjsDistribution.ts | 77 +++++++++++++++++----------- src/NextjsStaticAssets.ts | 12 ++++- 9 files changed, 97 insertions(+), 75 deletions(-) create mode 100644 examples/multiple-sites/README.md diff --git a/README.md b/README.md index f82846bc..c8e73b1e 100644 --- a/README.md +++ b/README.md @@ -38,39 +38,15 @@ new WebStack(app, 'web'); Available on [Construct Hub](https://constructs.dev/packages/cdk-nextjs-standalone/). -## Customization - -### Increased Security -```ts -import { RemovalPolicy, Stack } from "aws-cdk-lib"; -import { Construct } from "constructs"; -import { CfnWebAcl } from "aws-cdk-lib/aws-wafv2"; -import { SecurityPolicyProtocol, type DistributionProps } from "aws-cdk-lib/aws-cloudfront"; -import { Nextjs, type NextjsDistributionProps } from "cdk-nextjs-standalone"; -import { Bucket, BlockPublicAccess, BucketEncryption } from "aws-cdk-lib/aws-s3"; - -// Because of `WebAcl`, this stack must be deployed in us-east-1. If you want -// to deploy Nextjs in another region, add WAF in separate stack deployed in us-east-1 -export class UiStack { - constructor(scope: Construct, id: string) { - const webAcl = new CfnWebAcl(this, "WebAcl", { ... }); - new Nextjs(this, "NextSite", { - nextjsPath: "...", - defaults: { - distribution: { - functionUrlAuthType: FunctionUrlAuthType.AWS_IAM, - cdk: { - distribution: { - webAclId: webAcl.attrArn, - minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, - } as DistributionProps, - }, - } satisfies Partial, - }, - }); - } -} -``` +## Examples + +See example CDK apps [here](./examples) including: +- App Router +- Pages Router +- App/Pages Router +- High Security +- Multiple Sites +To deploy an example, make sure to read the [README.md](./examples/README.md) ### Discord Chat diff --git a/examples/README.md b/examples/README.md index 68dad067..6330b334 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,7 +4,7 @@ Each example app utilizes [open-next](https://github.com/sst/open-next)'s exampl ## Prerequisites 1. `git clone https://github.com/jetbridge/cdk-nextjs.git` 1. `yarn install` -1. `yarn build` (or faster option: `yarn compile`) +1. `yarn build` ## Setup Example Next.js Apps After cloning this repository in order to run the example apps or e2e tests, run: diff --git a/examples/multiple-sites/README.md b/examples/multiple-sites/README.md new file mode 100644 index 00000000..06aaea04 --- /dev/null +++ b/examples/multiple-sites/README.md @@ -0,0 +1,5 @@ +# Multiple Sites Example + +NOTE: in order for this example to work, you need to make the following manual updates: + - Update ../../open-next/examples/app-router/next.config.js to have a `basePath: "/app-router"` and `assetPrefix: "/app-router"` + - Update ../../open-next/examples/pages-router/next.config.js to have a `basePath: "/pages-router"` and `assetPrefix: "/app-router"` \ No newline at end of file diff --git a/examples/multiple-sites/src/app.ts b/examples/multiple-sites/src/app.ts index 4c4a4acd..daff799b 100644 --- a/examples/multiple-sites/src/app.ts +++ b/examples/multiple-sites/src/app.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node import * as cdk from 'aws-cdk-lib'; -import { PagesRouterStack } from './stack'; +import { MultipleSitesStack } from './stack'; const app = new cdk.App(); -new PagesRouterStack(app, 'pr'); // pr = pages router +new MultipleSitesStack(app, 'multi'); diff --git a/examples/multiple-sites/src/stack.ts b/examples/multiple-sites/src/stack.ts index 4068ac1f..d2d40185 100644 --- a/examples/multiple-sites/src/stack.ts +++ b/examples/multiple-sites/src/stack.ts @@ -4,7 +4,15 @@ import { HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; import { Construct } from 'constructs'; import { Nextjs } from 'cdk-nextjs-standalone'; -export class PagesRouterStack extends Stack { +/* + NOTE: in order for the below stack to work, you need to + - Update ../../open-next/examples/app-router/next.config.js to have a + `basePath: "/app-router"` and `assetPrefix: "/app-router"` + - Update ../../open-next/examples/pages-router/next.config.js to have a + `basePath: "/pages-router"` and `assetPrefix: "/app-router"` +*/ + +export class MultipleSitesStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); diff --git a/src/Nextjs.ts b/src/Nextjs.ts index 9fc2f0d5..bda58bed 100644 --- a/src/Nextjs.ts +++ b/src/Nextjs.ts @@ -1,6 +1,7 @@ import * as fs from 'node:fs'; import * as os from 'os'; import * as path from 'path'; +import { Distribution } from 'aws-cdk-lib/aws-cloudfront'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import { FunctionOptions } from 'aws-cdk-lib/aws-lambda'; import * as s3 from 'aws-cdk-lib/aws-s3'; @@ -55,6 +56,17 @@ export interface NextjsProps extends NextjsBaseProps { * @default false */ readonly skipBuild?: boolean; + /** + * Optional value to prefix the Next.js site under a /prefix path on CloudFront. + * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + * @example "/my-base-path" + */ + readonly basePath?: string; + /** + * Optional CloudFront Distribution created outside of this construct that will + * be used to add Next.js behaviors and origins onto. Useful with `basePath`. + */ + readonly distribution?: Distribution; } /** @@ -124,10 +136,10 @@ export class Nextjs extends Construct { // deploy nextjs static assets to s3 this.staticAssets = new NextjsStaticAssets(this, 'StaticAssets', { - ...props, bucket: props.defaults?.assetDeployment?.bucket, environment: props.environment, nextBuild: this.nextBuild, + basePath: props.basePath, }); this.serverFunction = new NextjsServer(this, 'Server', { diff --git a/src/NextjsBase.ts b/src/NextjsBase.ts index 7e579fcb..d8bdaff9 100644 --- a/src/NextjsBase.ts +++ b/src/NextjsBase.ts @@ -58,12 +58,6 @@ export interface NextjsBaseProps { * could be important for some users. */ readonly skipFullInvalidation?: boolean; - - /** - * Optional value to prefix the Next.js site under a /prefix path on CloudFront. - * Usually used when you deploy multiple Next.js sites on same domain using /sub-path - */ - readonly basePath?: string; } ///// stuff below taken from https://github.com/serverless-stack/sst/blob/8d377e941467ced81d8cc31ee67d5a06550f04d4/packages/resources/src/BaseSite.ts diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index 63dcc63f..b2fdb735 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -26,7 +26,7 @@ export interface NextjsDistributionCdkProps { * Pass in a value to override the default settings this construct uses to * create the CloudFront `Distribution` internally. */ - readonly distribution?: NextjsDistributionCdkOverrideProps | Distribution; + readonly distribution?: NextjsDistributionCdkOverrideProps; } export interface NextjsCachePolicyProps { @@ -125,6 +125,18 @@ export interface NextjsDistributionProps extends NextjsBaseProps { * @default "NONE" */ readonly functionUrlAuthType?: lambda.FunctionUrlAuthType; + + /** + * Optional value to prefix the Next.js site under a /prefix path on CloudFront. + * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + */ + readonly basePath?: string; + + /** + * Optional CloudFront Distribution created outside of this construct that will + * be used to add Next.js behaviors and origins onto. Useful with `basePath`. + */ + readonly distribution?: Distribution; } /** @@ -220,7 +232,7 @@ export class NextjsDistribution extends Construct { this.imageBehaviorOptions = this.createImageBehaviorOptions(); // Create CloudFront Distribution - this.distribution = this.createCloudFrontDistribution(); + this.distribution = this.getCloudFrontDistribution(); this.addStaticBehaviorsToDistribution(); this.addRootPathBehavior(); @@ -399,38 +411,42 @@ export class NextjsDistribution extends Construct { }; } - ///////////////////// - // CloudFront Distribution - ///////////////////// - - private createCloudFrontDistribution(): cloudfront.Distribution { - const { cdk: cdkProps } = this.props; - const cfDistributionProps = cdkProps?.distribution ?? ({} as NextjsDistributionCdkOverrideProps); - - const distribution = - 'node' in cfDistributionProps // if cdkProps.distribution is a cdk.cloudfront.Distribution - ? cfDistributionProps - : this.createDefaultCloudFrontDistribution(cfDistributionProps); - - const additionalBehaviors = { - // known dynamic routes - 'api/*': this.serverBehaviorOptions, - '_next/data/*': this.serverBehaviorOptions, - // dynamic images go to lambda - '_next/image*': this.imageBehaviorOptions, - }; - - // add additional behaviors - for (const [pathPattern, behaviorOptions] of Object.entries(additionalBehaviors)) { - const finalPathPattern = this.getPathPattern(pathPattern); - const { origin, ...options } = behaviorOptions; - distribution.addBehavior(finalPathPattern, origin, options); + /** + * Creates or uses user specified CloudFront Distribution adding behaviors + * needed for Next.js. + */ + private getCloudFrontDistribution(): cloudfront.Distribution { + let distribution: cloudfront.Distribution; + if (this.props.distribution) { + distribution = this.props.distribution; + } else { + distribution = this.createCloudFrontDistribution(); } + distribution.addBehavior( + this.getPathPattern('api/*'), + this.serverBehaviorOptions.origin, + this.serverBehaviorOptions + ); + distribution.addBehavior( + this.getPathPattern('_next/data/*'), + this.serverBehaviorOptions.origin, + this.serverBehaviorOptions + ); + distribution.addBehavior( + this.getPathPattern('_next/data/*'), + this.imageBehaviorOptions.origin, + this.imageBehaviorOptions + ); + return distribution; } - private createDefaultCloudFrontDistribution(cfDistributionProps?: NextjsDistributionCdkOverrideProps) { + /** + * Creates default CloudFront Distribution. Note, this construct will not + * create a CloudFront Distribution if one is passed in by user. + */ + private createCloudFrontDistribution(cfDistributionProps?: NextjsDistributionCdkOverrideProps) { // build domainNames const domainNames = this.buildDistributionDomainNames(); @@ -492,6 +508,9 @@ export class NextjsDistribution extends Construct { } } + /** + * Optionally prepends base path to given path pattern. + */ private getPathPattern(pathPattern: string) { if (this.props.basePath) { // because we already have a basePath we don't use / instead we use /base-path diff --git a/src/NextjsStaticAssets.ts b/src/NextjsStaticAssets.ts index 3da1bbc4..a5d0748e 100644 --- a/src/NextjsStaticAssets.ts +++ b/src/NextjsStaticAssets.ts @@ -6,11 +6,10 @@ import * as s3 from 'aws-cdk-lib/aws-s3'; import { Asset } from 'aws-cdk-lib/aws-s3-assets'; import { Construct } from 'constructs'; import { NEXTJS_CACHE_DIR } from './constants'; -import { NextjsBaseProps } from './NextjsBase'; import { NextjsBucketDeployment } from './NextjsBucketDeployment'; import { NextjsBuild } from './NextjsBuild'; -export interface NextjsStaticAssetsProps extends NextjsBaseProps { +export interface NextjsStaticAssetsProps { /** * Define your own bucket to store static assets. */ @@ -19,6 +18,15 @@ export interface NextjsStaticAssetsProps extends NextjsBaseProps { * The `NextjsBuild` instance representing the built Nextjs application. */ readonly nextBuild: NextjsBuild; + /** + * Custom environment variables to pass to the NextJS build and runtime. + */ + readonly environment?: Record; + /** + * Optional value to prefix the Next.js site under a /prefix path on CloudFront. + * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + */ + readonly basePath?: string; } /** From fc62cb6bc3e566a5986a2b3f2071410cd68fdfe9 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Fri, 20 Oct 2023 09:07:49 -0300 Subject: [PATCH 07/10] assert "distribution" and "cdk.distribution" --- src/NextjsDistribution.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index b2fdb735..c2fa6613 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -418,6 +418,12 @@ export class NextjsDistribution extends Construct { private getCloudFrontDistribution(): cloudfront.Distribution { let distribution: cloudfront.Distribution; if (this.props.distribution) { + if (this.props.cdk?.distribution) { + throw new Error( + 'You can either pass an existing "distribution" or pass configs to create one via "cdk.distribution".' + ); + } + distribution = this.props.distribution; } else { distribution = this.createCloudFrontDistribution(); From 6268fa9292e1b1e292ea69103d914b2dd450f354 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Fri, 20 Oct 2023 09:38:09 -0300 Subject: [PATCH 08/10] fix _next/image* distribution path --- src/NextjsDistribution.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index c2fa6613..e77b6f01 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -440,7 +440,7 @@ export class NextjsDistribution extends Construct { this.serverBehaviorOptions ); distribution.addBehavior( - this.getPathPattern('_next/data/*'), + this.getPathPattern('_next/image*'), this.imageBehaviorOptions.origin, this.imageBehaviorOptions ); From aae4c9d43b925923d029f48ffdb906ffade55ec1 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Fri, 20 Oct 2023 09:54:17 -0300 Subject: [PATCH 09/10] fix example multi --- examples/multiple-sites/src/stack.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/multiple-sites/src/stack.ts b/examples/multiple-sites/src/stack.ts index d2d40185..b513a4e6 100644 --- a/examples/multiple-sites/src/stack.ts +++ b/examples/multiple-sites/src/stack.ts @@ -24,24 +24,16 @@ export class MultipleSitesStack extends Stack { }, }) - const app1 = new Nextjs(this, 'app-router', { + new Nextjs(this, 'app-router', { nextjsPath: '../../open-next/examples/app-router', basePath: '/app-router', - defaults: { - distribution: { - cdk: { distribution } - } - } + distribution }); - const app2 = new Nextjs(this, 'pages-router', { + new Nextjs(this, 'pages-router', { nextjsPath: '../../open-next/examples/pages-router', basePath: '/pages-router', - defaults: { - distribution: { - cdk: { distribution } - } - } + distribution }); new CfnOutput(this, "CloudFrontDistributionDomain", { From 78883209a46ed022d1703f7af3921095d9eb6757 Mon Sep 17 00:00:00 2001 From: Marcelo Luiz Onhate Date: Mon, 23 Oct 2023 10:21:26 -0300 Subject: [PATCH 10/10] updated documentation --- src/Nextjs.ts | 4 ++++ src/NextjsDistribution.ts | 5 +++++ src/NextjsStaticAssets.ts | 5 +++++ 3 files changed, 14 insertions(+) diff --git a/src/Nextjs.ts b/src/Nextjs.ts index bda58bed..5cce9cb4 100644 --- a/src/Nextjs.ts +++ b/src/Nextjs.ts @@ -59,6 +59,10 @@ export interface NextjsProps extends NextjsBaseProps { /** * Optional value to prefix the Next.js site under a /prefix path on CloudFront. * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + * + * Note, you'll need to set [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) + * in your `next.config.ts` to this value and ensure any files in `public` + * folder have correct prefix. * @example "/my-base-path" */ readonly basePath?: string; diff --git a/src/NextjsDistribution.ts b/src/NextjsDistribution.ts index e77b6f01..cc7e1308 100644 --- a/src/NextjsDistribution.ts +++ b/src/NextjsDistribution.ts @@ -129,6 +129,11 @@ export interface NextjsDistributionProps extends NextjsBaseProps { /** * Optional value to prefix the Next.js site under a /prefix path on CloudFront. * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + * + * Note, you'll need to set [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) + * in your `next.config.ts` to this value and ensure any files in `public` + * folder have correct prefix. + * @example "/my-base-path" */ readonly basePath?: string; diff --git a/src/NextjsStaticAssets.ts b/src/NextjsStaticAssets.ts index a5d0748e..d8de86db 100644 --- a/src/NextjsStaticAssets.ts +++ b/src/NextjsStaticAssets.ts @@ -25,6 +25,11 @@ export interface NextjsStaticAssetsProps { /** * Optional value to prefix the Next.js site under a /prefix path on CloudFront. * Usually used when you deploy multiple Next.js sites on same domain using /sub-path + * + * Note, you'll need to set [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) + * in your `next.config.ts` to this value and ensure any files in `public` + * folder have correct prefix. + * @example "/my-base-path" */ readonly basePath?: string; }