├── examples ├── app-router │ ├── README.md │ ├── .gitignore │ ├── src │ │ ├── app.ts │ │ ├── app-router-stack.ts │ │ └── overrides-stack.ts │ ├── package.json │ ├── tsconfig.json │ └── cdk.json ├── high-security │ ├── .gitignore │ ├── README.md │ ├── src │ │ ├── app.ts │ │ └── stack.ts │ ├── package.json │ ├── tsconfig.json │ └── cdk.json ├── pages-router │ ├── .gitignore │ ├── src │ │ ├── app.ts │ │ └── stack.ts │ ├── package.json │ ├── tsconfig.json │ └── cdk.json ├── app-pages-router │ ├── .gitignore │ ├── src │ │ ├── app.ts │ │ └── stack.ts │ ├── package.json │ ├── tsconfig.json │ └── cdk.json ├── multiple-sites │ ├── .gitignore │ ├── src │ │ ├── app.ts │ │ └── stack.ts │ ├── README.md │ ├── package.json │ ├── tsconfig.json │ └── cdk.json ├── install.sh └── README.md ├── .github ├── pull_request_template.md └── workflows │ ├── pull-request-lint.yml │ ├── upgrade-main.yml │ ├── build.yml │ └── release.yml ├── .prettierrc.yaml ├── .vscode └── settings.json ├── .gitmodules ├── src ├── utils │ ├── convert-path.ts │ ├── list-directories.ts │ ├── common-lambda-props.ts │ └── create-archive.ts ├── constants.ts ├── generated-structs │ ├── OptionalNextjsBuildProps.ts │ ├── OptionalNextjsRevalidationProps.ts │ ├── OptionalNextjsImageProps.ts │ ├── OptionalHostedZoneProviderProps.ts │ ├── OptionalNextjsInvalidationProps.ts │ ├── OptionalNextjsServerProps.ts │ ├── OptionalCloudFrontFunctionProps.ts │ ├── OptionalNextjsStaticAssetsProps.ts │ ├── index.ts │ ├── OptionalNextjsDistributionProps.ts │ ├── OptionalCertificateProps.ts │ ├── OptionalNextjsBucketDeploymentProps.ts │ ├── OptionalS3OriginProps.ts │ ├── OptionalNextjsDomainProps.ts │ ├── OptionalCustomResourceProps.ts │ ├── OptionalARecordProps.ts │ ├── OptionalAaaaRecordProps.ts │ ├── OptionalAssetProps.ts │ ├── OptionalTablePropsV2.ts │ ├── OptionalProviderProps.ts │ └── OptionalDistributionProps.ts ├── index.ts ├── NextjsOverrides.ts ├── NextjsImage.ts ├── NextjsInvalidation.ts ├── NextjsStaticAssets.ts ├── lambdas │ └── sign-fn-url.ts ├── NextjsBucketDeployment.ts ├── NextjsServer.ts ├── NextjsRevalidation.ts ├── NextjsBuild.ts └── NextjsDomain.ts ├── .npmignore ├── .mergify.yml ├── tsconfig.dev.json ├── .projen ├── files.json └── deps.json ├── docs ├── contribute.md ├── major-changes.md └── code-deployment-flow.md ├── .gitignore ├── .gitattributes ├── .eslintrc.json ├── README.md └── package.json /examples/app-router/README.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: true 2 | printWidth: 120 3 | trailingComma: es5 4 | singleQuote: true 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": "explicit" 4 | }, 5 | } -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "open-next"] 2 | path = open-next 3 | url = https://github.com/opennextjs/opennextjs-aws.git 4 | -------------------------------------------------------------------------------- /examples/app-router/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | node_modules 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | !tsconfig.json -------------------------------------------------------------------------------- /examples/high-security/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | node_modules 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | !tsconfig.json -------------------------------------------------------------------------------- /examples/pages-router/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | node_modules 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | !tsconfig.json -------------------------------------------------------------------------------- /examples/app-pages-router/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | node_modules 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | !tsconfig.json -------------------------------------------------------------------------------- /examples/multiple-sites/.gitignore: -------------------------------------------------------------------------------- 1 | *.d.ts 2 | node_modules 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | 8 | !tsconfig.json -------------------------------------------------------------------------------- /src/utils/convert-path.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Fixes windows paths. Does not alter unix paths. 3 | */ 4 | export function fixPath(path: string) { 5 | return path.replace(/\/\//g, '/'); 6 | } 7 | -------------------------------------------------------------------------------- /examples/high-security/README.md: -------------------------------------------------------------------------------- 1 | # High Security Example 2 | 3 | - Must be deployed in us-east-1 because of WAF and CloudFront. It's possible to use other regions not without more complex configuration. -------------------------------------------------------------------------------- /examples/multiple-sites/src/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { MultipleSitesStack } from './stack'; 4 | 5 | const app = new cdk.App(); 6 | new MultipleSitesStack(app, 'multi'); 7 | -------------------------------------------------------------------------------- /examples/pages-router/src/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { PagesRouterStack } from './stack'; 4 | 5 | const app = new cdk.App(); 6 | new PagesRouterStack(app, 'pr'); // pr = pages router 7 | -------------------------------------------------------------------------------- /examples/app-router/src/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { AppRouterStack } from './app-router-stack'; 4 | 5 | const app = new cdk.App(); 6 | new AppRouterStack(app, 'ar'); // ar = app router 7 | -------------------------------------------------------------------------------- /examples/app-pages-router/src/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { AppPagesRouterStack } from './stack'; 4 | 5 | const app = new cdk.App(); 6 | new AppPagesRouterStack(app, 'apr'); // apr = app pages router 7 | -------------------------------------------------------------------------------- /examples/high-security/src/app.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { App, Aspects } from 'aws-cdk-lib'; 3 | import { HighSecurityStack } from './stack'; 4 | import { AwsSolutionsChecks } from "cdk-nag" 5 | 6 | const app = new App(); 7 | new HighSecurityStack(app, 'hs', { env: { region: "us-east-1" } }); 8 | Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); 9 | -------------------------------------------------------------------------------- /examples/multiple-sites/README.md: -------------------------------------------------------------------------------- 1 | # Multiple Sites Example 2 | 3 | NOTE: in order for this example to work, you need to make the following manual updates: 4 | - Update ../../open-next/examples/app-router/next.config.js to have a `basePath: "/app-router"` and `assetPrefix: "/app-router"` 5 | - Update ../../open-next/examples/pages-router/next.config.js to have a `basePath: "/pages-router"` and `assetPrefix: "/app-router"` -------------------------------------------------------------------------------- /examples/install.sh: -------------------------------------------------------------------------------- 1 | git submodule init 2 | git submodule update 3 | cd open-next 4 | pnpm i 5 | pnpm --filter open-next --filter @open-next/utils build 6 | # install twice b/c first time open-next bin fails b/c it hasn't been built yet 7 | # but we cannot build without installing. 2nd time installing creates successful bin 8 | cd ../../../examples/app-pages-router 9 | pnpm i 10 | cd ../app-router 11 | pnpm i 12 | cd ../pages-router 13 | pnpm i -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const CACHE_BUCKET_KEY_PREFIX = '_cache'; 2 | export const NEXTJS_STATIC_DIR = 'assets'; 3 | export const NEXTJS_BUILD_DIR = '.open-next'; 4 | export const NEXTJS_CACHE_DIR = 'cache'; 5 | export const NEXTJS_BUILD_REVALIDATE_FN_DIR = 'revalidation-function'; 6 | export const NEXTJS_BUILD_DYNAMODB_PROVIDER_FN_DIR = 'dynamodb-provider'; 7 | export const NEXTJS_BUILD_IMAGE_FN_DIR = 'image-optimization-function'; 8 | export const NEXTJS_BUILD_SERVER_FN_DIR = 'server-functions/default'; 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /dist/changelog.md 8 | /dist/version.txt 9 | /.mergify.yml 10 | /test/ 11 | /tsconfig.dev.json 12 | /src/ 13 | !/lib/ 14 | !/lib/**/*.js 15 | !/lib/**/*.d.ts 16 | dist 17 | /tsconfig.json 18 | /.github/ 19 | /.vscode/ 20 | /.idea/ 21 | /.projenrc.js 22 | tsconfig.tsbuildinfo 23 | /.eslintrc.json 24 | !.jsii 25 | !/assets/ 26 | /.gitattributes 27 | /.projenrc.ts 28 | /projenrc 29 | -------------------------------------------------------------------------------- /examples/app-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-nextjs-standalone-example-app-router", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "cdk": "cdk" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "20.10.4", 11 | "aws-cdk": "2.114.1", 12 | "esbuild": "^0.19.8", 13 | "tsx": "^4.6.2", 14 | "typescript": "~5.3.3" 15 | }, 16 | "dependencies": { 17 | "aws-cdk-lib": "2.114.1", 18 | "cdk-nextjs-standalone": "link:../..", 19 | "constructs": "^10.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/pages-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-nextjs-standalone-example-app-router", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "cdk": "cdk" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "20.10.4", 11 | "aws-cdk": "2.114.1", 12 | "esbuild": "^0.19.8", 13 | "tsx": "^4.6.2", 14 | "typescript": "~5.3.3" 15 | }, 16 | "dependencies": { 17 | "aws-cdk-lib": "2.114.1", 18 | "cdk-nextjs-standalone": "link:../..", 19 | "constructs": "^10.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/app-pages-router/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-nextjs-standalone-example-app-router", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "cdk": "cdk" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "20.10.4", 11 | "aws-cdk": "2.114.1", 12 | "esbuild": "^0.19.8", 13 | "tsx": "^4.6.2", 14 | "typescript": "~5.3.3" 15 | }, 16 | "dependencies": { 17 | "aws-cdk-lib": "2.114.1", 18 | "cdk-nextjs-standalone": "link:../..", 19 | "constructs": "^10.3.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/multiple-sites/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-nextjs-standalone-example-multiple-sites", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "cdk": "cdk" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "20.5.7", 11 | "aws-cdk": "2.94.0", 12 | "esbuild": "^0.19.3", 13 | "tsx": "^3.12.10", 14 | "typescript": "~5.2.2" 15 | }, 16 | "dependencies": { 17 | "aws-cdk-lib": "2.94.0", 18 | "cdk-nextjs-standalone": "link:../..", 19 | "constructs": "^10.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/high-security/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-nextjs-standalone-example-high-security", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "build": "tsc", 6 | "watch": "tsc -w", 7 | "cdk": "cdk" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "20.10.4", 11 | "aws-cdk": "2.114.1", 12 | "cdk-nag": "^2.27.211", 13 | "esbuild": "^0.19.8", 14 | "tsx": "^4.6.2", 15 | "typescript": "~5.3.3" 16 | }, 17 | "dependencies": { 18 | "aws-cdk-lib": "2.114.1", 19 | "cdk-nextjs-standalone": "link:../..", 20 | "constructs": "^10.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/utils/list-directories.ts: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | 4 | /** 5 | * List files in directory, recursively. 6 | */ 7 | export function listDirectory(dir: string) { 8 | const fileList: string[] = []; 9 | const publicFiles = readdirSync(dir); 10 | for (const filename of publicFiles) { 11 | const filepath = join(dir, filename); 12 | const stat = statSync(filepath); 13 | if (stat.isDirectory()) { 14 | fileList.push(...listDirectory(filepath)); 15 | } else { 16 | fileList.push(filepath); 17 | } 18 | } 19 | return fileList; 20 | } 21 | -------------------------------------------------------------------------------- /examples/pages-router/src/stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps, Token } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Nextjs } from 'cdk-nextjs-standalone'; 4 | 5 | export class PagesRouterStack extends Stack { 6 | constructor(scope: Construct, id: string, props?: StackProps) { 7 | super(scope, id, props); 8 | 9 | const nextjs = new Nextjs(this, 'nextjs', { 10 | nextjsPath: '../../open-next/examples/pages-router', 11 | // skipBuild: true, 12 | }); 13 | 14 | new CfnOutput(this, "CloudFrontDistributionDomain", { 15 | value: nextjs.distribution.distributionDomain, 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/app-pages-router/src/stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps, Token } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Nextjs } from 'cdk-nextjs-standalone'; 4 | 5 | export class AppPagesRouterStack extends Stack { 6 | constructor(scope: Construct, id: string, props?: StackProps) { 7 | super(scope, id, props); 8 | 9 | const nextjs = new Nextjs(this, 'nextjs', { 10 | nextjsPath: '../../open-next/examples/app-pages-router', 11 | // skipBuild: true, 12 | }); 13 | 14 | new CfnOutput(this, "CloudFrontDistributionDomain", { 15 | value: nextjs.distribution.distributionDomain, 16 | }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/app-router/src/app-router-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps, Token } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Nextjs } from 'cdk-nextjs-standalone'; 4 | 5 | /** 6 | * This stack showcases how to use cdk-nextjs with Next.js App Router 7 | */ 8 | export class AppRouterStack extends Stack { 9 | constructor(scope: Construct, id: string, props?: StackProps) { 10 | super(scope, id, props); 11 | 12 | const nextjs = new Nextjs(this, 'nextjs', { 13 | nextjsPath: '../../open-next/examples/app-router', 14 | // skipBuild: true, 15 | }); 16 | 17 | new CfnOutput(this, "CloudFrontDistributionDomain", { 18 | value: nextjs.distribution.distributionDomain, 19 | }); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | update_method: merge 6 | conditions: 7 | - "#approved-reviews-by>=1" 8 | - -label~=(do-not-merge) 9 | - status-success=build 10 | - status-success=package-js 11 | merge_method: squash 12 | commit_message_template: |- 13 | {{ title }} (#{{ number }}) 14 | 15 | {{ body }} 16 | pull_request_rules: 17 | - name: Automatic merge on approval and successful build 18 | actions: 19 | delete_head_branch: {} 20 | queue: 21 | name: default 22 | conditions: 23 | - "#approved-reviews-by>=1" 24 | - -label~=(do-not-merge) 25 | - status-success=build 26 | - status-success=package-js 27 | -------------------------------------------------------------------------------- /examples/app-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/app-pages-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/high-security/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/multiple-sites/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/pages-router/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2020", 7 | "dom" 8 | ], 9 | "declaration": true, 10 | "strict": true, 11 | "noImplicitAny": true, 12 | "strictNullChecks": true, 13 | "noImplicitThis": true, 14 | "alwaysStrict": true, 15 | "noUnusedLocals": false, 16 | "noUnusedParameters": false, 17 | "noImplicitReturns": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "inlineSourceMap": true, 20 | "inlineSources": true, 21 | "experimentalDecorators": true, 22 | "strictPropertyInitialization": false, 23 | "typeRoots": [ 24 | "./node_modules/@types" 25 | ] 26 | }, 27 | "exclude": [ 28 | "node_modules", 29 | "cdk.out" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsBuildProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | /** 4 | * OptionalNextjsBuildProps 5 | */ 6 | export interface OptionalNextjsBuildProps { 7 | /** 8 | * @stability stable 9 | */ 10 | readonly streaming?: boolean; 11 | /** 12 | * @stability stable 13 | */ 14 | readonly skipBuild?: boolean; 15 | /** 16 | * @stability stable 17 | */ 18 | readonly quiet?: boolean; 19 | /** 20 | * @stability stable 21 | */ 22 | readonly environment?: Record; 23 | /** 24 | * @stability stable 25 | */ 26 | readonly buildPath?: string; 27 | /** 28 | * @stability stable 29 | */ 30 | readonly buildCommand?: string; 31 | /** 32 | * @stability stable 33 | */ 34 | readonly nextjsPath?: string; 35 | } 36 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsRevalidationProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsBuild, NextjsRevalidationOverrides, NextjsServer } from '../'; 3 | import type { aws_lambda } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsRevalidationProps 7 | */ 8 | export interface OptionalNextjsRevalidationProps { 9 | /** 10 | * Override props for every construct. 11 | * @stability stable 12 | */ 13 | readonly overrides?: NextjsRevalidationOverrides; 14 | /** 15 | * Override function properties. 16 | * @stability stable 17 | */ 18 | readonly lambdaOptions?: aws_lambda.FunctionOptions; 19 | /** 20 | * @stability stable 21 | */ 22 | readonly serverFunction?: NextjsServer; 23 | /** 24 | * @stability stable 25 | */ 26 | readonly nextBuild?: NextjsBuild; 27 | } 28 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsImageProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsBuild, NextjsImageOverrides } from '../'; 3 | import type { aws_lambda, aws_s3 } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsImageProps 7 | */ 8 | export interface OptionalNextjsImageProps { 9 | /** 10 | * Override props for every construct. 11 | * @stability stable 12 | */ 13 | readonly overrides?: NextjsImageOverrides; 14 | /** 15 | * Override function properties. 16 | * @stability stable 17 | */ 18 | readonly lambdaOptions?: aws_lambda.FunctionOptions; 19 | /** 20 | * @stability stable 21 | */ 22 | readonly nextBuild?: NextjsBuild; 23 | /** 24 | * The S3 bucket holding application images. 25 | * @stability stable 26 | */ 27 | readonly bucket?: aws_s3.IBucket; 28 | } 29 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalHostedZoneProviderProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | /** 4 | * OptionalHostedZoneProviderProps 5 | */ 6 | export interface OptionalHostedZoneProviderProps { 7 | /** 8 | * Specifies the ID of the VPC associated with a private hosted zone. 9 | * If a VPC ID is provided and privateZone is false, no results will be returned 10 | * and an error will be raised 11 | * @default - No VPC ID 12 | * @stability stable 13 | */ 14 | readonly vpcId?: string; 15 | /** 16 | * Whether the zone that is being looked up is a private hosted zone. 17 | * @default false 18 | * @stability stable 19 | */ 20 | readonly privateZone?: boolean; 21 | /** 22 | * The zone domain e.g. example.com. 23 | * @stability stable 24 | */ 25 | readonly domainName?: string; 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | merge_group: {} 14 | jobs: 15 | validate: 16 | name: Validate PR title 17 | runs-on: ubuntu-latest 18 | permissions: 19 | pull-requests: write 20 | if: (github.event_name == 'pull_request' || github.event_name == 'pull_request_target') 21 | steps: 22 | - uses: amannn/action-semantic-pull-request@v5.4.0 23 | env: 24 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 25 | with: 26 | types: |- 27 | feat 28 | fix 29 | chore 30 | requireScope: false 31 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsInvalidationProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsInvalidationOverrides } from '../'; 3 | import type { aws_cloudfront } from 'aws-cdk-lib'; 4 | import type { Construct } from 'constructs'; 5 | 6 | /** 7 | * OptionalNextjsInvalidationProps 8 | */ 9 | export interface OptionalNextjsInvalidationProps { 10 | /** 11 | * Override props for every construct. 12 | * @stability stable 13 | */ 14 | readonly overrides?: NextjsInvalidationOverrides; 15 | /** 16 | * CloudFront Distribution to invalidate. 17 | * @stability stable 18 | */ 19 | readonly distribution?: aws_cloudfront.IDistribution; 20 | /** 21 | * Constructs that should complete before invalidating CloudFront Distribution. 22 | * Useful for assets that must be deployed/updated before invalidating. 23 | * @stability stable 24 | */ 25 | readonly dependencies?: Array; 26 | } 27 | -------------------------------------------------------------------------------- /src/utils/common-lambda-props.ts: -------------------------------------------------------------------------------- 1 | import { Duration, PhysicalName, Stack } from 'aws-cdk-lib'; 2 | import { Architecture, FunctionProps, Runtime } from 'aws-cdk-lib/aws-lambda'; 3 | import { Construct } from 'constructs'; 4 | 5 | export function getCommonFunctionProps(scope: Construct): Omit { 6 | return { 7 | architecture: Architecture.ARM_64, 8 | /** 9 | * 1536mb costs 1.5x but runs twice as fast for most scenarios. 10 | * @see {@link https://dev.to/dashbird/4-tips-for-aws-lambda-optimization-for-production-3if1} 11 | */ 12 | memorySize: 1536, 13 | runtime: Runtime.NODEJS_22_X, 14 | timeout: Duration.seconds(10), 15 | // prevents "Resolution error: Cannot use resource in a cross-environment 16 | // fashion, the resource's physical name must be explicit set or use 17 | // PhysicalName.GENERATE_IF_NEEDED." 18 | functionName: Stack.of(scope).region !== 'us-east-1' ? PhysicalName.GENERATE_IF_NEEDED : undefined, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "compilerOptions": { 4 | "alwaysStrict": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "inlineSourceMap": true, 9 | "inlineSources": true, 10 | "lib": [ 11 | "es2019" 12 | ], 13 | "module": "CommonJS", 14 | "noEmitOnError": false, 15 | "noFallthroughCasesInSwitch": true, 16 | "noImplicitAny": true, 17 | "noImplicitReturns": true, 18 | "noImplicitThis": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "resolveJsonModule": true, 22 | "strict": true, 23 | "strictNullChecks": true, 24 | "strictPropertyInitialization": true, 25 | "stripInternal": true, 26 | "target": "ES2019", 27 | "skipLibCheck": true 28 | }, 29 | "include": [ 30 | "src/**/*.ts", 31 | "test/**/*.ts", 32 | ".projenrc.ts", 33 | "projenrc/**/*.ts" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } 39 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsServerProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsBuild, NextjsServerOverrides } from '../'; 3 | import type { aws_lambda, aws_s3 } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsServerProps 7 | */ 8 | export interface OptionalNextjsServerProps { 9 | /** 10 | * @stability stable 11 | */ 12 | readonly quiet?: boolean; 13 | /** 14 | * Override props for every construct. 15 | * @stability stable 16 | */ 17 | readonly overrides?: NextjsServerOverrides; 18 | /** 19 | * Override function properties. 20 | * @stability stable 21 | */ 22 | readonly lambda?: aws_lambda.FunctionOptions; 23 | /** 24 | * @stability stable 25 | */ 26 | readonly environment?: Record; 27 | /** 28 | * Static asset bucket. 29 | * Function needs bucket to read from cache. 30 | * @stability stable 31 | */ 32 | readonly staticAssetBucket?: aws_s3.IBucket; 33 | /** 34 | * @stability stable 35 | */ 36 | readonly nextBuild?: NextjsBuild; 37 | } 38 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { NextjsStaticAssets, NextjsStaticAssetsProps, NextjsStaticAssetOverrides } from './NextjsStaticAssets'; 2 | export { NextjsRevalidation, NextjsRevalidationProps, NextjsRevalidationOverrides } from './NextjsRevalidation'; 3 | export { NextjsBuild, NextjsBuildProps } from './NextjsBuild'; 4 | export { EnvironmentVars, NextjsServer, NextjsServerProps, NextjsServerOverrides } from './NextjsServer'; 5 | export { NextjsImage, NextjsImageProps, NextjsImageOverrides } from './NextjsImage'; 6 | export { 7 | NextjsBucketDeployment, 8 | NextjsBucketDeploymentProps, 9 | NextjsBucketDeploymentOverrides, 10 | } from './NextjsBucketDeployment'; 11 | export { 12 | NextjsDistribution, 13 | NextjsDistributionProps, 14 | NextjsDistributionOverrides, 15 | ViewerRequestFunctionProps, 16 | NextjsDistributionDefaults, 17 | } from './NextjsDistribution'; 18 | export { NextjsInvalidation, NextjsInvalidationProps, NextjsInvalidationOverrides } from './NextjsInvalidation'; 19 | export { NextjsDomain, NextjsDomainProps, NextjsDomainOverrides } from './NextjsDomain'; 20 | export { Nextjs, NextjsProps, NextjsConstructOverrides } from './Nextjs'; 21 | export { NextjsOverrides } from './NextjsOverrides'; 22 | export * from './generated-structs'; 23 | -------------------------------------------------------------------------------- /src/NextjsOverrides.ts: -------------------------------------------------------------------------------- 1 | import { NextjsConstructOverrides } from './Nextjs'; 2 | import { NextjsBucketDeploymentOverrides } from './NextjsBucketDeployment'; 3 | import { NextjsDistributionOverrides } from './NextjsDistribution'; 4 | import { NextjsDomainOverrides } from './NextjsDomain'; 5 | import { NextjsImageOverrides } from './NextjsImage'; 6 | import { NextjsInvalidationOverrides } from './NextjsInvalidation'; 7 | import { NextjsRevalidationOverrides } from './NextjsRevalidation'; 8 | import { NextjsServerOverrides } from './NextjsServer'; 9 | import { NextjsStaticAssetOverrides } from './NextjsStaticAssets'; 10 | 11 | /** 12 | * Override props for every construct. 13 | */ 14 | export interface NextjsOverrides { 15 | readonly nextjs?: NextjsConstructOverrides; 16 | readonly nextjsBucketDeployment?: NextjsBucketDeploymentOverrides; 17 | readonly nextjsDistribution?: NextjsDistributionOverrides; 18 | readonly nextjsDomain?: NextjsDomainOverrides; 19 | readonly nextjsImage?: NextjsImageOverrides; 20 | readonly nextjsInvalidation?: NextjsInvalidationOverrides; 21 | readonly nextjsRevalidation?: NextjsRevalidationOverrides; 22 | readonly nextjsServer?: NextjsServerOverrides; 23 | readonly nextjsStaticAssets?: NextjsStaticAssetOverrides; 24 | } 25 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalCloudFrontFunctionProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_cloudfront, interfaces } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalCloudFrontFunctionProps 6 | */ 7 | export interface OptionalCloudFrontFunctionProps { 8 | /** 9 | * The runtime environment for the function. 10 | * @default FunctionRuntime.JS_1_0 (unless `keyValueStore` is specified, then `FunctionRuntime.JS_2_0`) 11 | * @stability stable 12 | */ 13 | readonly runtime?: aws_cloudfront.FunctionRuntime; 14 | /** 15 | * The Key Value Store to associate with this function. 16 | * In order to associate a Key Value Store, the `runtime` must be 17 | * `cloudfront-js-2.0` or newer. 18 | * @default - no key value store is associated 19 | * @stability stable 20 | */ 21 | readonly keyValueStore?: interfaces.aws_cloudfront.IKeyValueStoreRef; 22 | /** 23 | * A name to identify the function. 24 | * @default - generated from the `id` 25 | * @stability stable 26 | */ 27 | readonly functionName?: string; 28 | /** 29 | * A comment to describe the function. 30 | * @default - same as `functionName` 31 | * @stability stable 32 | */ 33 | readonly comment?: string; 34 | /** 35 | * A flag that determines whether to automatically publish the function to the LIVE stage when it’s created. 36 | * @default - true 37 | * @stability stable 38 | */ 39 | readonly autoPublish?: boolean; 40 | } 41 | -------------------------------------------------------------------------------- /examples/multiple-sites/src/stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps, Token } from 'aws-cdk-lib'; 2 | import { Distribution, OriginProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; 3 | import { HttpOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; 4 | import { Construct } from 'constructs'; 5 | import { Nextjs } from 'cdk-nextjs-standalone'; 6 | 7 | /* 8 | NOTE: in order for the below stack to work, you need to 9 | - Update ../../open-next/examples/app-router/next.config.js to have a 10 | `basePath: "/app-router"` and `assetPrefix: "/app-router"` 11 | - Update ../../open-next/examples/pages-router/next.config.js to have a 12 | `basePath: "/pages-router"` and `assetPrefix: "/app-router"` 13 | */ 14 | 15 | export class MultipleSitesStack extends Stack { 16 | constructor(scope: Construct, id: string, props?: StackProps) { 17 | super(scope, id, props); 18 | 19 | const distribution = new Distribution(this, 'distribution', { 20 | defaultBehavior: { 21 | origin: new HttpOrigin('constructs.dev', { 22 | protocolPolicy: OriginProtocolPolicy.MATCH_VIEWER, 23 | }), 24 | }, 25 | }) 26 | 27 | new Nextjs(this, 'app-router', { 28 | nextjsPath: '../../open-next/examples/app-router', 29 | basePath: '/app-router', 30 | distribution 31 | }); 32 | 33 | new Nextjs(this, 'pages-router', { 34 | nextjsPath: '../../open-next/examples/pages-router', 35 | basePath: '/pages-router', 36 | distribution 37 | }); 38 | 39 | new CfnOutput(this, "CloudFrontDistributionDomain", { 40 | value: distribution.distributionDomainName, 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsStaticAssetsProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsBuild, NextjsStaticAssetOverrides } from '../'; 3 | import type { aws_s3 } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsStaticAssetsProps 7 | */ 8 | export interface OptionalNextjsStaticAssetsProps { 9 | /** 10 | * If `true` (default), then removes old static assets after upload new static assets. 11 | * @default true 12 | * @stability stable 13 | */ 14 | readonly prune?: boolean; 15 | /** 16 | * Override props for every construct. 17 | * @stability stable 18 | */ 19 | readonly overrides?: NextjsStaticAssetOverrides; 20 | /** 21 | * Custom environment variables to pass to the NextJS build and runtime. 22 | * @stability stable 23 | */ 24 | readonly environment?: Record; 25 | /** 26 | * Define your own bucket to store static assets. 27 | * @stability stable 28 | */ 29 | readonly bucket?: aws_s3.IBucket; 30 | /** 31 | * 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. 32 | * Note, you'll need to set [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) 33 | * in your `next.config.ts` to this value and ensure any files in `public` 34 | * folder have correct prefix. 35 | * @stability stable 36 | */ 37 | readonly basePath?: string; 38 | /** 39 | * The `NextjsBuild` instance representing the built Nextjs application. 40 | * @stability stable 41 | */ 42 | readonly nextBuild?: NextjsBuild; 43 | } 44 | -------------------------------------------------------------------------------- /src/generated-structs/index.ts: -------------------------------------------------------------------------------- 1 | export { OptionalARecordProps } from './OptionalARecordProps'; 2 | export { OptionalAaaaRecordProps } from './OptionalAaaaRecordProps'; 3 | export { OptionalAssetProps } from './OptionalAssetProps'; 4 | export { OptionalCertificateProps } from './OptionalCertificateProps'; 5 | export { OptionalCloudFrontFunctionProps } from './OptionalCloudFrontFunctionProps'; 6 | export { OptionalCustomResourceProps } from './OptionalCustomResourceProps'; 7 | export { OptionalDistributionProps } from './OptionalDistributionProps'; 8 | export { OptionalEdgeFunctionProps } from './OptionalEdgeFunctionProps'; 9 | export { OptionalFunctionProps } from './OptionalFunctionProps'; 10 | export { OptionalHostedZoneProviderProps } from './OptionalHostedZoneProviderProps'; 11 | export { OptionalNextjsBucketDeploymentProps } from './OptionalNextjsBucketDeploymentProps'; 12 | export { OptionalNextjsBuildProps } from './OptionalNextjsBuildProps'; 13 | export { OptionalNextjsDistributionProps } from './OptionalNextjsDistributionProps'; 14 | export { OptionalNextjsDomainProps } from './OptionalNextjsDomainProps'; 15 | export { OptionalNextjsImageProps } from './OptionalNextjsImageProps'; 16 | export { OptionalNextjsInvalidationProps } from './OptionalNextjsInvalidationProps'; 17 | export { OptionalNextjsRevalidationProps } from './OptionalNextjsRevalidationProps'; 18 | export { OptionalNextjsServerProps } from './OptionalNextjsServerProps'; 19 | export { OptionalNextjsStaticAssetsProps } from './OptionalNextjsStaticAssetsProps'; 20 | export { OptionalProviderProps } from './OptionalProviderProps'; 21 | export { OptionalS3OriginProps } from './OptionalS3OriginProps'; 22 | export { OptionalTablePropsV2 } from './OptionalTablePropsV2'; 23 | -------------------------------------------------------------------------------- /src/NextjsImage.ts: -------------------------------------------------------------------------------- 1 | import { Code, Function as LambdaFunction, FunctionOptions } from 'aws-cdk-lib/aws-lambda'; 2 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 3 | import { Construct } from 'constructs'; 4 | import { OptionalFunctionProps } from './generated-structs'; 5 | import type { NextjsBuild } from './NextjsBuild'; 6 | import { getCommonFunctionProps } from './utils/common-lambda-props'; 7 | 8 | export interface NextjsImageOverrides { 9 | readonly functionProps?: OptionalFunctionProps; 10 | } 11 | 12 | export interface NextjsImageProps { 13 | /** 14 | * The S3 bucket holding application images. 15 | */ 16 | readonly bucket: IBucket; 17 | /** 18 | * Override function properties. 19 | */ 20 | readonly lambdaOptions?: FunctionOptions; 21 | /** 22 | * @see {@link NextjsBuild} 23 | */ 24 | readonly nextBuild: NextjsBuild; 25 | /** 26 | * Override props for every construct. 27 | */ 28 | readonly overrides?: NextjsImageOverrides; 29 | } 30 | 31 | /** 32 | * This lambda handles image optimization. 33 | */ 34 | export class NextjsImage extends LambdaFunction { 35 | constructor(scope: Construct, id: string, props: NextjsImageProps) { 36 | const { lambdaOptions, bucket } = props; 37 | 38 | const commonFnProps = getCommonFunctionProps(scope); 39 | super(scope, id, { 40 | ...commonFnProps, 41 | code: Code.fromAsset(props.nextBuild.nextImageFnDir), 42 | handler: 'index.handler', 43 | description: 'Next.js Image Optimization Function', 44 | ...lambdaOptions, 45 | environment: { 46 | BUCKET_NAME: bucket.bucketName, 47 | ...lambdaOptions?.environment, 48 | }, 49 | ...props.overrides?.functionProps, 50 | }); 51 | 52 | bucket.grantRead(this); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/utils/create-archive.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import * as fs from 'node:fs'; 3 | import * as os from 'node:os'; 4 | import * as path from 'node:path'; 5 | 6 | export interface CreateArchiveArgs { 7 | readonly directory: string; 8 | readonly zipFileName: string; 9 | readonly fileGlob?: string; 10 | readonly quiet?: boolean; 11 | } 12 | 13 | /** 14 | * Zip up a directory and return path to zip file 15 | * 16 | * Cannot rely on native CDK zipping b/c it disregards symlinks which is necessary 17 | * for PNPM monorepos. See more here: https://github.com/aws/aws-cdk/issues/9251 18 | */ 19 | export function createArchive({ directory, zipFileName, fileGlob = '.', quiet }: CreateArchiveArgs): string { 20 | const zipOutDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cdk-nextjs-archive-')); 21 | const zipFilePath = path.join(zipOutDir, zipFileName); 22 | 23 | // delete existing zip file 24 | if (fs.existsSync(zipFilePath)) { 25 | fs.unlinkSync(zipFilePath); 26 | } 27 | 28 | // run script to create zipfile, preserving symlinks for node_modules (e.g. pnpm structure) 29 | const isWindows = process.platform === 'win32'; 30 | if (isWindows) { 31 | // TODO: test on windows 32 | execSync(`Compress-Archive -Path '${directory}\\*' -DestinationPath '${zipFilePath}' -CompressionLevel Optimal`, { 33 | stdio: 'inherit', 34 | shell: 'powershell.exe', 35 | }); 36 | } else { 37 | execSync(`zip -ryq9 '${zipFilePath}' ${fileGlob}`, { 38 | stdio: quiet ? 'ignore' : 'inherit', 39 | cwd: directory, 40 | }); 41 | } 42 | // check output 43 | if (!fs.existsSync(zipFilePath)) { 44 | throw new Error( 45 | `There was a problem generating the archive for ${directory}; the archive is missing in ${zipFilePath}.` 46 | ); 47 | } 48 | 49 | return zipFilePath; 50 | } 51 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/release.yml", 9 | ".github/workflows/upgrade-main.yml", 10 | ".gitignore", 11 | ".mergify.yml", 12 | ".projen/deps.json", 13 | ".projen/files.json", 14 | ".projen/tasks.json", 15 | "LICENSE", 16 | "src/generated-structs/OptionalAaaaRecordProps.ts", 17 | "src/generated-structs/OptionalARecordProps.ts", 18 | "src/generated-structs/OptionalAssetProps.ts", 19 | "src/generated-structs/OptionalCertificateProps.ts", 20 | "src/generated-structs/OptionalCloudFrontFunctionProps.ts", 21 | "src/generated-structs/OptionalCustomResourceProps.ts", 22 | "src/generated-structs/OptionalDistributionProps.ts", 23 | "src/generated-structs/OptionalEdgeFunctionProps.ts", 24 | "src/generated-structs/OptionalFunctionProps.ts", 25 | "src/generated-structs/OptionalHostedZoneProviderProps.ts", 26 | "src/generated-structs/OptionalNextjsBucketDeploymentProps.ts", 27 | "src/generated-structs/OptionalNextjsBuildProps.ts", 28 | "src/generated-structs/OptionalNextjsDistributionProps.ts", 29 | "src/generated-structs/OptionalNextjsDomainProps.ts", 30 | "src/generated-structs/OptionalNextjsImageProps.ts", 31 | "src/generated-structs/OptionalNextjsInvalidationProps.ts", 32 | "src/generated-structs/OptionalNextjsRevalidationProps.ts", 33 | "src/generated-structs/OptionalNextjsServerProps.ts", 34 | "src/generated-structs/OptionalNextjsStaticAssetsProps.ts", 35 | "src/generated-structs/OptionalProviderProps.ts", 36 | "src/generated-structs/OptionalS3OriginProps.ts", 37 | "src/generated-structs/OptionalTablePropsV2.ts", 38 | "tsconfig.dev.json" 39 | ], 40 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 41 | } 42 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # cdk-nextjs-standalone Examples 2 | Each example app utilizes [open-next](https://github.com/sst/open-next)'s example Next.js apps. open-next's example apps are built to test core Next.js functionality so they're helpful for testing `cdk-nextjs-standalone`. We also don't want to reinvent the wheel. In order to use open-next's code within this repository we use git submodules. Read [this guide](https://www.atlassian.com/git/tutorials/git-submodule) for more info. 3 | 4 | ## Prerequisites 5 | 1. `git clone https://github.com/jetbridge/cdk-nextjs.git` 6 | 1. `yarn install` 7 | 1. `yarn compile` 8 | 1. `yarn build` 9 | 10 | ## Setup Example Next.js Apps 11 | After cloning this repository in order to run the example apps or e2e tests, run: 12 | 1. Initialize git submodule: `git submodule init && git submodule update` 13 | 1. Install dependencies: `cd open-next && pnpm i` 14 | 1. Build packages `pnpm build` 15 | 16 | ## Deploy Manually 17 | To deploy an app manually to test them out for yourself, run: 18 | 1. `cd app-router # or any other example` 19 | 1. `pnpm install` 20 | 1. Inject AWS Credentials into your terminal 21 | 1. `cdk deploy` 22 | 23 | ## Locally Run E2E Tests with Playwright 24 | 1. Change directory into package with tests: `cd open-next/packages/tests-e2e`. 25 | 1. Set URL environment variable for the [project](https://playwright.dev/docs/test-projects) you want to test: `APP_ROUTER_URL` for `appRouter` project, `PAGES_ROUTER_URL` for `pagesRouter` project, and/or `APP_PAGE_ROUTER_URL` for `appPagesRouter` project. These urls will be the CloudFront domains from deployed `examples/` CDK apps. You can find these in AWS Console or they'll be printed in your terminal after running `cdk deploy`. 26 | 1. Run e2e tests with ui: `pnpm playwright test --ui`. 27 | 1. Hit play (green play button) and watch tests run! 28 | 29 | ## E2E Testing in CI 30 | See .projenrc.ts `run-e2e-tests` workflow towards bottom. This functionality is commented out until an AWS account can be used to deploy the example apps and run the tests. 31 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsDistributionProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsBuild, NextjsDistributionDefaults, NextjsDistributionOverrides, NextjsDomain } from '../'; 3 | import type { aws_cloudfront, aws_lambda, aws_s3 } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsDistributionProps 7 | */ 8 | export interface OptionalNextjsDistributionProps { 9 | /** 10 | * Supress the creation of default policies if none are provided by you. 11 | * @stability stable 12 | */ 13 | readonly supressDefaults?: NextjsDistributionDefaults; 14 | /** 15 | * @stability stable 16 | */ 17 | readonly streaming?: boolean; 18 | /** 19 | * Override props for every construct. 20 | * @stability stable 21 | */ 22 | readonly overrides?: NextjsDistributionOverrides; 23 | /** 24 | * @stability stable 25 | */ 26 | readonly nextDomain?: NextjsDomain; 27 | /** 28 | * Override lambda function url auth type. 29 | * @default "NONE" 30 | * @stability stable 31 | */ 32 | readonly functionUrlAuthType?: aws_lambda.FunctionUrlAuthType; 33 | /** 34 | * @stability stable 35 | */ 36 | readonly distribution?: aws_cloudfront.Distribution; 37 | /** 38 | * @stability stable 39 | */ 40 | readonly basePath?: string; 41 | /** 42 | * Bucket containing static assets. 43 | * Must be provided if you want to serve static files. 44 | * @stability stable 45 | */ 46 | readonly staticAssetsBucket?: aws_s3.IBucket; 47 | /** 48 | * Lambda function to route all non-static requests to. 49 | * Must be provided if you want to serve dynamic requests. 50 | * @stability stable 51 | */ 52 | readonly serverFunction?: aws_lambda.IFunction; 53 | /** 54 | * @stability stable 55 | */ 56 | readonly nextjsPath?: string; 57 | /** 58 | * @stability stable 59 | */ 60 | readonly nextBuild?: NextjsBuild; 61 | /** 62 | * Lambda function to optimize images. 63 | * Must be provided if you want to serve dynamic requests. 64 | * @stability stable 65 | */ 66 | readonly imageOptFunction?: aws_lambda.IFunction; 67 | } 68 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute 2 | 3 | Hey there, we value every new contribution. Thank you! 🙏🏼 4 | 5 | Here is a short before you get started: 6 | 7 | 1. Please make sure to create an issue first. 8 | 2. Link the bug in your pull request. 9 | 3. If first time building you must run `yarn install` and then `yarn compile`. 10 | 4. Run `yarn build` after you made your changes and before you open a pull request. 11 | 12 | ## Projen 13 | This project uses [Projen](https://projen.io/). Don't manually update package.json or use `yarn add`. Update dependencies in .projenrc.ts then run `yarn projen`. 14 | 15 | ## JSII Struct Builder 16 | When you want to reuse interfaces/structs from the AWS CDK library and customize them so all of their properties are optional, you cannot simply use the TypeScript utility type, [Partial](https://www.typescriptlang.org/docs/handbook/utility-types.html#partialtype), because of the TypeScript [limitations](https://aws.github.io/jsii/user-guides/lib-author/typescript-restrictions/#typescript-mapped-types) of JSII. To solve this problem, this construct library uses [@mrgrain/jsii-struct-builder](https://github.com/mrgrain/jsii-struct-builder) to generate partial types. These types are defined in the .projenrc.ts files (you'll need to scroll down to see them) and are primarily used in `NextjsOverrides`. They files are in the src/generated-structs folder. 17 | 18 | ## Bootstrap Issue 19 | `@mrgrain/jsii-struct-builder` is also used to generate optional structs of code within this repository (`OptionalNextjsBucketDeploymentProps`, etc.). In order for `@mrgrain/jsii-struct-builder` to read the source code struct to create a generate struct with optional properties, the JSII assembly must exist. If you simply run `projen build` this would fail because the JSII assembly of the source code hasn't been created yet. We can get around this issue by running `projen compile` first to create the JSII assembly, then `projen build` to use `@mrgrain/jsii-struct-builder` to create the optional version of the struct. The `.projenrc.ts` patches the build GitHub Workflow and Job to compile then build. See more [here](https://github.com/mrgrain/jsii-struct-builder/issues/174#issuecomment-1850496788). -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/LICENSE 9 | !/.npmignore 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | lerna-debug.log* 16 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | lib-cov 22 | coverage 23 | *.lcov 24 | .nyc_output 25 | build/Release 26 | node_modules/ 27 | jspm_packages/ 28 | *.tsbuildinfo 29 | .eslintcache 30 | *.tgz 31 | .yarn-integrity 32 | .cache 33 | .idea 34 | .DS_Store 35 | /test-reports/ 36 | junit.xml 37 | /coverage/ 38 | !/.github/workflows/build.yml 39 | /dist/changelog.md 40 | /dist/version.txt 41 | !/.github/workflows/release.yml 42 | !/.mergify.yml 43 | !/.github/workflows/upgrade-main.yml 44 | !/.github/pull_request_template.md 45 | !/test/ 46 | !/tsconfig.dev.json 47 | !/src/ 48 | /lib 49 | /dist/ 50 | !/.eslintrc.json 51 | .jsii 52 | tsconfig.json 53 | !/API.md 54 | /assets/ 55 | !/.projenrc.ts 56 | !/src/generated-structs/OptionalFunctionProps.ts 57 | !/src/generated-structs/OptionalCustomResourceProps.ts 58 | !/src/generated-structs/OptionalS3OriginProps.ts 59 | !/src/generated-structs/OptionalEdgeFunctionProps.ts 60 | !/src/generated-structs/OptionalCloudFrontFunctionProps.ts 61 | !/src/generated-structs/OptionalDistributionProps.ts 62 | !/src/generated-structs/OptionalHostedZoneProviderProps.ts 63 | !/src/generated-structs/OptionalCertificateProps.ts 64 | !/src/generated-structs/OptionalARecordProps.ts 65 | !/src/generated-structs/OptionalAaaaRecordProps.ts 66 | !/src/generated-structs/OptionalTablePropsV2.ts 67 | !/src/generated-structs/OptionalProviderProps.ts 68 | !/src/generated-structs/OptionalAssetProps.ts 69 | !/src/generated-structs/OptionalNextjsBucketDeploymentProps.ts 70 | !/src/generated-structs/OptionalNextjsBuildProps.ts 71 | !/src/generated-structs/OptionalNextjsStaticAssetsProps.ts 72 | !/src/generated-structs/OptionalNextjsServerProps.ts 73 | !/src/generated-structs/OptionalNextjsImageProps.ts 74 | !/src/generated-structs/OptionalNextjsRevalidationProps.ts 75 | !/src/generated-structs/OptionalNextjsDomainProps.ts 76 | !/src/generated-structs/OptionalNextjsDistributionProps.ts 77 | !/src/generated-structs/OptionalNextjsInvalidationProps.ts 78 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | * text=auto eol=lf 4 | *.snap linguist-generated 5 | /.eslintrc.json linguist-generated 6 | /.gitattributes linguist-generated 7 | /.github/pull_request_template.md linguist-generated 8 | /.github/workflows/build.yml linguist-generated 9 | /.github/workflows/pull-request-lint.yml linguist-generated 10 | /.github/workflows/release.yml linguist-generated 11 | /.github/workflows/upgrade-main.yml linguist-generated 12 | /.gitignore linguist-generated 13 | /.mergify.yml linguist-generated 14 | /.npmignore linguist-generated 15 | /.projen/** linguist-generated 16 | /.projen/deps.json linguist-generated 17 | /.projen/files.json linguist-generated 18 | /.projen/tasks.json linguist-generated 19 | /API.md linguist-generated 20 | /LICENSE linguist-generated 21 | /package.json linguist-generated 22 | /src/generated-structs/OptionalAaaaRecordProps.ts linguist-generated 23 | /src/generated-structs/OptionalARecordProps.ts linguist-generated 24 | /src/generated-structs/OptionalAssetProps.ts linguist-generated 25 | /src/generated-structs/OptionalCertificateProps.ts linguist-generated 26 | /src/generated-structs/OptionalCloudFrontFunctionProps.ts linguist-generated 27 | /src/generated-structs/OptionalCustomResourceProps.ts linguist-generated 28 | /src/generated-structs/OptionalDistributionProps.ts linguist-generated 29 | /src/generated-structs/OptionalEdgeFunctionProps.ts linguist-generated 30 | /src/generated-structs/OptionalFunctionProps.ts linguist-generated 31 | /src/generated-structs/OptionalHostedZoneProviderProps.ts linguist-generated 32 | /src/generated-structs/OptionalNextjsBucketDeploymentProps.ts linguist-generated 33 | /src/generated-structs/OptionalNextjsBuildProps.ts linguist-generated 34 | /src/generated-structs/OptionalNextjsDistributionProps.ts linguist-generated 35 | /src/generated-structs/OptionalNextjsDomainProps.ts linguist-generated 36 | /src/generated-structs/OptionalNextjsImageProps.ts linguist-generated 37 | /src/generated-structs/OptionalNextjsInvalidationProps.ts linguist-generated 38 | /src/generated-structs/OptionalNextjsRevalidationProps.ts linguist-generated 39 | /src/generated-structs/OptionalNextjsServerProps.ts linguist-generated 40 | /src/generated-structs/OptionalNextjsStaticAssetsProps.ts linguist-generated 41 | /src/generated-structs/OptionalProviderProps.ts linguist-generated 42 | /src/generated-structs/OptionalS3OriginProps.ts linguist-generated 43 | /src/generated-structs/OptionalTablePropsV2.ts linguist-generated 44 | /tsconfig.dev.json linguist-generated 45 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /src/generated-structs/OptionalCertificateProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_certificatemanager } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalCertificateProps 6 | */ 7 | export interface OptionalCertificateProps { 8 | /** 9 | * How to validate this certificate. 10 | * @default CertificateValidation.fromEmail() 11 | * @stability stable 12 | */ 13 | readonly validation?: aws_certificatemanager.CertificateValidation; 14 | /** 15 | * Enable or disable transparency logging for this certificate. 16 | * Once a certificate has been logged, it cannot be removed from the log. 17 | * Opting out at that point will have no effect. If you opt out of logging 18 | * when you request a certificate and then choose later to opt back in, 19 | * your certificate will not be logged until it is renewed. 20 | * If you want the certificate to be logged immediately, we recommend that you issue a new one. 21 | * @default true 22 | * @stability stable 23 | */ 24 | readonly transparencyLoggingEnabled?: boolean; 25 | /** 26 | * Alternative domain names on your certificate. 27 | * Use this to register alternative domain names that represent the same site. 28 | * @default - No additional FQDNs will be included as alternative domain names. 29 | * @stability stable 30 | */ 31 | readonly subjectAlternativeNames?: Array; 32 | /** 33 | * Specifies the algorithm of the public and private key pair that your certificate uses to encrypt data. 34 | * @default KeyAlgorithm.RSA_2048 35 | * @stability stable 36 | */ 37 | readonly keyAlgorithm?: aws_certificatemanager.KeyAlgorithm; 38 | /** 39 | * The Certificate name. 40 | * Since the Certificate resource doesn't support providing a physical name, the value provided here will be recorded in the `Name` tag 41 | * @default the full, absolute path of this construct 42 | * @stability stable 43 | */ 44 | readonly certificateName?: string; 45 | /** 46 | * Enable or disable export of this certificate. 47 | * If you issue an exportable public certificate, there is a charge at certificate issuance and again when the certificate renews. 48 | * Ref: https://aws.amazon.com/certificate-manager/pricing 49 | * @default false 50 | * @stability stable 51 | */ 52 | readonly allowExport?: boolean; 53 | /** 54 | * Fully-qualified domain name to request a certificate for. 55 | * May contain wildcards, such as ``*.domain.com``. 56 | * @stability stable 57 | */ 58 | readonly domainName?: string; 59 | } 60 | -------------------------------------------------------------------------------- /src/NextjsInvalidation.ts: -------------------------------------------------------------------------------- 1 | import { Stack } from 'aws-cdk-lib'; 2 | import { IDistribution } from 'aws-cdk-lib/aws-cloudfront'; 3 | import { PolicyStatement } from 'aws-cdk-lib/aws-iam'; 4 | import { 5 | AwsCustomResource, 6 | AwsSdkCall, 7 | AwsCustomResourcePolicy, 8 | PhysicalResourceId, 9 | AwsCustomResourceProps, 10 | } from 'aws-cdk-lib/custom-resources'; 11 | import { Construct } from 'constructs'; 12 | 13 | export interface NextjsInvalidationOverrides { 14 | readonly awsCustomResourceProps?: AwsCustomResourceProps; 15 | } 16 | 17 | export interface NextjsInvalidationProps { 18 | /** 19 | * CloudFront Distribution to invalidate 20 | */ 21 | readonly distribution: IDistribution; 22 | /** 23 | * Constructs that should complete before invalidating CloudFront Distribution. 24 | * 25 | * Useful for assets that must be deployed/updated before invalidating. 26 | */ 27 | readonly dependencies: Construct[]; 28 | /** 29 | * Override props for every construct. 30 | */ 31 | readonly overrides?: NextjsInvalidationOverrides; 32 | } 33 | 34 | export class NextjsInvalidation extends Construct { 35 | constructor(scope: Construct, id: string, props: NextjsInvalidationProps) { 36 | super(scope, id); 37 | const awsSdkCall: AwsSdkCall = { 38 | // make `physicalResourceId` change each time to invalidate CloudFront 39 | // distribution on each change 40 | physicalResourceId: PhysicalResourceId.of(`${props.distribution.distributionId}-${Date.now()}`), 41 | action: 'CreateInvalidationCommand', 42 | service: '@aws-sdk/client-cloudfront', 43 | parameters: { 44 | DistributionId: props.distribution.distributionId, 45 | InvalidationBatch: { 46 | CallerReference: new Date().toISOString(), 47 | Paths: { 48 | Quantity: 1, 49 | Items: ['/*'], 50 | }, 51 | }, 52 | }, 53 | }; 54 | const awsCustomResource = new AwsCustomResource(this, 'AwsCR', { 55 | onCreate: awsSdkCall, 56 | onUpdate: awsSdkCall, 57 | policy: AwsCustomResourcePolicy.fromStatements([ 58 | new PolicyStatement({ 59 | actions: ['cloudfront:CreateInvalidation'], 60 | resources: [ 61 | Stack.of(this).formatArn({ 62 | resource: `distribution/${props.distribution.distributionId}`, 63 | service: 'cloudfront', 64 | region: '', 65 | }), 66 | ], 67 | }), 68 | ]), 69 | ...props.overrides?.awsCustomResourceProps, 70 | }); 71 | for (const dependency of props.dependencies) { 72 | dependency.node.addDependency(awsCustomResource); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/app-router/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "pnpm tsx src/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/high-security/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "pnpm tsx src/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/multiple-sites/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "pnpm tsx src/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/pages-router/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "pnpm tsx src/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/app-pages-router/cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "pnpm tsx src/app.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-lambda:recognizeLayerVersion": true, 21 | "@aws-cdk/core:checkSecretUsage": true, 22 | "@aws-cdk/core:target-partitions": [ 23 | "aws", 24 | "aws-cn" 25 | ], 26 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 27 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 28 | "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, 29 | "@aws-cdk/aws-iam:minimizePolicies": true, 30 | "@aws-cdk/core:validateSnapshotRemovalPolicy": true, 31 | "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, 32 | "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, 33 | "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, 34 | "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, 35 | "@aws-cdk/core:enablePartitionLiterals": true, 36 | "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, 37 | "@aws-cdk/aws-iam:standardizedServicePrincipals": true, 38 | "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, 39 | "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, 40 | "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, 41 | "@aws-cdk/aws-route53-patters:useCertificate": true, 42 | "@aws-cdk/customresources:installLatestAwsSdkDefault": false, 43 | "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, 44 | "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, 45 | "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, 46 | "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, 47 | "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, 48 | "@aws-cdk/aws-redshift:columnId": true, 49 | "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, 50 | "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, 51 | "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, 52 | "@aws-cdk/aws-kms:aliasNameRef": true, 53 | "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, 54 | "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, 55 | "@aws-cdk/aws-efs:denyAnonymousAccess": true, 56 | "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, 57 | "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, 58 | "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /examples/high-security/src/stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Nextjs } from 'cdk-nextjs-standalone'; 4 | import { CfnWebACL } from 'aws-cdk-lib/aws-wafv2'; 5 | import { Function as CdkFunction, FunctionUrlAuthType } from 'aws-cdk-lib/aws-lambda'; 6 | 7 | export class HighSecurityStack extends Stack { 8 | private nextjs: Nextjs; 9 | constructor(scope: Construct, id: string, props?: StackProps) { 10 | super(scope, id, props); 11 | 12 | const webAcl = this.createWebAcl(); 13 | 14 | this.nextjs = new Nextjs(this, 'nextjs', { 15 | nextjsPath: '../../open-next/examples/app-router', 16 | skipBuild: false, 17 | overrides: { 18 | nextjs: { 19 | nextjsDistributionProps: { 20 | functionUrlAuthType: FunctionUrlAuthType.AWS_IAM, 21 | } 22 | }, 23 | nextjsDistribution: { 24 | distributionProps: { 25 | webAclId: webAcl.attrArn 26 | } 27 | } 28 | } 29 | }); 30 | this.retainEdgeFnOnDelete(); 31 | 32 | new CfnOutput(this, "CloudFrontDistributionDomain", { 33 | value: this.nextjs.distribution.distributionDomain, 34 | }); 35 | } 36 | 37 | /** 38 | * Don't fail on CloudFormation delete due to replicated function 39 | * @link https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html 40 | */ 41 | private retainEdgeFnOnDelete() { 42 | const edgeFn = this.nextjs.distribution?.node 43 | .tryFindChild("EdgeFn") 44 | ?.node.tryFindChild("Fn"); 45 | if (edgeFn instanceof CdkFunction) { 46 | edgeFn.applyRemovalPolicy(RemovalPolicy.RETAIN); 47 | } 48 | } 49 | 50 | private createWebAcl() { 51 | return new CfnWebACL(this, "WebAcl", { 52 | defaultAction: { 53 | allow: {}, // allow if no managed rule matches 54 | }, 55 | scope: "CLOUDFRONT", 56 | rules: [ 57 | { 58 | // Set the override action to none to leave the rule group rule actions in effect 59 | overrideAction: { none: {} }, 60 | name: "AWSManagedRulesCommonRuleSet", 61 | statement: { 62 | managedRuleGroupStatement: { 63 | vendorName: "AWS", 64 | name: "AWSManagedRulesCommonRuleSet", 65 | }, 66 | }, 67 | priority: 10, 68 | visibilityConfig: { 69 | cloudWatchMetricsEnabled: false, 70 | metricName: "AWSManagedRulesCommonRuleSetMetric", 71 | sampledRequestsEnabled: false, 72 | }, 73 | } 74 | ], 75 | visibilityConfig: { 76 | cloudWatchMetricsEnabled: false, 77 | metricName: "WebACLMetrics", 78 | sampledRequestsEnabled: false, 79 | } 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsBucketDeploymentProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsBucketDeploymentOverrides } from '../'; 3 | import type { aws_s3, aws_s3_assets } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsBucketDeploymentProps 7 | */ 8 | export interface OptionalNextjsBucketDeploymentProps { 9 | /** 10 | * If `true` then files will be zipped before writing to destination bucket. 11 | * Useful for Lambda functions. 12 | * @default false 13 | * @stability stable 14 | */ 15 | readonly zip?: boolean; 16 | /** 17 | * Replace placeholders in all files in `asset`. 18 | * Placeholder targets are 19 | * defined by keys of record. Values to replace placeholders with are defined 20 | * by values of record. 21 | * @stability stable 22 | */ 23 | readonly substitutionConfig?: Record; 24 | /** 25 | * The number of files to upload in parallel. 26 | * @stability stable 27 | */ 28 | readonly queueSize?: number; 29 | /** 30 | * Mapping of files to PUT options for `PutObjectCommand`. 31 | * Keys of 32 | * record must be a glob pattern (uses micromatch). Values of record are options 33 | * for PUT command for AWS SDK JS V3. See [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-s3/Interface/PutObjectRequest/) 34 | * for options. If a file matches multiple globs, configuration will be 35 | * merged. Later entries override earlier entries. 36 | * 37 | * `Bucket`, `Key`, and `Body` PUT options cannot be set. 38 | * @stability stable 39 | */ 40 | readonly putConfig?: Record>; 41 | /** 42 | * If `true`, then delete old objects in `destinationBucket`/`destinationKeyPrefix` **after** uploading new objects. Only applies if `zip` is `false`. 43 | * Old objects are determined by listing objects 44 | * in bucket before creating new objects and finding the objects that aren't in 45 | * the new objects. 46 | * 47 | * Note, if this is set to true then clients who have old HTML files (browser tabs opened before deployment) 48 | * will reference JS, CSS files that do not exist in S3 reslting in 404s. 49 | * @default false 50 | * @stability stable 51 | */ 52 | readonly prune?: boolean; 53 | /** 54 | * Override props for every construct. 55 | * @stability stable 56 | */ 57 | readonly overrides?: NextjsBucketDeploymentOverrides; 58 | /** 59 | * Destination S3 Bucket Key Prefix. 60 | * @stability stable 61 | */ 62 | readonly destinationKeyPrefix?: string; 63 | /** 64 | * Enable verbose output of Custom Resource Lambda. 65 | * @default false 66 | * @stability stable 67 | */ 68 | readonly debug?: boolean; 69 | /** 70 | * Destination S3 Bucket. 71 | * @stability stable 72 | */ 73 | readonly destinationBucket?: aws_s3.IBucket; 74 | /** 75 | * Source `Asset`. 76 | * @stability stable 77 | */ 78 | readonly asset?: aws_s3_assets.Asset; 79 | } 80 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalS3OriginProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_cloudfront, Duration } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalS3OriginProps 6 | */ 7 | export interface OptionalS3OriginProps { 8 | /** 9 | * An optional Origin Access Identity of the origin identity cloudfront will use when calling your s3 bucket. 10 | * @default - An Origin Access Identity will be created. 11 | * @stability stable 12 | */ 13 | readonly originAccessIdentity?: aws_cloudfront.IOriginAccessIdentity; 14 | /** 15 | * An optional path that CloudFront appends to the origin domain name when CloudFront requests content from the origin. 16 | * Must begin, but not end, with '/' (e.g., '/production/images'). 17 | * @default '/' 18 | * @stability stable 19 | */ 20 | readonly originPath?: string; 21 | /** 22 | * The time that a request from CloudFront to the origin can stay open and wait for a response. 23 | * If the complete response isn't received from the origin by this time, CloudFront ends the connection. 24 | * 25 | * Valid values are 1-3600 seconds, inclusive. 26 | * @default undefined - AWS CloudFront default is not enforcing a maximum value 27 | * @stability stable 28 | */ 29 | readonly responseCompletionTimeout?: Duration; 30 | /** 31 | * When you enable Origin Shield in the AWS Region that has the lowest latency to your origin, you can get better network performance. 32 | * @default - origin shield not enabled 33 | * @stability stable 34 | */ 35 | readonly originShieldRegion?: string; 36 | /** 37 | * Origin Shield is enabled by setting originShieldRegion to a valid region, after this to disable Origin Shield again you must set this flag to false. 38 | * @default - true 39 | * @stability stable 40 | */ 41 | readonly originShieldEnabled?: boolean; 42 | /** 43 | * A unique identifier for the origin. 44 | * This value must be unique within the distribution. 45 | * @default - an originid will be generated for you 46 | * @stability stable 47 | */ 48 | readonly originId?: string; 49 | /** 50 | * The unique identifier of an origin access control for this origin. 51 | * @default - no origin access control 52 | * @stability stable 53 | */ 54 | readonly originAccessControlId?: string; 55 | /** 56 | * A list of HTTP header names and values that CloudFront adds to requests it sends to the origin. 57 | * @default {} 58 | * @stability stable 59 | */ 60 | readonly customHeaders?: Record; 61 | /** 62 | * The number of seconds that CloudFront waits when trying to establish a connection to the origin. 63 | * Valid values are 1-10 seconds, inclusive. 64 | * @default Duration.seconds(10) 65 | * @stability stable 66 | */ 67 | readonly connectionTimeout?: Duration; 68 | /** 69 | * The number of times that CloudFront attempts to connect to the origin; 70 | * valid values are 1, 2, or 3 attempts. 71 | * @default 3 72 | * @stability stable 73 | */ 74 | readonly connectionAttempts?: number; 75 | } 76 | -------------------------------------------------------------------------------- /docs/major-changes.md: -------------------------------------------------------------------------------- 1 | # Nextjs Breaking Changes 2 | 3 | ## v4 4 | - Renamed `NextjsLambda` to `NextjsServer` 5 | - Renamed `ImageOptimizationLambda` to `NextjsImage` 6 | - Renamed `NextjsCachePolicyProps.lambdaCachePolicy` to `NextjsCachePolicyProps.serverCachePolicy` 7 | - Removed `NextjsOriginRequestPolicyProps.fallbackOriginRequestPolicy` 8 | - Renamed `NextjsOriginRequestPolicyProps.lambdaOriginRequestPolicy` to `NextjsOriginRequestPolicyProps.serverOriginRequestPolicy` 9 | - Removed `NextjsDistribution.staticCachePolicyProps` 10 | - Renamed `NextjsDistribution.lambdaCachePolicyProps` to `NextjsDistribution.serverCachePolicyProps` 11 | - Renamed `NextjsDistribution.lambdaOriginRequestPolicyProps` to `NextjsDistribution.serverOriginRequestPolicyProps` 12 | - Removed `NextjsDistribution.fallbackOriginRequestPolicyProps` 13 | - Removed `NextjsDistribution.imageOptimizationOriginRequestPolicyProps` 14 | - NOTE: when upgrading to v4 from v3, the Lambda@Edge function will be renamed or removed. CloudFormation will fail to delete the function b/c they're replicated a take ~15 min to delete (more [here](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html)). You can either deploy CloudFormation with it's "no rollback" feature for a clean deployment or mark the Lambda@Edge function as "retain on delete". 15 | - Remove `NextjsBuild.nextMiddlewareFnDir` 16 | - Remove `BaseSiteEnvironmentOutputsInfo, BaseSiteReplaceProps` exports as not used anymore 17 | - Remove `compressionLevel` to simplify configuration. We use optimal for windows or max compression for unix 18 | - Remove `nodeEnv` because it can be configured through `environment` prop. 19 | - Remove `sharpLayerArn` because it's not used 20 | - Remove `projectRoot` because it's not used 21 | - Remove `NextjsBaseProps` to simplify props 22 | - Remove `projectRoot` as it's not being used 23 | - Remove `tempBuildDir` as it's not being used 24 | - Create `NextjsDomain`. Remove custom domain related props from `NextjsDistribution`. 25 | - Add more customizable `NextjsOverrides` in favor of `NextjsDefaultsProps`. (Remove `NextjsProps.defaults`) 26 | - `NextjsDefaultsProps.assetDeployment` -> `NextjsOverrides.staticAssets` 27 | - `NextjsDefaultsProps.lambda` -> `NextjsOverrides.nextjsServer` 28 | - `NextjsDefaultsProps.distribution` -> `NextjsOverrides.nextjsDistribution` 29 | - Remove `NextjsDistributionProps.originRequestPolicies` in favor of `NextjsOverrides.nextjsDistribution.*BehaviorOptions` 30 | - Remove `NextjsDistributionProps.cachePolicies` in favor of `NextjsOverrides.nextjsDistribution.*CachePolicies` 31 | 32 | 33 | ## v3 34 | Using open-next for building, ARM64 architecture for image handling, new build options. 35 | 36 | ## v2 37 | SST wrapper changed, lambda/assets/distribution defaults now are in the `defaults` prop, refactored distribution settings into the new NextjsDistribution construct. If you are upgrading, you must temporarily remove the `customDomain` on your existing 1.x.x app before upgrading to >=2.x.x because the CloudFront distribution will get recreated due to refactoring, and the custom domain must be globally unique across all CloudFront distributions. Prepare for downtime. -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 1 * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v4 23 | with: 24 | node-version: ">=20.0.0" 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - name: Find mutations 30 | id: create_patch 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 34 | working-directory: ./ 35 | - name: Upload patch 36 | if: steps.create_patch.outputs.patch_created 37 | uses: actions/upload-artifact@v4.4.0 38 | with: 39 | name: repo.patch 40 | path: repo.patch 41 | overwrite: true 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: read 48 | if: ${{ needs.upgrade.outputs.patch_created }} 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v4 52 | with: 53 | ref: main 54 | - name: Download patch 55 | uses: actions/download-artifact@v4 56 | with: 57 | name: repo.patch 58 | path: ${{ runner.temp }} 59 | - name: Apply patch 60 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 61 | - name: Set git identity 62 | run: |- 63 | git config user.name "github-actions" 64 | git config user.email "github-actions@github.com" 65 | - name: Create Pull Request 66 | id: create-pr 67 | uses: peter-evans/create-pull-request@v6 68 | with: 69 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 70 | commit-message: |- 71 | chore(deps): upgrade dependencies 72 | 73 | Upgrades project dependencies. See details in [workflow run]. 74 | 75 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 76 | 77 | ------ 78 | 79 | *Automatically created by projen via the "upgrade-main" workflow* 80 | branch: github-actions/upgrade-main 81 | title: "chore(deps): upgrade dependencies" 82 | body: |- 83 | Upgrades project dependencies. See details in [workflow run]. 84 | 85 | [Workflow Run]: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} 86 | 87 | ------ 88 | 89 | *Automatically created by projen via the "upgrade-main" workflow* 90 | author: github-actions 91 | committer: github-actions 92 | signoff: true 93 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript", 20 | "plugin:prettier/recommended" 21 | ], 22 | "settings": { 23 | "import/parsers": { 24 | "@typescript-eslint/parser": [ 25 | ".ts", 26 | ".tsx" 27 | ] 28 | }, 29 | "import/resolver": { 30 | "node": {}, 31 | "typescript": { 32 | "project": "./tsconfig.dev.json", 33 | "alwaysTryTypes": true 34 | } 35 | } 36 | }, 37 | "ignorePatterns": [ 38 | "examples/", 39 | "e2e-tests/", 40 | "generated-structs/", 41 | "!.projenrc.ts", 42 | "!projenrc/**/*.ts" 43 | ], 44 | "rules": { 45 | "@typescript-eslint/no-require-imports": [ 46 | "error" 47 | ], 48 | "import/no-extraneous-dependencies": [ 49 | "error", 50 | { 51 | "devDependencies": [ 52 | "**/test/**", 53 | "**/build-tools/**", 54 | ".projenrc.ts", 55 | "projenrc/**/*.ts" 56 | ], 57 | "optionalDependencies": false, 58 | "peerDependencies": true 59 | } 60 | ], 61 | "import/no-unresolved": [ 62 | "error" 63 | ], 64 | "import/order": [ 65 | "warn", 66 | { 67 | "groups": [ 68 | "builtin", 69 | "external" 70 | ], 71 | "alphabetize": { 72 | "order": "asc", 73 | "caseInsensitive": true 74 | } 75 | } 76 | ], 77 | "import/no-duplicates": [ 78 | "error" 79 | ], 80 | "no-shadow": [ 81 | "off" 82 | ], 83 | "@typescript-eslint/no-shadow": [ 84 | "error" 85 | ], 86 | "key-spacing": [ 87 | "error" 88 | ], 89 | "no-multiple-empty-lines": [ 90 | "error" 91 | ], 92 | "@typescript-eslint/no-floating-promises": [ 93 | "error" 94 | ], 95 | "no-return-await": [ 96 | "off" 97 | ], 98 | "@typescript-eslint/return-await": [ 99 | "error" 100 | ], 101 | "no-trailing-spaces": [ 102 | "error" 103 | ], 104 | "dot-notation": [ 105 | "error" 106 | ], 107 | "no-bitwise": [ 108 | "error" 109 | ], 110 | "@typescript-eslint/member-ordering": [ 111 | "error", 112 | { 113 | "default": [ 114 | "public-static-field", 115 | "public-static-method", 116 | "protected-static-field", 117 | "protected-static-method", 118 | "private-static-field", 119 | "private-static-method", 120 | "field", 121 | "constructor", 122 | "method" 123 | ] 124 | } 125 | ] 126 | }, 127 | "overrides": [ 128 | { 129 | "files": [ 130 | ".projenrc.ts" 131 | ], 132 | "rules": { 133 | "@typescript-eslint/no-require-imports": "off", 134 | "import/no-extraneous-dependencies": "off" 135 | } 136 | } 137 | ] 138 | } 139 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalNextjsDomainProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { NextjsDomainOverrides } from '../'; 3 | import type { aws_certificatemanager, aws_route53 } from 'aws-cdk-lib'; 4 | 5 | /** 6 | * OptionalNextjsDomainProps 7 | */ 8 | export interface OptionalNextjsDomainProps { 9 | /** 10 | * Override props for every construct. 11 | * @stability stable 12 | */ 13 | readonly overrides?: NextjsDomainOverrides; 14 | /** 15 | * You must create the hosted zone out-of-band. 16 | * You can lookup the hosted zone outside this construct and pass it in via this prop. 17 | * Alternatively if this prop is `undefined`, then the hosted zone will be 18 | * **looked up** (not created) via `HostedZone.fromLookup` with {@link NextjsDomainProps.domainName}. 19 | * @stability stable 20 | */ 21 | readonly hostedZone?: aws_route53.IHostedZone; 22 | /** 23 | * The domain name used in this construct when creating an ACM `Certificate`. 24 | * Useful 25 | * when passing {@link NextjsDomainProps.alternateNames} and you need to specify 26 | * a wildcard domain like "*.example.com". If `undefined`, then {@link NextjsDomainProps.domainName} 27 | * will be used. 28 | * 29 | * If {@link NextjsDomainProps.certificate} is passed, then this prop is ignored. 30 | * @stability stable 31 | */ 32 | readonly certificateDomainName?: string; 33 | /** 34 | * If this prop is `undefined` then an ACM `Certificate` will be created based on {@link NextjsDomainProps.domainName} with DNS Validation. This prop allows you to control the TLS/SSL certificate created. The certificate you create must be in the `us-east-1` (N. Virginia) region as required by AWS CloudFront. 35 | * Set this option if you have an existing certificate in the `us-east-1` region in AWS Certificate Manager you want to use. 36 | * @stability stable 37 | */ 38 | readonly certificate?: aws_certificatemanager.ICertificate; 39 | /** 40 | * Alternate domain names that should route to the Cloudfront Distribution. 41 | * For example, if you specificied `"example.com"` as your {@link NextjsDomainProps.domainName}, 42 | * you could specify `["www.example.com", "api.example.com"]`. 43 | * Learn more about the [requirements](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-requirements) 44 | * and [restrictions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-restrictions) 45 | * for using alternate domain names with CloudFront. 46 | * 47 | * Note, in order to use alternate domain names, they must be covered by your 48 | * certificate. By default, the certificate created in this construct only covers 49 | * the {@link NextjsDomainProps.domainName}. Therefore, you'll need to specify 50 | * a wildcard domain name like `"*.example.com"` with {@link NextjsDomainProps.certificateDomainName} 51 | * so that this construct will create the certificate the covers the alternate 52 | * domain names. Otherwise, you can use {@link NextjsDomainProps.certificate} 53 | * to create the certificate yourself where you'll need to ensure it has a 54 | * wildcard or uses subject alternative names including the 55 | * alternative names specified here. 56 | * @stability stable 57 | */ 58 | readonly alternateNames?: Array; 59 | /** 60 | * An easy to remember address of your website. 61 | * Only supports domains hosted 62 | * on [Route 53](https://aws.amazon.com/route53/). Used as `domainName` for 63 | * ACM `Certificate` if {@link NextjsDomainProps.certificate} and 64 | * {@link NextjsDomainProps.certificateDomainName} are `undefined`. 65 | * @stability stable 66 | */ 67 | readonly domainName?: string; 68 | } 69 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@aws-crypto/sha256-js", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@aws-sdk/client-s3", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@aws-sdk/lib-storage", 13 | "type": "build" 14 | }, 15 | { 16 | "name": "@mrgrain/jsii-struct-builder", 17 | "type": "build" 18 | }, 19 | { 20 | "name": "@smithy/signature-v4", 21 | "type": "build" 22 | }, 23 | { 24 | "name": "@types/adm-zip", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "@types/aws-lambda", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "@types/jest", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "@types/micromatch", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "@types/mime-types", 41 | "type": "build" 42 | }, 43 | { 44 | "name": "@types/node", 45 | "version": "^20", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "@typescript-eslint/eslint-plugin", 50 | "version": "^8", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "@typescript-eslint/parser", 55 | "version": "^8", 56 | "type": "build" 57 | }, 58 | { 59 | "name": "aws-lambda", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "commit-and-tag-version", 64 | "version": "^12", 65 | "type": "build" 66 | }, 67 | { 68 | "name": "esbuild", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "eslint-config-prettier", 73 | "type": "build" 74 | }, 75 | { 76 | "name": "eslint-import-resolver-typescript", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "eslint-plugin-import", 81 | "type": "build" 82 | }, 83 | { 84 | "name": "eslint-plugin-prettier", 85 | "type": "build" 86 | }, 87 | { 88 | "name": "eslint", 89 | "version": "^9", 90 | "type": "build" 91 | }, 92 | { 93 | "name": "jest", 94 | "type": "build" 95 | }, 96 | { 97 | "name": "jest-junit", 98 | "version": "^16", 99 | "type": "build" 100 | }, 101 | { 102 | "name": "jsii-diff", 103 | "type": "build" 104 | }, 105 | { 106 | "name": "jsii-docgen", 107 | "version": "^10.5.0", 108 | "type": "build" 109 | }, 110 | { 111 | "name": "jsii-pacmak", 112 | "type": "build" 113 | }, 114 | { 115 | "name": "jsii-rosetta", 116 | "version": "~5.7.1", 117 | "type": "build" 118 | }, 119 | { 120 | "name": "jsii", 121 | "version": "~5.7.1", 122 | "type": "build" 123 | }, 124 | { 125 | "name": "jszip", 126 | "type": "build" 127 | }, 128 | { 129 | "name": "micromatch", 130 | "type": "build" 131 | }, 132 | { 133 | "name": "mime-types", 134 | "type": "build" 135 | }, 136 | { 137 | "name": "prettier", 138 | "type": "build" 139 | }, 140 | { 141 | "name": "projen", 142 | "type": "build" 143 | }, 144 | { 145 | "name": "ts-jest", 146 | "type": "build" 147 | }, 148 | { 149 | "name": "ts-node", 150 | "type": "build" 151 | }, 152 | { 153 | "name": "typescript", 154 | "type": "build" 155 | }, 156 | { 157 | "name": "aws-cdk-lib", 158 | "version": "^2.232.1", 159 | "type": "peer" 160 | }, 161 | { 162 | "name": "constructs", 163 | "version": "^10.0.5", 164 | "type": "peer" 165 | } 166 | ], 167 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 168 | } 169 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Deploy NextJS with CDK 2 | 3 | [![View on Construct Hub](https://constructs.dev/badge?package=cdk-nextjs-standalone)](https://constructs.dev/packages/cdk-nextjs-standalone) 4 | 5 | ## What is this? 6 | 7 | A CDK construct to deploy a NextJS app using AWS CDK. 8 | Supported NextJs versions: >=12.3.0+ (includes 13.0.0+) 9 | 10 | Uses the [standalone output](https://nextjs.org/docs/advanced-features/output-file-tracing) build mode. 11 | 12 | ## Quickstart 13 | 14 | ```ts 15 | import { App, Stack, StackProps } from 'aws-cdk-lib'; 16 | import { Construct } from 'constructs'; 17 | import { Nextjs } from 'cdk-nextjs-standalone'; 18 | 19 | class WebStack extends Stack { 20 | constructor(scope: Construct, id: string, props?: StackProps) { 21 | super(scope, id, props); 22 | const nextjs = new Nextjs(this, 'Nextjs', { 23 | nextjsPath: './web', // relative path from your project root to NextJS 24 | }); 25 | new CfnOutput(this, 'CloudFrontDistributionDomain', { 26 | value: nextjs.distribution.distributionDomain, 27 | }); 28 | } 29 | } 30 | 31 | const app = new App(); 32 | new WebStack(app, 'web'); 33 | ``` 34 | 35 | ## Important Notes 36 | 37 | - Due to CloudFront's Distribution Cache Behavior pattern matching limitations, a cache behavior will be created for each top level file or directory in your `public/` folder. CloudFront has a soft limit of [25 cache behaviors per distribution](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html#limits-web-distributions). Therefore, it's recommended to include all assets that can be under a top level folder like `public/static/`. Learn more in open-next docs [here](https://github.com/sst/open-next/blob/main/README.md#workaround-create-one-cache-behavior-per-top-level-file-and-folder-in-public-aws-specific). 38 | 39 | ## Documentation 40 | 41 | Available on [Construct Hub](https://constructs.dev/packages/cdk-nextjs-standalone/). 42 | 43 | ## Examples 44 | 45 | See example CDK apps [here](./examples) including: 46 | 47 | - App Router 48 | - Pages Router 49 | - App/Pages Router 50 | - High Security 51 | - Multiple Sites 52 | 53 | To deploy an example, make sure to read the [README.md](./examples/README.md) 54 | 55 | ### Discord Chat 56 | 57 | We're in the #aws channel on the [Open-Next Discord](https://discord.gg/VqFVt4YtSq). 58 | 59 | ## About 60 | 61 | Deploys a NextJs static site with server-side rendering and API support. Uses AWS lambda and CloudFront. 62 | 63 | There is a new (since Next 12) [standalone output mode which uses output tracing](https://nextjs.org/docs/advanced-features/output-file-tracing) to generate a minimal server and static files. 64 | This standalone server can be converted into a CloudFront distribution and a lambda handler that handles SSR, API, and routing. 65 | 66 | The CloudFront default origin first checks S3 for static files and falls back to an HTTP origin using a lambda function URL. 67 | 68 | ## Benefits 69 | 70 | This approach is most compatible with new NextJs features such as ESM configuration, [middleware](https://nextjs.org/docs/advanced-features/middleware), next-auth, and React server components ("appDir"). 71 | 72 | The unmaintained [@serverless-nextjs project](https://github.com/serverless-nextjs/serverless-next.js) uses the deprecated `serverless` NextJs build target which [prevents the use of new features](https://github.com/serverless-nextjs/serverless-next.js/pull/2478). 73 | This construct was created to use the new `standalone` output build and newer AWS features like lambda function URLs and fallback origins. 74 | 75 | You may want to look at [Serverless Stack](https://sst.dev) and its [NextjsSite](https://docs.sst.dev/constructs/NextjsSite) construct for an improved developer experience if you are building serverless applications on CDK. 76 | 77 | ## Dependencies 78 | 79 | Built on top of [open-next](https://open-next.js.org/), which was partially built using the original core of cdk-nextjs-standalone. 80 | 81 | ## Heavily based on 82 | 83 | - [Open-next](https://open-next.js.org/) 84 | - 85 | - 86 | - 87 | - [Serverless Stack](https://github.com/serverless-stack/sst) 88 | - [RemixSite](https://github.com/serverless-stack/sst/blob/master/packages/resources/src/NextjsSite.ts) construct 89 | - [NextjsSite](https://github.com/serverless-stack/sst/blob/master/packages/resources/src/RemixSite.ts) construct 90 | 91 | ## Contribute 92 | 93 | See [Contribute](./docs/contribute.md). 94 | 95 | ## Breaking changes 96 | 97 | See [Major Changes](./docs/major-changes.md). 98 | -------------------------------------------------------------------------------- /examples/app-router/src/overrides-stack.ts: -------------------------------------------------------------------------------- 1 | import { CfnOutput, Stack, StackProps, Token } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import { Nextjs } from 'cdk-nextjs-standalone'; 4 | import { PriceClass } from 'aws-cdk-lib/aws-cloudfront'; 5 | import { Duration } from 'aws-cdk-lib'; 6 | import { Billing, Capacity } from 'aws-cdk-lib/aws-dynamodb'; 7 | import { SymlinkFollowMode } from 'aws-cdk-lib'; 8 | 9 | /** 10 | * This stack showcases how to use the `overrides` prop. 11 | */ 12 | export class OverridesStack extends Stack { 13 | constructor(scope: Construct, id: string, props?: StackProps) { 14 | super(scope, id, props); 15 | 16 | const nextjs = new Nextjs(this, 'nextjs', { 17 | nextjsPath: '../../open-next/examples/app-router', 18 | buildCommand: 'npx @opennextjs/aws@^3 build', 19 | // skipBuild: true, 20 | overrides: { 21 | nextjs: { 22 | nextjsBuildProps: {}, 23 | nextjsDistributionProps: {}, 24 | nextjsDomainProps: {}, 25 | nextjsImageProps: {}, 26 | nextjsInvalidationProps: {}, 27 | nextjsRevalidationProps: {}, 28 | nextjsServerProps: {}, 29 | nextjsStaticAssetsProps: {}, 30 | }, 31 | nextjsBucketDeployment: { 32 | functionProps: { 33 | memorySize: 512, 34 | } 35 | }, 36 | nextjsDistribution: { 37 | cloudFrontFunctionProps: { 38 | comment: "My CloudFront Function" 39 | }, 40 | distributionProps: { 41 | priceClass: PriceClass.PRICE_CLASS_100, 42 | }, 43 | edgeFunctionProps: { 44 | memorySize: 256 45 | }, 46 | imageBehaviorOptions: {}, 47 | imageCachePolicyProps: { 48 | maxTtl: Duration.days(30), 49 | }, 50 | imageHttpOriginProps: { 51 | customHeaders: { "x-custom-image-header": "1" } 52 | }, 53 | s3OriginProps: { 54 | customHeaders: { "x-custom-s3-header": "3" } 55 | }, 56 | serverBehaviorOptions: {}, 57 | serverCachePolicyProps: { 58 | maxTtl: Duration.seconds(10), 59 | }, 60 | serverHttpOriginProps: { 61 | customHeaders: { "x-custom-server-header": "2" } 62 | }, 63 | staticBehaviorOptions: { 64 | smoothStreaming: true, 65 | }, 66 | }, 67 | nextjsDomain: { 68 | aaaaRecordProps: { 69 | ttl: Duration.minutes(45) 70 | }, 71 | aRecordProps: { 72 | ttl: Duration.minutes(15) 73 | }, 74 | certificateProps: { 75 | transparencyLoggingEnabled: true, 76 | }, 77 | hostedZoneProviderProps: {}, 78 | }, 79 | nextjsImage: { 80 | functionProps: { 81 | memorySize: 640, 82 | }, 83 | }, 84 | nextjsInvalidation: { 85 | awsCustomResourceProps: { 86 | timeout: Duration.minutes(3), 87 | }, 88 | }, 89 | nextjsRevalidation: { 90 | insertCustomResourceProps: {}, 91 | insertFunctionProps: { 92 | memorySize: 768, 93 | }, 94 | insertProviderProps: { 95 | totalTimeout: Duration.minutes(1), 96 | }, 97 | queueFunctionProps: { 98 | memorySize: 896, 99 | }, 100 | queueProps: { 101 | visibilityTimeout: Duration.seconds(45), 102 | }, 103 | tableProps: { 104 | billing: Billing.provisioned({ 105 | readCapacity: Capacity.autoscaled({ maxCapacity: 10 }), 106 | writeCapacity: Capacity.autoscaled({ maxCapacity: 10 }), 107 | }) 108 | }, 109 | }, 110 | nextjsServer: { 111 | nextjsBucketDeploymentProps: {}, 112 | destinationCodeAssetProps: { 113 | exclude: ["secrets"], 114 | }, 115 | functionProps: { 116 | memorySize: 1024, 117 | }, 118 | sourceCodeAssetProps: { 119 | exclude: ["secrets"], 120 | }, 121 | }, 122 | nextjsStaticAssets: { 123 | assetProps: { 124 | followSymlinks: SymlinkFollowMode.BLOCK_EXTERNAL, 125 | }, 126 | bucketProps: { 127 | versioned: true, 128 | }, 129 | nextjsBucketDeploymentProps: {}, 130 | }, 131 | } 132 | }); 133 | 134 | new CfnOutput(this, "CloudFrontDistributionDomain", { 135 | value: nextjs.distribution.distributionDomain, 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Setup Node.js 23 | uses: actions/setup-node@v4 24 | with: 25 | node-version: ">=20.0.0" 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen compile && npx projen build 30 | - name: Find mutations 31 | id: self_mutation 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 35 | working-directory: ./ 36 | - name: Upload patch 37 | if: steps.self_mutation.outputs.self_mutation_happened 38 | uses: actions/upload-artifact@v4.4.0 39 | with: 40 | name: repo.patch 41 | path: repo.patch 42 | overwrite: true 43 | - name: Fail build on mutation 44 | if: steps.self_mutation.outputs.self_mutation_happened 45 | run: |- 46 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 47 | cat repo.patch 48 | exit 1 49 | - name: Backup artifact permissions 50 | run: cd dist && getfacl -R . > permissions-backup.acl 51 | continue-on-error: true 52 | - name: Upload artifact 53 | uses: actions/upload-artifact@v4.4.0 54 | with: 55 | name: build-artifact 56 | path: dist 57 | overwrite: true 58 | self-mutation: 59 | needs: build 60 | runs-on: ubuntu-latest 61 | permissions: 62 | contents: write 63 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 64 | steps: 65 | - name: Checkout 66 | uses: actions/checkout@v4 67 | with: 68 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 69 | ref: ${{ github.event.pull_request.head.ref }} 70 | repository: ${{ github.event.pull_request.head.repo.full_name }} 71 | - name: Download patch 72 | uses: actions/download-artifact@v4 73 | with: 74 | name: repo.patch 75 | path: ${{ runner.temp }} 76 | - name: Apply patch 77 | run: '[ -s ${{ runner.temp }}/repo.patch ] && git apply ${{ runner.temp }}/repo.patch || echo "Empty patch. Skipping."' 78 | - name: Set git identity 79 | run: |- 80 | git config user.name "github-actions" 81 | git config user.email "github-actions@github.com" 82 | - name: Push changes 83 | env: 84 | PULL_REQUEST_REF: ${{ github.event.pull_request.head.ref }} 85 | run: |- 86 | git add . 87 | git commit -s -m "chore: self mutation" 88 | git push origin HEAD:$PULL_REQUEST_REF 89 | package-js: 90 | needs: build 91 | runs-on: ubuntu-latest 92 | permissions: 93 | contents: read 94 | if: ${{ !needs.build.outputs.self_mutation_happened }} 95 | steps: 96 | - uses: actions/setup-node@v4 97 | with: 98 | node-version: ">=20.0.0" 99 | - name: Download build artifacts 100 | uses: actions/download-artifact@v4 101 | with: 102 | name: build-artifact 103 | path: dist 104 | - name: Restore build artifact permissions 105 | run: cd dist && setfacl --restore=permissions-backup.acl 106 | continue-on-error: true 107 | - name: Checkout 108 | uses: actions/checkout@v4 109 | with: 110 | ref: ${{ github.event.pull_request.head.ref }} 111 | repository: ${{ github.event.pull_request.head.repo.full_name }} 112 | path: .repo 113 | - name: Install Dependencies 114 | run: cd .repo && yarn install --check-files --frozen-lockfile 115 | - name: Extract build artifact 116 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 117 | - name: Move build artifact out of the way 118 | run: mv dist dist.old 119 | - name: Create js artifact 120 | run: cd .repo && npx projen package:js 121 | - name: Collect js artifact 122 | run: mv .repo/dist dist 123 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalCustomResourceProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { Duration, RemovalPolicy } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalCustomResourceProps 6 | */ 7 | export interface OptionalCustomResourceProps { 8 | /** 9 | * The maximum time that can elapse before a custom resource operation times out. 10 | * The value must be between 1 second and 3600 seconds. 11 | * 12 | * Maps to [ServiceTimeout](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-customresource.html#cfn-cloudformation-customresource-servicetimeout) property for the `AWS::CloudFormation::CustomResource` resource 13 | * 14 | * A token can be specified for this property, but it must be specified with `Duration.seconds()`. 15 | * @default Duration.seconds(3600) 16 | * @stability stable 17 | */ 18 | readonly serviceTimeout?: Duration; 19 | /** 20 | * For custom resources, you can specify AWS::CloudFormation::CustomResource (the default) as the resource type, or you can specify your own resource type name. 21 | * For example, you can use "Custom::MyCustomResourceTypeName". 22 | * 23 | * Custom resource type names must begin with "Custom::" and can include 24 | * alphanumeric characters and the following characters: _@-. You can specify 25 | * a custom resource type name up to a maximum length of 60 characters. You 26 | * cannot change the type during an update. 27 | * 28 | * Using your own resource type names helps you quickly differentiate the 29 | * types of custom resources in your stack. For example, if you had two custom 30 | * resources that conduct two different ping tests, you could name their type 31 | * as Custom::PingTester to make them easily identifiable as ping testers 32 | * (instead of using AWS::CloudFormation::CustomResource). 33 | * @default - AWS::CloudFormation::CustomResource 34 | * @stability stable 35 | */ 36 | readonly resourceType?: string; 37 | /** 38 | * The policy to apply when this resource is removed from the application. 39 | * @default cdk.RemovalPolicy.Destroy 40 | * @stability stable 41 | */ 42 | readonly removalPolicy?: RemovalPolicy; 43 | /** 44 | * Properties to pass to the Lambda. 45 | * Values in this `properties` dictionary can possibly overwrite other values in `CustomResourceProps` 46 | * E.g. `ServiceToken` and `ServiceTimeout` 47 | * It is recommended to avoid using same keys that exist in `CustomResourceProps` 48 | * @default - No properties. 49 | * @stability stable 50 | */ 51 | readonly properties?: Record; 52 | /** 53 | * Convert all property keys to pascal case. 54 | * @default false 55 | * @stability stable 56 | */ 57 | readonly pascalCaseProperties?: boolean; 58 | /** 59 | * The ARN of the provider which implements this custom resource type. 60 | * You can implement a provider by listening to raw AWS CloudFormation events 61 | * and specify the ARN of an SNS topic (`topic.topicArn`) or the ARN of an AWS 62 | * Lambda function (`lambda.functionArn`) or use the CDK's custom [resource 63 | * provider framework] which makes it easier to implement robust providers. 64 | * 65 | * [resource provider framework]: 66 | * https://docs.aws.amazon.com/cdk/api/latest/docs/custom-resources-readme.html 67 | * 68 | * Provider framework: 69 | * 70 | * ```ts 71 | * // use the provider framework from aws-cdk/custom-resources: 72 | * const provider = new customresources.Provider(this, 'ResourceProvider', { 73 | * onEventHandler, 74 | * isCompleteHandler, // optional 75 | * }); 76 | * 77 | * new CustomResource(this, 'MyResource', { 78 | * serviceToken: provider.serviceToken, 79 | * }); 80 | * ``` 81 | * 82 | * AWS Lambda function (not recommended to use AWS Lambda Functions directly, 83 | * see the module README): 84 | * 85 | * ```ts 86 | * // invoke an AWS Lambda function when a lifecycle event occurs: 87 | * new CustomResource(this, 'MyResource', { 88 | * serviceToken: myFunction.functionArn, 89 | * }); 90 | * ``` 91 | * 92 | * SNS topic (not recommended to use AWS Lambda Functions directly, see the 93 | * module README): 94 | * 95 | * ```ts 96 | * // publish lifecycle events to an SNS topic: 97 | * new CustomResource(this, 'MyResource', { 98 | * serviceToken: myTopic.topicArn, 99 | * }); 100 | * ``` 101 | * 102 | * Maps to [ServiceToken](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cloudformation-customresource.html#cfn-cloudformation-customresource-servicetoken) property for the `AWS::CloudFormation::CustomResource` resource 103 | * @stability stable 104 | */ 105 | readonly serviceToken?: string; 106 | } 107 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | concurrency: 10 | group: ${{ github.workflow }} 11 | cancel-in-progress: false 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: write 17 | outputs: 18 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 19 | tag_exists: ${{ steps.check_tag_exists.outputs.exists }} 20 | env: 21 | CI: "true" 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | - name: Set git identity 28 | run: |- 29 | git config user.name "github-actions" 30 | git config user.email "github-actions@github.com" 31 | - name: Setup Node.js 32 | uses: actions/setup-node@v4 33 | with: 34 | node-version: ">=20.0.0" 35 | - name: Install dependencies 36 | run: yarn install --check-files --frozen-lockfile 37 | - name: release 38 | run: npx projen compile && npx projen release 39 | - name: Check if version has already been tagged 40 | id: check_tag_exists 41 | run: |- 42 | TAG=$(cat dist/releasetag.txt) 43 | ([ ! -z "$TAG" ] && git ls-remote -q --exit-code --tags origin $TAG && (echo "exists=true" >> $GITHUB_OUTPUT)) || (echo "exists=false" >> $GITHUB_OUTPUT) 44 | cat $GITHUB_OUTPUT 45 | - name: Check for new commits 46 | id: git_remote 47 | run: |- 48 | echo "latest_commit=$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" >> $GITHUB_OUTPUT 49 | cat $GITHUB_OUTPUT 50 | - name: Backup artifact permissions 51 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 52 | run: cd dist && getfacl -R . > permissions-backup.acl 53 | continue-on-error: true 54 | - name: Upload artifact 55 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 56 | uses: actions/upload-artifact@v4.4.0 57 | with: 58 | name: build-artifact 59 | path: dist 60 | overwrite: true 61 | release_github: 62 | name: Publish to GitHub Releases 63 | needs: 64 | - release 65 | - release_npm 66 | runs-on: ubuntu-latest 67 | permissions: 68 | contents: write 69 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 70 | steps: 71 | - uses: actions/setup-node@v4 72 | with: 73 | node-version: ">=20.0.0" 74 | - name: Download build artifacts 75 | uses: actions/download-artifact@v4 76 | with: 77 | name: build-artifact 78 | path: dist 79 | - name: Restore build artifact permissions 80 | run: cd dist && setfacl --restore=permissions-backup.acl 81 | continue-on-error: true 82 | - name: Release 83 | env: 84 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | GITHUB_REPOSITORY: ${{ github.repository }} 86 | GITHUB_REF: ${{ github.sha }} 87 | run: errout=$(mktemp); gh release create $(cat dist/releasetag.txt) -R $GITHUB_REPOSITORY -F dist/changelog.md -t $(cat dist/releasetag.txt) --target $GITHUB_REF 2> $errout && true; exitcode=$?; if [ $exitcode -ne 0 ] && ! grep -q "Release.tag_name already exists" $errout; then cat $errout; exit $exitcode; fi 88 | release_npm: 89 | name: Publish to npm 90 | needs: release 91 | runs-on: ubuntu-latest 92 | permissions: 93 | id-token: write 94 | contents: read 95 | if: needs.release.outputs.tag_exists != 'true' && needs.release.outputs.latest_commit == github.sha 96 | steps: 97 | - uses: actions/setup-node@v4 98 | with: 99 | node-version: ">=20.0.0" 100 | - name: Download build artifacts 101 | uses: actions/download-artifact@v4 102 | with: 103 | name: build-artifact 104 | path: dist 105 | - name: Restore build artifact permissions 106 | run: cd dist && setfacl --restore=permissions-backup.acl 107 | continue-on-error: true 108 | - name: Checkout 109 | uses: actions/checkout@v4 110 | with: 111 | path: .repo 112 | - name: Install Dependencies 113 | run: cd .repo && yarn install --check-files --frozen-lockfile 114 | - name: Extract build artifact 115 | run: tar --strip-components=1 -xzvf dist/js/*.tgz -C .repo 116 | - name: Move build artifact out of the way 117 | run: mv dist dist.old 118 | - name: Create js artifact 119 | run: cd .repo && npx projen package:js 120 | - name: Collect js artifact 121 | run: mv .repo/dist dist 122 | - name: Release 123 | env: 124 | NPM_DIST_TAG: latest 125 | NPM_REGISTRY: registry.npmjs.org 126 | NPM_CONFIG_PROVENANCE: "true" 127 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 128 | run: npx -p publib@latest publib-npm 129 | -------------------------------------------------------------------------------- /docs/code-deployment-flow.md: -------------------------------------------------------------------------------- 1 | # Nextjs Code Deployment Flow 2 | 3 | Deep dive into `Nextjs` constructs code deployment flow - how your Next.js code gets deployed into AWS. 4 | 5 | 1. `cdk deploy "**"` 6 | 1. `Nextjs` is instantiated 7 | 1. `NextjsBuild` is instantiated which runs `npx open-next build` within user's Next.js repository. This command runs `next build` which creates a .next folder with build output. Then `open-next` copies the static assets and generates a cached files (ISR), server, image optimization, revalidation, and warmer lambda function handler code. When open-next's build is run, the process's environment variables, `NextjsProps.environment`, and `Nextjs.nodeEnv` are injected into the build process. However, any unresolved tokens in `NextjsProps.environment` are replaced with placeholders that look like `{{ BUCKET_NAME }}` as they're unresolved tokens so they're value looks like `${TOKEN[Bucket.Name.1234]}`. Learn more about AWS CDK Tokens [here](https://docs.aws.amazon.com/cdk/v2/guide/tokens.html). The placeholders will be replaced later in a [CloudFormation Custom Resource](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-custom-resources.html). 8 | 1. `NextjsStaticAssets` is instantiated which creates an S3 bucket, an `Asset` for your Next.js static assets, and a `NextjsBucketDeployment`. [Asset](https://docs.aws.amazon.com/cdk/v2/guide/assets.html) is uploaded to the S3 Bucket created during CDK Bootstrap in your AWS account (not bucket created in `NextjsStaticAssets`). `NextjsBucketDeployment` is a CloudFormation Custom Resource that downloads files from the CDK Assets Bucket, updates placeholder values, and then deploys the files to the target bucket. Placeholder values were unresolved tokens at synthesis time (because they reference values where names/ids aren't known yet) but at the time the code runs in the Custom Resource Lambda Function, those values have been resolved and are passed into custom resource through `ResourceProperties`. Only the public environment variable (NEXT_PUBLIC) placeholders are passed to `NextjsBucketDeployment.substitutionConfig` because server variables shouldn't live in static assets. Learn more about Next.js environment variables [here](https://nextjs.org/docs/app/building-your-application/configuring/environment-variables). It's important to note the deployment order so that we don't write the static assets to the bucket until they're placeholders are replaced, otherwise we risk a user downloading a file with placeholders which would result in an error. 9 | 1. `NextjsServer` is instantiated which creates an `Asset`, `NextjsBucketDeployment`, and lambda function to run Next.js server code. `NextjsBucketDeployment` will replace all (public and private) unresolved tokens within open-next generated server function code. Additional environment variables to support cache ISR feature are added: CACHE_BUCKET_NAME, CACHE_BUCKET_KEY, CACHE_BUCKET_REGION. `NextjsServer` also bundles lambda code with `esbuild`. The same note above about the important of deployment order applies here. 10 | 1. `NextjsImage` and `NextjsRevalidation` are instantiated with `Function` utilizing bundled code output from `open-next`. We don't need to replace environment variable placeholders because they don't any. 11 | 1. `NextjsInvalidation` is instantiated to invalidate CloudFront Distribution. This construct explicitly depends upon `NextjsStaticAssets`, `NextjsServer`, `NextjsImage` so that we ensure any resources that could impact cached resources (static assets, dynamic html, images) are up to date before invalidating CloudFront Distribution's cache. 12 | 13 | ## PNPM Monorepo Symlinks 14 | _Only applicable for PNPM Monorepos_ 15 | PNPM Monorepos use symlinks between workspace node_modules and the top level node_modules. CDK Assets do not support symlinks despite the configuration options available. Therefore, we must zip up the assets ourselves. Also, `nextjs-bucket-deployment.ts` handles symlinks to unzip and zip symlinks within Lambda Custom Resources (for ServerFnBucketDeployment). 16 | 17 | 18 | ## Conditional Build Logic 19 | 20 | `NextjjsBuild` will use the following logic to determine if a build is required or not to proceed. 21 | 22 | ``` 23 | | bundlingRequired | skipBuild | Scenario | Action | 24 | |------------------|-----------|---------------------------------|---------------------------------------------------| 25 | | true | true | deploy/synth with reused bundle | no build, check .open-next exists, fail if not | 26 | | true | false | regular deploy/synth | build, .open-next will exist | 27 | | false | false | destroy | no build, check if .open-next exists, if not mock | 28 | | false | true | destroy with reused bundle | no build, check if .open-next exists, if not mock | 29 | ``` 30 | 31 | *bundlingRequired* = `Stack.of(this).bundlingRequired` [see](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.Stack.html#bundlingrequired) 32 | *skipBuild* = `NextjsProps.skipBuild` 33 | 34 | 35 | Relevant GitHub Issues: 36 | - https://github.com/aws/aws-cdk/issues/9251 37 | - https://github.com/Stuk/jszip/issues/386#issuecomment-634773343 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cdk-nextjs-standalone", 3 | "description": "Deploy a NextJS app to AWS using CDK and OpenNext.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/jetbridge/cdk-nextjs.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "bundle": "npx projen bundle", 12 | "bundle:lambdas/nextjs-bucket-deployment": "npx projen bundle:lambdas/nextjs-bucket-deployment", 13 | "bundle:lambdas/nextjs-bucket-deployment:watch": "npx projen bundle:lambdas/nextjs-bucket-deployment:watch", 14 | "bundle:lambdas/sign-fn-url": "npx projen bundle:lambdas/sign-fn-url", 15 | "bundle:lambdas/sign-fn-url:watch": "npx projen bundle:lambdas/sign-fn-url:watch", 16 | "clobber": "npx projen clobber", 17 | "compat": "npx projen compat", 18 | "compile": "npx projen compile", 19 | "default": "npx projen default", 20 | "docgen": "npx projen docgen", 21 | "eject": "npx projen eject", 22 | "eslint": "npx projen eslint", 23 | "package": "npx projen package", 24 | "package-all": "npx projen package-all", 25 | "package:js": "npx projen package:js", 26 | "post-compile": "npx projen post-compile", 27 | "post-upgrade": "npx projen post-upgrade", 28 | "pre-compile": "npx projen pre-compile", 29 | "release": "npx projen release", 30 | "test": "npx projen test", 31 | "test:watch": "npx projen test:watch", 32 | "unbump": "npx projen unbump", 33 | "upgrade": "npx projen upgrade", 34 | "watch": "npx projen watch", 35 | "projen": "npx projen" 36 | }, 37 | "author": { 38 | "name": "JetBridge", 39 | "email": "mischa@jetbridge.com", 40 | "organization": true 41 | }, 42 | "devDependencies": { 43 | "@aws-crypto/sha256-js": "^5.2.0", 44 | "@aws-sdk/client-s3": "^3.758.0", 45 | "@aws-sdk/lib-storage": "^3.758.0", 46 | "@mrgrain/jsii-struct-builder": "^0.7.45", 47 | "@smithy/signature-v4": "^2.3.0", 48 | "@types/adm-zip": "^0.5.7", 49 | "@types/aws-lambda": "^8.10.147", 50 | "@types/jest": "^27", 51 | "@types/micromatch": "^4.0.9", 52 | "@types/mime-types": "^2.1.4", 53 | "@types/node": "^20", 54 | "@typescript-eslint/eslint-plugin": "^8", 55 | "@typescript-eslint/parser": "^8", 56 | "aws-cdk-lib": "2.232.1", 57 | "aws-lambda": "^1.0.7", 58 | "commit-and-tag-version": "^12", 59 | "constructs": "10.0.5", 60 | "esbuild": "^0.25.0", 61 | "eslint": "^9", 62 | "eslint-config-prettier": "^8.10.0", 63 | "eslint-import-resolver-typescript": "^3.8.3", 64 | "eslint-plugin-import": "^2.31.0", 65 | "eslint-plugin-prettier": "^4.2.1", 66 | "jest": "^27", 67 | "jest-junit": "^16", 68 | "jsii": "~5.7.1", 69 | "jsii-diff": "^1.108.0", 70 | "jsii-docgen": "^10.5.0", 71 | "jsii-pacmak": "^1.108.0", 72 | "jsii-rosetta": "~5.7.1", 73 | "jszip": "^3.10.1", 74 | "micromatch": "^4.0.8", 75 | "mime-types": "^2.1.35", 76 | "prettier": "^2.8.8", 77 | "projen": "^0.91.13", 78 | "ts-jest": "^27", 79 | "ts-node": "^10.9.2", 80 | "typescript": "4.9.5" 81 | }, 82 | "peerDependencies": { 83 | "aws-cdk-lib": "^2.232.1", 84 | "constructs": "^10.0.5" 85 | }, 86 | "keywords": [ 87 | "aws", 88 | "aws-cdk", 89 | "cdk", 90 | "cloud", 91 | "iac", 92 | "infrastructure", 93 | "next", 94 | "nextjs", 95 | "open-next", 96 | "serverless", 97 | "standalone" 98 | ], 99 | "engines": { 100 | "node": ">= 20.0.0" 101 | }, 102 | "main": "lib/index.js", 103 | "license": "Apache-2.0", 104 | "publishConfig": { 105 | "access": "public" 106 | }, 107 | "version": "0.0.0", 108 | "jest": { 109 | "coverageProvider": "v8", 110 | "testMatch": [ 111 | "/@(src|test)/**/*(*.)@(spec|test).ts?(x)", 112 | "/@(src|test)/**/__tests__/**/*.ts?(x)", 113 | "/@(projenrc)/**/*(*.)@(spec|test).ts?(x)", 114 | "/@(projenrc)/**/__tests__/**/*.ts?(x)" 115 | ], 116 | "clearMocks": true, 117 | "collectCoverage": true, 118 | "coverageReporters": [ 119 | "json", 120 | "lcov", 121 | "clover", 122 | "cobertura", 123 | "text" 124 | ], 125 | "coverageDirectory": "coverage", 126 | "coveragePathIgnorePatterns": [ 127 | "/node_modules/" 128 | ], 129 | "testPathIgnorePatterns": [ 130 | "/node_modules/" 131 | ], 132 | "watchPathIgnorePatterns": [ 133 | "/node_modules/" 134 | ], 135 | "reporters": [ 136 | "default", 137 | [ 138 | "jest-junit", 139 | { 140 | "outputDirectory": "test-reports" 141 | } 142 | ] 143 | ], 144 | "preset": "ts-jest", 145 | "globals": { 146 | "ts-jest": { 147 | "tsconfig": "tsconfig.dev.json" 148 | } 149 | } 150 | }, 151 | "types": "lib/index.d.ts", 152 | "stability": "stable", 153 | "jsii": { 154 | "outdir": "dist", 155 | "targets": {}, 156 | "tsc": { 157 | "outDir": "lib", 158 | "rootDir": "src" 159 | } 160 | }, 161 | "//": "~~ Generated by projen. To modify, edit .projenrc.ts and run \"npx projen\"." 162 | } 163 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalARecordProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_route53, Duration } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalARecordProps 6 | */ 7 | export interface OptionalARecordProps { 8 | /** 9 | * The target. 10 | * @stability stable 11 | */ 12 | readonly target?: aws_route53.RecordTarget; 13 | /** 14 | * Among resource record sets that have the same combination of DNS name and type, a value that determines the proportion of DNS queries that Amazon Route 53 responds to using the current resource record set. 15 | * Route 53 calculates the sum of the weights for the resource record sets that have the same combination of DNS name and type. 16 | * Route 53 then responds to queries based on the ratio of a resource's weight to the total. 17 | * 18 | * This value can be a number between 0 and 255. 19 | * @default - Do not set weighted routing 20 | * @stability stable 21 | */ 22 | readonly weight?: number; 23 | /** 24 | * The resource record cache time to live (TTL). 25 | * @default Duration.minutes(30) 26 | * @stability stable 27 | */ 28 | readonly ttl?: Duration; 29 | /** 30 | * A string used to distinguish between different records with the same combination of DNS name and type. 31 | * It can only be set when either weight or geoLocation is defined. 32 | * 33 | * This parameter must be between 1 and 128 characters in length. 34 | * @default - Auto generated string 35 | * @stability stable 36 | */ 37 | readonly setIdentifier?: string; 38 | /** 39 | * The Amazon EC2 Region where you created the resource that this resource record set refers to. 40 | * The resource typically is an AWS resource, such as an EC2 instance or an ELB load balancer, 41 | * and is referred to by an IP address or a DNS domain name, depending on the record type. 42 | * 43 | * When Amazon Route 53 receives a DNS query for a domain name and type for which you have created latency resource record sets, 44 | * Route 53 selects the latency resource record set that has the lowest latency between the end user and the associated Amazon EC2 Region. 45 | * Route 53 then returns the value that is associated with the selected resource record set. 46 | * @default - Do not set latency based routing 47 | * @stability stable 48 | */ 49 | readonly region?: string; 50 | /** 51 | * The subdomain name for this record. This should be relative to the zone root name. 52 | * For example, if you want to create a record for acme.example.com, specify 53 | * "acme". 54 | * 55 | * You can also specify the fully qualified domain name which terminates with a 56 | * ".". For example, "acme.example.com.". 57 | * @default zone root 58 | * @stability stable 59 | */ 60 | readonly recordName?: string; 61 | /** 62 | * Whether to return multiple values, such as IP addresses for your web servers, in response to DNS queries. 63 | * @default false 64 | * @stability stable 65 | */ 66 | readonly multiValueAnswer?: boolean; 67 | /** 68 | * The health check to associate with the record set. 69 | * Route53 will return this record set in response to DNS queries only if the health check is passing. 70 | * @default - No health check configured 71 | * @stability stable 72 | */ 73 | readonly healthCheck?: aws_route53.IHealthCheck; 74 | /** 75 | * The geographical origin for this record to return DNS records based on the user's location. 76 | * @stability stable 77 | */ 78 | readonly geoLocation?: aws_route53.GeoLocation; 79 | /** 80 | * Whether to delete the same record set in the hosted zone if it already exists (dangerous!). 81 | * This allows to deploy a new record set while minimizing the downtime because the 82 | * new record set will be created immediately after the existing one is deleted. It 83 | * also avoids "manual" actions to delete existing record sets. 84 | * 85 | * > **N.B.:** this feature is dangerous, use with caution! It can only be used safely when 86 | * > `deleteExisting` is set to `true` as soon as the resource is added to the stack. Changing 87 | * > an existing Record Set's `deleteExisting` property from `false -> true` after deployment 88 | * > will delete the record! 89 | * @default false 90 | * @deprecated This property is dangerous and can lead to unintended record deletion in case of deployment failure. 91 | * @stability deprecated 92 | */ 93 | readonly deleteExisting?: boolean; 94 | /** 95 | * A comment to add on the record. 96 | * @default no comment 97 | * @stability stable 98 | */ 99 | readonly comment?: string; 100 | /** 101 | * The object that is specified in resource record set object when you are linking a resource record set to a CIDR location. 102 | * A LocationName with an asterisk “*” can be used to create a default CIDR record. CollectionId is still required for default record. 103 | * @default - No CIDR routing configured 104 | * @stability stable 105 | */ 106 | readonly cidrRoutingConfig?: aws_route53.CidrRoutingConfig; 107 | /** 108 | * The hosted zone in which to define the new record. 109 | * @stability stable 110 | */ 111 | readonly zone?: aws_route53.IHostedZone; 112 | } 113 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalAaaaRecordProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_route53, Duration } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalAaaaRecordProps 6 | */ 7 | export interface OptionalAaaaRecordProps { 8 | /** 9 | * The target. 10 | * @stability stable 11 | */ 12 | readonly target?: aws_route53.RecordTarget; 13 | /** 14 | * Among resource record sets that have the same combination of DNS name and type, a value that determines the proportion of DNS queries that Amazon Route 53 responds to using the current resource record set. 15 | * Route 53 calculates the sum of the weights for the resource record sets that have the same combination of DNS name and type. 16 | * Route 53 then responds to queries based on the ratio of a resource's weight to the total. 17 | * 18 | * This value can be a number between 0 and 255. 19 | * @default - Do not set weighted routing 20 | * @stability stable 21 | */ 22 | readonly weight?: number; 23 | /** 24 | * The resource record cache time to live (TTL). 25 | * @default Duration.minutes(30) 26 | * @stability stable 27 | */ 28 | readonly ttl?: Duration; 29 | /** 30 | * A string used to distinguish between different records with the same combination of DNS name and type. 31 | * It can only be set when either weight or geoLocation is defined. 32 | * 33 | * This parameter must be between 1 and 128 characters in length. 34 | * @default - Auto generated string 35 | * @stability stable 36 | */ 37 | readonly setIdentifier?: string; 38 | /** 39 | * The Amazon EC2 Region where you created the resource that this resource record set refers to. 40 | * The resource typically is an AWS resource, such as an EC2 instance or an ELB load balancer, 41 | * and is referred to by an IP address or a DNS domain name, depending on the record type. 42 | * 43 | * When Amazon Route 53 receives a DNS query for a domain name and type for which you have created latency resource record sets, 44 | * Route 53 selects the latency resource record set that has the lowest latency between the end user and the associated Amazon EC2 Region. 45 | * Route 53 then returns the value that is associated with the selected resource record set. 46 | * @default - Do not set latency based routing 47 | * @stability stable 48 | */ 49 | readonly region?: string; 50 | /** 51 | * The subdomain name for this record. This should be relative to the zone root name. 52 | * For example, if you want to create a record for acme.example.com, specify 53 | * "acme". 54 | * 55 | * You can also specify the fully qualified domain name which terminates with a 56 | * ".". For example, "acme.example.com.". 57 | * @default zone root 58 | * @stability stable 59 | */ 60 | readonly recordName?: string; 61 | /** 62 | * Whether to return multiple values, such as IP addresses for your web servers, in response to DNS queries. 63 | * @default false 64 | * @stability stable 65 | */ 66 | readonly multiValueAnswer?: boolean; 67 | /** 68 | * The health check to associate with the record set. 69 | * Route53 will return this record set in response to DNS queries only if the health check is passing. 70 | * @default - No health check configured 71 | * @stability stable 72 | */ 73 | readonly healthCheck?: aws_route53.IHealthCheck; 74 | /** 75 | * The geographical origin for this record to return DNS records based on the user's location. 76 | * @stability stable 77 | */ 78 | readonly geoLocation?: aws_route53.GeoLocation; 79 | /** 80 | * Whether to delete the same record set in the hosted zone if it already exists (dangerous!). 81 | * This allows to deploy a new record set while minimizing the downtime because the 82 | * new record set will be created immediately after the existing one is deleted. It 83 | * also avoids "manual" actions to delete existing record sets. 84 | * 85 | * > **N.B.:** this feature is dangerous, use with caution! It can only be used safely when 86 | * > `deleteExisting` is set to `true` as soon as the resource is added to the stack. Changing 87 | * > an existing Record Set's `deleteExisting` property from `false -> true` after deployment 88 | * > will delete the record! 89 | * @default false 90 | * @deprecated This property is dangerous and can lead to unintended record deletion in case of deployment failure. 91 | * @stability deprecated 92 | */ 93 | readonly deleteExisting?: boolean; 94 | /** 95 | * A comment to add on the record. 96 | * @default no comment 97 | * @stability stable 98 | */ 99 | readonly comment?: string; 100 | /** 101 | * The object that is specified in resource record set object when you are linking a resource record set to a CIDR location. 102 | * A LocationName with an asterisk “*” can be used to create a default CIDR record. CollectionId is still required for default record. 103 | * @default - No CIDR routing configured 104 | * @stability stable 105 | */ 106 | readonly cidrRoutingConfig?: aws_route53.CidrRoutingConfig; 107 | /** 108 | * The hosted zone in which to define the new record. 109 | * @stability stable 110 | */ 111 | readonly zone?: aws_route53.IHostedZone; 112 | } 113 | -------------------------------------------------------------------------------- /src/NextjsStaticAssets.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import { tmpdir } from 'node:os'; 3 | import { resolve } from 'node:path'; 4 | import { RemovalPolicy, Stack } from 'aws-cdk-lib'; 5 | import * as s3 from 'aws-cdk-lib/aws-s3'; 6 | import { Asset } from 'aws-cdk-lib/aws-s3-assets'; 7 | import { Construct } from 'constructs'; 8 | import { CACHE_BUCKET_KEY_PREFIX } from './constants'; 9 | import { OptionalAssetProps, OptionalNextjsBucketDeploymentProps } from './generated-structs'; 10 | import { NextjsBucketDeployment } from './NextjsBucketDeployment'; 11 | import { NextjsBuild } from './NextjsBuild'; 12 | 13 | export interface NextjsStaticAssetOverrides { 14 | readonly bucketProps?: s3.BucketProps; 15 | readonly nextjsBucketDeploymentProps?: OptionalNextjsBucketDeploymentProps; 16 | readonly assetProps?: OptionalAssetProps; 17 | } 18 | 19 | export interface NextjsStaticAssetsProps { 20 | /** 21 | * Optional value to prefix the Next.js site under a /prefix path on CloudFront. 22 | * Usually used when you deploy multiple Next.js sites on same domain using /sub-path 23 | * 24 | * Note, you'll need to set [basePath](https://nextjs.org/docs/app/api-reference/next-config-js/basePath) 25 | * in your `next.config.ts` to this value and ensure any files in `public` 26 | * folder have correct prefix. 27 | * @example "/my-base-path" 28 | */ 29 | readonly basePath?: string; 30 | /** 31 | * Define your own bucket to store static assets. 32 | */ 33 | readonly bucket?: s3.IBucket | undefined; 34 | /** 35 | * Custom environment variables to pass to the NextJS build and runtime. 36 | */ 37 | readonly environment?: Record; 38 | /** 39 | * The `NextjsBuild` instance representing the built Nextjs application. 40 | */ 41 | readonly nextBuild: NextjsBuild; 42 | /** 43 | * Override props for every construct. 44 | */ 45 | readonly overrides?: NextjsStaticAssetOverrides; 46 | /** 47 | * If `true` (default), then removes old static assets after upload new static assets. 48 | * @default true 49 | */ 50 | readonly prune?: boolean; 51 | } 52 | 53 | /** 54 | * Uploads Nextjs built static and public files to S3. 55 | * 56 | * Will inject resolved environment variables that are unresolved at synthesis 57 | * in CloudFormation Custom Resource. 58 | */ 59 | export class NextjsStaticAssets extends Construct { 60 | /** 61 | * Bucket containing assets. 62 | */ 63 | bucket: s3.IBucket; 64 | 65 | protected props: NextjsStaticAssetsProps; 66 | 67 | private get buildEnvVars() { 68 | const buildEnvVars: Record = {}; 69 | for (const [k, v] of Object.entries(this.props.environment || {})) { 70 | if (k.startsWith('NEXT_PUBLIC')) { 71 | buildEnvVars[k] = v; 72 | } 73 | } 74 | return buildEnvVars; 75 | } 76 | 77 | constructor(scope: Construct, id: string, props: NextjsStaticAssetsProps) { 78 | super(scope, id); 79 | this.props = props; 80 | 81 | this.bucket = this.createBucket(); 82 | 83 | // when `cdk deploy "NonNextjsStack" --exclusively` is run, don't bundle assets since they will not exist 84 | if (Stack.of(this).bundlingRequired) { 85 | const asset = this.createAsset(); 86 | this.createBucketDeployment(asset); 87 | } 88 | } 89 | 90 | private createBucket(): s3.IBucket { 91 | return ( 92 | this.props.bucket ?? 93 | new s3.Bucket(this, 'Bucket', { 94 | removalPolicy: RemovalPolicy.DESTROY, 95 | autoDeleteObjects: true, 96 | enforceSSL: true, 97 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 98 | encryption: s3.BucketEncryption.S3_MANAGED, 99 | ...this.props.overrides?.bucketProps, 100 | }) 101 | ); 102 | } 103 | 104 | private createAsset(): Asset { 105 | // create temporary directory to join open-next's static output with cache output 106 | const tmpAssetsDir = fs.mkdtempSync(resolve(tmpdir(), 'cdk-nextjs-assets-')); 107 | fs.cpSync(this.props.nextBuild.nextStaticDir, tmpAssetsDir, { recursive: true }); 108 | fs.cpSync(this.props.nextBuild.nextCacheDir, resolve(tmpAssetsDir, CACHE_BUCKET_KEY_PREFIX), { recursive: true }); 109 | const asset = new Asset(this, 'Asset', { 110 | path: tmpAssetsDir, 111 | ...this.props.overrides?.assetProps, 112 | }); 113 | fs.rmSync(tmpAssetsDir, { recursive: true }); 114 | return asset; 115 | } 116 | 117 | private createBucketDeployment(asset: Asset) { 118 | const basePath = this.props.basePath?.replace(/^\//, ''); // remove leading slash (if present) 119 | const staticFiles = '**/_next/static/**/*'; 120 | 121 | return new NextjsBucketDeployment(this, 'BucketDeployment', { 122 | asset, 123 | destinationBucket: this.bucket, 124 | destinationKeyPrefix: basePath, 125 | debug: true, 126 | // only put env vars that are placeholders in custom resource properties 127 | // to be replaced. other env vars were injected at build time. 128 | substitutionConfig: NextjsBucketDeployment.getSubstitutionConfig(this.buildEnvVars), 129 | prune: this.props.prune, // defaults to false 130 | putConfig: { 131 | [staticFiles]: { 132 | CacheControl: 'public, max-age=31536000, immutable', 133 | }, 134 | }, 135 | ...this.props.overrides?.nextjsBucketDeploymentProps, 136 | }); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalAssetProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { AssetHashType, aws_iam, BundlingOptions, IgnoreMode, interfaces, SymlinkFollowMode } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalAssetProps 6 | */ 7 | export interface OptionalAssetProps { 8 | /** 9 | * The disk location of the asset. 10 | * The path should refer to one of the following: 11 | * - A regular file or a .zip file, in which case the file will be uploaded as-is to S3. 12 | * - A directory, in which case it will be archived into a .zip file and uploaded to S3. 13 | * @stability stable 14 | */ 15 | readonly path?: string; 16 | /** 17 | * The ARN of the KMS key used to encrypt the handler code. 18 | * @default - the default server-side encryption with Amazon S3 managed keys(SSE-S3) key will be used. 19 | * @stability stable 20 | */ 21 | readonly sourceKMSKey?: interfaces.aws_kms.IKeyRef; 22 | /** 23 | * A list of principals that should be able to read this asset from S3. 24 | * You can use `asset.grantRead(principal)` to grant read permissions later. 25 | * @default - No principals that can read file asset. 26 | * @stability stable 27 | */ 28 | readonly readers?: Array; 29 | /** 30 | * A display name for this asset. 31 | * If supplied, the display name will be used in locations where the asset 32 | * identifier is printed, like in the CLI progress information. If the same 33 | * asset is added multiple times, the display name of the first occurrence is 34 | * used. 35 | * 36 | * The default is the construct path of the Asset construct, with respect to 37 | * the enclosing stack. If the asset is produced by a construct helper 38 | * function (such as `lambda.Code.fromAsset()`), this will look like 39 | * `MyFunction/Code`. 40 | * 41 | * We use the stack-relative construct path so that in the common case where 42 | * you have multiple stacks with the same asset, we won't show something like 43 | * `/MyBetaStack/MyFunction/Code` when you are actually deploying to 44 | * production. 45 | * @default - Stack-relative construct path 46 | * @stability stable 47 | */ 48 | readonly displayName?: string; 49 | /** 50 | * Whether or not the asset needs to exist beyond deployment time; 51 | * i.e. 52 | * are copied over to a different location and not needed afterwards. 53 | * Setting this property to true has an impact on the lifecycle of the asset, 54 | * because we will assume that it is safe to delete after the CloudFormation 55 | * deployment succeeds. 56 | * 57 | * For example, Lambda Function assets are copied over to Lambda during 58 | * deployment. Therefore, it is not necessary to store the asset in S3, so 59 | * we consider those deployTime assets. 60 | * @default false 61 | * @stability stable 62 | */ 63 | readonly deployTime?: boolean; 64 | /** 65 | * The ignore behavior to use for `exclude` patterns. 66 | * @default IgnoreMode.GLOB 67 | * @stability stable 68 | */ 69 | readonly ignoreMode?: IgnoreMode; 70 | /** 71 | * A strategy for how to handle symlinks. 72 | * @default SymlinkFollowMode.NEVER 73 | * @stability stable 74 | */ 75 | readonly followSymlinks?: SymlinkFollowMode; 76 | /** 77 | * File paths matching the patterns will be excluded. 78 | * See `ignoreMode` to set the matching behavior. 79 | * Has no effect on Assets bundled using the `bundling` property. 80 | * @default - nothing is excluded 81 | * @stability stable 82 | */ 83 | readonly exclude?: Array; 84 | /** 85 | * Bundle the asset by executing a command in a Docker container or a custom bundling provider. 86 | * The asset path will be mounted at `/asset-input`. The Docker 87 | * container is responsible for putting content at `/asset-output`. 88 | * The content at `/asset-output` will be zipped and used as the 89 | * final asset. 90 | * @default - uploaded as-is to S3 if the asset is a regular file or a .zip file, 91 | archived into a .zip file and uploaded to S3 otherwise 92 | * @stability stable 93 | */ 94 | readonly bundling?: BundlingOptions; 95 | /** 96 | * Specifies the type of hash to calculate for this asset. 97 | * If `assetHash` is configured, this option must be `undefined` or 98 | * `AssetHashType.CUSTOM`. 99 | * @default - the default is `AssetHashType.SOURCE`, but if `assetHash` is 100 | explicitly specified this value defaults to `AssetHashType.CUSTOM`. 101 | * @stability stable 102 | */ 103 | readonly assetHashType?: AssetHashType; 104 | /** 105 | * Specify a custom hash for this asset. 106 | * If `assetHashType` is set it must 107 | * be set to `AssetHashType.CUSTOM`. For consistency, this custom hash will 108 | * be SHA256 hashed and encoded as hex. The resulting hash will be the asset 109 | * hash. 110 | * 111 | * NOTE: the hash is used in order to identify a specific revision of the asset, and 112 | * used for optimizing and caching deployment activities related to this asset such as 113 | * packaging, uploading to Amazon S3, etc. If you chose to customize the hash, you will 114 | * need to make sure it is updated every time the asset changes, or otherwise it is 115 | * possible that some deployments will not be invalidated. 116 | * @default - based on `assetHashType` 117 | * @stability stable 118 | */ 119 | readonly assetHash?: string; 120 | } 121 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalTablePropsV2.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_dynamodb, aws_iam, aws_kinesis, CfnTag, RemovalPolicy } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalTablePropsV2 6 | */ 7 | export interface OptionalTablePropsV2 { 8 | /** 9 | * The witness Region for the MRSC global table. 10 | * A MRSC global table can be configured with either three replicas, or with two replicas and one witness. 11 | * 12 | * Note: Witness region cannot be specified for a Multi-Region Eventual Consistency (MREC) Global Table. 13 | * Witness regions are only supported for Multi-Region Strong Consistency (MRSC) Global Tables. 14 | * @default - no witness region 15 | * @stability stable 16 | */ 17 | readonly witnessRegion?: string; 18 | /** 19 | * The warm throughput configuration for the table. 20 | * @default - no warm throughput is configured 21 | * @stability stable 22 | */ 23 | readonly warmThroughput?: aws_dynamodb.WarmThroughput; 24 | /** 25 | * The name of the TTL attribute. 26 | * @default - TTL is disabled 27 | * @stability stable 28 | */ 29 | readonly timeToLiveAttribute?: string; 30 | /** 31 | * The name of the table. 32 | * @default - generated by CloudFormation 33 | * @stability stable 34 | */ 35 | readonly tableName?: string; 36 | /** 37 | * Sort key attribute definition. 38 | * @default - no sort key 39 | * @stability stable 40 | */ 41 | readonly sortKey?: aws_dynamodb.Attribute; 42 | /** 43 | * Replica tables to deploy with the primary table. 44 | * Note: Adding replica tables allows you to use your table as a global table. You 45 | * cannot specify a replica table in the region that the primary table will be deployed 46 | * to. Replica tables will only be supported if the stack deployment region is defined. 47 | * @default - no replica tables 48 | * @stability stable 49 | */ 50 | readonly replicas?: Array; 51 | /** 52 | * The removal policy applied to the table. 53 | * @default RemovalPolicy.RETAIN 54 | * @stability stable 55 | */ 56 | readonly removalPolicy?: RemovalPolicy; 57 | /** 58 | * Specifies the consistency mode for a new global table. 59 | * @default MultiRegionConsistency.EVENTUAL 60 | * @stability stable 61 | */ 62 | readonly multiRegionConsistency?: aws_dynamodb.MultiRegionConsistency; 63 | /** 64 | * Local secondary indexes. 65 | * Note: You can only provide a maximum of 5 local secondary indexes. 66 | * @default - no local secondary indexes 67 | * @stability stable 68 | */ 69 | readonly localSecondaryIndexes?: Array; 70 | /** 71 | * Global secondary indexes. 72 | * Note: You can provide a maximum of 20 global secondary indexes. 73 | * @default - no global secondary indexes 74 | * @stability stable 75 | */ 76 | readonly globalSecondaryIndexes?: Array; 77 | /** 78 | * The server-side encryption. 79 | * @default TableEncryptionV2.dynamoOwnedKey() 80 | * @stability stable 81 | */ 82 | readonly encryption?: aws_dynamodb.TableEncryptionV2; 83 | /** 84 | * When an item in the table is modified, StreamViewType determines what information is written to the stream. 85 | * @default - streams are disabled if replicas are not configured and this property is 86 | not specified. If this property is not specified when replicas are configured, then 87 | NEW_AND_OLD_IMAGES will be the StreamViewType for all replicas 88 | * @stability stable 89 | */ 90 | readonly dynamoStream?: aws_dynamodb.StreamViewType; 91 | /** 92 | * The billing mode and capacity settings to apply to the table. 93 | * @default Billing.onDemand() 94 | * @stability stable 95 | */ 96 | readonly billing?: aws_dynamodb.Billing; 97 | /** 98 | * Partition key attribute definition. 99 | * @stability stable 100 | */ 101 | readonly partitionKey?: aws_dynamodb.Attribute; 102 | /** 103 | * Tags to be applied to the primary table (default replica table). 104 | * @default - no tags 105 | * @stability stable 106 | */ 107 | readonly tags?: Array; 108 | /** 109 | * The table class. 110 | * @default TableClass.STANDARD 111 | * @stability stable 112 | */ 113 | readonly tableClass?: aws_dynamodb.TableClass; 114 | /** 115 | * Resource policy to assign to DynamoDB Table. 116 | * @default - No resource policy statements are added to the created table. 117 | * @stability stable 118 | */ 119 | readonly resourcePolicy?: aws_iam.PolicyDocument; 120 | /** 121 | * Whether point-in-time recovery is enabled and recoveryPeriodInDays is set. 122 | * @default - point in time recovery is not enabled. 123 | * @stability stable 124 | */ 125 | readonly pointInTimeRecoverySpecification?: aws_dynamodb.PointInTimeRecoverySpecification; 126 | /** 127 | * Whether point-in-time recovery is enabled. 128 | * @default false - point in time recovery is not enabled. 129 | * @deprecated use `pointInTimeRecoverySpecification` instead 130 | * @stability deprecated 131 | */ 132 | readonly pointInTimeRecovery?: boolean; 133 | /** 134 | * Kinesis Data Stream to capture item level changes. 135 | * @default - no Kinesis Data Stream 136 | * @stability stable 137 | */ 138 | readonly kinesisStream?: aws_kinesis.IStream; 139 | /** 140 | * Whether deletion protection is enabled. 141 | * @default false 142 | * @stability stable 143 | */ 144 | readonly deletionProtection?: boolean; 145 | /** 146 | * Whether CloudWatch contributor insights is enabled and what mode is selected. 147 | * @default - contributor insights is not enabled 148 | * @stability stable 149 | */ 150 | readonly contributorInsightsSpecification?: aws_dynamodb.ContributorInsightsSpecification; 151 | /** 152 | * Whether CloudWatch contributor insights is enabled. 153 | * @default false 154 | * @deprecated use `contributorInsightsSpecification` instead 155 | * @stability deprecated 156 | */ 157 | readonly contributorInsights?: boolean; 158 | } 159 | -------------------------------------------------------------------------------- /src/lambdas/sign-fn-url.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { Sha256 } from '@aws-crypto/sha256-js'; 3 | import { SignatureV4 } from '@smithy/signature-v4'; 4 | import type { CloudFrontHeaders, CloudFrontRequest, CloudFrontRequestHandler } from 'aws-lambda'; 5 | 6 | const debug = false; 7 | 8 | /** 9 | * This Lambda@Edge handler fixes s3 requests, fixes the host header, and 10 | * signs requests as they're destined for Lambda Function URL that requires 11 | * IAM Auth. 12 | */ 13 | export const handler: CloudFrontRequestHandler = async (event) => { 14 | const request = event.Records[0].cf.request; 15 | if (debug) console.log('input request', JSON.stringify(request, null, 2)); 16 | 17 | escapeQuerystring(request); 18 | await signRequest(request); 19 | 20 | if (debug) console.log('output request', JSON.stringify(request), null, 2); 21 | return request; 22 | }; 23 | 24 | /** 25 | * Lambda URL will reject query parameters with brackets so we need to encode 26 | * https://github.dev/pwrdrvr/lambda-url-signing/blob/main/packages/edge-to-origin/src/translate-request.ts#L19-L31 27 | */ 28 | function escapeQuerystring(request: CloudFrontRequest) { 29 | request.querystring = request.querystring.replace(/\[/g, '%5B').replace(/]/g, '%5D'); 30 | } 31 | 32 | let sigv4: SignatureV4; 33 | 34 | /** 35 | * When `NextjsDistributionProps.functionUrlAuthType` is set to 36 | * `lambda.FunctionUrlAuthType.AWS_IAM` we need to sign the `CloudFrontRequest`s 37 | * with AWS IAM SigV4 so that CloudFront can invoke the Nextjs server and image 38 | * optimization functions via function URLs. When configured, this lambda@edge 39 | * function has the permissions, lambda:InvokeFunctionUrl and lambda:InvokeFunction, 40 | * to invoke both functions. 41 | * @link https://medium.com/@dario_26152/restrict-access-to-lambda-functionurl-to-cloudfront-using-aws-iam-988583834705 42 | */ 43 | export async function signRequest(request: CloudFrontRequest) { 44 | if (!sigv4) { 45 | const region = getRegionFromLambdaUrl(request.origin?.custom?.domainName || ''); 46 | sigv4 = getSigV4(region); 47 | } 48 | const headerBag = cfHeadersToHeaderBag(request.headers); 49 | let body: string | undefined; 50 | if (request.body?.data) { 51 | body = Buffer.from(request.body.data, 'base64').toString(); 52 | } 53 | const params = queryStringToQueryParamBag(request.querystring); 54 | 55 | // These headers tend to change from hop to hop 56 | const volatileHeaders = new Set(['via', 'x-forwarded-for']); 57 | const signed = await sigv4.sign( 58 | { 59 | method: request.method, 60 | headers: headerBag, 61 | hostname: headerBag.host, 62 | path: request.uri, 63 | body, 64 | query: params, 65 | protocol: 'https', 66 | }, 67 | { unsignableHeaders: volatileHeaders } 68 | ); 69 | request.headers = headerBagToCfHeaders(signed.headers); 70 | } 71 | 72 | function getSigV4(region: string): SignatureV4 { 73 | const accessKeyId = process.env.AWS_ACCESS_KEY_ID; 74 | const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; 75 | const sessionToken = process.env.AWS_SESSION_TOKEN; 76 | if (!region) throw new Error('AWS_REGION missing'); 77 | if (!accessKeyId) throw new Error('AWS_ACCESS_KEY_ID missing'); 78 | if (!secretAccessKey) throw new Error('AWS_SECRET_ACCESS_KEY missing'); 79 | if (!sessionToken) throw new Error('AWS_SESSION_TOKEN missing'); 80 | return new SignatureV4({ 81 | service: 'lambda', 82 | region, 83 | credentials: { 84 | accessKeyId, 85 | secretAccessKey, 86 | sessionToken, 87 | }, 88 | sha256: Sha256, 89 | }); 90 | } 91 | 92 | export function getRegionFromLambdaUrl(url: string): string { 93 | const region = url.split('.').at(2); 94 | if (!region) throw new Error("Region couldn't be extracted from Lambda Function URL"); 95 | return region; 96 | } 97 | 98 | /** 99 | * Bag or Map used for HeaderBag or QueryStringParameterBag for `sigv4.sign()` 100 | */ 101 | type Bag = Record; 102 | /** 103 | * Converts CloudFront headers (can have array of header values) to simple 104 | * header bag (object) required by `sigv4.sign` 105 | * 106 | * NOTE: only includes headers allowed by origin policy to prevent signature 107 | * mismatch 108 | */ 109 | export function cfHeadersToHeaderBag(headers: CloudFrontHeaders): Bag { 110 | const headerBag: Bag = {}; 111 | // assume first header value is the best match 112 | // headerKey is case insensitive whereas key (adjacent property value that is 113 | // not destructured) is case sensitive. we arbitrarily use case insensitive key 114 | for (const [headerKey, [{ value }]] of Object.entries(headers)) { 115 | headerBag[headerKey] = value; 116 | // if there is an authorization from CloudFront, move it as 117 | // it will be overwritten when the headers are signed 118 | if (headerKey === 'authorization') { 119 | headerBag['origin-authorization'] = value; 120 | } 121 | } 122 | return headerBag; 123 | } 124 | 125 | /** 126 | * Converts simple header bag (object) to CloudFront headers 127 | */ 128 | export function headerBagToCfHeaders(headerBag: Bag): CloudFrontHeaders { 129 | const cfHeaders: CloudFrontHeaders = {}; 130 | for (const [headerKey, value] of Object.entries(headerBag)) { 131 | /* 132 | When your Lambda function adds or modifies request headers and you don't include the header key field, Lambda@Edge automatically inserts a header key using the header name that you provide. Regardless of how you've formatted the header name, the header key that's inserted automatically is formatted with initial capitalization for each part, separated by hyphens (-). 133 | See: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html 134 | */ 135 | cfHeaders[headerKey] = [{ value }]; 136 | } 137 | return cfHeaders; 138 | } 139 | 140 | /** 141 | * Converts CloudFront querystring to QueryParamaterBag for IAM Sig V4 142 | */ 143 | export function queryStringToQueryParamBag(querystring: string): Bag { 144 | const oldParams = new URLSearchParams(querystring); 145 | const newParams: Bag = {}; 146 | for (const [k, v] of oldParams) { 147 | newParams[k] = v; 148 | } 149 | return newParams; 150 | } 151 | -------------------------------------------------------------------------------- /src/NextjsBucketDeployment.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'node:path'; 2 | import { CustomResource, Duration, Token } from 'aws-cdk-lib'; 3 | import { Code, Function } from 'aws-cdk-lib/aws-lambda'; 4 | import { IBucket } from 'aws-cdk-lib/aws-s3'; 5 | import { Asset } from 'aws-cdk-lib/aws-s3-assets'; 6 | import { Construct } from 'constructs'; 7 | import { OptionalCustomResourceProps, OptionalFunctionProps } from './generated-structs'; 8 | import { getCommonFunctionProps } from './utils/common-lambda-props'; 9 | 10 | export interface NextjsBucketDeploymentOverrides { 11 | readonly functionProps?: OptionalFunctionProps; 12 | readonly customResourceProps?: OptionalCustomResourceProps; 13 | } 14 | 15 | export interface NextjsBucketDeploymentProps { 16 | /** 17 | * Source `Asset` 18 | */ 19 | readonly asset: Asset; 20 | /** 21 | * Enable verbose output of Custom Resource Lambda 22 | * @default false 23 | */ 24 | readonly debug?: boolean | undefined; 25 | /** 26 | * If `true`, then delete old objects in `destinationBucket`/`destinationKeyPrefix` 27 | * **after** uploading new objects. Only applies if `zip` is `false`. 28 | * 29 | * Old objects are determined by listing objects 30 | * in bucket before creating new objects and finding the objects that aren't in 31 | * the new objects. 32 | * 33 | * Note, if this is set to true then clients who have old HTML files (browser tabs opened before deployment) 34 | * will reference JS, CSS files that do not exist in S3 reslting in 404s. 35 | * @default false 36 | */ 37 | readonly prune?: boolean | undefined; 38 | /** 39 | * Mapping of files to PUT options for `PutObjectCommand`. Keys of 40 | * record must be a glob pattern (uses micromatch). Values of record are options 41 | * for PUT command for AWS SDK JS V3. See [here](https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/Package/-aws-sdk-client-s3/Interface/PutObjectRequest/) 42 | * for options. If a file matches multiple globs, configuration will be 43 | * merged. Later entries override earlier entries. 44 | * 45 | * `Bucket`, `Key`, and `Body` PUT options cannot be set. 46 | */ 47 | readonly putConfig?: Record>; 48 | /** 49 | * Destination S3 Bucket 50 | */ 51 | readonly destinationBucket: IBucket; 52 | /** 53 | * Destination S3 Bucket Key Prefix 54 | */ 55 | readonly destinationKeyPrefix?: string | undefined; 56 | /** 57 | * Override props for every construct. 58 | */ 59 | readonly overrides?: NextjsBucketDeploymentOverrides; 60 | /** 61 | * Replace placeholders in all files in `asset`. Placeholder targets are 62 | * defined by keys of record. Values to replace placeholders with are defined 63 | * by values of record. 64 | */ 65 | readonly substitutionConfig?: Record; 66 | /** 67 | * If `true` then files will be zipped before writing to destination bucket. 68 | * 69 | * Useful for Lambda functions. 70 | * @default false 71 | */ 72 | readonly zip?: boolean | undefined; 73 | /** 74 | * The number of files to upload in parallel. 75 | */ 76 | readonly queueSize?: number | undefined; 77 | } 78 | 79 | /** 80 | * @internal 81 | */ 82 | export interface CustomResourceProperties { 83 | destinationBucketName: string; 84 | destinationKeyPrefix?: string; 85 | prune?: boolean | undefined; 86 | putConfig?: NextjsBucketDeploymentProps['putConfig']; 87 | queueSize?: number | undefined; 88 | substitutionConfig?: NextjsBucketDeploymentProps['substitutionConfig']; 89 | sourceBucketName: string; 90 | sourceKeyPrefix?: string | undefined; 91 | zip?: boolean | undefined; 92 | } 93 | 94 | /** 95 | * Similar to CDK's `BucketDeployment` construct, but with a focus on replacing 96 | * template placeholders (i.e. environment variables) and configuring PUT 97 | * options like cache control. 98 | */ 99 | export class NextjsBucketDeployment extends Construct { 100 | /** 101 | * Formats a string as a template value so custom resource knows to replace. 102 | */ 103 | static getSubstitutionValue(v: string): string { 104 | return `{{ ${v} }}`; 105 | } 106 | /** 107 | * Creates `substitutionConfig` an object by extracting unresolved tokens. 108 | */ 109 | static getSubstitutionConfig(env: Record): Record { 110 | const substitutionConfig: Record = {}; 111 | for (const [k, v] of Object.entries(env)) { 112 | if (Token.isUnresolved(v)) { 113 | substitutionConfig[NextjsBucketDeployment.getSubstitutionValue(k)] = v; 114 | } 115 | } 116 | return substitutionConfig; 117 | } 118 | /** 119 | * Lambda Function Provider for Custom Resource 120 | */ 121 | function: Function; 122 | private props: NextjsBucketDeploymentProps; 123 | 124 | constructor(scope: Construct, id: string, props: NextjsBucketDeploymentProps) { 125 | super(scope, id); 126 | this.props = props; 127 | this.function = this.createFunction(); 128 | this.createCustomResource(this.function.functionArn); 129 | } 130 | 131 | private createFunction() { 132 | const fn = new Function(this, 'Fn', { 133 | ...getCommonFunctionProps(this), 134 | code: Code.fromAsset(path.resolve(__dirname, '..', 'assets', 'lambdas', 'nextjs-bucket-deployment')), 135 | handler: 'index.handler', 136 | timeout: Duration.minutes(5), 137 | ...this.props.overrides?.functionProps, 138 | }); 139 | if (this.props.debug) { 140 | fn.addEnvironment('DEBUG', '1'); 141 | } 142 | this.props.asset.grantRead(fn); 143 | this.props.destinationBucket.grantReadWrite(fn); 144 | return fn; 145 | } 146 | 147 | private createCustomResource(serviceToken: string) { 148 | const properties: CustomResourceProperties = { 149 | sourceBucketName: this.props.asset.s3BucketName, 150 | sourceKeyPrefix: this.props.asset.s3ObjectKey, 151 | destinationBucketName: this.props.destinationBucket.bucketName, 152 | destinationKeyPrefix: this.props.destinationKeyPrefix, 153 | putConfig: this.props.putConfig, 154 | prune: this.props.prune ?? false, 155 | substitutionConfig: this.props.substitutionConfig, 156 | zip: this.props.zip, 157 | queueSize: this.props.queueSize, 158 | }; 159 | return new CustomResource(this, 'CustomResource', { 160 | properties, 161 | resourceType: 'Custom::NextjsBucketDeployment', 162 | serviceToken, 163 | ...this.props.overrides?.customResourceProps, 164 | }); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/NextjsServer.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'node:crypto'; 2 | import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; 3 | import { tmpdir } from 'node:os'; 4 | import { resolve } from 'node:path'; 5 | import { Stack } from 'aws-cdk-lib'; 6 | import { Code, Function, FunctionOptions } from 'aws-cdk-lib/aws-lambda'; 7 | import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3'; 8 | import { Asset } from 'aws-cdk-lib/aws-s3-assets'; 9 | import { Construct } from 'constructs'; 10 | import { CACHE_BUCKET_KEY_PREFIX } from './constants'; 11 | import { OptionalAssetProps, OptionalFunctionProps, OptionalNextjsBucketDeploymentProps } from './generated-structs'; 12 | import { NextjsProps } from './Nextjs'; 13 | import { NextjsBucketDeployment } from './NextjsBucketDeployment'; 14 | import { NextjsBuild } from './NextjsBuild'; 15 | import { getCommonFunctionProps } from './utils/common-lambda-props'; 16 | import { createArchive } from './utils/create-archive'; 17 | 18 | export interface NextjsServerOverrides { 19 | readonly sourceCodeAssetProps?: OptionalAssetProps; 20 | readonly destinationCodeAssetProps?: OptionalAssetProps; 21 | readonly functionProps?: OptionalFunctionProps; 22 | readonly nextjsBucketDeploymentProps?: OptionalNextjsBucketDeploymentProps; 23 | } 24 | 25 | export type EnvironmentVars = Record; 26 | 27 | export interface NextjsServerProps { 28 | /** 29 | * @see {@link NextjsProps.environment} 30 | */ 31 | readonly environment?: NextjsProps['environment']; 32 | /** 33 | * Override function properties. 34 | */ 35 | readonly lambda?: FunctionOptions; 36 | /** 37 | * @see {@link NextjsBuild} 38 | */ 39 | readonly nextBuild: NextjsBuild; 40 | /** 41 | * Override props for every construct. 42 | */ 43 | readonly overrides?: NextjsServerOverrides; 44 | /** 45 | * @see {@link NextjsProps.quiet} 46 | */ 47 | readonly quiet?: NextjsProps['quiet']; 48 | /** 49 | * Static asset bucket. Function needs bucket to read from cache. 50 | */ 51 | readonly staticAssetBucket: IBucket; 52 | } 53 | 54 | /** 55 | * Build a lambda function from a NextJS application to handle server-side rendering, API routes, and image optimization. 56 | */ 57 | export class NextjsServer extends Construct { 58 | configBucket?: Bucket; 59 | lambdaFunction: Function; 60 | 61 | private props: NextjsServerProps; 62 | private get environment(): Record { 63 | return { 64 | ...this.props.environment, 65 | ...this.props.lambda?.environment, 66 | CACHE_BUCKET_NAME: this.props.staticAssetBucket.bucketName, 67 | CACHE_BUCKET_REGION: Stack.of(this.props.staticAssetBucket).region, 68 | CACHE_BUCKET_KEY_PREFIX, 69 | }; 70 | } 71 | 72 | constructor(scope: Construct, id: string, props: NextjsServerProps) { 73 | super(scope, id); 74 | this.props = props; 75 | 76 | // must create code asset separately (typically it is implicitly created in 77 | //`Function` construct) b/c we need to substitute unresolved env vars 78 | const sourceAsset = this.createSourceCodeAsset(); 79 | // source and destination assets are defined separately so that source 80 | // assets are immutable (easier debugging). Technically we could overwrite 81 | // source asset 82 | const destinationAsset = this.createDestinationCodeAsset(); 83 | const bucketDeployment = this.createBucketDeployment(sourceAsset, destinationAsset); 84 | this.lambdaFunction = this.createFunction(destinationAsset); 85 | // don't update lambda function until bucket deployment is complete 86 | this.lambdaFunction.node.addDependency(bucketDeployment); 87 | } 88 | 89 | private createSourceCodeAsset() { 90 | const archivePath = createArchive({ 91 | directory: this.props.nextBuild.nextServerFnDir, 92 | quiet: this.props.quiet, 93 | zipFileName: 'server-fn.zip', 94 | }); 95 | const asset = new Asset(this, 'SourceCodeAsset', { 96 | path: archivePath, 97 | ...this.props.overrides?.sourceCodeAssetProps, 98 | }); 99 | // new Asset() creates copy of zip into cdk.out/. This cleans up tmp folder 100 | rmSync(archivePath, { recursive: true }); 101 | return asset; 102 | } 103 | 104 | private createDestinationCodeAsset() { 105 | // create dummy directory to upload with random values so it's uploaded each time 106 | // TODO: look into caching? 107 | const assetsTmpDir = mkdtempSync(resolve(tmpdir(), 'bucket-deployment-dest-asset-')); 108 | // this code will never run b/c we explicitly declare dependency between 109 | // lambda function and bucket deployment. 110 | writeFileSync(resolve(assetsTmpDir, 'index.mjs'), `export function handler() { return '${randomUUID()}' }`); 111 | const destinationAsset = new Asset(this, 'DestinationCodeAsset', { 112 | path: assetsTmpDir, 113 | ...this.props.overrides?.destinationCodeAssetProps, 114 | }); 115 | rmSync(assetsTmpDir, { recursive: true }); 116 | return destinationAsset; 117 | } 118 | 119 | private createBucketDeployment(sourceAsset: Asset, destinationAsset: Asset) { 120 | const bucketDeployment = new NextjsBucketDeployment(this, 'BucketDeployment', { 121 | asset: sourceAsset, 122 | debug: true, 123 | destinationBucket: destinationAsset.bucket, 124 | destinationKeyPrefix: destinationAsset.s3ObjectKey, 125 | prune: false, // not applicable b/c zip: true 126 | // this.props.environment is for build time, not this.environment which is for runtime 127 | substitutionConfig: NextjsBucketDeployment.getSubstitutionConfig(this.props.environment || {}), 128 | zip: true, 129 | ...this.props.overrides?.nextjsBucketDeploymentProps, 130 | }); 131 | return bucketDeployment; 132 | } 133 | 134 | private createFunction(asset: Asset) { 135 | // until after the build time env vars in code zip asset are substituted 136 | const fn = new Function(this, 'Fn', { 137 | ...getCommonFunctionProps(this), 138 | code: Code.fromBucket(asset.bucket, asset.s3ObjectKey), 139 | handler: 'index.handler', 140 | description: 'Next.js Server Handler', 141 | ...this.props.lambda, 142 | // `environment` needs to go after `this.props.lambda` b/c if 143 | // `this.props.lambda.environment` is defined, it will override 144 | // CACHE_* environment variables which are required 145 | environment: { ...this.environment, ...this.props.lambda?.environment }, 146 | ...this.props.overrides?.functionProps, 147 | }); 148 | this.props.staticAssetBucket.grantReadWrite(fn); 149 | 150 | return fn; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalProviderProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_ec2, aws_iam, aws_lambda, aws_logs, custom_resources, Duration, interfaces } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalProviderProps 6 | */ 7 | export interface OptionalProviderProps { 8 | /** 9 | * Defines what execution history events of the waiter state machine are logged and where they are logged. 10 | * @default - A default log group will be created if logging for the waiter state machine is enabled. 11 | * @stability stable 12 | */ 13 | readonly waiterStateMachineLogOptions?: custom_resources.LogOptions; 14 | /** 15 | * Which subnets from the VPC to place the lambda functions in. 16 | * Only used if 'vpc' is supplied. Note: internet access for Lambdas 17 | * requires a NAT gateway, so picking Public subnets is not allowed. 18 | * @default - the Vpc default strategy if not specified 19 | * @stability stable 20 | */ 21 | readonly vpcSubnets?: aws_ec2.SubnetSelection; 22 | /** 23 | * The vpc to provision the lambda functions in. 24 | * @default - functions are not provisioned inside a vpc. 25 | * @stability stable 26 | */ 27 | readonly vpc?: aws_ec2.IVpc; 28 | /** 29 | * Total timeout for the entire operation. 30 | * The maximum timeout is 1 hour (yes, it can exceed the AWS Lambda 15 minutes) 31 | * @default Duration.minutes(30) 32 | * @stability stable 33 | */ 34 | readonly totalTimeout?: Duration; 35 | /** 36 | * Security groups to attach to the provider functions. 37 | * Only used if 'vpc' is supplied 38 | * @default - If `vpc` is not supplied, no security groups are attached. Otherwise, a dedicated security 39 | group is created for each function. 40 | * @stability stable 41 | */ 42 | readonly securityGroups?: Array; 43 | /** 44 | * AWS Lambda execution role. 45 | * The role is shared by provider framework's onEvent, isComplete lambda, and onTimeout Lambda functions. 46 | * This role will be assumed by the AWS Lambda, so it must be assumable by the 'lambda.amazonaws.com' 47 | * service principal. 48 | * @default - A default role will be created. 49 | * @deprecated - Use frameworkOnEventRole, frameworkCompleteAndTimeoutRole 50 | * @stability deprecated 51 | */ 52 | readonly role?: aws_iam.IRole; 53 | /** 54 | * Time between calls to the `isComplete` handler which determines if the resource has been stabilized. 55 | * The first `isComplete` will be called immediately after `handler` and then 56 | * every `queryInterval` seconds, and until `timeout` has been reached or until 57 | * `isComplete` returns `true`. 58 | * @default Duration.seconds(5) 59 | * @stability stable 60 | */ 61 | readonly queryInterval?: Duration; 62 | /** 63 | * Provider Lambda name. 64 | * The provider lambda function name. 65 | * @default - CloudFormation default name from unique physical ID 66 | * @stability stable 67 | */ 68 | readonly providerFunctionName?: string; 69 | /** 70 | * AWS KMS key used to encrypt provider lambda's environment variables. 71 | * @default - AWS Lambda creates and uses an AWS managed customer master key (CMK) 72 | * @stability stable 73 | */ 74 | readonly providerFunctionEnvEncryption?: interfaces.aws_kms.IKeyRef; 75 | /** 76 | * The number of days framework log events are kept in CloudWatch Logs. 77 | * When 78 | * updating this property, unsetting it doesn't remove the log retention policy. 79 | * To remove the retention policy, set the value to `INFINITE`. 80 | * 81 | * This is a legacy API and we strongly recommend you migrate to `logGroup` if you can. 82 | * `logGroup` allows you to create a fully customizable log group and instruct the Lambda function to send logs to it. 83 | * @default logs.RetentionDays.INFINITE 84 | * @stability stable 85 | */ 86 | readonly logRetention?: aws_logs.RetentionDays; 87 | /** 88 | * The Log Group used for logging of events emitted by the custom resource's lambda function. 89 | * Providing a user-controlled log group was rolled out to commercial regions on 2023-11-16. 90 | * If you are deploying to another type of region, please check regional availability first. 91 | * @default - a default log group created by AWS Lambda 92 | * @stability stable 93 | */ 94 | readonly logGroup?: aws_logs.ILogGroup; 95 | /** 96 | * The AWS Lambda function to invoke in order to determine if the operation is complete. 97 | * This function will be called immediately after `onEvent` and then 98 | * periodically based on the configured query interval as long as it returns 99 | * `false`. If the function still returns `false` and the alloted timeout has 100 | * passed, the operation will fail. 101 | * @default - provider is synchronous. This means that the `onEvent` handler 102 | is expected to finish all lifecycle operations within the initial invocation. 103 | * @stability stable 104 | */ 105 | readonly isCompleteHandler?: aws_lambda.IFunction; 106 | /** 107 | * Lambda execution role for provider framework's onEvent Lambda function. 108 | * Note that this role must be assumed 109 | * by the 'lambda.amazonaws.com' service principal. 110 | * 111 | * This property cannot be used with 'role' property 112 | * @default - A default role will be created. 113 | * @stability stable 114 | */ 115 | readonly frameworkOnEventRole?: aws_iam.IRole; 116 | /** 117 | * Log level of the provider framework lambda. 118 | * @default true - Logging is disabled by default 119 | * @stability stable 120 | */ 121 | readonly frameworkLambdaLoggingLevel?: aws_lambda.ApplicationLogLevel; 122 | /** 123 | * Lambda execution role for provider framework's isComplete/onTimeout Lambda function. 124 | * Note that this role 125 | * must be assumed by the 'lambda.amazonaws.com' service principal. To prevent circular dependency problem 126 | * in the provider framework, please ensure you specify a different IAM Role for 'frameworkCompleteAndTimeoutRole' 127 | * from 'frameworkOnEventRole'. 128 | * 129 | * This property cannot be used with 'role' property 130 | * @default - A default role will be created. 131 | * @stability stable 132 | */ 133 | readonly frameworkCompleteAndTimeoutRole?: aws_iam.IRole; 134 | /** 135 | * Whether logging for the waiter state machine is disabled. 136 | * @default - true 137 | * @stability stable 138 | */ 139 | readonly disableWaiterStateMachineLogging?: boolean; 140 | /** 141 | * The AWS Lambda function to invoke for all resource lifecycle operations (CREATE/UPDATE/DELETE). 142 | * This function is responsible to begin the requested resource operation 143 | * (CREATE/UPDATE/DELETE) and return any additional properties to add to the 144 | * event, which will later be passed to `isComplete`. The `PhysicalResourceId` 145 | * property must be included in the response. 146 | * @stability stable 147 | */ 148 | readonly onEventHandler?: aws_lambda.IFunction; 149 | } 150 | -------------------------------------------------------------------------------- /src/NextjsRevalidation.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import { CustomResource, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; 3 | import { AttributeType, Billing, TableV2 as Table } from 'aws-cdk-lib/aws-dynamodb'; 4 | import { AnyPrincipal, Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; 5 | import { Code, Function as LambdaFunction, FunctionOptions } from 'aws-cdk-lib/aws-lambda'; 6 | import { SqsEventSource } from 'aws-cdk-lib/aws-lambda-event-sources'; 7 | import { RetentionDays } from 'aws-cdk-lib/aws-logs'; 8 | import { Queue, QueueProps } from 'aws-cdk-lib/aws-sqs'; 9 | import { Provider } from 'aws-cdk-lib/custom-resources'; 10 | import { Construct } from 'constructs'; 11 | import { 12 | OptionalCustomResourceProps, 13 | OptionalFunctionProps, 14 | OptionalProviderProps, 15 | OptionalTablePropsV2, 16 | } from './generated-structs'; 17 | import { NextjsBuild } from './NextjsBuild'; 18 | import { NextjsServer } from './NextjsServer'; 19 | import { getCommonFunctionProps } from './utils/common-lambda-props'; 20 | 21 | export interface NextjsRevalidationOverrides { 22 | readonly queueProps?: QueueProps; 23 | readonly queueFunctionProps?: OptionalFunctionProps; 24 | readonly tableProps?: OptionalTablePropsV2; 25 | readonly insertFunctionProps?: OptionalFunctionProps; 26 | readonly insertProviderProps?: OptionalProviderProps; 27 | readonly insertCustomResourceProps?: OptionalCustomResourceProps; 28 | } 29 | 30 | export interface NextjsRevalidationProps { 31 | /** 32 | * Override function properties. 33 | */ 34 | readonly lambdaOptions?: FunctionOptions; 35 | /** 36 | * @see {@link NextjsBuild} 37 | */ 38 | readonly nextBuild: NextjsBuild; 39 | /** 40 | * Override props for every construct. 41 | */ 42 | readonly overrides?: NextjsRevalidationOverrides; 43 | /** 44 | * @see {@link NextjsServer} 45 | */ 46 | readonly serverFunction: NextjsServer; 47 | } 48 | 49 | /** 50 | * Builds the system for revalidating Next.js resources. This includes a Lambda function handler and queue system as well 51 | * as the DynamoDB table and provider function. 52 | * 53 | * @see {@link https://github.com/serverless-stack/open-next/blob/main/README.md?plain=1#L65} 54 | * 55 | */ 56 | export class NextjsRevalidation extends Construct { 57 | queue: Queue; 58 | table: Table; 59 | queueFunction: LambdaFunction; 60 | tableFunction: LambdaFunction | undefined; 61 | private props: NextjsRevalidationProps; 62 | 63 | constructor(scope: Construct, id: string, props: NextjsRevalidationProps) { 64 | super(scope, id); 65 | this.props = props; 66 | 67 | this.queue = this.createQueue(); 68 | this.queueFunction = this.createQueueFunction(); 69 | 70 | this.table = this.createRevalidationTable(); 71 | this.tableFunction = this.createRevalidationInsertFunction(this.table); 72 | 73 | this.props.serverFunction.lambdaFunction.addEnvironment('CACHE_DYNAMO_TABLE', this.table.tableName); 74 | 75 | if (this.props.serverFunction.lambdaFunction.role) { 76 | this.table.grantReadWriteData(this.props.serverFunction.lambdaFunction.role); 77 | } 78 | 79 | this.props.serverFunction.lambdaFunction // allow server fn to send messages to queue 80 | ?.addEnvironment('REVALIDATION_QUEUE_URL', this.queue.queueUrl); 81 | props.serverFunction.lambdaFunction?.addEnvironment('REVALIDATION_QUEUE_REGION', Stack.of(this).region); 82 | } 83 | 84 | private createQueue(): Queue { 85 | const queue = new Queue(this, 'Queue', { 86 | fifo: true, 87 | receiveMessageWaitTime: Duration.seconds(20), 88 | ...this.props.overrides?.queueProps, 89 | }); 90 | // https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-least-privilege-policy.html 91 | queue.addToResourcePolicy( 92 | new PolicyStatement({ 93 | sid: 'DenyUnsecureTransport', 94 | actions: ['sqs:*'], 95 | effect: Effect.DENY, 96 | principals: [new AnyPrincipal()], 97 | resources: [queue.queueArn], 98 | conditions: { 99 | Bool: { 'aws:SecureTransport': 'false' }, 100 | }, 101 | }) 102 | ); 103 | // Allow server to send messages to the queue 104 | queue.grantSendMessages(this.props.serverFunction.lambdaFunction); 105 | return queue; 106 | } 107 | 108 | private createQueueFunction(): LambdaFunction { 109 | const commonFnProps = getCommonFunctionProps(this); 110 | const fn = new LambdaFunction(this, 'QueueFn', { 111 | ...commonFnProps, 112 | // open-next revalidation-function 113 | // see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L65 114 | code: Code.fromAsset(this.props.nextBuild.nextRevalidateFnDir), 115 | handler: 'index.handler', 116 | description: 'Next.js Queue Revalidation Function', 117 | timeout: Duration.seconds(30), 118 | ...this.props.overrides?.queueFunctionProps, 119 | }); 120 | fn.addEventSource(new SqsEventSource(this.queue, { batchSize: 5 })); 121 | return fn; 122 | } 123 | 124 | private createRevalidationTable() { 125 | return new Table(this, 'Table', { 126 | partitionKey: { name: 'tag', type: AttributeType.STRING }, 127 | sortKey: { name: 'path', type: AttributeType.STRING }, 128 | billing: Billing.onDemand(), 129 | globalSecondaryIndexes: [ 130 | { 131 | indexName: 'revalidate', 132 | partitionKey: { name: 'path', type: AttributeType.STRING }, 133 | sortKey: { name: 'revalidatedAt', type: AttributeType.NUMBER }, 134 | }, 135 | ], 136 | removalPolicy: RemovalPolicy.DESTROY, 137 | ...this.props.overrides?.tableProps, 138 | }); 139 | } 140 | 141 | /** 142 | * This function will insert the initial batch of tag / path / revalidation data into the DynamoDB table during deployment. 143 | * @see: {@link https://open-next.js.org/inner_workings/isr#tags} 144 | * 145 | * @param revalidationTable table to grant function access to 146 | * @returns the revalidation insert provider function 147 | */ 148 | private createRevalidationInsertFunction(revalidationTable: Table) { 149 | const dynamodbProviderPath = this.props.nextBuild.nextRevalidateDynamoDBProviderFnDir; 150 | 151 | // note the function may not exist - it only exists if there are cache tags values defined in Next.js build meta files to be inserted 152 | // see: https://github.com/sst/open-next/blob/c2b05e3a5f82de40da1181e11c087265983c349d/packages/open-next/src/build.ts#L426-L458 153 | if (fs.existsSync(dynamodbProviderPath)) { 154 | const commonFnProps = getCommonFunctionProps(this); 155 | const insertFn = new LambdaFunction(this, 'DynamoDBProviderFn', { 156 | ...commonFnProps, 157 | // open-next revalidation-function 158 | // see: https://github.com/serverless-stack/open-next/blob/274d446ed7e940cfbe7ce05a21108f4c854ee37a/README.md?plain=1#L65 159 | code: Code.fromAsset(this.props.nextBuild.nextRevalidateDynamoDBProviderFnDir), 160 | handler: 'index.handler', 161 | description: 'Next.js Revalidation DynamoDB Provider', 162 | timeout: Duration.minutes(1), 163 | environment: { 164 | CACHE_DYNAMO_TABLE: revalidationTable.tableName, 165 | }, 166 | ...this.props.overrides?.insertFunctionProps, 167 | }); 168 | 169 | revalidationTable.grantReadWriteData(insertFn); 170 | 171 | const provider = new Provider(this, 'DynamoDBProvider', { 172 | onEventHandler: insertFn, 173 | logRetention: RetentionDays.ONE_DAY, 174 | ...this.props.overrides?.insertProviderProps, 175 | }); 176 | 177 | new CustomResource(this, 'DynamoDBResource', { 178 | serviceToken: provider.serviceToken, 179 | properties: { 180 | version: Date.now().toString(), 181 | }, 182 | ...this.props.overrides?.insertCustomResourceProps, 183 | }); 184 | 185 | return insertFn; 186 | } 187 | 188 | return undefined; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/NextjsBuild.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { Stack, Token } from 'aws-cdk-lib'; 5 | import { Construct } from 'constructs'; 6 | import { 7 | NEXTJS_BUILD_DIR, 8 | NEXTJS_BUILD_DYNAMODB_PROVIDER_FN_DIR, 9 | NEXTJS_BUILD_IMAGE_FN_DIR, 10 | NEXTJS_BUILD_REVALIDATE_FN_DIR, 11 | NEXTJS_BUILD_SERVER_FN_DIR, 12 | NEXTJS_CACHE_DIR, 13 | NEXTJS_STATIC_DIR, 14 | } from './constants'; 15 | import type { NextjsProps } from './Nextjs'; 16 | import { NextjsBucketDeployment } from './NextjsBucketDeployment'; 17 | import { listDirectory } from './utils/list-directories'; 18 | 19 | export interface NextjsBuildProps { 20 | /** 21 | * @see {@link NextjsProps.buildCommand} 22 | */ 23 | readonly buildCommand?: NextjsProps['buildCommand']; 24 | /** 25 | * @see {@link NextjsProps.buildPath} 26 | */ 27 | readonly buildPath?: NextjsProps['buildPath']; 28 | /** 29 | * @see {@link NextjsProps.environment} 30 | */ 31 | readonly environment?: NextjsProps['environment']; 32 | /** 33 | * @see {@link NextjsProps.nextjsPath} 34 | */ 35 | readonly nextjsPath: NextjsProps['nextjsPath']; 36 | /** 37 | * @see {@link NextjsProps.quiet} 38 | */ 39 | readonly quiet?: NextjsProps['quiet']; 40 | /** 41 | * @see {@link NextjsProps.skipBuild} 42 | */ 43 | readonly skipBuild?: NextjsProps['skipBuild']; 44 | /** 45 | * @see {@link NextjsProps.streaming} 46 | */ 47 | readonly streaming?: NextjsProps['streaming']; 48 | } 49 | 50 | /** 51 | * Build Next.js app. 52 | */ 53 | export class NextjsBuild extends Construct { 54 | /** 55 | * Contains server code and dependencies. 56 | */ 57 | public get nextServerFnDir(): string { 58 | const dir = path.join(this.getNextBuildDir(), NEXTJS_BUILD_SERVER_FN_DIR); 59 | this.warnIfMissing(dir); 60 | return dir; 61 | } 62 | /** 63 | * Contains function for processessing image requests. 64 | * Should be arm64. 65 | */ 66 | public get nextImageFnDir(): string { 67 | const fnPath = path.join(this.getNextBuildDir(), NEXTJS_BUILD_IMAGE_FN_DIR); 68 | this.warnIfMissing(fnPath); 69 | return fnPath; 70 | } 71 | /** 72 | * Contains function for processing items from revalidation queue. 73 | */ 74 | public get nextRevalidateFnDir(): string { 75 | const fnPath = path.join(this.getNextBuildDir(), NEXTJS_BUILD_REVALIDATE_FN_DIR); 76 | this.warnIfMissing(fnPath); 77 | return fnPath; 78 | } 79 | /** 80 | * Contains function for inserting revalidation items into the table. 81 | */ 82 | public get nextRevalidateDynamoDBProviderFnDir(): string { 83 | const fnPath = path.join(this.getNextBuildDir(), NEXTJS_BUILD_DYNAMODB_PROVIDER_FN_DIR); 84 | this.warnIfMissing(fnPath); 85 | return fnPath; 86 | } 87 | /** 88 | * Static files containing client-side code. 89 | */ 90 | public get nextStaticDir(): string { 91 | const dir = path.join(this.getNextBuildDir(), NEXTJS_STATIC_DIR); 92 | this.warnIfMissing(dir); 93 | return dir; 94 | } 95 | /** 96 | * Cache directory for generated data. 97 | */ 98 | public get nextCacheDir(): string { 99 | const dir = path.join(this.getNextBuildDir(), NEXTJS_CACHE_DIR); 100 | this.warnIfMissing(dir); 101 | return dir; 102 | } 103 | 104 | public props: NextjsBuildProps; 105 | 106 | constructor(scope: Construct, id: string, props: NextjsBuildProps) { 107 | super(scope, id); 108 | this.props = props; 109 | this.validatePaths(); 110 | 111 | const bundlingRequired = Stack.of(this).bundlingRequired; 112 | const skipBuild = this.props.skipBuild; 113 | 114 | // for more info see docs/code-deployment-flow.md Conditional Build Logic section 115 | if (bundlingRequired) { 116 | // deploy/synth 117 | if (skipBuild) { 118 | this.assertBuildDirExists(true); 119 | } else { 120 | this.build(); 121 | } 122 | } else { 123 | // destroy 124 | this.mockNextBuildDir(); 125 | } 126 | } 127 | 128 | /** 129 | * Validate required paths/files for NextjsBuild 130 | */ 131 | private validatePaths() { 132 | const nextjsPath = this.props.nextjsPath; 133 | // validate site path exists 134 | if (!fs.existsSync(nextjsPath)) { 135 | throw new Error(`Invalid nextjsPath ${nextjsPath} - directory does not exist at "${path.resolve(nextjsPath)}"`); 136 | } 137 | // Ensure that the site has a build script defined 138 | if (!fs.existsSync(path.join(nextjsPath, 'package.json'))) { 139 | throw new Error(`No package.json found at "${nextjsPath}".`); 140 | } 141 | const packageJson = JSON.parse(fs.readFileSync(path.join(nextjsPath, 'package.json'), 'utf8')); 142 | if (!packageJson.scripts || !packageJson.scripts.build) { 143 | throw new Error(`No "build" script found within package.json in "${nextjsPath}".`); 144 | } 145 | } 146 | 147 | private build() { 148 | const buildPath = this.props.buildPath ?? this.props.nextjsPath; 149 | const buildCommand = this.props.buildCommand ?? `npx @opennextjs/aws@^3 build`; 150 | // run build 151 | if (!this.props.quiet) { 152 | console.debug(`Running "${buildCommand}" in`, buildPath); 153 | } 154 | // will throw if build fails - which is desired 155 | execSync(buildCommand, { 156 | cwd: buildPath, 157 | stdio: this.props.quiet ? 'ignore' : 'inherit', 158 | env: this.getBuildEnvVars(), 159 | }); 160 | } 161 | 162 | /** 163 | * Gets environment variables for build time (when `open-next build` is called). 164 | * Unresolved tokens are replace with placeholders like {{ TOKEN_NAME }} and 165 | * will be resolved later in `NextjsBucketDeployment` custom resource. 166 | */ 167 | private getBuildEnvVars() { 168 | const env: Record = {}; 169 | for (const [k, v] of Object.entries(process.env)) { 170 | if (v) { 171 | env[k] = v; 172 | } 173 | } 174 | for (const [k, v] of Object.entries(this.props.environment || {})) { 175 | // don't replace server only env vars for static assets 176 | if (Token.isUnresolved(v) && k.startsWith('NEXT_PUBLIC_')) { 177 | env[k] = NextjsBucketDeployment.getSubstitutionValue(k); 178 | } else { 179 | env[k] = v; 180 | } 181 | } 182 | return env; 183 | } 184 | 185 | readPublicFileList() { 186 | if (!fs.existsSync(this.nextStaticDir)) return []; 187 | return listDirectory(this.nextStaticDir).map((file) => path.join('/', path.relative(this.nextStaticDir, file))); 188 | } 189 | 190 | private assertBuildDirExists(throwIfMissing = true) { 191 | const dir = this.getNextBuildDir(); 192 | if (!fs.existsSync(dir)) { 193 | if (throwIfMissing) { 194 | throw new Error(`Build directory "${dir}" does not exist. Try removing skipBuild: true option.`); 195 | } 196 | return false; 197 | } 198 | return true; 199 | } 200 | 201 | private getNextBuildDir(): string { 202 | const dir = path.resolve(this.props.nextjsPath, NEXTJS_BUILD_DIR); 203 | this.warnIfMissing(dir); 204 | return dir; 205 | } 206 | 207 | private warnIfMissing(dir: string) { 208 | if (!fs.existsSync(dir)) { 209 | console.warn(`Warning: ${dir} does not exist.`); 210 | } 211 | } 212 | 213 | private mockNextBuildDir() { 214 | function createMockDirAndFile(dir: string) { 215 | fs.mkdirSync(dir, { recursive: true }); 216 | fs.writeFileSync(path.join(dir, 'package.json'), '{}', 'utf8'); 217 | } 218 | 219 | const buildDirExists = this.assertBuildDirExists(false); 220 | if (!buildDirExists) { 221 | // mock .open-next 222 | createMockDirAndFile(this.getNextBuildDir()); 223 | createMockDirAndFile(this.nextServerFnDir); 224 | createMockDirAndFile(this.nextImageFnDir); 225 | createMockDirAndFile(this.nextRevalidateFnDir); 226 | createMockDirAndFile(this.nextRevalidateDynamoDBProviderFnDir); 227 | createMockDirAndFile(this.nextStaticDir); 228 | createMockDirAndFile(this.nextCacheDir); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/generated-structs/OptionalDistributionProps.ts: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.ts and run "npx projen". 2 | import type { aws_certificatemanager, aws_cloudfront, aws_s3 } from 'aws-cdk-lib'; 3 | 4 | /** 5 | * OptionalDistributionProps 6 | */ 7 | export interface OptionalDistributionProps { 8 | /** 9 | * Unique identifier that specifies the AWS WAF web ACL to associate with this CloudFront distribution. 10 | * To specify a web ACL created using the latest version of AWS WAF, use the ACL ARN, for example 11 | * `arn:aws:wafv2:us-east-1:123456789012:global/webacl/ExampleWebACL/473e64fd-f30b-4765-81a0-62ad96dd167a`. 12 | * To specify a web ACL created using AWS WAF Classic, use the ACL ID, for example `473e64fd-f30b-4765-81a0-62ad96dd167a`. 13 | * @default - No AWS Web Application Firewall web access control list (web ACL). 14 | * @stability stable 15 | */ 16 | readonly webAclId?: string; 17 | /** 18 | * The SSL method CloudFront will use for your distribution. 19 | * Server Name Indication (SNI) - is an extension to the TLS computer networking protocol by which a client indicates 20 | * which hostname it is attempting to connect to at the start of the handshaking process. This allows a server to present 21 | * multiple certificates on the same IP address and TCP port number and hence allows multiple secure (HTTPS) websites 22 | * (or any other service over TLS) to be served by the same IP address without requiring all those sites to use the same certificate. 23 | * 24 | * CloudFront can use SNI to host multiple distributions on the same IP - which a large majority of clients will support. 25 | * 26 | * If your clients cannot support SNI however - CloudFront can use dedicated IPs for your distribution - but there is a prorated monthly charge for 27 | * using this feature. By default, we use SNI - but you can optionally enable dedicated IPs (VIP). 28 | * 29 | * See the CloudFront SSL for more details about pricing : https://aws.amazon.com/cloudfront/custom-ssl-domains/ 30 | * @default SSLMethod.SNI 31 | * @stability stable 32 | */ 33 | readonly sslSupportMethod?: aws_cloudfront.SSLMethod; 34 | /** 35 | * Whether to enable additional CloudWatch metrics. 36 | * @default false 37 | * @stability stable 38 | */ 39 | readonly publishAdditionalMetrics?: boolean; 40 | /** 41 | * The price class that corresponds with the maximum price that you want to pay for CloudFront service. 42 | * If you specify PriceClass_All, CloudFront responds to requests for your objects from all CloudFront edge locations. 43 | * If you specify a price class other than PriceClass_All, CloudFront serves your objects from the CloudFront edge location 44 | * that has the lowest latency among the edge locations in your price class. 45 | * @default PriceClass.PRICE_CLASS_ALL 46 | * @stability stable 47 | */ 48 | readonly priceClass?: aws_cloudfront.PriceClass; 49 | /** 50 | * The minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. 51 | * CloudFront serves your objects only to browsers or devices that support at 52 | * least the SSL version that you specify. 53 | * @default - SecurityPolicyProtocol.TLS_V1_2_2021 if the '@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021' feature flag is set; otherwise, SecurityPolicyProtocol.TLS_V1_2_2019. 54 | * @stability stable 55 | */ 56 | readonly minimumProtocolVersion?: aws_cloudfront.SecurityPolicyProtocol; 57 | /** 58 | * Specifies whether you want CloudFront to include cookies in access logs. 59 | * @default false 60 | * @stability stable 61 | */ 62 | readonly logIncludesCookies?: boolean; 63 | /** 64 | * An optional string that you want CloudFront to prefix to the access log filenames for this distribution. 65 | * @default - no prefix 66 | * @stability stable 67 | */ 68 | readonly logFilePrefix?: string; 69 | /** 70 | * The Amazon S3 bucket to store the access logs in. 71 | * Make sure to set `objectOwnership` to `s3.ObjectOwnership.OBJECT_WRITER` in your custom bucket. 72 | * @default - A bucket is created if `enableLogging` is true 73 | * @stability stable 74 | */ 75 | readonly logBucket?: aws_s3.IBucket; 76 | /** 77 | * Specify the maximum HTTP version that you want viewers to use to communicate with CloudFront. 78 | * For viewers and CloudFront to use HTTP/2, viewers must support TLS 1.2 or later, and must support server name identification (SNI). 79 | * @default HttpVersion.HTTP2 80 | * @stability stable 81 | */ 82 | readonly httpVersion?: aws_cloudfront.HttpVersion; 83 | /** 84 | * Controls the countries in which your content is distributed. 85 | * @default - No geographic restrictions 86 | * @stability stable 87 | */ 88 | readonly geoRestriction?: aws_cloudfront.GeoRestriction; 89 | /** 90 | * How CloudFront should handle requests that are not successful (e.g., PageNotFound). 91 | * @default - No custom error responses. 92 | * @stability stable 93 | */ 94 | readonly errorResponses?: Array; 95 | /** 96 | * Enable access logging for the distribution. 97 | * @default - false, unless `logBucket` is specified. 98 | * @stability stable 99 | */ 100 | readonly enableLogging?: boolean; 101 | /** 102 | * Whether CloudFront will respond to IPv6 DNS requests with an IPv6 address. 103 | * If you specify false, CloudFront responds to IPv6 DNS requests with the DNS response code NOERROR and with no IP addresses. 104 | * This allows viewers to submit a second request, for an IPv4 address for your distribution. 105 | * @default true 106 | * @stability stable 107 | */ 108 | readonly enableIpv6?: boolean; 109 | /** 110 | * Enable or disable the distribution. 111 | * @default true 112 | * @stability stable 113 | */ 114 | readonly enabled?: boolean; 115 | /** 116 | * Alternative domain names for this distribution. 117 | * If you want to use your own domain name, such as www.example.com, instead of the cloudfront.net domain name, 118 | * you can add an alternate domain name to your distribution. If you attach a certificate to the distribution, 119 | * you should add (at least one of) the domain names of the certificate to this list. 120 | * 121 | * When you want to move a domain name between distributions, you can associate a certificate without specifying any domain names. 122 | * For more information, see the _Moving an alternate domain name to a different distribution_ section in the README. 123 | * @default - The distribution will only support the default generated name (e.g., d111111abcdef8.cloudfront.net) 124 | * @stability stable 125 | */ 126 | readonly domainNames?: Array; 127 | /** 128 | * The object that you want CloudFront to request from your origin (for example, index.html) when a viewer requests the root URL for your distribution. If no default object is set, the request goes to the origin's root (e.g., example.com/). 129 | * @default - no default root object 130 | * @stability stable 131 | */ 132 | readonly defaultRootObject?: string; 133 | /** 134 | * Any comments you want to include about the distribution. 135 | * @default - no comment 136 | * @stability stable 137 | */ 138 | readonly comment?: string; 139 | /** 140 | * A certificate to associate with the distribution. 141 | * The certificate must be located in N. Virginia (us-east-1). 142 | * @default - the CloudFront wildcard certificate (*.cloudfront.net) will be used. 143 | * @stability stable 144 | */ 145 | readonly certificate?: aws_certificatemanager.ICertificate; 146 | /** 147 | * Additional behaviors for the distribution, mapped by the pathPattern that specifies which requests to apply the behavior to. 148 | * @default - no additional behaviors are added. 149 | * @stability stable 150 | */ 151 | readonly additionalBehaviors?: Record; 152 | /** 153 | * The default behavior for the distribution. 154 | * @stability stable 155 | */ 156 | readonly defaultBehavior?: aws_cloudfront.BehaviorOptions; 157 | } 158 | -------------------------------------------------------------------------------- /src/NextjsDomain.ts: -------------------------------------------------------------------------------- 1 | import { ICertificate, Certificate, CertificateValidation } from 'aws-cdk-lib/aws-certificatemanager'; 2 | import { Distribution } from 'aws-cdk-lib/aws-cloudfront'; 3 | import { 4 | ARecord, 5 | ARecordProps, 6 | AaaaRecord, 7 | AaaaRecordProps, 8 | HostedZone, 9 | IHostedZone, 10 | RecordTarget, 11 | } from 'aws-cdk-lib/aws-route53'; 12 | import { CloudFrontTarget } from 'aws-cdk-lib/aws-route53-targets'; 13 | import { Construct } from 'constructs'; 14 | import { NextjsProps } from '.'; 15 | import { 16 | OptionalAaaaRecordProps, 17 | OptionalCertificateProps, 18 | OptionalHostedZoneProviderProps, 19 | OptionalARecordProps, 20 | } from './generated-structs'; 21 | 22 | export interface NextjsDomainOverrides { 23 | readonly certificateProps?: OptionalCertificateProps; 24 | readonly hostedZoneProviderProps?: OptionalHostedZoneProviderProps; 25 | readonly aRecordProps?: OptionalARecordProps; 26 | readonly aaaaRecordProps?: OptionalAaaaRecordProps; 27 | } 28 | 29 | export interface NextjsDomainProps { 30 | /** 31 | * An easy to remember address of your website. Only supports domains hosted 32 | * on [Route 53](https://aws.amazon.com/route53/). Used as `domainName` for 33 | * ACM `Certificate` if {@link NextjsDomainProps.certificate} and 34 | * {@link NextjsDomainProps.certificateDomainName} are `undefined`. 35 | * @example "example.com" 36 | */ 37 | readonly domainName: string; 38 | /** 39 | * Alternate domain names that should route to the Cloudfront Distribution. 40 | * For example, if you specificied `"example.com"` as your {@link NextjsDomainProps.domainName}, 41 | * you could specify `["www.example.com", "api.example.com"]`. 42 | * Learn more about the [requirements](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-requirements) 43 | * and [restrictions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/CNAMEs.html#alternate-domain-names-restrictions) 44 | * for using alternate domain names with CloudFront. 45 | * 46 | * Note, in order to use alternate domain names, they must be covered by your 47 | * certificate. By default, the certificate created in this construct only covers 48 | * the {@link NextjsDomainProps.domainName}. Therefore, you'll need to specify 49 | * a wildcard domain name like `"*.example.com"` with {@link NextjsDomainProps.certificateDomainName} 50 | * so that this construct will create the certificate the covers the alternate 51 | * domain names. Otherwise, you can use {@link NextjsDomainProps.certificate} 52 | * to create the certificate yourself where you'll need to ensure it has a 53 | * wildcard or uses subject alternative names including the 54 | * alternative names specified here. 55 | * @example ["www.example.com", "api.example.com"] 56 | */ 57 | readonly alternateNames?: string[]; 58 | /** 59 | * You must create the hosted zone out-of-band. 60 | * You can lookup the hosted zone outside this construct and pass it in via this prop. 61 | * Alternatively if this prop is `undefined`, then the hosted zone will be 62 | * **looked up** (not created) via `HostedZone.fromLookup` with {@link NextjsDomainProps.domainName}. 63 | */ 64 | readonly hostedZone?: IHostedZone; 65 | /** 66 | * If this prop is `undefined` then an ACM `Certificate` will be created based on {@link NextjsDomainProps.domainName} 67 | * with DNS Validation. This prop allows you to control the TLS/SSL 68 | * certificate created. The certificate you create must be in the `us-east-1` 69 | * (N. Virginia) region as required by AWS CloudFront. 70 | * 71 | * Set this option if you have an existing certificate in the `us-east-1` region in AWS Certificate Manager you want to use. 72 | */ 73 | readonly certificate?: ICertificate; 74 | /** 75 | * The domain name used in this construct when creating an ACM `Certificate`. Useful 76 | * when passing {@link NextjsDomainProps.alternateNames} and you need to specify 77 | * a wildcard domain like "*.example.com". If `undefined`, then {@link NextjsDomainProps.domainName} 78 | * will be used. 79 | * 80 | * If {@link NextjsDomainProps.certificate} is passed, then this prop is ignored. 81 | */ 82 | readonly certificateDomainName?: string; 83 | /** 84 | * Override props for every construct. 85 | */ 86 | readonly overrides?: NextjsDomainOverrides; 87 | } 88 | 89 | /** 90 | * Use a custom domain with `Nextjs`. Requires a Route53 hosted zone to have been 91 | * created within the same AWS account. For DNS setups where you cannot use a 92 | * Route53 hosted zone in the same AWS account, use the `overrides.nextjsDistribution.distributionProps` 93 | * prop of {@link NextjsProps}. 94 | * 95 | * See {@link NextjsDomainProps} TS Doc comments for detailed docs on how to customize. 96 | * This construct is helpful to user to not have to worry about interdependencies 97 | * between Route53 Hosted Zone, CloudFront Distribution, and Route53 Hosted Zone Records. 98 | * 99 | * Note, if you're using another service for domain name registration, you can 100 | * still create a Route53 hosted zone. Please see [Configuring DNS Delegation from 101 | * CloudFlare to AWS Route53](https://veducate.co.uk/dns-delegation-route53/) 102 | * as an example. 103 | */ 104 | export class NextjsDomain extends Construct { 105 | /** 106 | * Concatentation of {@link NextjsDomainProps.domainName} and {@link NextjsDomainProps.alternateNames}. 107 | * Used in instantiation of CloudFront Distribution in NextjsDistribution 108 | */ 109 | get domainNames(): string[] { 110 | const names = [this.props.domainName]; 111 | if (this.props.alternateNames?.length) { 112 | names.push(...this.props.alternateNames); 113 | } 114 | return names; 115 | } 116 | /** 117 | * Route53 Hosted Zone. 118 | */ 119 | hostedZone: IHostedZone; 120 | /** 121 | * ACM Certificate. 122 | */ 123 | certificate: ICertificate; 124 | 125 | private props: NextjsDomainProps; 126 | 127 | constructor(scope: Construct, id: string, props: NextjsDomainProps) { 128 | super(scope, id); 129 | this.props = props; 130 | this.hostedZone = this.getHostedZone(); 131 | this.certificate = this.getCertificate(); 132 | } 133 | 134 | private getHostedZone(): IHostedZone { 135 | if (!this.props.hostedZone) { 136 | return HostedZone.fromLookup(this, 'HostedZone', { 137 | domainName: this.props.domainName, 138 | ...this.props.overrides?.hostedZoneProviderProps, 139 | }); 140 | } else { 141 | return this.props.hostedZone; 142 | } 143 | } 144 | 145 | private getCertificate(): ICertificate { 146 | if (!this.props.certificate) { 147 | return new Certificate(this, 'Certificate', { 148 | domainName: this.props.certificateDomainName ?? this.props.domainName, 149 | validation: CertificateValidation.fromDns(this.hostedZone), 150 | ...this.props.overrides?.certificateProps, 151 | }); 152 | } else { 153 | return this.props.certificate; 154 | } 155 | } 156 | 157 | /** 158 | * Creates DNS records (A and AAAA) records for {@link NextjsDomainProps.domainName} 159 | * and {@link NextjsDomainProps.alternateNames} if defined. 160 | */ 161 | createDnsRecords(distribution: Distribution): void { 162 | // Create DNS record 163 | const recordProps: ARecordProps & AaaaRecordProps = { 164 | recordName: this.props.domainName, 165 | zone: this.hostedZone, 166 | target: RecordTarget.fromAlias(new CloudFrontTarget(distribution)), 167 | }; 168 | new ARecord(this, 'ARecordMain', { 169 | ...recordProps, 170 | ...this.props.overrides?.aRecordProps, 171 | }); // IPv4 172 | new AaaaRecord(this, 'AaaaRecordMain', { 173 | ...recordProps, 174 | ...this.props.overrides?.aaaaRecordProps, 175 | }); // IPv6 176 | if (this.props.alternateNames?.length) { 177 | let i = 1; 178 | for (const alternateName of this.props.alternateNames) { 179 | new ARecord(this, 'ARecordAlt' + i, { 180 | ...recordProps, 181 | recordName: `${alternateName}.`, 182 | ...this.props.overrides?.aRecordProps, 183 | }); 184 | new AaaaRecord(this, 'AaaaRecordAlt' + i, { 185 | ...recordProps, 186 | recordName: `${alternateName}.`, 187 | ...this.props.overrides?.aaaaRecordProps, 188 | }); 189 | i++; 190 | } 191 | } 192 | } 193 | } 194 | --------------------------------------------------------------------------------