├── .eslintrc.json ├── .gitattributes ├── .github ├── pull_request_template.md └── workflows │ ├── auto-approve.yml │ ├── build.yml │ ├── pull-request-lint.yml │ ├── release.yml │ └── upgrade-main.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── API.md ├── LICENSE ├── README.md ├── bin └── build-serverless-nextjs.js ├── cdk.json ├── functions └── index-indirect.js ├── images └── rayova-fintech-corp.png ├── package.json ├── src ├── api-lambda.ts ├── assets-deployment.ts ├── constants.ts ├── default-lambda.ts ├── has-manifest.ts ├── image-lambda.ts ├── incremental-static-regeneration.ts ├── index.ts ├── lambda-at-edge-role.ts ├── nextjs-artifact.ts ├── serverless-nextjs.ts └── static-assets.ts ├── test ├── __snapshots__ │ └── snapshot.empty-artifact.test.ts.snap ├── create-nextjs-example.ts ├── integ.main.lit.ts ├── nextjs-artifact.test.ts ├── nextjs-test-files │ ├── api.js │ └── time.js ├── snapshot.empty-artifact.test.ts └── snapshot.empty-artifact.ts ├── tsconfig.dev.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true, 4 | "node": true 5 | }, 6 | "root": true, 7 | "plugins": [ 8 | "@typescript-eslint", 9 | "import" 10 | ], 11 | "parser": "@typescript-eslint/parser", 12 | "parserOptions": { 13 | "ecmaVersion": 2018, 14 | "sourceType": "module", 15 | "project": "./tsconfig.dev.json" 16 | }, 17 | "extends": [ 18 | "plugin:import/typescript" 19 | ], 20 | "settings": { 21 | "import/parsers": { 22 | "@typescript-eslint/parser": [ 23 | ".ts", 24 | ".tsx" 25 | ] 26 | }, 27 | "import/resolver": { 28 | "node": {}, 29 | "typescript": { 30 | "project": "./tsconfig.dev.json", 31 | "alwaysTryTypes": true 32 | } 33 | } 34 | }, 35 | "ignorePatterns": [ 36 | "*.js", 37 | "!.projenrc.js", 38 | "*.d.ts", 39 | "node_modules/", 40 | "*.generated.ts", 41 | "coverage" 42 | ], 43 | "rules": { 44 | "indent": [ 45 | "off" 46 | ], 47 | "@typescript-eslint/indent": [ 48 | "error", 49 | 2 50 | ], 51 | "quotes": [ 52 | "error", 53 | "single", 54 | { 55 | "avoidEscape": true 56 | } 57 | ], 58 | "comma-dangle": [ 59 | "error", 60 | "always-multiline" 61 | ], 62 | "comma-spacing": [ 63 | "error", 64 | { 65 | "before": false, 66 | "after": true 67 | } 68 | ], 69 | "no-multi-spaces": [ 70 | "error", 71 | { 72 | "ignoreEOLComments": false 73 | } 74 | ], 75 | "array-bracket-spacing": [ 76 | "error", 77 | "never" 78 | ], 79 | "array-bracket-newline": [ 80 | "error", 81 | "consistent" 82 | ], 83 | "object-curly-spacing": [ 84 | "error", 85 | "always" 86 | ], 87 | "object-curly-newline": [ 88 | "error", 89 | { 90 | "multiline": true, 91 | "consistent": true 92 | } 93 | ], 94 | "object-property-newline": [ 95 | "error", 96 | { 97 | "allowAllPropertiesOnSameLine": true 98 | } 99 | ], 100 | "keyword-spacing": [ 101 | "error" 102 | ], 103 | "brace-style": [ 104 | "error", 105 | "1tbs", 106 | { 107 | "allowSingleLine": true 108 | } 109 | ], 110 | "space-before-blocks": [ 111 | "error" 112 | ], 113 | "curly": [ 114 | "error", 115 | "multi-line", 116 | "consistent" 117 | ], 118 | "@typescript-eslint/member-delimiter-style": [ 119 | "error" 120 | ], 121 | "semi": [ 122 | "error", 123 | "always" 124 | ], 125 | "max-len": [ 126 | "error", 127 | { 128 | "code": 150, 129 | "ignoreUrls": true, 130 | "ignoreStrings": true, 131 | "ignoreTemplateLiterals": true, 132 | "ignoreComments": true, 133 | "ignoreRegExpLiterals": true 134 | } 135 | ], 136 | "quote-props": [ 137 | "error", 138 | "consistent-as-needed" 139 | ], 140 | "@typescript-eslint/no-require-imports": [ 141 | "error" 142 | ], 143 | "import/no-extraneous-dependencies": [ 144 | "error", 145 | { 146 | "devDependencies": [ 147 | "**/test/**", 148 | "**/build-tools/**" 149 | ], 150 | "optionalDependencies": false, 151 | "peerDependencies": true 152 | } 153 | ], 154 | "import/no-unresolved": [ 155 | "error" 156 | ], 157 | "import/order": [ 158 | "warn", 159 | { 160 | "groups": [ 161 | "builtin", 162 | "external" 163 | ], 164 | "alphabetize": { 165 | "order": "asc", 166 | "caseInsensitive": true 167 | } 168 | } 169 | ], 170 | "no-duplicate-imports": [ 171 | "error" 172 | ], 173 | "no-shadow": [ 174 | "off" 175 | ], 176 | "@typescript-eslint/no-shadow": [ 177 | "error" 178 | ], 179 | "key-spacing": [ 180 | "error" 181 | ], 182 | "no-multiple-empty-lines": [ 183 | "error" 184 | ], 185 | "@typescript-eslint/no-floating-promises": [ 186 | "error" 187 | ], 188 | "no-return-await": [ 189 | "off" 190 | ], 191 | "@typescript-eslint/return-await": [ 192 | "error" 193 | ], 194 | "no-trailing-spaces": [ 195 | "error" 196 | ], 197 | "dot-notation": [ 198 | "error" 199 | ], 200 | "no-bitwise": [ 201 | "error" 202 | ], 203 | "@typescript-eslint/member-ordering": [ 204 | "error", 205 | { 206 | "default": [ 207 | "public-static-field", 208 | "public-static-method", 209 | "protected-static-field", 210 | "protected-static-method", 211 | "private-static-field", 212 | "private-static-method", 213 | "field", 214 | "constructor", 215 | "method" 216 | ] 217 | } 218 | ] 219 | }, 220 | "overrides": [ 221 | { 222 | "files": [ 223 | ".projenrc.js" 224 | ], 225 | "rules": { 226 | "@typescript-eslint/no-require-imports": "off", 227 | "import/no-extraneous-dependencies": "off" 228 | } 229 | } 230 | ] 231 | } 232 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | *.snap linguist-generated 4 | /.eslintrc.json linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/auto-approve.yml 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 | /LICENSE linguist-generated 20 | /package.json linguist-generated 21 | /tsconfig.dev.json linguist-generated 22 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Fixes # -------------------------------------------------------------------------------- /.github/workflows/auto-approve.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: auto-approve 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | jobs: 13 | approve: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pull-requests: write 17 | if: contains(github.event.pull_request.labels.*.name, 'auto-approve') && (github.event.pull_request.user.login == 'rayova-bot') 18 | steps: 19 | - uses: hmarr/auto-approve-action@v2.1.0 20 | with: 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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@v2 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@v2.2.0 24 | with: 25 | node-version: "14" 26 | - name: Install dependencies 27 | run: yarn install --check-files 28 | - name: build 29 | run: npx projen build 30 | - id: self_mutation 31 | name: Find mutations 32 | run: |- 33 | git add . 34 | git diff --staged --patch --exit-code > .repo.patch || echo "::set-output name=self_mutation_happened::true" 35 | - if: steps.self_mutation.outputs.self_mutation_happened 36 | name: Upload patch 37 | uses: actions/upload-artifact@v2 38 | with: 39 | name: .repo.patch 40 | path: .repo.patch 41 | - name: Fail build on mutation 42 | if: steps.self_mutation.outputs.self_mutation_happened 43 | run: |- 44 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 45 | cat .repo.patch 46 | exit 1 47 | - name: Upload artifact 48 | uses: actions/upload-artifact@v2.1.1 49 | with: 50 | name: build-artifact 51 | path: dist 52 | container: 53 | image: jsii/superchain:1-buster-slim-node14 54 | self-mutation: 55 | needs: build 56 | runs-on: ubuntu-latest 57 | permissions: 58 | contents: write 59 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 60 | steps: 61 | - name: Checkout 62 | uses: actions/checkout@v2 63 | with: 64 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 65 | ref: ${{ github.event.pull_request.head.ref }} 66 | repository: ${{ github.event.pull_request.head.repo.full_name }} 67 | - name: Download patch 68 | uses: actions/download-artifact@v2 69 | with: 70 | name: .repo.patch 71 | path: ${{ runner.temp }} 72 | - name: Apply patch 73 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 74 | - name: Set git identity 75 | run: |- 76 | git config user.name "github-actions" 77 | git config user.email "github-actions@github.com" 78 | - name: Push changes 79 | run: |2- 80 | git add . 81 | git commit -s -m "chore: self mutation" 82 | git push origin HEAD:${{ github.event.pull_request.head.ref }} 83 | package-js: 84 | needs: build 85 | runs-on: ubuntu-latest 86 | permissions: {} 87 | if: "! needs.build.outputs.self_mutation_happened" 88 | steps: 89 | - uses: actions/setup-node@v2 90 | with: 91 | node-version: 14.x 92 | - name: Download build artifacts 93 | uses: actions/download-artifact@v2 94 | with: 95 | name: build-artifact 96 | path: dist 97 | - name: Prepare Repository 98 | run: mv dist .repo 99 | - name: Install Dependencies 100 | run: cd .repo && yarn install --check-files --frozen-lockfile 101 | - name: Create js artifact 102 | run: cd .repo && npx projen package:js 103 | - name: Collect js Artifact 104 | run: mv .repo/dist dist 105 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v3.4.6 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: release 4 | on: 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: {} 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | outputs: 15 | latest_commit: ${{ steps.git_remote.outputs.latest_commit }} 16 | env: 17 | CI: "true" 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 23 | - name: Set git identity 24 | run: |- 25 | git config user.name "github-actions" 26 | git config user.email "github-actions@github.com" 27 | - name: Setup Node.js 28 | uses: actions/setup-node@v2.2.0 29 | with: 30 | node-version: "14" 31 | - name: Install dependencies 32 | run: yarn install --check-files --frozen-lockfile 33 | - name: release 34 | run: npx projen release 35 | - name: Check for new commits 36 | id: git_remote 37 | run: echo ::set-output name=latest_commit::"$(git ls-remote origin -h ${{ github.ref }} | cut -f1)" 38 | - name: Upload artifact 39 | if: ${{ steps.git_remote.outputs.latest_commit == github.sha }} 40 | uses: actions/upload-artifact@v2.1.1 41 | with: 42 | name: build-artifact 43 | path: dist 44 | container: 45 | image: jsii/superchain:1-buster-slim-node14 46 | release_github: 47 | name: Publish to GitHub Releases 48 | needs: release 49 | runs-on: ubuntu-latest 50 | permissions: 51 | contents: write 52 | if: needs.release.outputs.latest_commit == github.sha 53 | steps: 54 | - uses: actions/setup-node@v2 55 | with: 56 | node-version: 14.x 57 | - name: Download build artifacts 58 | uses: actions/download-artifact@v2 59 | with: 60 | name: build-artifact 61 | path: dist 62 | - name: Prepare Repository 63 | run: mv dist .repo 64 | - name: Collect GitHub Metadata 65 | run: mv .repo/dist dist 66 | - name: Release 67 | 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 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | GITHUB_REPOSITORY: ${{ github.repository }} 71 | GITHUB_REF: ${{ github.ref }} 72 | release_npm: 73 | name: Publish to npm 74 | needs: release 75 | runs-on: ubuntu-latest 76 | permissions: 77 | contents: read 78 | if: needs.release.outputs.latest_commit == github.sha 79 | steps: 80 | - uses: actions/setup-node@v2 81 | with: 82 | node-version: 14.x 83 | - name: Download build artifacts 84 | uses: actions/download-artifact@v2 85 | with: 86 | name: build-artifact 87 | path: dist 88 | - name: Prepare Repository 89 | run: mv dist .repo 90 | - name: Install Dependencies 91 | run: cd .repo && yarn install --check-files --frozen-lockfile 92 | - name: Create js artifact 93 | run: cd .repo && npx projen package:js 94 | - name: Collect js Artifact 95 | run: mv .repo/dist dist 96 | - name: Release 97 | run: npx -p publib@latest publib-npm 98 | env: 99 | NPM_DIST_TAG: latest 100 | NPM_REGISTRY: registry.npmjs.org 101 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 102 | -------------------------------------------------------------------------------- /.github/workflows/upgrade-main.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade-main 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 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@v2 19 | with: 20 | ref: main 21 | - name: Setup Node.js 22 | uses: actions/setup-node@v2.2.0 23 | with: 24 | node-version: "14" 25 | - name: Install dependencies 26 | run: yarn install --check-files --frozen-lockfile 27 | - name: Upgrade dependencies 28 | run: npx projen upgrade 29 | - id: create_patch 30 | name: Find mutations 31 | run: |- 32 | git add . 33 | git diff --staged --patch --exit-code > .repo.patch || echo "::set-output name=patch_created::true" 34 | - if: steps.create_patch.outputs.patch_created 35 | name: Upload patch 36 | uses: actions/upload-artifact@v2 37 | with: 38 | name: .repo.patch 39 | path: .repo.patch 40 | container: 41 | image: jsii/superchain:1-buster-slim-node14 42 | pr: 43 | name: Create Pull Request 44 | needs: upgrade 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: write 48 | pull-requests: write 49 | if: ${{ needs.upgrade.outputs.patch_created }} 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v2 53 | with: 54 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 55 | ref: main 56 | - name: Download patch 57 | uses: actions/download-artifact@v2 58 | with: 59 | name: .repo.patch 60 | path: ${{ runner.temp }} 61 | - name: Apply patch 62 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 63 | - name: Set git identity 64 | run: |- 65 | git config user.name "github-actions" 66 | git config user.email "github-actions@github.com" 67 | - name: Create Pull Request 68 | id: create-pr 69 | uses: peter-evans/create-pull-request@v3 70 | with: 71 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 72 | commit-message: |- 73 | chore(deps): upgrade dependencies 74 | 75 | Upgrades project dependencies. See details in [workflow run]. 76 | 77 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 78 | 79 | ------ 80 | 81 | *Automatically created by projen via the "upgrade-main" workflow* 82 | branch: github-actions/upgrade-main 83 | title: "chore(deps): upgrade dependencies" 84 | labels: auto-approve 85 | body: |- 86 | Upgrades project dependencies. See details in [workflow run]. 87 | 88 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 89 | 90 | ------ 91 | 92 | *Automatically created by projen via the "upgrade-main" workflow* 93 | author: github-actions 94 | committer: github-actions 95 | signoff: true 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js 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 | !/.github/workflows/auto-approve.yml 8 | !/package.json 9 | !/LICENSE 10 | !/.npmignore 11 | logs 12 | *.log 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | lerna-debug.log* 17 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | lib-cov 23 | coverage 24 | *.lcov 25 | .nyc_output 26 | build/Release 27 | node_modules/ 28 | jspm_packages/ 29 | *.tsbuildinfo 30 | .eslintcache 31 | *.tgz 32 | .yarn-integrity 33 | .cache 34 | !/.projenrc.js 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/pull_request_template.md 44 | !/test/ 45 | !/tsconfig.dev.json 46 | !/src/ 47 | /lib 48 | /dist/ 49 | !/.eslintrc.json 50 | .jsii 51 | tsconfig.json 52 | !/API.md 53 | /.idea 54 | /cdk.out 55 | tmp-* 56 | !/.github/workflows/upgrade-main.yml 57 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | conditions: 6 | - "#approved-reviews-by>=1" 7 | - -label~=(do-not-merge) 8 | - status-success=build 9 | - status-success=package-js 10 | pull_request_rules: 11 | - name: Automatic merge on approval and successful build 12 | actions: 13 | delete_head_branch: {} 14 | queue: 15 | method: squash 16 | name: default 17 | commit_message_template: |- 18 | {{ title }} (#{{ number }}) 19 | 20 | {{ body }} 21 | conditions: 22 | - "#approved-reviews-by>=1" 23 | - -label~=(do-not-merge) 24 | - status-success=build 25 | - status-success=package-js 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | /dist/changelog.md 7 | /dist/version.txt 8 | /.mergify.yml 9 | /test/ 10 | /tsconfig.dev.json 11 | /src/ 12 | !/lib/ 13 | !/lib/**/*.js 14 | !/lib/**/*.d.ts 15 | dist 16 | /tsconfig.json 17 | /.github/ 18 | /.vscode/ 19 | /.idea/ 20 | /.projenrc.js 21 | tsconfig.tsbuildinfo 22 | /.eslintrc.json 23 | !.jsii 24 | /.idea 25 | /cdk.out 26 | tmp-* 27 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/fs-extra", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/jest", 9 | "type": "build" 10 | }, 11 | { 12 | "name": "@types/node", 13 | "version": "^12", 14 | "type": "build" 15 | }, 16 | { 17 | "name": "@typescript-eslint/eslint-plugin", 18 | "version": "^5", 19 | "type": "build" 20 | }, 21 | { 22 | "name": "@typescript-eslint/parser", 23 | "version": "^5", 24 | "type": "build" 25 | }, 26 | { 27 | "name": "@wheatstalk/lit-snip", 28 | "version": "^0.0", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "aws-cdk", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "aws-cdk-lib", 37 | "version": "2.8.0", 38 | "type": "build" 39 | }, 40 | { 41 | "name": "constructs", 42 | "version": "10.0.5", 43 | "type": "build" 44 | }, 45 | { 46 | "name": "eslint-import-resolver-node", 47 | "type": "build" 48 | }, 49 | { 50 | "name": "eslint-import-resolver-typescript", 51 | "type": "build" 52 | }, 53 | { 54 | "name": "eslint-plugin-import", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "eslint", 59 | "version": "^8", 60 | "type": "build" 61 | }, 62 | { 63 | "name": "jest", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "jest-junit", 68 | "version": "^13", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "jsii", 73 | "type": "build" 74 | }, 75 | { 76 | "name": "jsii-diff", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "jsii-docgen", 81 | "type": "build" 82 | }, 83 | { 84 | "name": "jsii-pacmak", 85 | "type": "build" 86 | }, 87 | { 88 | "name": "json-schema", 89 | "type": "build" 90 | }, 91 | { 92 | "name": "markmac", 93 | "version": "^0.1", 94 | "type": "build" 95 | }, 96 | { 97 | "name": "npm-check-updates", 98 | "version": "^12", 99 | "type": "build" 100 | }, 101 | { 102 | "name": "projen", 103 | "type": "build" 104 | }, 105 | { 106 | "name": "shx", 107 | "type": "build" 108 | }, 109 | { 110 | "name": "standard-version", 111 | "version": "^9", 112 | "type": "build" 113 | }, 114 | { 115 | "name": "ts-jest", 116 | "type": "build" 117 | }, 118 | { 119 | "name": "ts-node", 120 | "type": "build" 121 | }, 122 | { 123 | "name": "typescript", 124 | "type": "build" 125 | }, 126 | { 127 | "name": "@sls-next/lambda-at-edge", 128 | "type": "bundled" 129 | }, 130 | { 131 | "name": "execa", 132 | "type": "bundled" 133 | }, 134 | { 135 | "name": "fs-extra", 136 | "type": "bundled" 137 | }, 138 | { 139 | "name": "yargs", 140 | "type": "bundled" 141 | }, 142 | { 143 | "name": "aws-cdk-lib", 144 | "version": "^2.8.0", 145 | "type": "peer" 146 | }, 147 | { 148 | "name": "constructs", 149 | "version": "^10.0.5", 150 | "type": "peer" 151 | } 152 | ], 153 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 154 | } 155 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/auto-approve.yml", 7 | ".github/workflows/build.yml", 8 | ".github/workflows/pull-request-lint.yml", 9 | ".github/workflows/release.yml", 10 | ".github/workflows/upgrade-main.yml", 11 | ".gitignore", 12 | ".mergify.yml", 13 | ".projen/deps.json", 14 | ".projen/files.json", 15 | ".projen/tasks.json", 16 | "LICENSE", 17 | "tsconfig.dev.json" 18 | ], 19 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 20 | } 21 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bump": { 28 | "name": "bump", 29 | "description": "Bumps version based on latest git tag and generates a changelog entry", 30 | "env": { 31 | "OUTFILE": "package.json", 32 | "CHANGELOG": "dist/changelog.md", 33 | "BUMPFILE": "dist/version.txt", 34 | "RELEASETAG": "dist/releasetag.txt", 35 | "RELEASE_TAG_PREFIX": "" 36 | }, 37 | "steps": [ 38 | { 39 | "builtin": "release/bump-version" 40 | } 41 | ], 42 | "condition": "! git log --oneline -1 | grep -q \"chore(release):\"" 43 | }, 44 | "clobber": { 45 | "name": "clobber", 46 | "description": "hard resets to HEAD of origin and cleans the local repo", 47 | "env": { 48 | "BRANCH": "$(git branch --show-current)" 49 | }, 50 | "steps": [ 51 | { 52 | "exec": "git checkout -b scratch", 53 | "name": "save current HEAD in \"scratch\" branch" 54 | }, 55 | { 56 | "exec": "git checkout $BRANCH" 57 | }, 58 | { 59 | "exec": "git fetch origin", 60 | "name": "fetch latest changes from origin" 61 | }, 62 | { 63 | "exec": "git reset --hard origin/$BRANCH", 64 | "name": "hard reset to origin commit" 65 | }, 66 | { 67 | "exec": "git clean -fdx", 68 | "name": "clean all untracked files" 69 | }, 70 | { 71 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 72 | } 73 | ], 74 | "condition": "git diff --exit-code > /dev/null" 75 | }, 76 | "compat": { 77 | "name": "compat", 78 | "description": "Perform API compatibility check against latest version", 79 | "steps": [ 80 | { 81 | "exec": "jsii-diff npm:$(node -p \"require('./package.json').name\") -k --ignore-file .compatignore || (echo \"\nUNEXPECTED BREAKING CHANGES: add keys such as 'removed:constructs.Node.of' to .compatignore to skip.\n\" && exit 1)" 82 | } 83 | ] 84 | }, 85 | "compile": { 86 | "name": "compile", 87 | "description": "Only compile", 88 | "steps": [ 89 | { 90 | "exec": "jsii --silence-warnings=reserved-word --no-fix-peer-dependencies" 91 | } 92 | ] 93 | }, 94 | "default": { 95 | "name": "default", 96 | "description": "Synthesize project files", 97 | "steps": [ 98 | { 99 | "exec": "node .projenrc.js" 100 | } 101 | ] 102 | }, 103 | "docgen": { 104 | "name": "docgen", 105 | "description": "Generate API.md from .jsii manifest", 106 | "steps": [ 107 | { 108 | "exec": "jsii-docgen -o API.md" 109 | } 110 | ] 111 | }, 112 | "eject": { 113 | "name": "eject", 114 | "description": "Remove projen from the project", 115 | "env": { 116 | "PROJEN_EJECTING": "true" 117 | }, 118 | "steps": [ 119 | { 120 | "spawn": "default" 121 | } 122 | ] 123 | }, 124 | "eslint": { 125 | "name": "eslint", 126 | "description": "Runs eslint against the codebase", 127 | "steps": [ 128 | { 129 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 130 | } 131 | ] 132 | }, 133 | "package": { 134 | "name": "package", 135 | "description": "Creates the distribution package", 136 | "steps": [ 137 | { 138 | "exec": "if [ ! -z ${CI} ]; then mkdir -p dist && rsync -a . dist --exclude .git --exclude node_modules; else npx projen package-all; fi" 139 | } 140 | ] 141 | }, 142 | "package-all": { 143 | "name": "package-all", 144 | "description": "Packages artifacts for all target languages", 145 | "steps": [ 146 | { 147 | "spawn": "package:js" 148 | } 149 | ] 150 | }, 151 | "package:js": { 152 | "name": "package:js", 153 | "description": "Create js language bindings", 154 | "steps": [ 155 | { 156 | "exec": "jsii-pacmak -v --target js" 157 | } 158 | ] 159 | }, 160 | "post-compile": { 161 | "name": "post-compile", 162 | "description": "Runs after successful compilation", 163 | "steps": [ 164 | { 165 | "spawn": "docgen" 166 | }, 167 | { 168 | "spawn": "readme-macros" 169 | } 170 | ] 171 | }, 172 | "post-upgrade": { 173 | "name": "post-upgrade", 174 | "description": "Runs after upgrading dependencies" 175 | }, 176 | "pre-compile": { 177 | "name": "pre-compile", 178 | "description": "Prepare the project for compilation" 179 | }, 180 | "readme-macros": { 181 | "name": "readme-macros", 182 | "steps": [ 183 | { 184 | "exec": "shx mv README.md README.md.bak" 185 | }, 186 | { 187 | "exec": "shx cat README.md.bak | markmac > README.md" 188 | }, 189 | { 190 | "exec": "shx rm README.md.bak" 191 | } 192 | ] 193 | }, 194 | "release": { 195 | "name": "release", 196 | "description": "Prepare a release from \"main\" branch", 197 | "env": { 198 | "RELEASE": "true" 199 | }, 200 | "steps": [ 201 | { 202 | "exec": "rm -fr dist" 203 | }, 204 | { 205 | "spawn": "bump" 206 | }, 207 | { 208 | "spawn": "build" 209 | }, 210 | { 211 | "spawn": "unbump" 212 | }, 213 | { 214 | "exec": "git diff --ignore-space-at-eol --exit-code" 215 | } 216 | ] 217 | }, 218 | "test": { 219 | "name": "test", 220 | "description": "Run tests", 221 | "steps": [ 222 | { 223 | "exec": "jest --passWithNoTests --all --updateSnapshot" 224 | }, 225 | { 226 | "spawn": "eslint" 227 | } 228 | ] 229 | }, 230 | "test:update": { 231 | "name": "test:update", 232 | "description": "Update jest snapshots", 233 | "steps": [ 234 | { 235 | "exec": "jest --updateSnapshot" 236 | } 237 | ] 238 | }, 239 | "test:watch": { 240 | "name": "test:watch", 241 | "description": "Run jest in watch mode", 242 | "steps": [ 243 | { 244 | "exec": "jest --watch" 245 | } 246 | ] 247 | }, 248 | "unbump": { 249 | "name": "unbump", 250 | "description": "Restores version to 0.0.0", 251 | "env": { 252 | "OUTFILE": "package.json", 253 | "CHANGELOG": "dist/changelog.md", 254 | "BUMPFILE": "dist/version.txt", 255 | "RELEASETAG": "dist/releasetag.txt", 256 | "RELEASE_TAG_PREFIX": "" 257 | }, 258 | "steps": [ 259 | { 260 | "builtin": "release/reset-version" 261 | } 262 | ] 263 | }, 264 | "upgrade": { 265 | "name": "upgrade", 266 | "description": "upgrade dependencies", 267 | "env": { 268 | "CI": "0" 269 | }, 270 | "steps": [ 271 | { 272 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='aws-cdk-lib,constructs'" 273 | }, 274 | { 275 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='aws-cdk-lib,constructs'" 276 | }, 277 | { 278 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='aws-cdk-lib,constructs'" 279 | }, 280 | { 281 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='aws-cdk-lib,constructs'" 282 | }, 283 | { 284 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='aws-cdk-lib,constructs'" 285 | }, 286 | { 287 | "exec": "yarn install --check-files" 288 | }, 289 | { 290 | "exec": "yarn upgrade" 291 | }, 292 | { 293 | "exec": "npx projen" 294 | }, 295 | { 296 | "spawn": "post-upgrade" 297 | } 298 | ] 299 | }, 300 | "watch": { 301 | "name": "watch", 302 | "description": "Watch & compile in the background", 303 | "steps": [ 304 | { 305 | "exec": "jsii -w --silence-warnings=reserved-word --no-fix-peer-dependencies" 306 | } 307 | ] 308 | } 309 | }, 310 | "env": { 311 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 312 | }, 313 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 314 | } 315 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk } = require('projen'); 2 | 3 | const project = new awscdk.AwsCdkConstructLibrary({ 4 | author: 'Josh Kellendonk', 5 | authorAddress: 'josh@rayova.com', 6 | cdkVersion: '2.8.0', 7 | defaultReleaseBranch: 'main', 8 | name: '@rayova/cdk-serverless-nextjs', 9 | repositoryUrl: 'https://github.com/rayova/cdk-serverless-nextjs.git', 10 | description: 'Deploy Serverless Next.js on Lambda @ Edge with the AWS CDK', 11 | 12 | keywords: [ 13 | 'cdk', 14 | 'lambda', 15 | 'cloudfront', 16 | 'next', 17 | 'nextjs', 18 | 'serverless', 19 | 'ssr', 20 | 'isr', 21 | 'react', 22 | ], 23 | 24 | releaseEveryCommit: true, 25 | releaseToNpm: true, 26 | 27 | depsUpgradeOptions: { 28 | ignoreProjen: false, 29 | }, 30 | 31 | autoApproveUpgrades: true, 32 | autoApproveOptions: { 33 | allowedUsernames: ['rayova-bot'], 34 | }, 35 | 36 | workflowNodeVersion: '14', 37 | 38 | bundledDeps: [ 39 | 'fs-extra', 40 | 'execa', 41 | 'yargs', 42 | '@sls-next/lambda-at-edge', 43 | ], 44 | 45 | devDeps: [ 46 | 'aws-cdk', 47 | '@types/fs-extra', 48 | 'ts-node', 49 | 'markmac@^0.1', 50 | 'shx', 51 | '@wheatstalk/lit-snip@^0.0', 52 | ], 53 | 54 | tsconfig: { 55 | exclude: [ 56 | 'cdk.out', 57 | 'test/pages', 58 | 'test/tmp-*', 59 | ], 60 | }, 61 | 62 | // cdkDependencies: undefined, /* Which AWS CDK modules (those that start with "@aws-cdk/") does this library require when consumed? */ 63 | // cdkTestDependencies: undefined, /* AWS CDK modules required for testing. */ 64 | // deps: [], /* Runtime dependencies of this module. */ 65 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 66 | // devDeps: [], /* Build dependencies for this module. */ 67 | // packageName: undefined, /* The "name" in package.json. */ 68 | // projectType: ProjectType.UNKNOWN, /* Which type of project this is (library/app). */ 69 | // release: undefined, /* Add release management to this project. */ 70 | }); 71 | 72 | project.package.setScript('integ:main', 'cdk --app "ts-node -P tsconfig.dev.json test/integ.main.lit.ts"'); 73 | 74 | const ignores = [ 75 | '/.idea', 76 | '/cdk.out', 77 | 'tmp-*', 78 | ]; 79 | 80 | for (const ignore of ignores) { 81 | project.addGitIgnore(ignore); 82 | project.addPackageIgnore(ignore); 83 | } 84 | 85 | const macros = project.addTask('readme-macros'); 86 | macros.exec('shx mv README.md README.md.bak'); 87 | macros.exec('shx cat README.md.bak | markmac > README.md'); 88 | macros.exec('shx rm README.md.bak'); 89 | project.postCompileTask.spawn(macros); 90 | 91 | project.synth(); -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # API Reference 2 | 3 | ## Constructs 4 | 5 | ### ServerlessNextjs 6 | 7 | Deploy Next.js as Lambda@Edge. 8 | 9 | > https://github.com/serverless-nextjs/serverless-next.js#architecture 10 | 11 | #### Initializers 12 | 13 | ```typescript 14 | import { ServerlessNextjs } from '@rayova/cdk-serverless-nextjs' 15 | 16 | new ServerlessNextjs(scope: Construct, id: string, props: ServerlessNextjsProps) 17 | ``` 18 | 19 | ##### `scope`Required 20 | 21 | - *Type:* [`constructs.Construct`](#constructs.Construct) 22 | 23 | --- 24 | 25 | ##### `id`Required 26 | 27 | - *Type:* `string` 28 | 29 | --- 30 | 31 | ##### `props`Required 32 | 33 | - *Type:* [`@rayova/cdk-serverless-nextjs.ServerlessNextjsProps`](#@rayova/cdk-serverless-nextjs.ServerlessNextjsProps) 34 | 35 | --- 36 | 37 | 38 | 39 | #### Properties 40 | 41 | ##### `cloudFrontConfig`Required 42 | 43 | ```typescript 44 | public readonly cloudFrontConfig: ServerlessNextjsCloudFrontConfig; 45 | ``` 46 | 47 | - *Type:* [`@rayova/cdk-serverless-nextjs.ServerlessNextjsCloudFrontConfig`](#@rayova/cdk-serverless-nextjs.ServerlessNextjsCloudFrontConfig) 48 | 49 | --- 50 | 51 | 52 | ## Structs 53 | 54 | ### NextjsArtifactFromBuildOptions 55 | 56 | #### Initializer 57 | 58 | ```typescript 59 | import { NextjsArtifactFromBuildOptions } from '@rayova/cdk-serverless-nextjs' 60 | 61 | const nextjsArtifactFromBuildOptions: NextjsArtifactFromBuildOptions = { ... } 62 | ``` 63 | 64 | ##### `buildCommand`Required 65 | 66 | ```typescript 67 | public readonly buildCommand: string[]; 68 | ``` 69 | 70 | - *Type:* `string`[] 71 | 72 | The command to build nextjs's .next directory. 73 | 74 | i.e., ['yarn', 'build'] or ['yarn', 'next', 'build'] 75 | 76 | --- 77 | 78 | ##### `nextjsDirectory`Required 79 | 80 | ```typescript 81 | public readonly nextjsDirectory: string; 82 | ``` 83 | 84 | - *Type:* `string` 85 | 86 | The directory containing the Next.js project. 87 | 88 | --- 89 | 90 | ### ServerlessNextjsCloudFrontConfig 91 | 92 | CloudFront configuration. 93 | 94 | #### Initializer 95 | 96 | ```typescript 97 | import { ServerlessNextjsCloudFrontConfig } from '@rayova/cdk-serverless-nextjs' 98 | 99 | const serverlessNextjsCloudFrontConfig: ServerlessNextjsCloudFrontConfig = { ... } 100 | ``` 101 | 102 | ##### `additionalBehaviors`Required 103 | 104 | ```typescript 105 | public readonly additionalBehaviors: {[ key: string ]: BehaviorOptions}; 106 | ``` 107 | 108 | - *Type:* {[ key: string ]: [`aws-cdk-lib.aws_cloudfront.BehaviorOptions`](#aws-cdk-lib.aws_cloudfront.BehaviorOptions)} 109 | 110 | Additional behaviors. 111 | 112 | --- 113 | 114 | ##### `defaultBehavior`Required 115 | 116 | ```typescript 117 | public readonly defaultBehavior: BehaviorOptions; 118 | ``` 119 | 120 | - *Type:* [`aws-cdk-lib.aws_cloudfront.BehaviorOptions`](#aws-cdk-lib.aws_cloudfront.BehaviorOptions) 121 | 122 | CDN default behavior. 123 | 124 | --- 125 | 126 | ### ServerlessNextjsProps 127 | 128 | #### Initializer 129 | 130 | ```typescript 131 | import { ServerlessNextjsProps } from '@rayova/cdk-serverless-nextjs' 132 | 133 | const serverlessNextjsProps: ServerlessNextjsProps = { ... } 134 | ``` 135 | 136 | ##### `nextjsArtifact`Required 137 | 138 | ```typescript 139 | public readonly nextjsArtifact: NextjsArtifact; 140 | ``` 141 | 142 | - *Type:* [`@rayova/cdk-serverless-nextjs.NextjsArtifact`](#@rayova/cdk-serverless-nextjs.NextjsArtifact) 143 | 144 | The Next.js artifact. 145 | 146 | --- 147 | 148 | ## Classes 149 | 150 | ### NextjsArtifact 151 | 152 | #### Initializers 153 | 154 | ```typescript 155 | import { NextjsArtifact } from '@rayova/cdk-serverless-nextjs' 156 | 157 | new NextjsArtifact() 158 | ``` 159 | 160 | 161 | #### Static Functions 162 | 163 | ##### `fromBuild` 164 | 165 | ```typescript 166 | import { NextjsArtifact } from '@rayova/cdk-serverless-nextjs' 167 | 168 | NextjsArtifact.fromBuild(options: NextjsArtifactFromBuildOptions) 169 | ``` 170 | 171 | ###### `options`Required 172 | 173 | - *Type:* [`@rayova/cdk-serverless-nextjs.NextjsArtifactFromBuildOptions`](#@rayova/cdk-serverless-nextjs.NextjsArtifactFromBuildOptions) 174 | 175 | --- 176 | 177 | 178 | 179 | 180 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Rayova A Fintech Corporation][logo] 2 | 3 | # CDK Serverless Next.js 4 | 5 | This project provides an AWS CDK construct to Deploy Serverless Next.js to 6 | Lambda@Edge. We designed the construct following these principles: 7 | 8 | * Sensible defaults 9 | * Feature parity with Next.js 10 | * Fast deployments 11 | * Build the AWS Lambda deployment package for you 12 | * Provide `BehaviorOptions` for you to add to your CloudFront Distribution 13 | 14 | ## Supported Next.js features 15 | 16 | This construct library aims for feature parity with Next.js. Through 17 | [@sls-next/lambda-at-edge][@sls-next/lambda-at-edge], this CDK construct 18 | supports: 19 | 20 | * Next.js 10/11 21 | * Static pages 22 | * Server-side props 23 | * Static props and static prop fallback 24 | * Incremental static regeneration 25 | * Image optimization 26 | * API routes 27 | 28 | ## Example Usage 29 | 30 | 31 | ```ts 32 | // Create a ServerlessNextjs construct in your stack to get started. 33 | // Your stack MUST be in us-east-1 as that's the only region in which AWS 34 | // supports deploying edge lambdas. 35 | const serverlessNextjs = new ServerlessNextjs(scope, 'NextJs', { 36 | // Then produce and add a Next.js artifact to ServerlessNextjs by 37 | // building it from your project directory. 38 | nextjsArtifact: NextjsArtifact.fromBuild({ 39 | // Provide your Next.js project directory. 40 | nextjsDirectory: pathToYourProjectDirectory, 41 | 42 | // Provide the commands you need NextjsArtifact to run to build the 43 | // `.next` directory. 44 | buildCommand: ['yarn', 'next', 'build'], 45 | }), 46 | }); 47 | 48 | // Create CloudFront distribution 49 | const dist = new cloudfront.Distribution(scope, 'Distribution', { 50 | // And configure it with serverless Next.js as Lambda @ Edge 51 | ...serverlessNextjs.cloudFrontConfig, 52 | 53 | // Add anything else you need on your CloudFront distribution here, like 54 | // certificates, cnames, price classes, logging, etc. 55 | }); 56 | ``` 57 | 58 | 59 | [logo]: images/rayova-fintech-corp.png 60 | [@sls-next/lambda-at-edge]: https://www.npmjs.com/package/@sls-next/lambda-at-edge 61 | -------------------------------------------------------------------------------- /bin/build-serverless-nextjs.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const yargs = require('yargs/yargs'); 4 | const { hideBin } = require('yargs/helpers'); 5 | const { Builder } = require('@sls-next/lambda-at-edge'); 6 | 7 | const argv = yargs(hideBin(process.argv)).argv; 8 | const positional = Array.from(argv._); 9 | 10 | const nextConfigDir = positional.shift(); 11 | const outputDir = argv.output || path.join(nextConfigDir, '.serverless_nextjs'); 12 | 13 | const cmd = positional.shift(); 14 | const cmdArgs = positional; 15 | 16 | if (!fs.existsSync(nextConfigDir)) { 17 | throw new Error(`The next directory ${nextConfigDir} does not exist`); 18 | } 19 | 20 | console.log(`Building contents of ${nextConfigDir} into ${outputDir} using ${cmd} ${cmdArgs.join(' ')}`); 21 | const builder = new Builder(nextConfigDir, outputDir, { 22 | cwd: nextConfigDir, 23 | cmd: cmd, 24 | args: cmdArgs, 25 | }); 26 | 27 | builder.build() 28 | .catch(e => { 29 | console.error(e); 30 | process.exit(1); 31 | }); 32 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": { 3 | "include": [ 4 | "**" 5 | ], 6 | "exclude": [ 7 | "README.md", 8 | "cdk*.json", 9 | "**/*.d.ts", 10 | "**/*.js", 11 | "tsconfig.json", 12 | "package*.json", 13 | "yarn.lock", 14 | "node_modules", 15 | "test" 16 | ] 17 | }, 18 | "context": { 19 | "@aws-cdk/core:newStyleStackSynthesis": true 20 | } 21 | } -------------------------------------------------------------------------------- /functions/index-indirect.js: -------------------------------------------------------------------------------- 1 | const index = require('./index'); 2 | 3 | exports.handler = handler; 4 | 5 | /** 6 | * A lambda handler that injects contextual information about the CloudFront 7 | * event into the environment variables, then hands the request off to the 8 | * `@sls-next/lambda-at-edge` handler. 9 | */ 10 | async function handler(event, context) { 11 | const request = event.Records[0].cf.request; 12 | process.env.CDK_SERVERLESS_NEXTJS_ORIGIN_REGION = s3RegionFromRequest(request); 13 | process.env.CDK_SERVERLESS_NEXTJS_ORIGIN_BUCKET = s3BucketNameFromRequest(request); 14 | 15 | return await index.handler(event, context); 16 | } 17 | 18 | function s3RegionFromRequest(request) { 19 | return request && request.origin && request.origin.s3 && request.origin.s3.region 20 | ? request.origin.s3.region 21 | : ''; 22 | } 23 | 24 | function s3OriginFromRequest(request) { 25 | return request && request.origin && request.origin.s3 26 | ? request.origin.s3 27 | : {}; 28 | } 29 | 30 | function s3BucketNameFromRequest(request) { 31 | const s3 = s3OriginFromRequest(request); 32 | 33 | const region = s3.region; 34 | const domainName = s3.domainName || ''; 35 | 36 | return !!region && domainName.includes(region) 37 | ? domainName.replace(`.s3.${region}.amazonaws.com`, '') 38 | : domainName.replace(`.s3.amazonaws.com`, ''); 39 | } -------------------------------------------------------------------------------- /images/rayova-fintech-corp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rayova/cdk-serverless-nextjs/409ba98034b5cfeb13a62c59422cf7c7fd343729/images/rayova-fintech-corp.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@rayova/cdk-serverless-nextjs", 3 | "description": "Deploy Serverless Next.js on Lambda @ Edge with the AWS CDK", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/rayova/cdk-serverless-nextjs.git" 7 | }, 8 | "scripts": { 9 | "build": "npx projen build", 10 | "bump": "npx projen bump", 11 | "clobber": "npx projen clobber", 12 | "compat": "npx projen compat", 13 | "compile": "npx projen compile", 14 | "default": "npx projen default", 15 | "docgen": "npx projen docgen", 16 | "eject": "npx projen eject", 17 | "eslint": "npx projen eslint", 18 | "package": "npx projen package", 19 | "package-all": "npx projen package-all", 20 | "package:js": "npx projen package:js", 21 | "post-compile": "npx projen post-compile", 22 | "post-upgrade": "npx projen post-upgrade", 23 | "pre-compile": "npx projen pre-compile", 24 | "readme-macros": "npx projen readme-macros", 25 | "release": "npx projen release", 26 | "test": "npx projen test", 27 | "test:update": "npx projen test:update", 28 | "test:watch": "npx projen test:watch", 29 | "unbump": "npx projen unbump", 30 | "upgrade": "npx projen upgrade", 31 | "watch": "npx projen watch", 32 | "projen": "npx projen", 33 | "integ:main": "cdk --app \"ts-node -P tsconfig.dev.json test/integ.main.lit.ts\"" 34 | }, 35 | "author": { 36 | "name": "Josh Kellendonk", 37 | "email": "josh@rayova.com", 38 | "organization": false 39 | }, 40 | "devDependencies": { 41 | "@types/fs-extra": "^9.0.13", 42 | "@types/jest": "^27.4.1", 43 | "@types/node": "^12", 44 | "@typescript-eslint/eslint-plugin": "^5", 45 | "@typescript-eslint/parser": "^5", 46 | "@wheatstalk/lit-snip": "^0.0", 47 | "aws-cdk": "^1.149.0", 48 | "aws-cdk-lib": "2.8.0", 49 | "constructs": "10.0.5", 50 | "eslint": "^8", 51 | "eslint-import-resolver-node": "^0.3.6", 52 | "eslint-import-resolver-typescript": "^2.5.0", 53 | "eslint-plugin-import": "^2.25.4", 54 | "jest": "^27.5.1", 55 | "jest-junit": "^13", 56 | "jsii": "^1.55.1", 57 | "jsii-diff": "^1.55.1", 58 | "jsii-docgen": "^3.8.31", 59 | "jsii-pacmak": "^1.55.1", 60 | "json-schema": "^0.4.0", 61 | "markmac": "^0.1", 62 | "npm-check-updates": "^12", 63 | "projen": "^0.53.1", 64 | "shx": "^0.3.4", 65 | "standard-version": "^9", 66 | "ts-jest": "^27.1.3", 67 | "ts-node": "^10.7.0", 68 | "typescript": "^4.6.2" 69 | }, 70 | "peerDependencies": { 71 | "aws-cdk-lib": "^2.8.0", 72 | "constructs": "^10.0.5" 73 | }, 74 | "dependencies": { 75 | "@sls-next/lambda-at-edge": "^3.6.0", 76 | "execa": "^5.1.1", 77 | "fs-extra": "^10.0.1", 78 | "yargs": "^17.3.1" 79 | }, 80 | "bundledDependencies": [ 81 | "@sls-next/lambda-at-edge", 82 | "execa", 83 | "fs-extra", 84 | "yargs" 85 | ], 86 | "keywords": [ 87 | "cdk", 88 | "cloudfront", 89 | "isr", 90 | "lambda", 91 | "next", 92 | "nextjs", 93 | "react", 94 | "serverless", 95 | "ssr" 96 | ], 97 | "main": "lib/index.js", 98 | "license": "Apache-2.0", 99 | "version": "0.0.0", 100 | "jest": { 101 | "testMatch": [ 102 | "/src/**/__tests__/**/*.ts?(x)", 103 | "/(test|src)/**/?(*.)+(spec|test).ts?(x)" 104 | ], 105 | "clearMocks": true, 106 | "collectCoverage": true, 107 | "coverageReporters": [ 108 | "json", 109 | "lcov", 110 | "clover", 111 | "cobertura", 112 | "text" 113 | ], 114 | "coverageDirectory": "coverage", 115 | "coveragePathIgnorePatterns": [ 116 | "/node_modules/" 117 | ], 118 | "testPathIgnorePatterns": [ 119 | "/node_modules/" 120 | ], 121 | "watchPathIgnorePatterns": [ 122 | "/node_modules/" 123 | ], 124 | "reporters": [ 125 | "default", 126 | [ 127 | "jest-junit", 128 | { 129 | "outputDirectory": "test-reports" 130 | } 131 | ] 132 | ], 133 | "preset": "ts-jest", 134 | "globals": { 135 | "ts-jest": { 136 | "tsconfig": "tsconfig.dev.json" 137 | } 138 | } 139 | }, 140 | "types": "lib/index.d.ts", 141 | "stability": "stable", 142 | "jsii": { 143 | "outdir": "dist", 144 | "targets": {}, 145 | "tsc": { 146 | "outDir": "lib", 147 | "rootDir": "src" 148 | } 149 | }, 150 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 151 | } -------------------------------------------------------------------------------- /src/api-lambda.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as s3 from 'aws-cdk-lib/aws-s3'; 7 | import { Construct } from 'constructs'; 8 | import { API_LAMBDA_SUBPATH, NEXTJS_LAMBDA_RUNTIME } from './constants'; 9 | import { hasManifest } from './has-manifest'; 10 | import { LambdaAtEdgeRole } from './lambda-at-edge-role'; 11 | 12 | export interface ApiLambdaProps { 13 | readonly bucket: s3.IBucket; 14 | readonly buildOutputDir: string; 15 | } 16 | 17 | export class ApiLambda extends Construct { 18 | private readonly behaviorOptions?: cloudfront.BehaviorOptions; 19 | 20 | constructor(scope: Construct, id: string, props: ApiLambdaProps) { 21 | super(scope, id); 22 | 23 | const lambdaPath = path.join(props.buildOutputDir, API_LAMBDA_SUBPATH); 24 | 25 | // Only create resource if the handler has a manifest. 26 | if (!hasManifest(lambdaPath)) return; 27 | 28 | const origin = new cloudfront_origins.S3Origin(props.bucket); 29 | 30 | const apiLambda = new lambda.Function(this, 'Lambda', { 31 | runtime: NEXTJS_LAMBDA_RUNTIME, 32 | code: lambda.Code.fromAsset(lambdaPath), 33 | handler: 'index-indirect.handler', 34 | role: new LambdaAtEdgeRole(this, 'Role'), 35 | memorySize: 512, 36 | timeout: cdk.Duration.seconds(30), 37 | }); 38 | 39 | const cachePolicy = new cloudfront.CachePolicy(this, 'CachePolicy', { 40 | queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(), 41 | headerBehavior: cloudfront.CacheHeaderBehavior.none(), 42 | defaultTtl: cdk.Duration.seconds(0), 43 | maxTtl: cdk.Duration.days(365), 44 | minTtl: cdk.Duration.seconds(0), 45 | enableAcceptEncodingBrotli: true, 46 | enableAcceptEncodingGzip: true, 47 | }); 48 | 49 | this.behaviorOptions = { 50 | origin, 51 | cachePolicy, 52 | edgeLambdas: [ 53 | { 54 | includeBody: true, 55 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, 56 | functionVersion: apiLambda.currentVersion, 57 | }, 58 | ], 59 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 60 | allowedMethods: cloudfront.AllowedMethods.ALLOW_ALL, 61 | cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, 62 | compress: true, 63 | }; 64 | } 65 | 66 | addAdditionalBehaviors(additionalBehaviors: Record) { 67 | if (this.behaviorOptions) { 68 | additionalBehaviors['api/*'] = this.behaviorOptions; 69 | } 70 | } 71 | } 72 | 73 | -------------------------------------------------------------------------------- /src/assets-deployment.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs'; 2 | import * as path from 'path'; 3 | import * as s3 from 'aws-cdk-lib/aws-s3'; 4 | import * as s3_deployment from 'aws-cdk-lib/aws-s3-deployment'; 5 | import { Construct } from 'constructs'; 6 | import { ASSETS_SUBPATH } from './constants'; 7 | 8 | export interface AssetsDeploymentProps { 9 | /** Bucket to deploy the assets to */ 10 | readonly bucket: s3.Bucket; 11 | /** The directory containing the assets */ 12 | readonly buildOutputDir: string; 13 | } 14 | 15 | /** Deploys Next.js assets */ 16 | export class AssetsDeployment extends Construct { 17 | constructor(scope: Construct, id: string, props: AssetsDeploymentProps) { 18 | super(scope, id); 19 | 20 | for (const config of ASSET_DEPLOYMENT_CONFIGS) { 21 | const localAssetPath = path.join( 22 | props.buildOutputDir, 23 | ASSETS_SUBPATH, 24 | config.assetPath, 25 | ); 26 | 27 | if (!fs.existsSync(localAssetPath)) continue; 28 | 29 | new s3_deployment.BucketDeployment(this, `Deploy_${config.assetPath}`, { 30 | destinationBucket: props.bucket, 31 | destinationKeyPrefix: config.assetPath, 32 | sources: [s3_deployment.Source.asset(localAssetPath)], 33 | cacheControl: [config.cacheControl], 34 | }); 35 | } 36 | } 37 | } 38 | 39 | export interface AssetDeploymentConfig { 40 | /** Subdirectory of assets and destination key prefix */ 41 | readonly assetPath: string; 42 | /** Cache headers */ 43 | readonly cacheControl: s3_deployment.CacheControl; 44 | } 45 | 46 | const CACHE_CONTROL_PUBLIC = s3_deployment.CacheControl.fromString( 47 | 'public, max-age=31536000, must-revalidate', 48 | ); 49 | const CACHE_CONTROL_SERVER = s3_deployment.CacheControl.fromString( 50 | 'public, max-age=0, s-maxage=2678400, must-revalidate', 51 | ); 52 | const CACHE_CONTROL_IMMUTABLE = s3_deployment.CacheControl.fromString( 53 | 'public, max-age=31536000, immutable', 54 | ); 55 | 56 | export const ASSET_DEPLOYMENT_CONFIGS: AssetDeploymentConfig[] = [ 57 | { 58 | assetPath: 'public', 59 | cacheControl: CACHE_CONTROL_PUBLIC, 60 | }, 61 | { 62 | assetPath: 'static', 63 | cacheControl: CACHE_CONTROL_PUBLIC, 64 | }, 65 | { 66 | assetPath: 'static-pages', 67 | cacheControl: CACHE_CONTROL_SERVER, 68 | }, 69 | { 70 | assetPath: '_next/data', 71 | cacheControl: CACHE_CONTROL_SERVER, 72 | }, 73 | { 74 | assetPath: '_next/static', 75 | cacheControl: CACHE_CONTROL_IMMUTABLE, 76 | }, 77 | ]; 78 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 3 | 4 | export const NEXTJS_LAMBDA_RUNTIME = lambda.Runtime.NODEJS_12_X; 5 | export const BUILD_SERVERLESS_NEXTJS_SCRIPT = path.join(__dirname, '..', 'bin', 'build-serverless-nextjs.js'); 6 | 7 | export const DEFAULT_LAMBDA_SUBPATH = 'default-lambda'; 8 | export const IMAGE_LAMBDA_SUBPATH = 'image-lambda'; 9 | export const API_LAMBDA_SUBPATH = 'api-lambda'; 10 | export const REGENERATION_LAMBDA_SUBPATH = 'regeneration-lambda'; 11 | export const ASSETS_SUBPATH = 'assets'; 12 | -------------------------------------------------------------------------------- /src/default-lambda.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as s3 from 'aws-cdk-lib/aws-s3'; 7 | import { Construct } from 'constructs'; 8 | import { DEFAULT_LAMBDA_SUBPATH, NEXTJS_LAMBDA_RUNTIME } from './constants'; 9 | import { IncrementalStaticRegeneration } from './incremental-static-regeneration'; 10 | import { LambdaAtEdgeRole } from './lambda-at-edge-role'; 11 | 12 | export interface DefaultLambdaProps { 13 | readonly bucket: s3.IBucket; 14 | readonly incrementalStaticRegeneration: IncrementalStaticRegeneration; 15 | readonly buildOutputDir: string; 16 | } 17 | 18 | export class DefaultLambda extends Construct { 19 | public readonly defaultLambda: lambda.Function; 20 | private readonly origin: cloudfront_origins.S3Origin; 21 | private readonly cachePolicy: cloudfront.ICachePolicy; 22 | 23 | constructor(scope: Construct, id: string, props: DefaultLambdaProps) { 24 | super(scope, id); 25 | 26 | this.origin = new cloudfront_origins.S3Origin(props.bucket); 27 | this.cachePolicy = cloudfront.CachePolicy.CACHING_DISABLED; 28 | 29 | const lambdaDir = path.join(props.buildOutputDir, DEFAULT_LAMBDA_SUBPATH); 30 | 31 | this.defaultLambda = new lambda.Function(this, 'Lambda', { 32 | runtime: NEXTJS_LAMBDA_RUNTIME, 33 | code: lambda.Code.fromAsset(lambdaDir), 34 | handler: 'index-indirect.handler', 35 | role: new LambdaAtEdgeRole(this, 'Role'), 36 | memorySize: 512, 37 | timeout: cdk.Duration.seconds(30), 38 | }); 39 | props.bucket.grantReadWrite(this.defaultLambda); 40 | 41 | const incrementalStaticRegeneration = props.incrementalStaticRegeneration; 42 | incrementalStaticRegeneration.regenerationQueue?.grantSendMessages( 43 | this.defaultLambda, 44 | ); 45 | incrementalStaticRegeneration.regenerationFunction?.grantInvoke( 46 | this.defaultLambda, 47 | ); 48 | } 49 | 50 | get cdnBehaviorOptions(): cloudfront.BehaviorOptions { 51 | return { 52 | origin: this.origin, 53 | cachePolicy: this.cachePolicy, 54 | edgeLambdas: [ 55 | { 56 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, 57 | functionVersion: this.defaultLambda.currentVersion, 58 | includeBody: true, 59 | }, 60 | { 61 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_RESPONSE, 62 | functionVersion: this.defaultLambda.currentVersion, 63 | }, 64 | ], 65 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 66 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS, 67 | cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, 68 | compress: true, 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/has-manifest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | 4 | export function hasManifest(lambdaPath: string) { 5 | return fs.existsSync(path.join(lambdaPath, 'manifest.json')); 6 | } -------------------------------------------------------------------------------- /src/image-lambda.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins'; 5 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 6 | import * as s3 from 'aws-cdk-lib/aws-s3'; 7 | import { Construct } from 'constructs'; 8 | import { IMAGE_LAMBDA_SUBPATH, NEXTJS_LAMBDA_RUNTIME } from './constants'; 9 | import { hasManifest } from './has-manifest'; 10 | import { LambdaAtEdgeRole } from './lambda-at-edge-role'; 11 | 12 | export interface ImageLambdaProps { 13 | /** The origin bucket in which to find image assets */ 14 | readonly originBucket: s3.IBucket; 15 | /** Path of the directory containing the image lambda handler */ 16 | readonly buildOutputDir: string; 17 | } 18 | 19 | /** Create an image lambda that handles image optimization */ 20 | export class ImageLambda extends Construct { 21 | private readonly behaviorOptions?: cloudfront.BehaviorOptions; 22 | 23 | constructor(scope: Construct, id: string, props: ImageLambdaProps) { 24 | super(scope, id); 25 | 26 | const lambdaPath = path.join(props.buildOutputDir, IMAGE_LAMBDA_SUBPATH); 27 | if (!hasManifest(lambdaPath)) return; 28 | 29 | const origin = new cloudfront_origins.S3Origin(props.originBucket); 30 | 31 | const imageLambda = new lambda.Function(this, 'Lambda', { 32 | runtime: NEXTJS_LAMBDA_RUNTIME, 33 | code: lambda.Code.fromAsset(lambdaPath), 34 | handler: 'index.handler', 35 | role: new LambdaAtEdgeRole(this, 'Role'), 36 | timeout: cdk.Duration.seconds(30), 37 | // The Lambda needs a fair bit of memory or it runs quite slowly. 38 | memorySize: 2048, 39 | }); 40 | props.originBucket.grantReadWrite(imageLambda); 41 | 42 | const originRequestPolicy = new cloudfront.OriginRequestPolicy( 43 | this, 44 | 'OriginRequestPolicy', 45 | { 46 | queryStringBehavior: cloudfront.OriginRequestQueryStringBehavior.all(), 47 | }, 48 | ); 49 | 50 | const cachePolicy = new cloudfront.CachePolicy(this, 'CachePolicy', { 51 | queryStringBehavior: cloudfront.CacheQueryStringBehavior.all(), 52 | headerBehavior: cloudfront.CacheHeaderBehavior.allowList('Accept'), 53 | cookieBehavior: cloudfront.CacheCookieBehavior.none(), 54 | defaultTtl: cdk.Duration.days(1), 55 | maxTtl: cdk.Duration.days(365), 56 | minTtl: cdk.Duration.days(0), 57 | enableAcceptEncodingBrotli: true, 58 | enableAcceptEncodingGzip: true, 59 | }); 60 | 61 | this.behaviorOptions = { 62 | origin, 63 | cachePolicy, 64 | originRequestPolicy, 65 | edgeLambdas: [ 66 | { 67 | eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST, 68 | functionVersion: imageLambda.currentVersion, 69 | }, 70 | ], 71 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 72 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS, 73 | cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, 74 | compress: true, 75 | }; 76 | } 77 | 78 | addAdditionalBehaviors(additionalBehaviors: Record) { 79 | if (!this.behaviorOptions) return; 80 | 81 | additionalBehaviors['_next/image*'] = this.behaviorOptions; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/incremental-static-regeneration.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 4 | import * as lambda_es from 'aws-cdk-lib/aws-lambda-event-sources'; 5 | import * as s3 from 'aws-cdk-lib/aws-s3'; 6 | import * as sqs from 'aws-cdk-lib/aws-sqs'; 7 | import { Construct } from 'constructs'; 8 | import { NEXTJS_LAMBDA_RUNTIME, REGENERATION_LAMBDA_SUBPATH } from './constants'; 9 | import { hasManifest } from './has-manifest'; 10 | import { LambdaAtEdgeRole } from './lambda-at-edge-role'; 11 | 12 | export interface IncrementalStaticRegenerationProps { 13 | /** Origin bucket */ 14 | readonly originBucket: s3.IBucket; 15 | /** Path to the regeneration lambda bundle */ 16 | readonly buildOutputDir: string; 17 | } 18 | 19 | /** Creates a queue-driven incremental static regeneration lambda. */ 20 | export class IncrementalStaticRegeneration extends Construct { 21 | /** The queue that drives regeneration */ 22 | public readonly regenerationQueue?: sqs.Queue; 23 | /** The regeneration function */ 24 | public readonly regenerationFunction?: lambda.Function; 25 | 26 | constructor(scope: Construct, id: string, props: IncrementalStaticRegenerationProps) { 27 | super(scope, id); 28 | 29 | const lambdaPath = path.join(props.buildOutputDir, REGENERATION_LAMBDA_SUBPATH); 30 | if (!hasManifest(lambdaPath)) return; 31 | 32 | this.regenerationQueue = new sqs.Queue(this, 'Queue', { 33 | // Lambda@Edge does not support environment variables, but the associated 34 | // DefaultLambda handler still needs to find this queue. So, as a 35 | // work-around, DefaultLambda uses the origin bucket name as a convention 36 | // to locate other resources, including this queue. 37 | queueName: `${props.originBucket.bucketName}.fifo`, 38 | fifo: true, 39 | }); 40 | 41 | this.regenerationFunction = new lambda.Function(this, 'Function', { 42 | runtime: NEXTJS_LAMBDA_RUNTIME, 43 | handler: 'index.handler', 44 | timeout: cdk.Duration.seconds(30), 45 | role: new LambdaAtEdgeRole(this, 'Role'), 46 | code: lambda.Code.fromAsset(lambdaPath), 47 | events: [new lambda_es.SqsEventSource(this.regenerationQueue)], 48 | }); 49 | props.originBucket.grantReadWrite(this.regenerationFunction); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './serverless-nextjs'; 2 | export * from './nextjs-artifact'; 3 | export { REGENERATION_LAMBDA_SUBPATH } from './constants'; 4 | -------------------------------------------------------------------------------- /src/lambda-at-edge-role.ts: -------------------------------------------------------------------------------- 1 | import * as iam from 'aws-cdk-lib/aws-iam'; 2 | import { Construct } from 'constructs'; 3 | 4 | export class LambdaAtEdgeRole extends iam.Role { 5 | constructor(scope: Construct, id: string) { 6 | super(scope, id, { 7 | assumedBy: new iam.CompositePrincipal( 8 | new iam.ServicePrincipal('lambda.amazonaws.com'), 9 | new iam.ServicePrincipal('edgelambda.amazonaws.com'), 10 | ), 11 | managedPolicies: [AWS_LAMBDA_BASIC_EXECUTION_ROLE_POLICY], 12 | }); 13 | } 14 | } 15 | 16 | const AWS_LAMBDA_BASIC_EXECUTION_ROLE_POLICY = 17 | iam.ManagedPolicy.fromAwsManagedPolicyName( 18 | 'service-role/AWSLambdaBasicExecutionRole', 19 | ); 20 | -------------------------------------------------------------------------------- /src/nextjs-artifact.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os'; 2 | import * as path from 'path'; 3 | import * as execa from 'execa'; 4 | import * as fs from 'fs-extra'; 5 | import { ASSET_DEPLOYMENT_CONFIGS } from './assets-deployment'; 6 | import { 7 | API_LAMBDA_SUBPATH, 8 | BUILD_SERVERLESS_NEXTJS_SCRIPT, 9 | DEFAULT_LAMBDA_SUBPATH, 10 | IMAGE_LAMBDA_SUBPATH, 11 | REGENERATION_LAMBDA_SUBPATH, 12 | } from './constants'; 13 | 14 | export abstract class NextjsArtifact { 15 | /** Build a NextjsArtifact from your project directory */ 16 | static fromBuild(options: NextjsArtifactFromBuildOptions): NextjsArtifact { 17 | return new FromBuild(options); 18 | } 19 | 20 | /** @internal */ 21 | static _emptyArtifact(): NextjsArtifact { 22 | return new EmptyArtifact(); 23 | } 24 | 25 | /** @internal */ 26 | abstract _bind(): ServerlessNextjsArtifactConfig; 27 | } 28 | 29 | /** @internal */ 30 | interface ServerlessNextjsArtifactConfig { 31 | readonly buildOutputDir: string; 32 | } 33 | 34 | export interface NextjsArtifactFromBuildOptions { 35 | /** The directory containing the Next.js project. */ 36 | readonly nextjsDirectory: string; 37 | 38 | /** 39 | * The command to build nextjs's .next directory. 40 | * 41 | * i.e., ['yarn', 'build'] or ['yarn', 'next', 'build'] 42 | */ 43 | readonly buildCommand: string[]; 44 | } 45 | 46 | class FromBuild extends NextjsArtifact { 47 | private readonly nextjsDirectory: string; 48 | private readonly buildCommand: string[]; 49 | 50 | constructor(options: NextjsArtifactFromBuildOptions) { 51 | super(); 52 | 53 | this.nextjsDirectory = options.nextjsDirectory; 54 | this.buildCommand = options.buildCommand; 55 | } 56 | 57 | _bind(): ServerlessNextjsArtifactConfig { 58 | execa.sync('node', [ 59 | BUILD_SERVERLESS_NEXTJS_SCRIPT, 60 | this.nextjsDirectory, 61 | ...this.buildCommand, 62 | ]); 63 | 64 | const outputDir = path.normalize(path.join(this.nextjsDirectory, '.serverless_nextjs')); 65 | 66 | // Insert a custom handler that provides extra context for the Next.js app. 67 | const indirectHandler = 'index-indirect.js'; 68 | const indirectHandlerSrc = path.join(__dirname, '..', 'functions', indirectHandler); 69 | fs.copyFileSync(indirectHandlerSrc, path.join(outputDir, DEFAULT_LAMBDA_SUBPATH, indirectHandler)); 70 | fs.copyFileSync(indirectHandlerSrc, path.join(outputDir, API_LAMBDA_SUBPATH, indirectHandler)); 71 | 72 | return { 73 | buildOutputDir: outputDir, 74 | }; 75 | } 76 | } 77 | 78 | /** Create a stable NextjsArtifact suitable for snapshot tests */ 79 | class EmptyArtifact extends NextjsArtifact { 80 | _bind(): ServerlessNextjsArtifactConfig { 81 | const buildOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nextjsartifact')); 82 | 83 | const lambdas = [ 84 | API_LAMBDA_SUBPATH, 85 | DEFAULT_LAMBDA_SUBPATH, 86 | IMAGE_LAMBDA_SUBPATH, 87 | REGENERATION_LAMBDA_SUBPATH, 88 | ]; 89 | 90 | for (const lambda of lambdas) { 91 | fs.mkdirSync(path.join(buildOutputDir, lambda)); 92 | fs.writeFileSync(path.join(buildOutputDir, lambda, 'index.js'), 'some handler code'); 93 | } 94 | 95 | const assetsDir = path.join(buildOutputDir, 'assets'); 96 | for (const asset of ASSET_DEPLOYMENT_CONFIGS) { 97 | const assetDeploymentPath = path.join(assetsDir, asset.assetPath); 98 | fs.mkdirSync(assetDeploymentPath, { recursive: true }); 99 | fs.writeFileSync(path.join(assetDeploymentPath, 'file.txt'), 'asset content'); 100 | } 101 | 102 | return { 103 | buildOutputDir: buildOutputDir, 104 | }; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/serverless-nextjs.ts: -------------------------------------------------------------------------------- 1 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 2 | import * as s3 from 'aws-cdk-lib/aws-s3'; 3 | import { Construct } from 'constructs'; 4 | import { ApiLambda } from './api-lambda'; 5 | import { AssetsDeployment } from './assets-deployment'; 6 | import { DefaultLambda } from './default-lambda'; 7 | import { ImageLambda } from './image-lambda'; 8 | import { IncrementalStaticRegeneration } from './incremental-static-regeneration'; 9 | import { NextjsArtifact } from './nextjs-artifact'; 10 | import { StaticAssets } from './static-assets'; 11 | 12 | export interface ServerlessNextjsProps { 13 | /** The Next.js artifact */ 14 | readonly nextjsArtifact: NextjsArtifact; 15 | } 16 | 17 | /** CloudFront configuration */ 18 | export interface ServerlessNextjsCloudFrontConfig { 19 | /** CDN default behavior */ 20 | readonly defaultBehavior: cloudfront.BehaviorOptions; 21 | /** Additional behaviors */ 22 | readonly additionalBehaviors: Record; 23 | } 24 | 25 | /** 26 | * Deploy Next.js as Lambda@Edge. 27 | * @see https://github.com/serverless-nextjs/serverless-next.js#architecture 28 | */ 29 | export class ServerlessNextjs extends Construct { 30 | private readonly buildOutputDir: string; 31 | private readonly bucket: s3.Bucket; 32 | private readonly defaultLambda: DefaultLambda; 33 | private readonly bucketAssets: StaticAssets; 34 | private readonly imageLambda: ImageLambda; 35 | private readonly incrementalStaticRegeneration?: IncrementalStaticRegeneration; 36 | private readonly apiLambda: ApiLambda; 37 | 38 | constructor(scope: Construct, id: string, props: ServerlessNextjsProps) { 39 | super(scope, id); 40 | 41 | this.buildOutputDir = props.nextjsArtifact._bind().buildOutputDir; 42 | 43 | this.bucket = new s3.Bucket(this, 'Bucket'); 44 | new AssetsDeployment(this, 'AssetsDeployment', { 45 | bucket: this.bucket, 46 | buildOutputDir: this.buildOutputDir, 47 | }); 48 | 49 | this.bucketAssets = new StaticAssets(this, 'BucketAssets', { 50 | originBucket: this.bucket, 51 | }); 52 | 53 | this.imageLambda = new ImageLambda(this, 'ImageLambda', { 54 | originBucket: this.bucket, 55 | buildOutputDir: this.buildOutputDir, 56 | }); 57 | 58 | this.incrementalStaticRegeneration = new IncrementalStaticRegeneration(this, 59 | 'IncrementalStaticRegeneration', 60 | { 61 | originBucket: this.bucket, 62 | buildOutputDir: this.buildOutputDir, 63 | }, 64 | ); 65 | 66 | this.defaultLambda = new DefaultLambda(this, 'Default', { 67 | bucket: this.bucket, 68 | incrementalStaticRegeneration: this.incrementalStaticRegeneration, 69 | buildOutputDir: this.buildOutputDir, 70 | }); 71 | 72 | this.apiLambda = new ApiLambda(this, 'Api', { 73 | bucket: this.bucket, 74 | buildOutputDir: this.buildOutputDir, 75 | }); 76 | } 77 | 78 | get cloudFrontConfig(): ServerlessNextjsCloudFrontConfig { 79 | const additionalBehaviors: Record = {}; 80 | 81 | this.imageLambda.addAdditionalBehaviors(additionalBehaviors); 82 | this.apiLambda.addAdditionalBehaviors(additionalBehaviors); 83 | 84 | additionalBehaviors['_next/*'] = this.bucketAssets.cdnBehaviorOptions; 85 | additionalBehaviors['static/*'] = this.bucketAssets.cdnBehaviorOptions; 86 | 87 | return { 88 | defaultBehavior: this.defaultLambda.cdnBehaviorOptions, 89 | additionalBehaviors, 90 | }; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/static-assets.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 3 | import * as cloudfront_origins from 'aws-cdk-lib/aws-cloudfront-origins'; 4 | import * as s3 from 'aws-cdk-lib/aws-s3'; 5 | import { Construct } from 'constructs'; 6 | 7 | export interface StaticAssetsProps { 8 | /** The bucket to create as an origin containing the assets */ 9 | readonly originBucket: s3.IBucket; 10 | } 11 | 12 | /** Creates CDN Behavior Options for static assets */ 13 | export class StaticAssets extends Construct { 14 | public readonly cdnBehaviorOptions: cloudfront.BehaviorOptions; 15 | 16 | constructor(scope: Construct, id: string, props: StaticAssetsProps) { 17 | super(scope, id); 18 | 19 | const origin = new cloudfront_origins.S3Origin(props.originBucket); 20 | 21 | const cachePolicy = new cloudfront.CachePolicy(this, 'CachePolicy', { 22 | queryStringBehavior: cloudfront.CacheQueryStringBehavior.none(), 23 | headerBehavior: cloudfront.CacheHeaderBehavior.none(), 24 | cookieBehavior: cloudfront.CacheCookieBehavior.none(), 25 | defaultTtl: cdk.Duration.days(30), 26 | maxTtl: cdk.Duration.days(30), 27 | minTtl: cdk.Duration.days(30), 28 | enableAcceptEncodingBrotli: true, 29 | enableAcceptEncodingGzip: true, 30 | }); 31 | 32 | this.cdnBehaviorOptions = { 33 | origin, 34 | cachePolicy, 35 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 36 | allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD_OPTIONS, 37 | cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD_OPTIONS, 38 | compress: true, 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/__snapshots__/snapshot.empty-artifact.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshot 1`] = ` 4 | Object { 5 | "Parameters": Object { 6 | "BootstrapVersion": Object { 7 | "Default": "/cdk-bootstrap/hnb659fds/version", 8 | "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]", 9 | "Type": "AWS::SSM::Parameter::Value", 10 | }, 11 | }, 12 | "Resources": Object { 13 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": Object { 14 | "DependsOn": Array [ 15 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", 16 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", 17 | ], 18 | "Properties": Object { 19 | "Code": Object { 20 | "S3Bucket": Object { 21 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 22 | }, 23 | "S3Key": "e3d9996b6fafcc7da88312672e15e3cc925b02cffc6f01a615d81f22303e3ae0.zip", 24 | }, 25 | "Handler": "index.handler", 26 | "Layers": Array [ 27 | Object { 28 | "Ref": "ServerlessNextJsAssetsDeploymentDeploypublicAwsCliLayerE21BE73E", 29 | }, 30 | ], 31 | "Role": Object { 32 | "Fn::GetAtt": Array [ 33 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", 34 | "Arn", 35 | ], 36 | }, 37 | "Runtime": "python3.7", 38 | "Timeout": 900, 39 | }, 40 | "Type": "AWS::Lambda::Function", 41 | }, 42 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": Object { 43 | "Properties": Object { 44 | "AssumeRolePolicyDocument": Object { 45 | "Statement": Array [ 46 | Object { 47 | "Action": "sts:AssumeRole", 48 | "Effect": "Allow", 49 | "Principal": Object { 50 | "Service": "lambda.amazonaws.com", 51 | }, 52 | }, 53 | ], 54 | "Version": "2012-10-17", 55 | }, 56 | "ManagedPolicyArns": Array [ 57 | Object { 58 | "Fn::Join": Array [ 59 | "", 60 | Array [ 61 | "arn:", 62 | Object { 63 | "Ref": "AWS::Partition", 64 | }, 65 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 66 | ], 67 | ], 68 | }, 69 | ], 70 | }, 71 | "Type": "AWS::IAM::Role", 72 | }, 73 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": Object { 74 | "Properties": Object { 75 | "PolicyDocument": Object { 76 | "Statement": Array [ 77 | Object { 78 | "Action": Array [ 79 | "s3:GetObject*", 80 | "s3:GetBucket*", 81 | "s3:List*", 82 | ], 83 | "Effect": "Allow", 84 | "Resource": Array [ 85 | Object { 86 | "Fn::Join": Array [ 87 | "", 88 | Array [ 89 | "arn:", 90 | Object { 91 | "Ref": "AWS::Partition", 92 | }, 93 | ":s3:::", 94 | Object { 95 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 96 | }, 97 | ], 98 | ], 99 | }, 100 | Object { 101 | "Fn::Join": Array [ 102 | "", 103 | Array [ 104 | "arn:", 105 | Object { 106 | "Ref": "AWS::Partition", 107 | }, 108 | ":s3:::", 109 | Object { 110 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 111 | }, 112 | "/*", 113 | ], 114 | ], 115 | }, 116 | ], 117 | }, 118 | Object { 119 | "Action": Array [ 120 | "s3:GetObject*", 121 | "s3:GetBucket*", 122 | "s3:List*", 123 | "s3:DeleteObject*", 124 | "s3:PutObject", 125 | "s3:Abort*", 126 | ], 127 | "Effect": "Allow", 128 | "Resource": Array [ 129 | Object { 130 | "Fn::GetAtt": Array [ 131 | "ServerlessNextJsBucket352C45DE", 132 | "Arn", 133 | ], 134 | }, 135 | Object { 136 | "Fn::Join": Array [ 137 | "", 138 | Array [ 139 | Object { 140 | "Fn::GetAtt": Array [ 141 | "ServerlessNextJsBucket352C45DE", 142 | "Arn", 143 | ], 144 | }, 145 | "/*", 146 | ], 147 | ], 148 | }, 149 | ], 150 | }, 151 | ], 152 | "Version": "2012-10-17", 153 | }, 154 | "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", 155 | "Roles": Array [ 156 | Object { 157 | "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", 158 | }, 159 | ], 160 | }, 161 | "Type": "AWS::IAM::Policy", 162 | }, 163 | "Distribution830FAC52": Object { 164 | "Properties": Object { 165 | "DistributionConfig": Object { 166 | "CacheBehaviors": Array [ 167 | Object { 168 | "AllowedMethods": Array [ 169 | "GET", 170 | "HEAD", 171 | "OPTIONS", 172 | ], 173 | "CachePolicyId": Object { 174 | "Ref": "ServerlessNextJsBucketAssetsCachePolicyE658DAF4", 175 | }, 176 | "CachedMethods": Array [ 177 | "GET", 178 | "HEAD", 179 | "OPTIONS", 180 | ], 181 | "Compress": true, 182 | "PathPattern": "_next/*", 183 | "TargetOriginId": "SnapshotDistributionOrigin21410E148", 184 | "ViewerProtocolPolicy": "redirect-to-https", 185 | }, 186 | Object { 187 | "AllowedMethods": Array [ 188 | "GET", 189 | "HEAD", 190 | "OPTIONS", 191 | ], 192 | "CachePolicyId": Object { 193 | "Ref": "ServerlessNextJsBucketAssetsCachePolicyE658DAF4", 194 | }, 195 | "CachedMethods": Array [ 196 | "GET", 197 | "HEAD", 198 | "OPTIONS", 199 | ], 200 | "Compress": true, 201 | "PathPattern": "static/*", 202 | "TargetOriginId": "SnapshotDistributionOrigin21410E148", 203 | "ViewerProtocolPolicy": "redirect-to-https", 204 | }, 205 | ], 206 | "DefaultCacheBehavior": Object { 207 | "AllowedMethods": Array [ 208 | "GET", 209 | "HEAD", 210 | "OPTIONS", 211 | ], 212 | "CachePolicyId": "4135ea2d-6df8-44a3-9df3-4b5a84be39ad", 213 | "CachedMethods": Array [ 214 | "GET", 215 | "HEAD", 216 | "OPTIONS", 217 | ], 218 | "Compress": true, 219 | "LambdaFunctionAssociations": Array [ 220 | Object { 221 | "EventType": "origin-request", 222 | "IncludeBody": true, 223 | "LambdaFunctionARN": Object { 224 | "Ref": "ServerlessNextJsLambdaCurrentVersionA807374Aeb608ea7bf0d0fb7901faacf6e8862f7", 225 | }, 226 | }, 227 | Object { 228 | "EventType": "origin-response", 229 | "LambdaFunctionARN": Object { 230 | "Ref": "ServerlessNextJsLambdaCurrentVersionA807374Aeb608ea7bf0d0fb7901faacf6e8862f7", 231 | }, 232 | }, 233 | ], 234 | "TargetOriginId": "SnapshotDistributionOrigin18AF93D65", 235 | "ViewerProtocolPolicy": "redirect-to-https", 236 | }, 237 | "Enabled": true, 238 | "HttpVersion": "http2", 239 | "IPV6Enabled": true, 240 | "Origins": Array [ 241 | Object { 242 | "DomainName": Object { 243 | "Fn::GetAtt": Array [ 244 | "ServerlessNextJsBucket352C45DE", 245 | "RegionalDomainName", 246 | ], 247 | }, 248 | "Id": "SnapshotDistributionOrigin18AF93D65", 249 | "S3OriginConfig": Object { 250 | "OriginAccessIdentity": Object { 251 | "Fn::Join": Array [ 252 | "", 253 | Array [ 254 | "origin-access-identity/cloudfront/", 255 | Object { 256 | "Ref": "DistributionOrigin1S3Origin5F5C0696", 257 | }, 258 | ], 259 | ], 260 | }, 261 | }, 262 | }, 263 | Object { 264 | "DomainName": Object { 265 | "Fn::GetAtt": Array [ 266 | "ServerlessNextJsBucket352C45DE", 267 | "RegionalDomainName", 268 | ], 269 | }, 270 | "Id": "SnapshotDistributionOrigin21410E148", 271 | "S3OriginConfig": Object { 272 | "OriginAccessIdentity": Object { 273 | "Fn::Join": Array [ 274 | "", 275 | Array [ 276 | "origin-access-identity/cloudfront/", 277 | Object { 278 | "Ref": "DistributionOrigin2S3OriginE484D4BF", 279 | }, 280 | ], 281 | ], 282 | }, 283 | }, 284 | }, 285 | ], 286 | }, 287 | }, 288 | "Type": "AWS::CloudFront::Distribution", 289 | }, 290 | "DistributionOrigin1S3Origin5F5C0696": Object { 291 | "Properties": Object { 292 | "CloudFrontOriginAccessIdentityConfig": Object { 293 | "Comment": "Identity for SnapshotDistributionOrigin18AF93D65", 294 | }, 295 | }, 296 | "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", 297 | }, 298 | "DistributionOrigin2S3OriginE484D4BF": Object { 299 | "Properties": Object { 300 | "CloudFrontOriginAccessIdentityConfig": Object { 301 | "Comment": "Identity for SnapshotDistributionOrigin21410E148", 302 | }, 303 | }, 304 | "Type": "AWS::CloudFront::CloudFrontOriginAccessIdentity", 305 | }, 306 | "ServerlessNextJsAssetsDeploymentDeploynextdataAwsCliLayer3A9A023C": Object { 307 | "Properties": Object { 308 | "Content": Object { 309 | "S3Bucket": Object { 310 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 311 | }, 312 | "S3Key": "e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68.zip", 313 | }, 314 | "Description": "/opt/awscli/aws", 315 | }, 316 | "Type": "AWS::Lambda::LayerVersion", 317 | }, 318 | "ServerlessNextJsAssetsDeploymentDeploynextdataCustomResourceE8CDBBD7": Object { 319 | "DeletionPolicy": "Delete", 320 | "Properties": Object { 321 | "DestinationBucketKeyPrefix": "_next/data", 322 | "DestinationBucketName": Object { 323 | "Ref": "ServerlessNextJsBucket352C45DE", 324 | }, 325 | "Prune": true, 326 | "ServiceToken": Object { 327 | "Fn::GetAtt": Array [ 328 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", 329 | "Arn", 330 | ], 331 | }, 332 | "SourceBucketNames": Array [ 333 | Object { 334 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 335 | }, 336 | ], 337 | "SourceObjectKeys": Array [ 338 | "a192a89cb04f674d88a9152374f0fd21f112bcbe3bf8196e13e0bc05c37b011f.zip", 339 | ], 340 | "SystemMetadata": Object { 341 | "cache-control": "public, max-age=0, s-maxage=2678400, must-revalidate", 342 | }, 343 | }, 344 | "Type": "Custom::CDKBucketDeployment", 345 | "UpdateReplacePolicy": "Delete", 346 | }, 347 | "ServerlessNextJsAssetsDeploymentDeploynextstaticAwsCliLayer99BF5922": Object { 348 | "Properties": Object { 349 | "Content": Object { 350 | "S3Bucket": Object { 351 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 352 | }, 353 | "S3Key": "e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68.zip", 354 | }, 355 | "Description": "/opt/awscli/aws", 356 | }, 357 | "Type": "AWS::Lambda::LayerVersion", 358 | }, 359 | "ServerlessNextJsAssetsDeploymentDeploynextstaticCustomResourceCEC89944": Object { 360 | "DeletionPolicy": "Delete", 361 | "Properties": Object { 362 | "DestinationBucketKeyPrefix": "_next/static", 363 | "DestinationBucketName": Object { 364 | "Ref": "ServerlessNextJsBucket352C45DE", 365 | }, 366 | "Prune": true, 367 | "ServiceToken": Object { 368 | "Fn::GetAtt": Array [ 369 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", 370 | "Arn", 371 | ], 372 | }, 373 | "SourceBucketNames": Array [ 374 | Object { 375 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 376 | }, 377 | ], 378 | "SourceObjectKeys": Array [ 379 | "a192a89cb04f674d88a9152374f0fd21f112bcbe3bf8196e13e0bc05c37b011f.zip", 380 | ], 381 | "SystemMetadata": Object { 382 | "cache-control": "public, max-age=31536000, immutable", 383 | }, 384 | }, 385 | "Type": "Custom::CDKBucketDeployment", 386 | "UpdateReplacePolicy": "Delete", 387 | }, 388 | "ServerlessNextJsAssetsDeploymentDeploypublicAwsCliLayerE21BE73E": Object { 389 | "Properties": Object { 390 | "Content": Object { 391 | "S3Bucket": Object { 392 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 393 | }, 394 | "S3Key": "e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68.zip", 395 | }, 396 | "Description": "/opt/awscli/aws", 397 | }, 398 | "Type": "AWS::Lambda::LayerVersion", 399 | }, 400 | "ServerlessNextJsAssetsDeploymentDeploypublicCustomResource5B1F4DE8": Object { 401 | "DeletionPolicy": "Delete", 402 | "Properties": Object { 403 | "DestinationBucketKeyPrefix": "public", 404 | "DestinationBucketName": Object { 405 | "Ref": "ServerlessNextJsBucket352C45DE", 406 | }, 407 | "Prune": true, 408 | "ServiceToken": Object { 409 | "Fn::GetAtt": Array [ 410 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", 411 | "Arn", 412 | ], 413 | }, 414 | "SourceBucketNames": Array [ 415 | Object { 416 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 417 | }, 418 | ], 419 | "SourceObjectKeys": Array [ 420 | "a192a89cb04f674d88a9152374f0fd21f112bcbe3bf8196e13e0bc05c37b011f.zip", 421 | ], 422 | "SystemMetadata": Object { 423 | "cache-control": "public, max-age=31536000, must-revalidate", 424 | }, 425 | }, 426 | "Type": "Custom::CDKBucketDeployment", 427 | "UpdateReplacePolicy": "Delete", 428 | }, 429 | "ServerlessNextJsAssetsDeploymentDeploystaticAwsCliLayer10555A7C": Object { 430 | "Properties": Object { 431 | "Content": Object { 432 | "S3Bucket": Object { 433 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 434 | }, 435 | "S3Key": "e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68.zip", 436 | }, 437 | "Description": "/opt/awscli/aws", 438 | }, 439 | "Type": "AWS::Lambda::LayerVersion", 440 | }, 441 | "ServerlessNextJsAssetsDeploymentDeploystaticCustomResource840FF715": Object { 442 | "DeletionPolicy": "Delete", 443 | "Properties": Object { 444 | "DestinationBucketKeyPrefix": "static", 445 | "DestinationBucketName": Object { 446 | "Ref": "ServerlessNextJsBucket352C45DE", 447 | }, 448 | "Prune": true, 449 | "ServiceToken": Object { 450 | "Fn::GetAtt": Array [ 451 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", 452 | "Arn", 453 | ], 454 | }, 455 | "SourceBucketNames": Array [ 456 | Object { 457 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 458 | }, 459 | ], 460 | "SourceObjectKeys": Array [ 461 | "a192a89cb04f674d88a9152374f0fd21f112bcbe3bf8196e13e0bc05c37b011f.zip", 462 | ], 463 | "SystemMetadata": Object { 464 | "cache-control": "public, max-age=31536000, must-revalidate", 465 | }, 466 | }, 467 | "Type": "Custom::CDKBucketDeployment", 468 | "UpdateReplacePolicy": "Delete", 469 | }, 470 | "ServerlessNextJsAssetsDeploymentDeploystaticpagesAwsCliLayerF92C0A7F": Object { 471 | "Properties": Object { 472 | "Content": Object { 473 | "S3Bucket": Object { 474 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 475 | }, 476 | "S3Key": "e9882ab123687399f934da0d45effe675ecc8ce13b40cb946f3e1d6141fe8d68.zip", 477 | }, 478 | "Description": "/opt/awscli/aws", 479 | }, 480 | "Type": "AWS::Lambda::LayerVersion", 481 | }, 482 | "ServerlessNextJsAssetsDeploymentDeploystaticpagesCustomResource5E8513F1": Object { 483 | "DeletionPolicy": "Delete", 484 | "Properties": Object { 485 | "DestinationBucketKeyPrefix": "static-pages", 486 | "DestinationBucketName": Object { 487 | "Ref": "ServerlessNextJsBucket352C45DE", 488 | }, 489 | "Prune": true, 490 | "ServiceToken": Object { 491 | "Fn::GetAtt": Array [ 492 | "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", 493 | "Arn", 494 | ], 495 | }, 496 | "SourceBucketNames": Array [ 497 | Object { 498 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 499 | }, 500 | ], 501 | "SourceObjectKeys": Array [ 502 | "a192a89cb04f674d88a9152374f0fd21f112bcbe3bf8196e13e0bc05c37b011f.zip", 503 | ], 504 | "SystemMetadata": Object { 505 | "cache-control": "public, max-age=0, s-maxage=2678400, must-revalidate", 506 | }, 507 | }, 508 | "Type": "Custom::CDKBucketDeployment", 509 | "UpdateReplacePolicy": "Delete", 510 | }, 511 | "ServerlessNextJsBucket352C45DE": Object { 512 | "DeletionPolicy": "Retain", 513 | "Properties": Object { 514 | "Tags": Array [ 515 | Object { 516 | "Key": "aws-cdk:cr-owned:_next/data:6f66766d", 517 | "Value": "true", 518 | }, 519 | Object { 520 | "Key": "aws-cdk:cr-owned:_next/static:16a0745c", 521 | "Value": "true", 522 | }, 523 | Object { 524 | "Key": "aws-cdk:cr-owned:public:cece9ccb", 525 | "Value": "true", 526 | }, 527 | Object { 528 | "Key": "aws-cdk:cr-owned:static-pages:cc2587ed", 529 | "Value": "true", 530 | }, 531 | Object { 532 | "Key": "aws-cdk:cr-owned:static:f34e323d", 533 | "Value": "true", 534 | }, 535 | ], 536 | }, 537 | "Type": "AWS::S3::Bucket", 538 | "UpdateReplacePolicy": "Retain", 539 | }, 540 | "ServerlessNextJsBucketAssetsCachePolicyE658DAF4": Object { 541 | "Properties": Object { 542 | "CachePolicyConfig": Object { 543 | "DefaultTTL": 2592000, 544 | "MaxTTL": 2592000, 545 | "MinTTL": 2592000, 546 | "Name": Object { 547 | "Fn::Join": Array [ 548 | "", 549 | Array [ 550 | "SnapshotServerlessNextJsBucketAssetsCachePolicy9B8BD577-", 551 | Object { 552 | "Ref": "AWS::Region", 553 | }, 554 | ], 555 | ], 556 | }, 557 | "ParametersInCacheKeyAndForwardedToOrigin": Object { 558 | "CookiesConfig": Object { 559 | "CookieBehavior": "none", 560 | }, 561 | "EnableAcceptEncodingBrotli": true, 562 | "EnableAcceptEncodingGzip": true, 563 | "HeadersConfig": Object { 564 | "HeaderBehavior": "none", 565 | }, 566 | "QueryStringsConfig": Object { 567 | "QueryStringBehavior": "none", 568 | }, 569 | }, 570 | }, 571 | }, 572 | "Type": "AWS::CloudFront::CachePolicy", 573 | }, 574 | "ServerlessNextJsBucketPolicy079C42BC": Object { 575 | "Properties": Object { 576 | "Bucket": Object { 577 | "Ref": "ServerlessNextJsBucket352C45DE", 578 | }, 579 | "PolicyDocument": Object { 580 | "Statement": Array [ 581 | Object { 582 | "Action": "s3:GetObject", 583 | "Effect": "Allow", 584 | "Principal": Object { 585 | "CanonicalUser": Object { 586 | "Fn::GetAtt": Array [ 587 | "DistributionOrigin1S3Origin5F5C0696", 588 | "S3CanonicalUserId", 589 | ], 590 | }, 591 | }, 592 | "Resource": Object { 593 | "Fn::Join": Array [ 594 | "", 595 | Array [ 596 | Object { 597 | "Fn::GetAtt": Array [ 598 | "ServerlessNextJsBucket352C45DE", 599 | "Arn", 600 | ], 601 | }, 602 | "/*", 603 | ], 604 | ], 605 | }, 606 | }, 607 | Object { 608 | "Action": "s3:GetObject", 609 | "Effect": "Allow", 610 | "Principal": Object { 611 | "CanonicalUser": Object { 612 | "Fn::GetAtt": Array [ 613 | "DistributionOrigin2S3OriginE484D4BF", 614 | "S3CanonicalUserId", 615 | ], 616 | }, 617 | }, 618 | "Resource": Object { 619 | "Fn::Join": Array [ 620 | "", 621 | Array [ 622 | Object { 623 | "Fn::GetAtt": Array [ 624 | "ServerlessNextJsBucket352C45DE", 625 | "Arn", 626 | ], 627 | }, 628 | "/*", 629 | ], 630 | ], 631 | }, 632 | }, 633 | ], 634 | "Version": "2012-10-17", 635 | }, 636 | }, 637 | "Type": "AWS::S3::BucketPolicy", 638 | }, 639 | "ServerlessNextJsLambdaCF85BA9F": Object { 640 | "DependsOn": Array [ 641 | "ServerlessNextJsRoleDefaultPolicyC343B163", 642 | "ServerlessNextJsRole3D5E58EB", 643 | ], 644 | "Properties": Object { 645 | "Code": Object { 646 | "S3Bucket": Object { 647 | "Fn::Sub": "cdk-hnb659fds-assets-\${AWS::AccountId}-\${AWS::Region}", 648 | }, 649 | "S3Key": "b5f1f675ebad4ecbdec1554c42c5308a68f628f979a81f7b12665f2445df3ee7.zip", 650 | }, 651 | "Handler": "index-indirect.handler", 652 | "MemorySize": 512, 653 | "Role": Object { 654 | "Fn::GetAtt": Array [ 655 | "ServerlessNextJsRole3D5E58EB", 656 | "Arn", 657 | ], 658 | }, 659 | "Runtime": "nodejs12.x", 660 | "Timeout": 30, 661 | }, 662 | "Type": "AWS::Lambda::Function", 663 | }, 664 | "ServerlessNextJsLambdaCurrentVersionA807374Aeb608ea7bf0d0fb7901faacf6e8862f7": Object { 665 | "Properties": Object { 666 | "FunctionName": Object { 667 | "Ref": "ServerlessNextJsLambdaCF85BA9F", 668 | }, 669 | }, 670 | "Type": "AWS::Lambda::Version", 671 | }, 672 | "ServerlessNextJsRole3D5E58EB": Object { 673 | "Properties": Object { 674 | "AssumeRolePolicyDocument": Object { 675 | "Statement": Array [ 676 | Object { 677 | "Action": "sts:AssumeRole", 678 | "Effect": "Allow", 679 | "Principal": Object { 680 | "Service": "lambda.amazonaws.com", 681 | }, 682 | }, 683 | Object { 684 | "Action": "sts:AssumeRole", 685 | "Effect": "Allow", 686 | "Principal": Object { 687 | "Service": "edgelambda.amazonaws.com", 688 | }, 689 | }, 690 | ], 691 | "Version": "2012-10-17", 692 | }, 693 | "ManagedPolicyArns": Array [ 694 | Object { 695 | "Fn::Join": Array [ 696 | "", 697 | Array [ 698 | "arn:", 699 | Object { 700 | "Ref": "AWS::Partition", 701 | }, 702 | ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole", 703 | ], 704 | ], 705 | }, 706 | ], 707 | }, 708 | "Type": "AWS::IAM::Role", 709 | }, 710 | "ServerlessNextJsRoleDefaultPolicyC343B163": Object { 711 | "Properties": Object { 712 | "PolicyDocument": Object { 713 | "Statement": Array [ 714 | Object { 715 | "Action": Array [ 716 | "s3:GetObject*", 717 | "s3:GetBucket*", 718 | "s3:List*", 719 | "s3:DeleteObject*", 720 | "s3:PutObject", 721 | "s3:Abort*", 722 | ], 723 | "Effect": "Allow", 724 | "Resource": Array [ 725 | Object { 726 | "Fn::GetAtt": Array [ 727 | "ServerlessNextJsBucket352C45DE", 728 | "Arn", 729 | ], 730 | }, 731 | Object { 732 | "Fn::Join": Array [ 733 | "", 734 | Array [ 735 | Object { 736 | "Fn::GetAtt": Array [ 737 | "ServerlessNextJsBucket352C45DE", 738 | "Arn", 739 | ], 740 | }, 741 | "/*", 742 | ], 743 | ], 744 | }, 745 | ], 746 | }, 747 | ], 748 | "Version": "2012-10-17", 749 | }, 750 | "PolicyName": "ServerlessNextJsRoleDefaultPolicyC343B163", 751 | "Roles": Array [ 752 | Object { 753 | "Ref": "ServerlessNextJsRole3D5E58EB", 754 | }, 755 | ], 756 | }, 757 | "Type": "AWS::IAM::Policy", 758 | }, 759 | }, 760 | "Rules": Object { 761 | "CheckBootstrapVersion": Object { 762 | "Assertions": Array [ 763 | Object { 764 | "Assert": Object { 765 | "Fn::Not": Array [ 766 | Object { 767 | "Fn::Contains": Array [ 768 | Array [ 769 | "1", 770 | "2", 771 | "3", 772 | "4", 773 | "5", 774 | ], 775 | Object { 776 | "Ref": "BootstrapVersion", 777 | }, 778 | ], 779 | }, 780 | ], 781 | }, 782 | "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.", 783 | }, 784 | ], 785 | }, 786 | }, 787 | } 788 | `; 789 | -------------------------------------------------------------------------------- /test/create-nextjs-example.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as execa from 'execa'; 3 | import * as fs from 'fs-extra'; 4 | 5 | export function createNextjsExample(example: string) { 6 | const subdir = `tmp-${example}`; 7 | const baseDir = path.normalize(path.join(__dirname, '..')); 8 | const buildDir = path.join(baseDir, subdir); 9 | 10 | if (!fs.existsSync(buildDir)) { 11 | execa.sync('npx', ['create-next-app', '--example', example, subdir], { 12 | cwd: baseDir, 13 | }); 14 | } 15 | 16 | execa.sync('yarn', ['install'], { 17 | cwd: buildDir, 18 | }); 19 | 20 | const nextBuildDir = path.join(buildDir, '.next'); 21 | if (fs.existsSync(nextBuildDir)) { 22 | fs.rmdirSync(nextBuildDir, { recursive: true }); 23 | } 24 | 25 | const slsNextBuildDir = path.join(buildDir, '.serverless_nextjs'); 26 | if (fs.existsSync(slsNextBuildDir)) { 27 | fs.rmdirSync(slsNextBuildDir, { recursive: true }); 28 | } 29 | 30 | return buildDir; 31 | } -------------------------------------------------------------------------------- /test/integ.main.lit.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as cdk from 'aws-cdk-lib'; 3 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 4 | import { Construct } from 'constructs'; 5 | import * as fs from 'fs-extra'; 6 | import { NextjsArtifact, ServerlessNextjs } from '../src'; 7 | import { createNextjsExample } from './create-nextjs-example'; 8 | 9 | export interface IntegMainLitProps extends cdk.StackProps { 10 | readonly nextjsDirectory: string; 11 | } 12 | 13 | export class IntegMainLit extends cdk.Stack { 14 | constructor(scope_: Construct, props: IntegMainLitProps) { 15 | super(scope_, 'cdk-serverless-nextjs-integ-main', props); 16 | 17 | const scope = this; 18 | const pathToYourProjectDirectory = props.nextjsDirectory; 19 | 20 | // ::SNIP 21 | // Create a ServerlessNextjs construct in your stack to get started. 22 | // Your stack MUST be in us-east-1 as that's the only region in which AWS 23 | // supports deploying edge lambdas. 24 | const serverlessNextjs = new ServerlessNextjs(scope, 'NextJs', { 25 | // Then produce and add a Next.js artifact to ServerlessNextjs by 26 | // building it from your project directory. 27 | nextjsArtifact: NextjsArtifact.fromBuild({ 28 | // Provide your Next.js project directory. 29 | nextjsDirectory: pathToYourProjectDirectory, 30 | 31 | // Provide the commands you need NextjsArtifact to run to build the 32 | // `.next` directory. 33 | buildCommand: ['yarn', 'next', 'build'], 34 | }), 35 | }); 36 | 37 | // Create CloudFront distribution 38 | const dist = new cloudfront.Distribution(scope, 'Distribution', { 39 | // And configure it with serverless Next.js as Lambda @ Edge 40 | ...serverlessNextjs.cloudFrontConfig, 41 | 42 | // Add anything else you need on your CloudFront distribution here, like 43 | // certificates, cnames, price classes, logging, etc. 44 | }); 45 | // ::END-SNIP 46 | 47 | new cdk.CfnOutput(scope, 'Url', { 48 | value: cdk.Fn.join('', [ 49 | 'https://', 50 | dist.distributionDomainName, 51 | '/', 52 | ]), 53 | }); 54 | } 55 | } 56 | 57 | if (require.main === module) { 58 | // Create a Next.js project from an example. 59 | const nextjsDirectory = createNextjsExample('blog-starter'); 60 | // Copy a page in that has revalidation to test ISR. 61 | fs.copyFileSync(path.join(__dirname, 'nextjs-test-files', 'time.js'), path.join(nextjsDirectory, 'pages', 'time.js')); 62 | 63 | const apiDir = path.join(nextjsDirectory, 'pages', 'api'); 64 | fs.mkdirSync(apiDir, { recursive: true }); 65 | fs.copyFileSync(path.join(__dirname, 'nextjs-test-files', 'api.js'), path.join(apiDir, 'api.js')); 66 | 67 | const app = new cdk.App(); 68 | new IntegMainLit(app, { 69 | nextjsDirectory, 70 | env: { 71 | account: process.env.CDK_DEFAULT_ACCOUNT, 72 | region: 'us-east-1', 73 | }, 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /test/nextjs-artifact.test.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs-extra'; 3 | import { NextjsArtifact } from '../src'; 4 | import { createNextjsExample } from './create-nextjs-example'; 5 | 6 | it('generates the lambda bundle', () => { 7 | // Create a Next.js project from an example. 8 | const nextjsDirectory = createNextjsExample('blog-starter'); 9 | 10 | const artifact = NextjsArtifact.fromBuild({ 11 | nextjsDirectory, 12 | buildCommand: ['yarn', 'next', 'build'], 13 | }); 14 | 15 | // WHEN 16 | const { buildOutputDir } = artifact._bind(); 17 | 18 | // THEN 19 | expect(fs.existsSync(path.join(buildOutputDir, 'default-lambda'))).toBe(true); 20 | expect(fs.existsSync(path.join(buildOutputDir, 'image-lambda'))).toBe(true); 21 | expect(fs.existsSync(path.join(buildOutputDir, 'regeneration-lambda'))).toBe(true); 22 | expect(fs.existsSync(path.join(buildOutputDir, 'api-lambda'))).toBe(true); 23 | expect(fs.existsSync(path.join(buildOutputDir, 'assets'))).toBe(true); 24 | }); -------------------------------------------------------------------------------- /test/nextjs-test-files/api.js: -------------------------------------------------------------------------------- 1 | export default function handler(req, res) { 2 | res.status(200); 3 | res.setHeader('Cache-Control', 'no-cache'); 4 | res.json({ 5 | randomness: Math.random(), 6 | date: new Date().toISOString(), 7 | info: extractFunctionInfo(process.env.AWS_LAMBDA_FUNCTION_NAME), 8 | CDK_SERVERLESS_NEXTJS_ORIGIN_REGION: process.env.CDK_SERVERLESS_NEXTJS_ORIGIN_REGION || false, 9 | CDK_SERVERLESS_NEXTJS_ORIGIN_BUCKET: process.env.CDK_SERVERLESS_NEXTJS_ORIGIN_BUCKET || false, 10 | }); 11 | } 12 | 13 | function extractFunctionInfo(name) { 14 | const [region, functionName] = name.split('.'); 15 | return { 16 | region, 17 | functionName, 18 | }; 19 | } -------------------------------------------------------------------------------- /test/nextjs-test-files/time.js: -------------------------------------------------------------------------------- 1 | // noinspection JSXNamespaceValidation,HtmlUnknownTarget,JSUnresolvedVariable 2 | 3 | import Head from 'next/head' 4 | 5 | export default function Home(props) { 6 | return ( 7 |
8 | 9 | Time of Day 10 | 11 | 12 | 13 | 14 |
15 |

Time of day

16 |

WorldTimeApi says the time of day is: {props.timeOfDay}

17 |
18 |
19 | ) 20 | } 21 | 22 | export async function getStaticProps() { 23 | const res = await fetch('https://worldtimeapi.org/api/timezone/America/Edmonton'); 24 | const resJson = await res.json(); 25 | 26 | return { 27 | props: { 28 | timeOfDay: resJson.datetime, 29 | }, 30 | revalidate: 10, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/snapshot.empty-artifact.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as assertions from 'aws-cdk-lib/assertions'; 3 | import { SnapshotEmptyArtifact } from './snapshot.empty-artifact'; 4 | 5 | test('snapshot', () => { 6 | const app = new cdk.App(); 7 | const stack = new SnapshotEmptyArtifact(app, 'Snapshot'); 8 | 9 | expect(assertions.Template.fromStack(stack).toJSON()).toMatchSnapshot(); 10 | }); -------------------------------------------------------------------------------- /test/snapshot.empty-artifact.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; 3 | import { Construct } from 'constructs'; 4 | import { NextjsArtifact, ServerlessNextjs } from '../src'; 5 | 6 | export class SnapshotEmptyArtifact extends cdk.Stack { 7 | constructor(scope: Construct, id: string, props: cdk.StackProps = {}) { 8 | super(scope, id, props); 9 | 10 | const serverlessNextjs = new ServerlessNextjs(this, 'ServerlessNextJs', { 11 | nextjsArtifact: NextjsArtifact._emptyArtifact(), 12 | }); 13 | 14 | new cloudfront.Distribution(this, 'Distribution', { 15 | ...serverlessNextjs.cloudFrontConfig, 16 | }); 17 | } 18 | } -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2019" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2019" 26 | }, 27 | "include": [ 28 | ".projenrc.js", 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules", 34 | "cdk.out", 35 | "test/pages", 36 | "test/tmp-*" 37 | ], 38 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 39 | } 40 | --------------------------------------------------------------------------------