├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples └── nextjs-v14 │ ├── .eslintrc.json │ ├── README.md │ ├── __tests__ │ └── pages │ │ └── index.test.tsx │ ├── app │ ├── favicon.ico │ ├── globals.css │ └── layout.tsx │ ├── function.js │ ├── jest.config.js │ ├── next.config.js │ ├── package-lock.json │ ├── package.json │ ├── pages │ ├── _app.tsx │ ├── api │ │ └── hello.ts │ ├── index.tsx │ ├── optimized-images │ │ └── index.tsx │ └── ssr │ │ ├── SSR.tsx │ │ └── index.tsx │ ├── postcss.config.js │ ├── public │ ├── images │ │ ├── sample.avif │ │ ├── sample.gif │ │ ├── sample.jpg │ │ ├── sample.png │ │ └── sample.webp │ ├── next.svg │ └── vercel.svg │ ├── styles │ ├── Home.module.css │ └── globals.css │ ├── tailwind.config.ts │ ├── terraform │ ├── main.tf │ ├── outputs.tf │ ├── providers.tf │ └── variables.tf │ └── tsconfig.json ├── main.tf ├── modules ├── distribution │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── image-optimization │ ├── main.tf │ ├── outputs.tf │ ├── variables.tf │ └── versions.tf ├── public-assets-hosting │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── server-side-rendering │ ├── main.tf │ ├── outputs.tf │ └── variables.tf └── static-assets-hosting │ ├── main.tf │ ├── outputs.tf │ └── variables.tf ├── original-license.md ├── outputs.tf ├── packages ├── ns-build │ ├── LICENSE │ ├── README.md │ ├── bin │ │ └── ns-build.sh │ ├── package-lock.json │ ├── package.json │ ├── server.js │ ├── server.ts │ └── tsconfig.json ├── ns-img-opt │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── scripts │ │ └── pre-publish.sh │ ├── src │ │ ├── constants.ts │ │ ├── helpers.ts │ │ └── index.ts │ └── tsconfig.json └── ns-img-rdr │ ├── LICENSE │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── scripts │ └── pre-publish.sh │ ├── src │ └── index.ts │ └── tsconfig.json ├── variables.tf ├── versions.tf └── visuals ├── cache.drawio ├── cache.webp ├── diagram.excalidraw ├── distribution.drawio ├── distribution.webp ├── module.drawio └── module.webp /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | **/.pnp 6 | .pnp.js 7 | 8 | # testing 9 | **/coverage 10 | 11 | # next.js 12 | **/.next/ 13 | **/.swc/ 14 | **/.next-tf/ 15 | **/out/ 16 | 17 | # production 18 | **/build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # debug 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | .pnpm-debug.log* 29 | 30 | # local env files 31 | .env*.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | #terraform 41 | **/.terraform/* 42 | **/tf-plan 43 | .terraform.lock.hcl 44 | .terraform 45 | examples/nextjs-v13/terraform/.terraform* 46 | env.tfvars 47 | 48 | **/.DS_Store 49 | 50 | **/deployments/ 51 | **/standalone/ 52 | 53 | packages/**/*.zip 54 | 55 | .idea/ 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | 6 | 7 | 8 | 9 | ## [v1.8.1] - 2025-04-22 10 | 11 | - Bugfix: cached paths 12 | 13 | ## [v1.8.0] - 2025-04-16 14 | 15 | - Use AWS-managed cache policies 16 | - Drop `referrer` in favor of `host` 17 | - Switch `301` to `302` 18 | - Add `viewer-request` cloudfront function to the distribution 19 | - Add `custom_cache_policy_id` tf variable 20 | 21 | ## [v1.7.1] - 2025-04-11 22 | 23 | - Bugfix: SPA (followup) 24 | 25 | ## [v1.7.0] - 2025-04-10 26 | 27 | - Bugfix: SPA 28 | 29 | ## [v1.6.1] - 2025-03-20 30 | 31 | - Bugfix: SPA for dynamic routes 32 | 33 | ## [v1.6.0] - 2025-03-19 34 | 35 | - Bugfix: SPA for dynamic routes 36 | 37 | ## [v1.5.1] - 2025-03-14 38 | 39 | - Bugfix: SPA for static routes 40 | 41 | ## [v1.5.0] - 2025-03-14 42 | 43 | - Bugfix: SPA for static routes 44 | 45 | ## [v1.4.0] - 2025-03-03 46 | 47 | - Feature: enabled compression for cached paths 48 | 49 | ## [v1.3.2] - 2025-02-11 50 | 51 | - Bugfix: add function assosiation for cached paths 52 | 53 | ## [v1.3.1] - 2025-02-11 54 | 55 | - Bugfix: reference to resourse in distribution module 56 | 57 | ## [v1.3.0] - 2025-02-11 58 | 59 | - Add `cloudfront_cached_paths` tf variable 60 | 61 | ## [v1.2.0] - 2024-12-23 62 | 63 | - Add `enable_image_optimization` tf variable 64 | 65 | ## [v1.1.2] - 2024-10-11 66 | 67 | - Add `delete_resized_versions` tf variable 68 | 69 | ## [v1.1.1] - 2024-10-11 70 | 71 | - Bugfix: having multiple images to pre-build was breaking terraform 72 | 73 | ## [v1.1.0] - 2024-10-11 74 | 75 | - Add `pre_resize_images` tf variable 76 | - Image optimization now returns speficic widths (16, 32, 64, 128, 256, 512, 1024) 77 | - Image quality for optimized images is now fix to 75% 78 | - On deployment, all image versions are optimized & stores to s3 79 | - Image optimization now redirects to `resized-assets/`, which serves already optimizated images 80 | 81 | ## [v1.0.3] - 2024-06-28 82 | 83 | - Add required dependencies for the module 84 | 85 | ## [v1.0.2] - 2024-06-07 86 | 87 | - Mark `next` as peer dependency 88 | - Silence zip process 89 | - Remove `app/` route example 90 | - pump version 91 | 92 | ## [v1.0.0] - 2024-05-15 93 | 94 | - Finally, we reached the point that we can consider this module as `stable`, while supporting the most popular **Next.js** feature for **pages/** router 95 | 96 | ## [v0.4.6] - 2024-05-15 97 | 98 | - Pump default nodejs runtime to v18 99 | - Completely disable caching for ssr paths 100 | - Fix redirect issue for public assets 101 | 102 | ## [v0.4.5] - 2024-05-15 103 | 104 | - Bugfix: some default image types were breaking ssr lambda 105 | 106 | ## [v0.4.3] - 2024-05-15 107 | 108 | - Add `custom_image_types` tf variable 109 | 110 | ## [v0.4.2] - 2024-05-14 111 | 112 | - Add `create_cloudfront_invalidation` tf variable 113 | 114 | ## [v0.4.1] - 2024-05-14 115 | 116 | - Bugfix: disable caching for server-side rendering 117 | - Add cloudfront invalidation after every deployment 118 | 119 | ## [v0.4.0] - 2024-05-14 120 | 121 | - Support `context` argument on `getServerSideProps()`, when using the `pages/` router 122 | 123 | ## [v0.3.9] - 2024-05-13 124 | 125 | - Support all components for rendering images (`'webp', 'jpeg', 'png', 'gif', 'avif', 'svg'`) 126 | 127 | ## [v0.3.8] - 2024-04-30 128 | 129 | - Add `x-forwarded-host` headers to the request info 130 | - Deprecate `override_host_header` variable 131 | 132 | ## [v0.3.7] - 2024-04-29 133 | 134 | - Add `cloudfront_function_associations` variable to associate cloudfront functions with the defaulf distribution 135 | - Add `override_host_header` variable to enabled overriding of host header, by the custom domain 136 | - Add `wait_for_distribution_deployment` variable to stop waiting for the distribution status to change from `InProgress` to `Deployed` 137 | 138 | ## [v0.3.6] - 2024-04-25 139 | 140 | - Bug fix next handler 141 | 142 | ## [v0.3.5] - 2024-04-25 143 | 144 | - Bug fix next handler 145 | 146 | ## [v0.3.4] - 2024-04-25 147 | 148 | - Add `show_debug_logs` variable to enabled debug logs 149 | 150 | ## [v0.3.3] - 2024-04-25 151 | 152 | - Add `use_default_server_side_props_handler` variable to enabled usage of the default server side props handler, instead of the our custom one 153 | 154 | ## [v0.3.2] - 2024-04-23 155 | 156 | Updates: 157 | 158 | - Rename SSR Lambda 159 | - Pump default nodejs runtime to v18 160 | 161 | **Breaking Changes** 162 | 163 | - Require an aws provider for global region (must be `us-east-1`) 164 | 165 | ## [v0.3.1] - 2024-03-26 166 | 167 | - Fix git urls 168 | 169 | ## [v0.3.0] - 2024-03-09 170 | 171 | - Set up forked repo, module & packages 172 | 173 | ## [v0.2.20] - 2023-12-13 174 | 175 | - Fix broken SPA feature 176 | 177 | ## [v0.2.19] - 2023-12-06 178 | 179 | - Option to copy all packages directly into the next_lambda 180 | 181 | ## [v0.2.18] - 2023-12-04 182 | 183 | - Support remote images 184 | 185 | ## [v0.2.17] - 2023-11-23 186 | 187 | Updates: 188 | 189 | - add & update customization options of the module through terraform variables terraform-aws-nextjs-serverless/pull/42) 190 | - store next_lambda source zip in s3 191 | - expand image examples 192 | - update cache diagram 193 | - update dependencies versions to latest 194 | 195 | **Breaking Changes** 196 | 197 | - `lambda_memory_size` is replaced by `next_lambda_memory_size` & `image_optimization_lambda_memory_size` 198 | - `runtime` is replaced by `next_lambda_runtime` & `image_optimization_runtime` 199 | - `logs_retention` is replaced by `next_lambda_logs_retention` & `image_optimization_logs_retention` 200 | 201 | ## [v0.2.16] - 2023-11-20 202 | 203 | - Bugfix: next_lambda_layer was not updating 204 | - `ns-build`: Option to copy some packages directly into the next_lambda, since in some rare cases import from the layer fails 205 | 206 | ## [v0.2.13] - 2023-11-16 207 | 208 | - Add docs about dependancies 209 | - Bugfix: Image redirection had issues with deeply nested files 210 | 211 | ## [v0.2.11] - 2023-11-03 212 | 213 | - Add SSR example 214 | - Add CHANGELOG docs 215 | - Fix: Set a version for every package used by `ns-build` 216 | 217 | ## [v0.2.10] - 2023-11-01 218 | 219 | **Add License** 220 | 221 | - Add License for the module 222 | - Add License for the modulethe packages 223 | 224 | ## [v0.2.8] - 2023-11-01 225 | 226 | **Intialize Terraform Tests** 227 | 228 | - Add terraform tests using TerraTest 229 | - Improve visualizations 230 | - Improved Documentation 231 | 232 | ## [v0.2.6] - 2023-10-26 233 | 234 | - Improve visualizations 235 | 236 | ## [v0.2.5] - 2023-10-26 237 | 238 | - Add visualization diagrams for the module and the distribution 239 | 240 | ## [v0.2.4] - 2023-10-23 241 | 242 | - Fix: S3 cross-region access for Image Optimization on Lambda@Edge 243 | 244 | ## [v0.2.1] - 2023-10-23 245 | 246 | **Improve Image Optimization** 247 | 248 | - Image Optimization: fetch images from S3, instead of public S3 URL 249 | 250 | ## [v0.2.0] - 2023-10-23 251 | 252 | **Restructure Modules** 253 | 254 | - Restructure the Modules' structure to support future plans 255 | - Release a functional version of Image Optimization feature 256 | 257 | ## [v0.1.1] - 2023-10-20 258 | 259 | - Fix: lambda@edge source code read 260 | 261 | ## [v0.1.0] - 2023-10-20 262 | 263 | **Intial Image Optimization Feature Releaze** 264 | 265 | - Serve public assets using Lambda@Edge to optimize size, file type, quality 266 | 267 | ## [v0.0.7] - 2023-09-12 268 | 269 | - Fix cloudwatch log group name mis-configuration 270 | 271 | ## [v0.0.6] - 2023-09-12 272 | 273 | - Add next_lambda_policy_statements option 274 | 275 | ## [v0.0.4] - 2023-09-12 276 | 277 | - Change: Store next_lambda layer in S3, instead of uploading it directly 278 | 279 | ## [v0.0.2] - 2023-09-07 280 | 281 | - Add the custom domain option for the CloudFront distribution terraform-aws-nextjs-serverless/pull/5) 282 | - Add the option for next_lambda env vars 283 | - Fix BucketACL issue 284 | 285 | ## [v0.0.1] - 2023-09-04 286 | 287 | **Initial Release** 288 | 289 | - Serve next.js app with AWS Lambda & API Gateway 290 | - Serve static assets with CloudFront & S3 291 | - Serve public assets with CloudFront & S3 292 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Feel free to contribute to this module. 4 | 5 | As a general advice it is always a good idea to raise an [issue](https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues) before creating a new pull request.
This ensures that we don't have to reject [pull requests](https://github.com/emyriounis/terraform-aws-nextjs-serverless/pulls) that are not aligning with our roadmap and not wasting your valuable time. 6 | 7 | ## Reporting Bugs 8 | 9 | If you encounter a bug, please open an [issue](https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues) on the GitHub repository.
Be sure to include as much information as possible to help us understand and reproduce the problem. 10 | 11 | ## Feature Requests 12 | 13 | If you have an idea for a new feature or enhancement, please feel free to open an [issue](https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues) and describe it.
We'd love to hear your suggestions! 14 | 15 | ## Testing 16 | 17 | Please check our [testing guidelines](https://github.com/emyriounis/terraform-aws-nextjs-serverless/blob/main/tests). 18 | 19 | ## Development Workflow 20 | 21 | ### ns-build package 22 | 23 | 1. Edit the `examples/[selected_example]/node_modules/ns-build/bin/ns-build.sh` file. 24 | 2. Run `npm run ns-build` to build the Next.js app using the custom script. 25 | 3. Re-deploy the module. 26 | 4. Make sure your changes are applied and existing functionality is not broken. 27 | 5. Copy the updates you made to the `packages/ns-build/bin/ns-build.sh` file. 28 | 29 | ### ns-img-rdr packages 30 | 31 | 1. Make your changes 32 | 2. Run `npm run prepare-lambda` 33 | 3. Copy `packages/ns-img-rdr/source.zip` to `examples/nextjs-v13/deployments/ns-img-rdr/source.zip` 34 | 4. Re-deploy the module. 35 | 5. Make sure your changes are applied and existing functionality is not broken. 36 | 37 | ### ns-img-opt packages 38 | 39 | 1. Make your changes 40 | 2. Run `npm run prepare-lambda` 41 | 3. Copy `packages/ns-img-opt/source.zip` to `examples/nextjs-v13/deployments/ns-img-opt/source.zip` 42 | 4. Re-deploy the module. 43 | 5. Make sure your changes are applied and existing functionality is not broken. 44 | 45 | ### next_serverless module 46 | 47 | 1. Switch to the local source 48 | 49 | ```diff 50 | module "tf_next" { 51 | - source = "emyriounis/nextjs-serverless/aws" 52 | - version = "1.8.1" 53 | + source = "../../../" 54 | ... 55 | } 56 | ``` 57 | 58 | 2. Make your changes to the terraform files 59 | 3. Validate them (`terraform validate`) 60 | 4. Re-deploy the module. 61 | 5. Make sure your changes are applied and existing functionality is not broken. 62 | 6. Follow existing format (`terraform fmt -recursive`) 63 | 64 | ## Issues 65 | 66 | If you face any problem, feel free to open an [issue](https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues). 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | the copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor to discuss and improve the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by the Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributors that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, the Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assuming any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2024\ 179 | Eleftherios Myriounis 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Next.js module for AWS 2 | 3 | Zero-Config Terraform Module to deploy Next.js Apps on AWS using Serverless resources 4 | 5 | ## Usage 6 | 7 | ### Dependencies 8 | 9 | - Node: 16+ 10 | - Terraform: 1.6.3+ 11 | - bash 12 | - zip 13 | 14 | ### Prepare 15 | 16 | Add the following dependencies & script to your _package.json_ file 17 | 18 | ```json 19 | package.json 20 | 21 | { 22 | "scripts": { 23 | "ns-build": "ns-build", 24 | ... 25 | }, 26 | "dependencies": { 27 | "ns-build": "latest", 28 | "next": "^14", 29 | ... 30 | }, 31 | ... 32 | } 33 | ``` 34 | 35 | Add the `output: "standalone"` option to the _next.config.js_ file 36 | 37 | ```json 38 | next.config.js 39 | 40 | const nextConfig = { 41 | ... 42 | "output": "standalone", 43 | ... 44 | } 45 | 46 | module.exports = nextConfig 47 | 48 | ``` 49 | 50 | ### Create Terraform deployment 51 | 52 | Check it on [Terraform Registry](https://registry.terraform.io/modules/emyriounis/nextjs-serverless) for more details. 53 | 54 | _Ensure that the deployment name is unique since its used for creating s3 buckets._ 55 | 56 | ``` 57 | main.tf 58 | 59 | provider "aws" { 60 | region = "eu-central-1" #customize your region 61 | } 62 | 63 | provider "aws" { 64 | alias = "global_region" 65 | region = "us-east-1" #must be us-east-1 66 | } 67 | 68 | module "next_serverless" { 69 | source = "emyriounis/nextjs-serverless/aws" 70 | 71 | deployment_name = "nextjs-serverless" #needs to be unique since it will create s3 buckets 72 | region = "eu-central-1" #customize your region 73 | base_dir = "./" #The base directory of the next.js app 74 | } 75 | 76 | output "next_serverless" { 77 | value = module.next_serverless 78 | } 79 | ``` 80 | 81 | ### Deployment 82 | 83 | Build the Next.js Code and deploy 84 | 85 | ```bash 86 | npm i ns-build 87 | npm run ns-build 88 | 89 | terraform init 90 | terraform apply 91 | ``` 92 | 93 | ## Architecture 94 | 95 | ### Module 96 | 97 | ![Module ](https://github.com/emyriounis/terraform-aws-nextjs-serverless/blob/main/visuals/module.webp?raw=true) 98 | 99 | ### Distribution 100 | 101 | ![Distribution ](https://github.com/emyriounis/terraform-aws-nextjs-serverless/blob/main/visuals/distribution.webp?raw=true) 102 | 103 | ### Cache 104 | 105 | ![Cache ](https://github.com/emyriounis/terraform-aws-nextjs-serverless/blob/main/visuals/cache.webp?raw=true) 106 | 107 | ## Examples 108 | 109 | - [Next.js v14](https://github.com/emyriounis/terraform-aws-nextjs-serverless/tree/main/examples/nextjs-v14) Complete example with SSR, API, static pages, image optimization & custom domain 110 | 111 | ## Known Issues 112 | 113 | - The `ns-build` _package's version_ must match the `next_serverless` _module's version_ 114 | - The `app/` folder must be in the root directory (ex. not in the `src/` directory) 115 | - When destroying the `next_serverless` module, Lambda@Edge function need at least 15mins to be destroy, since they're [replicated functions](https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-delete-replicas.html) 116 | - In some rare cases, some modules can not be imported by next_lambda (for unknown reasons). To solve this issue use `ns-build --copyAllPackages` to copy all the packages or `ns-build --packages-to-copy=package_1,package_2,package_3` to copy specific packages only 117 | - Currently, we support the `pages/` router for node v16, v18, v20 & `app/` router for node v16 118 | 119 | ## Contributing 120 | 121 | Feel free to improve this module.
Our [contributing guidelines](https://github.com/emyriounis/terraform-aws-nextjs-serverless/tree/main/CONTRIBUTING.md) will help you get started. 122 | 123 | ## License 124 | 125 | Apache-2.0 - see [LICENSE](https://github.com/emyriounis/terraform-aws-nextjs-serverless/tree/main/LICENSE) for details.\ 126 | Disclaimer: This module was originally developed by [me](https://github.com/emyriounis) during my time at Nexode Consulting GmbH. 127 | -------------------------------------------------------------------------------- /examples/nextjs-v14/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs-v14/README.md: -------------------------------------------------------------------------------- 1 | ## Next.js v13 example 2 | 3 | - update providers,tf 4 | - update terraform.tfvars 5 | 6 | 7 | ``` 8 | npm i 9 | npm run ns-build 10 | terraform apply ... 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/nextjs-v14/__tests__/pages/index.test.tsx: -------------------------------------------------------------------------------- 1 | // pages/__tests__/index.test.js 2 | import { render, screen } from '@testing-library/react' 3 | import '@testing-library/jest-dom' 4 | 5 | import Home from '../../pages/index' 6 | 7 | describe('Index page', () => { 8 | it('renders welcome message', () => { 9 | render() 10 | 11 | const title = screen.getByText('Welcome to') 12 | const subtitle = screen.getByText('Get started by editing') 13 | 14 | expect(title).toBeInTheDocument() 15 | expect(subtitle).toBeInTheDocument() 16 | }) 17 | }) 18 | -------------------------------------------------------------------------------- /examples/nextjs-v14/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/examples/nextjs-v14/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs-v14/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); 27 | } 28 | -------------------------------------------------------------------------------- /examples/nextjs-v14/app/layout.tsx: -------------------------------------------------------------------------------- 1 | // import './globals.css' 2 | // import type { Metadata } from 'next' 3 | // import { Inter } from 'next/font/google' 4 | 5 | // const inter = Inter({ subsets: ['latin'] }) 6 | 7 | // export const metadata: Metadata = { 8 | // title: 'Create Next App', 9 | // description: 'Generated by create next app', 10 | // } 11 | 12 | // export default function RootLayout({ 13 | // children, 14 | // }: { 15 | // children: React.ReactNode 16 | // }) { 17 | // return ( 18 | // 19 | // {children} 20 | // 21 | // ) 22 | // } 23 | -------------------------------------------------------------------------------- /examples/nextjs-v14/function.js: -------------------------------------------------------------------------------- 1 | function handler(event) { 2 | const request = event.request 3 | const headers = request.headers 4 | const host = request.headers.host.value 5 | headers['x-forwarded-host'] = { value: host } 6 | 7 | return request 8 | } 9 | -------------------------------------------------------------------------------- /examples/nextjs-v14/jest.config.js: -------------------------------------------------------------------------------- 1 | const nextJest = require('next/jest'); 2 | 3 | const createJestConfig = nextJest({ 4 | dir: './', 5 | }); 6 | 7 | const customJestConfig = { 8 | preset: 'ts-jest', 9 | testEnvironment: 'jsdom', 10 | moduleDirectories: ['node_modules', '/'], 11 | testEnvironment: 'jest-environment-jsdom', 12 | // setupFilesAfterEnv: ['/jest.setup.js'], 13 | modulePathIgnorePatterns: ['__tests__/testing-utils'], 14 | } 15 | 16 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 17 | module.exports = createJestConfig(customJestConfig); 18 | -------------------------------------------------------------------------------- /examples/nextjs-v14/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | swcMinify: true, 5 | output: 'standalone', 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /examples/nextjs-v14/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.2.19", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "test": "jest", 8 | "build": "next build", 9 | "export": "next export", 10 | "ns-build": "ns-build", 11 | "start": "next start", 12 | "lint": "next lint" 13 | }, 14 | "dependencies": { 15 | "autoprefixer": "^10.4.15", 16 | "next": "^14.2.3", 17 | "ns-build": "^1.8.1", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "tailwindcss": "^3.3.3" 21 | }, 22 | "devDependencies": { 23 | "@testing-library/jest-dom": "^6.0.1", 24 | "@testing-library/react": "^14.0.0", 25 | "@types/jest": "^29.5.3", 26 | "@types/node": "20.5.1", 27 | "@types/react": "18.2.20", 28 | "@types/react-dom": "18.2.7", 29 | "eslint": "8.47.0", 30 | "eslint-config-next": "^14.2.3", 31 | "jest": "^29.6.3", 32 | "jest-environment-jsdom": "^29.6.3", 33 | "ts-jest": "^29.1.1", 34 | "typescript": "5.1.6" 35 | } 36 | } -------------------------------------------------------------------------------- /examples/nextjs-v14/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import '../styles/globals.css' 3 | import type { AppProps } from 'next/app' 4 | 5 | function MyApp({ Component, pageProps }: AppProps) { 6 | return ( 7 | <> 8 | Routes: 9 |
    10 |
  • 11 | /api/hello 12 |
  • 13 |
  • 14 | /optimized-images 15 |
  • 16 |
  • 17 | /ssr 18 |
  • 19 |
  • 20 | / 21 |
  • 22 |
23 | 24 | 25 | ) 26 | } 27 | 28 | export default MyApp 29 | -------------------------------------------------------------------------------- /examples/nextjs-v14/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | _req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'Terry Myriounis' }) 13 | } 14 | -------------------------------------------------------------------------------- /examples/nextjs-v14/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Head from 'next/head' 3 | import Image from 'next/image' 4 | import styles from '../styles/Home.module.css' 5 | 6 | const Home: NextPage = () => { 7 | return ( 8 | 88 | ) 89 | } 90 | 91 | export default Home 92 | -------------------------------------------------------------------------------- /examples/nextjs-v14/pages/optimized-images/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from 'next' 2 | import Image from 'next/image' 3 | import styles from '../../styles/Home.module.css' 4 | 5 | const Home: NextPage = () => { 6 | return ( 7 |
8 |
9 | .avif 10 | Sample Image 16 |
17 |
18 | .gif 19 | Sample Image 26 |
27 |
28 | .jpeg 29 | Sample Image 35 |
36 |
37 | .png 38 | Sample Image 44 |
45 |
46 | .webp 47 | Sample Image 53 |
54 |
55 | img component 56 | sample image 57 |
58 |
59 | bg-img (css) 60 |
69 |
70 |
71 | ) 72 | } 73 | 74 | export default Home 75 | -------------------------------------------------------------------------------- /examples/nextjs-v14/pages/ssr/SSR.tsx: -------------------------------------------------------------------------------- 1 | import { useContext, useEffect, useState } from 'react' 2 | import styles from '../../styles/Home.module.css' 3 | import { Context } from '.' 4 | 5 | const SSR = ({ data, status, env }: any) => { 6 | const { lang, setLang } = useContext(Context) 7 | const [state, setState] = useState(0) 8 | 9 | useEffect(() => void setTimeout(() => setState(status), 1500), [status]) 10 | 11 | return ( 12 |
13 |
status: {state}
14 |
body: {state ? JSON.stringify(data) : 'fake loading...'}
15 |
env: {env}
16 | 17 |
lang: {lang}
18 | 19 | 20 |
21 | ) 22 | } 23 | 24 | export default SSR 25 | -------------------------------------------------------------------------------- /examples/nextjs-v14/pages/ssr/index.tsx: -------------------------------------------------------------------------------- 1 | import type { GetServerSidePropsContext, NextPage } from 'next' 2 | import { Dispatch, SetStateAction, createContext, useState } from 'react' 3 | import SSR from './SSR' 4 | 5 | export const Context = createContext({ 6 | lang: 'en', 7 | setLang: (() => {}) as Dispatch>, 8 | }) 9 | 10 | const Home: NextPage = (props: any) => { 11 | const [lang, setLang] = useState('en') 12 | 13 | return ( 14 | 15 |
lang: {lang}
16 | 17 |
18 | ) 19 | } 20 | 21 | // This gets called on every request 22 | export async function getServerSideProps(context: GetServerSidePropsContext) { 23 | console.log({ context }) 24 | 25 | // Fetch data from nextjs API 26 | const host = context.req.headers.host 27 | const res = await fetch('https://' + host + '/api/hello') 28 | const date = new Date() 29 | const data = { ...(await res.json()), date: date.toISOString() } 30 | 31 | // Pass data to the page via props 32 | return { 33 | props: { 34 | data, 35 | status: res.status, 36 | env: process.env.AWS_EXECUTION_ENV ?? null, 37 | }, 38 | } 39 | } 40 | 41 | export default Home 42 | -------------------------------------------------------------------------------- /examples/nextjs-v14/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /examples/nextjs-v14/public/images/sample.avif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/examples/nextjs-v14/public/images/sample.avif -------------------------------------------------------------------------------- /examples/nextjs-v14/public/images/sample.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/examples/nextjs-v14/public/images/sample.gif -------------------------------------------------------------------------------- /examples/nextjs-v14/public/images/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/examples/nextjs-v14/public/images/sample.jpg -------------------------------------------------------------------------------- /examples/nextjs-v14/public/images/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/examples/nextjs-v14/public/images/sample.png -------------------------------------------------------------------------------- /examples/nextjs-v14/public/images/sample.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/examples/nextjs-v14/public/images/sample.webp -------------------------------------------------------------------------------- /examples/nextjs-v14/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-v14/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-v14/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | /* min-height: 100vh; */ 7 | padding: 4rem 4rem; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 65 | Bitstream Vera Sans Mono, Courier New, monospace; 66 | } 67 | 68 | .grid { 69 | display: flex; 70 | align-items: center; 71 | justify-content: center; 72 | flex-wrap: wrap; 73 | max-width: 800px; 74 | } 75 | 76 | .card { 77 | margin: 1rem; 78 | padding: 1.5rem; 79 | text-align: left; 80 | color: inherit; 81 | text-decoration: none; 82 | border: 1px solid #eaeaea; 83 | border-radius: 10px; 84 | transition: color 0.15s ease, border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | 118 | @media (prefers-color-scheme: dark) { 119 | .card, 120 | .footer { 121 | border-color: #222; 122 | } 123 | .code { 124 | background: #111; 125 | } 126 | .logo img { 127 | filter: invert(1); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /examples/nextjs-v14/styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 7 | } 8 | 9 | a { 10 | color: inherit; 11 | text-decoration: none; 12 | } 13 | 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | html { 20 | color-scheme: dark; 21 | } 22 | body { 23 | color: white; 24 | background: black; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/nextjs-v14/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'tailwindcss' 2 | 3 | const config: Config = { 4 | content: [ 5 | './pages/**/*.{js,ts,jsx,tsx,mdx}', 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 13 | 'gradient-conic': 14 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | } 20 | export default config 21 | -------------------------------------------------------------------------------- /examples/nextjs-v14/terraform/main.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_function" "test" { 2 | name = "test" 3 | runtime = "cloudfront-js-2.0" 4 | comment = "my function" 5 | publish = true 6 | code = file("../function.js") 7 | } 8 | 9 | module "next_serverless" { 10 | # source = "../../../" 11 | source = "emyriounis/nextjs-serverless/aws" 12 | version = "1.8.1" 13 | 14 | providers = { 15 | aws.global_region = aws.global_region 16 | } 17 | 18 | deployment_name = var.deployment_name 19 | region = var.region 20 | base_dir = var.base_dir 21 | 22 | cloudfront_acm_certificate_arn = (var.deployment_domain != null) ? module.next_cloudfront_certificate[0].acm_certificate_arn : null 23 | cloudfront_aliases = (var.deployment_domain != null) ? [var.deployment_domain] : [] 24 | 25 | pre_resize_images = true 26 | wait_for_distribution_deployment = false 27 | show_debug_logs = true 28 | use_default_server_side_props_handler = false 29 | 30 | cloudfront_function_associations = [{ 31 | event_type = "viewer-request" 32 | function_arn = aws_cloudfront_function.test.arn 33 | }] 34 | 35 | next_lambda_env_vars = { 36 | NODE_ENV = "production" 37 | } 38 | } 39 | 40 | module "next_cloudfront_certificate" { 41 | count = (var.deployment_domain != null) ? 1 : 0 42 | 43 | source = "terraform-aws-modules/acm/aws" 44 | version = "4.3.2" 45 | 46 | domain_name = (var.deployment_domain != null) ? var.deployment_domain : null 47 | zone_id = (var.deployment_domain != null) ? data.aws_route53_zone.hosted_zone[0].zone_id : null 48 | 49 | providers = { 50 | aws = aws.global_region 51 | } 52 | } 53 | 54 | data "aws_route53_zone" "hosted_zone" { 55 | count = (var.hosted_zone != null) ? 1 : 0 56 | 57 | name = var.hosted_zone 58 | } 59 | 60 | resource "aws_route53_record" "next_cloudfront_alias" { 61 | count = (var.deployment_domain != null) ? 1 : 0 62 | 63 | zone_id = data.aws_route53_zone.hosted_zone[0].zone_id 64 | name = var.deployment_domain 65 | type = "A" 66 | 67 | allow_overwrite = true 68 | 69 | alias { 70 | name = module.next_serverless.cloudfront_url 71 | zone_id = module.next_serverless.distribution.next_distribution.hosted_zone_id 72 | evaluate_target_health = false 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /examples/nextjs-v14/terraform/outputs.tf: -------------------------------------------------------------------------------- 1 | output "next_serverless" { 2 | value = module.next_serverless.cloudfront_url 3 | } 4 | -------------------------------------------------------------------------------- /examples/nextjs-v14/terraform/providers.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.region 3 | } 4 | 5 | provider "aws" { 6 | alias = "global_region" 7 | region = var.global_region 8 | } 9 | -------------------------------------------------------------------------------- /examples/nextjs-v14/terraform/variables.tf: -------------------------------------------------------------------------------- 1 | variable "deployment_name" { 2 | type = string 3 | default = "tm-next-serverless" 4 | } 5 | 6 | variable "region" { 7 | type = string 8 | default = "eu-central-1" 9 | } 10 | 11 | variable "global_region" { 12 | type = string 13 | default = "us-east-1" 14 | } 15 | 16 | variable "hosted_zone" { 17 | description = "Hosted Zone in Route53, e.g. my-dns-zone.de" 18 | type = string 19 | default = null 20 | } 21 | 22 | variable "deployment_domain" { 23 | type = string 24 | description = "Url where the deployment should be availale at, e.g. website1.my-dns-zone.de" 25 | default = null 26 | } 27 | 28 | variable "base_dir" { 29 | type = string 30 | default = "../" 31 | } 32 | -------------------------------------------------------------------------------- /examples/nextjs-v14/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "strict": true, 12 | "noEmit": true, 13 | "esModuleInterop": true, 14 | "module": "esnext", 15 | "moduleResolution": "node", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve", 19 | "incremental": true, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "paths": { 26 | "@/*": [ 27 | "./app/*" 28 | ] 29 | }, 30 | "forceConsistentCasingInFileNames": true 31 | }, 32 | "include": [ 33 | "next-env.d.ts", 34 | "**/*.ts", 35 | "**/*.tsx", 36 | ".next-standalone/types/**/*.ts", 37 | ".next/types/**/*.ts" 38 | , "server.js" ], 39 | "exclude": [ 40 | "node_modules" 41 | ] 42 | } -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | module "static-assets-hosting" { 2 | source = "./modules/static-assets-hosting" 3 | 4 | deployment_name = var.deployment_name 5 | base_dir = var.base_dir 6 | } 7 | 8 | module "public-assets-hosting" { 9 | source = "./modules/public-assets-hosting" 10 | 11 | deployment_name = var.deployment_name 12 | base_dir = var.base_dir 13 | } 14 | 15 | module "server-side-rendering" { 16 | source = "./modules/server-side-rendering" 17 | 18 | deployment_name = var.deployment_name 19 | base_dir = var.base_dir 20 | 21 | next_lambda_memory_size = var.next_lambda_memory_size 22 | next_lambda_logs_retention = var.next_lambda_logs_retention 23 | next_lambda_runtime = var.next_lambda_runtime 24 | next_lambda_ephemeral_storage_size = var.next_lambda_ephemeral_storage_size 25 | 26 | next_lambda_env_vars = var.next_lambda_env_vars 27 | custom_image_types = var.custom_image_types 28 | next_lambda_policy_statements = var.next_lambda_policy_statements 29 | show_debug_logs = var.show_debug_logs 30 | use_default_server_side_props_handler = var.use_default_server_side_props_handler 31 | 32 | api_gateway_log_format = var.api_gateway_log_format 33 | } 34 | 35 | module "image-optimization" { 36 | source = "./modules/image-optimization" 37 | 38 | providers = { 39 | aws.global_region = aws.global_region 40 | } 41 | 42 | deployment_name = var.deployment_name 43 | base_dir = var.base_dir 44 | 45 | image_optimization_runtime = var.image_optimization_runtime 46 | image_optimization_logs_retention = var.image_optimization_logs_retention 47 | image_optimization_lambda_memory_size = var.image_optimization_lambda_memory_size 48 | image_optimization_ephemeral_storage_size = var.image_optimization_ephemeral_storage_size 49 | 50 | public_assets_bucket = module.public-assets-hosting.public_assets_bucket 51 | } 52 | 53 | module "distribution" { 54 | source = "./modules/distribution" 55 | 56 | static_assets_bucket = module.static-assets-hosting.static_assets_bucket 57 | static_assets_origin_id = module.static-assets-hosting.static_assets_oai 58 | 59 | public_assets_bucket = module.public-assets-hosting.public_assets_bucket 60 | public_assets_bucket_region = var.region 61 | public_assets_origin_id = module.public-assets-hosting.public_assets_oai 62 | 63 | dynamic_origin_domain_name = module.server-side-rendering.api_gateway.default_apigatewayv2_stage_domain_name 64 | 65 | enable_image_optimization = var.enable_image_optimization 66 | image_optimization_qualified_arn = module.image-optimization.image_optimization.lambda_function_qualified_arn 67 | image_redirection_qualified_arn = module.image-optimization.image_redirection.lambda_function_qualified_arn 68 | 69 | cloudfront_acm_certificate_arn = var.cloudfront_acm_certificate_arn 70 | cloudfront_aliases = var.cloudfront_aliases 71 | cloudfront_price_class = var.cloudfront_price_class 72 | 73 | cloudfront_cached_paths = var.cloudfront_cached_paths 74 | custom_cache_policy_id = var.custom_cache_policy_id 75 | cloudfront_cache_default_ttl = var.cloudfront_cache_default_ttl 76 | cloudfront_cache_max_ttl = var.cloudfront_cache_max_ttl 77 | cloudfront_cache_min_ttl = var.cloudfront_cache_min_ttl 78 | 79 | deployment_name = var.deployment_name 80 | base_dir = var.base_dir 81 | cloudfront_function_associations = var.cloudfront_function_associations 82 | wait_for_distribution_deployment = var.wait_for_distribution_deployment 83 | } 84 | 85 | # delete previously resized public assets 86 | resource "null_resource" "delete_resized_versions" { 87 | count = var.delete_resized_versions ? 1 : 0 88 | 89 | provisioner "local-exec" { 90 | command = < uri.includes('.' + type))) { 48 | const response = { 49 | statusCode: 302, 50 | // Don't use `host` as it's not custom domain 51 | headers: { location: { value: '/assets' + uri } }, 52 | }; 53 | 54 | return response; 55 | } 56 | 57 | headers['x-forwarded-host'] = { value: host }; 58 | return request; 59 | } 60 | EOF 61 | } 62 | 63 | resource "aws_cloudfront_cache_policy" "custom_paths_cache" { 64 | count = var.custom_cache_policy_id == null && length(var.cloudfront_cached_paths.paths) > 0 ? 1 : 0 65 | name = "${var.deployment_name}-custom-paths-cache-policy" 66 | 67 | default_ttl = var.cloudfront_cached_paths.default_ttl 68 | max_ttl = var.cloudfront_cached_paths.max_ttl 69 | min_ttl = var.cloudfront_cached_paths.min_ttl 70 | 71 | parameters_in_cache_key_and_forwarded_to_origin { 72 | cookies_config { 73 | cookie_behavior = "none" 74 | } 75 | headers_config { 76 | header_behavior = "whitelist" 77 | headers { 78 | items = ["x-forwarded-host"] 79 | } 80 | } 81 | query_strings_config { 82 | query_string_behavior = "all" 83 | } 84 | 85 | enable_accept_encoding_brotli = true 86 | enable_accept_encoding_gzip = true 87 | } 88 | } 89 | 90 | resource "aws_cloudfront_distribution" "next_distribution" { 91 | origin { 92 | domain_name = var.public_assets_bucket.s3_bucket_bucket_regional_domain_name 93 | origin_id = var.public_assets_origin_id.id 94 | 95 | s3_origin_config { 96 | origin_access_identity = var.public_assets_origin_id.cloudfront_access_identity_path 97 | } 98 | } 99 | 100 | origin { 101 | domain_name = var.static_assets_bucket.s3_bucket_bucket_regional_domain_name 102 | origin_id = var.static_assets_origin_id.id 103 | 104 | s3_origin_config { 105 | origin_access_identity = var.static_assets_origin_id.cloudfront_access_identity_path 106 | } 107 | } 108 | 109 | origin { 110 | domain_name = var.dynamic_origin_domain_name 111 | origin_id = aws_cloudfront_origin_access_identity.dynamic_assets_oai.id 112 | 113 | custom_origin_config { 114 | http_port = "80" 115 | https_port = "443" 116 | origin_protocol_policy = "https-only" 117 | origin_ssl_protocols = ["TLSv1.2"] 118 | } 119 | } 120 | 121 | origin { 122 | domain_name = "example.com" 123 | origin_id = aws_cloudfront_origin_access_identity.image_redirection_oai.id 124 | 125 | custom_origin_config { 126 | http_port = "80" 127 | https_port = "443" 128 | origin_protocol_policy = "https-only" 129 | origin_ssl_protocols = ["TLSv1.2"] 130 | } 131 | 132 | custom_header { 133 | name = "Enable-Image-Optimization" 134 | value = var.enable_image_optimization ? "true" : "false" 135 | } 136 | } 137 | 138 | origin { 139 | domain_name = "example.com" 140 | origin_id = aws_cloudfront_origin_access_identity.image_optimization_oai.id 141 | 142 | custom_origin_config { 143 | http_port = "80" 144 | https_port = "443" 145 | origin_protocol_policy = "https-only" 146 | origin_ssl_protocols = ["TLSv1.2"] 147 | } 148 | 149 | custom_header { 150 | name = "S3-Region" 151 | value = var.public_assets_bucket_region 152 | } 153 | 154 | custom_header { 155 | name = "Public-Assets-Bucket" 156 | value = var.public_assets_bucket.s3_bucket_id 157 | } 158 | } 159 | 160 | enabled = true 161 | is_ipv6_enabled = true 162 | wait_for_deployment = var.wait_for_distribution_deployment 163 | 164 | aliases = var.cloudfront_aliases 165 | default_root_object = null 166 | 167 | ordered_cache_behavior { 168 | path_pattern = "/_next/image/*" 169 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 170 | cached_methods = ["GET", "HEAD", "OPTIONS"] 171 | target_origin_id = aws_cloudfront_origin_access_identity.image_optimization_oai.id 172 | 173 | lambda_function_association { 174 | event_type = "origin-request" 175 | lambda_arn = var.image_optimization_qualified_arn 176 | } 177 | 178 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id 179 | 180 | viewer_protocol_policy = "redirect-to-https" 181 | compress = true 182 | } 183 | 184 | ordered_cache_behavior { 185 | path_pattern = "/_next/image*" 186 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 187 | cached_methods = ["GET", "HEAD", "OPTIONS"] 188 | target_origin_id = aws_cloudfront_origin_access_identity.image_redirection_oai.id 189 | 190 | lambda_function_association { 191 | event_type = "viewer-request" 192 | lambda_arn = var.image_redirection_qualified_arn 193 | include_body = true 194 | } 195 | 196 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id 197 | 198 | viewer_protocol_policy = "redirect-to-https" 199 | compress = true 200 | } 201 | 202 | ordered_cache_behavior { 203 | path_pattern = "/_next/static/*" 204 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 205 | cached_methods = ["GET", "HEAD", "OPTIONS"] 206 | target_origin_id = var.static_assets_origin_id.id 207 | 208 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id 209 | 210 | viewer_protocol_policy = "redirect-to-https" 211 | compress = true 212 | } 213 | 214 | ordered_cache_behavior { 215 | path_pattern = "/assets/*" 216 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 217 | cached_methods = ["GET", "HEAD", "OPTIONS"] 218 | target_origin_id = var.public_assets_origin_id.id 219 | 220 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id 221 | 222 | viewer_protocol_policy = "redirect-to-https" 223 | compress = true 224 | } 225 | 226 | ordered_cache_behavior { 227 | path_pattern = "/resized-assets/*" 228 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 229 | cached_methods = ["GET", "HEAD", "OPTIONS"] 230 | target_origin_id = var.public_assets_origin_id.id 231 | 232 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_optimized.id 233 | 234 | viewer_protocol_policy = "redirect-to-https" 235 | compress = true 236 | } 237 | 238 | dynamic "ordered_cache_behavior" { 239 | for_each = var.cloudfront_cached_paths.paths 240 | content { 241 | path_pattern = ordered_cache_behavior.value 242 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 243 | cached_methods = ["GET", "HEAD", "OPTIONS"] 244 | target_origin_id = aws_cloudfront_origin_access_identity.dynamic_assets_oai.id 245 | 246 | cache_policy_id = var.custom_cache_policy_id != null ? var.custom_cache_policy_id : aws_cloudfront_cache_policy.custom_paths_cache[0].id 247 | 248 | dynamic "function_association" { 249 | for_each = concat([{ event_type : "viewer-request", function_arn : aws_cloudfront_function.viewer_request.arn }], var.cloudfront_function_associations) 250 | content { 251 | event_type = function_association.value.event_type 252 | function_arn = function_association.value.function_arn 253 | } 254 | } 255 | 256 | viewer_protocol_policy = "redirect-to-https" 257 | compress = true 258 | } 259 | } 260 | 261 | default_cache_behavior { 262 | allowed_methods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"] 263 | cached_methods = ["GET", "HEAD"] 264 | target_origin_id = aws_cloudfront_origin_access_identity.dynamic_assets_oai.id 265 | 266 | cache_policy_id = data.aws_cloudfront_cache_policy.caching_disabled.id 267 | origin_request_policy_id = data.aws_cloudfront_origin_request_policy.caching_all_viewer_except_host_header.id 268 | 269 | dynamic "function_association" { 270 | for_each = concat([{ event_type : "viewer-request", function_arn : aws_cloudfront_function.viewer_request.arn }], var.cloudfront_function_associations) 271 | content { 272 | event_type = function_association.value.event_type 273 | function_arn = function_association.value.function_arn 274 | } 275 | } 276 | 277 | viewer_protocol_policy = "redirect-to-https" 278 | compress = true 279 | } 280 | 281 | price_class = var.cloudfront_price_class 282 | 283 | viewer_certificate { 284 | cloudfront_default_certificate = var.cloudfront_acm_certificate_arn == null 285 | acm_certificate_arn = var.cloudfront_acm_certificate_arn 286 | minimum_protocol_version = var.cloudfront_acm_certificate_arn == null ? "TLSv1" : "TLSv1.2_2021" 287 | ssl_support_method = var.cloudfront_acm_certificate_arn != null ? "sni-only" : null 288 | } 289 | 290 | restrictions { 291 | geo_restriction { 292 | restriction_type = "none" 293 | } 294 | } 295 | } 296 | 297 | resource "aws_cloudfront_monitoring_subscription" "next_distribution_monitoring" { 298 | distribution_id = aws_cloudfront_distribution.next_distribution.id 299 | 300 | monitoring_subscription { 301 | realtime_metrics_subscription_config { 302 | realtime_metrics_subscription_status = "Enabled" 303 | } 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /modules/distribution/outputs.tf: -------------------------------------------------------------------------------- 1 | output "next_distribution" { 2 | value = aws_cloudfront_distribution.next_distribution 3 | } 4 | -------------------------------------------------------------------------------- /modules/distribution/variables.tf: -------------------------------------------------------------------------------- 1 | variable "deployment_name" { 2 | type = string 3 | } 4 | 5 | variable "base_dir" { 6 | type = string 7 | } 8 | 9 | variable "enable_image_optimization" { 10 | type = bool 11 | } 12 | 13 | variable "dynamic_origin_domain_name" { 14 | type = string 15 | } 16 | 17 | variable "cloudfront_acm_certificate_arn" { 18 | type = string 19 | default = null 20 | } 21 | 22 | variable "cloudfront_price_class" { 23 | type = string 24 | } 25 | 26 | variable "cloudfront_aliases" { 27 | type = list(string) 28 | } 29 | 30 | variable "image_optimization_qualified_arn" { 31 | type = string 32 | } 33 | 34 | variable "image_redirection_qualified_arn" { 35 | type = string 36 | } 37 | 38 | variable "static_assets_bucket" { 39 | type = map(any) 40 | } 41 | 42 | variable "static_assets_origin_id" { 43 | type = map(any) 44 | } 45 | 46 | variable "public_assets_bucket" { 47 | type = map(any) 48 | } 49 | 50 | variable "public_assets_bucket_region" { 51 | type = string 52 | } 53 | 54 | variable "public_assets_origin_id" { 55 | type = map(any) 56 | } 57 | 58 | variable "cloudfront_cached_paths" { 59 | type = object({ 60 | paths = list(string) 61 | min_ttl = number 62 | default_ttl = number 63 | max_ttl = number 64 | }) 65 | } 66 | 67 | variable "custom_cache_policy_id" { 68 | type = string 69 | } 70 | 71 | variable "cloudfront_cache_default_ttl" { 72 | type = number 73 | } 74 | 75 | variable "cloudfront_cache_max_ttl" { 76 | type = number 77 | } 78 | 79 | variable "cloudfront_cache_min_ttl" { 80 | type = number 81 | } 82 | 83 | variable "cloudfront_function_associations" { 84 | type = list(object({ 85 | event_type = string 86 | function_arn = string 87 | })) 88 | } 89 | 90 | variable "wait_for_distribution_deployment" { 91 | type = bool 92 | } 93 | -------------------------------------------------------------------------------- /modules/image-optimization/main.tf: -------------------------------------------------------------------------------- 1 | #################################### 2 | ######## image_optimization ######## 3 | #################################### 4 | 5 | module "image_optimization" { 6 | providers = { 7 | aws = aws.global_region 8 | } 9 | 10 | source = "terraform-aws-modules/lambda/aws" 11 | version = "6.5.0" 12 | 13 | function_name = "${var.deployment_name}-image-optimization" 14 | description = "${var.deployment_name} Image Optimization" 15 | 16 | lambda_at_edge = true 17 | publish = true 18 | runtime = var.image_optimization_runtime 19 | memory_size = var.image_optimization_lambda_memory_size 20 | ephemeral_storage_size = var.image_optimization_ephemeral_storage_size 21 | timeout = 30 22 | maximum_event_age_in_seconds = 60 23 | maximum_retry_attempts = 0 24 | 25 | create_package = false 26 | local_existing_package = "${var.base_dir}deployments/image-optimization/source.zip" 27 | handler = "index.handler" 28 | 29 | attach_network_policy = false 30 | cloudwatch_logs_retention_in_days = var.image_optimization_logs_retention 31 | 32 | cors = { 33 | allow_credentials = true 34 | allow_origins = ["*"] 35 | allow_methods = ["*"] 36 | } 37 | 38 | attach_policy_statements = true 39 | policy_statements = { 40 | s3_public_assets_bucket = { 41 | effect = "Allow", 42 | actions = ["s3:GetObject", "s3:PutObject"], 43 | resources = ["${var.public_assets_bucket.s3_bucket_arn}/*"] 44 | } 45 | } 46 | } 47 | 48 | #################################### 49 | ######### image_redirection ######## 50 | #################################### 51 | 52 | module "image_redirection" { 53 | providers = { 54 | aws = aws.global_region 55 | } 56 | 57 | source = "terraform-aws-modules/lambda/aws" 58 | version = "6.5.0" 59 | 60 | function_name = "${var.deployment_name}-ns-img-rdr" 61 | description = "${var.deployment_name} Image Redirection" 62 | 63 | lambda_at_edge = true 64 | publish = true 65 | runtime = var.image_optimization_runtime 66 | memory_size = 128 67 | ephemeral_storage_size = 512 68 | timeout = 5 69 | maximum_event_age_in_seconds = 60 70 | maximum_retry_attempts = 0 71 | 72 | create_package = false 73 | local_existing_package = "${var.base_dir}deployments/ns-img-rdr/source.zip" 74 | handler = "index.handler" 75 | 76 | attach_network_policy = false 77 | cloudwatch_logs_retention_in_days = var.image_optimization_logs_retention 78 | 79 | cors = { 80 | allow_credentials = true 81 | allow_origins = ["*"] 82 | allow_methods = ["*"] 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /modules/image-optimization/outputs.tf: -------------------------------------------------------------------------------- 1 | output "image_optimization" { 2 | value = module.image_optimization 3 | } 4 | 5 | output "image_redirection" { 6 | value = module.image_redirection 7 | } 8 | -------------------------------------------------------------------------------- /modules/image-optimization/variables.tf: -------------------------------------------------------------------------------- 1 | variable "image_optimization_runtime" { 2 | type = string 3 | } 4 | 5 | variable "image_optimization_logs_retention" { 6 | type = number 7 | } 8 | 9 | variable "deployment_name" { 10 | type = string 11 | } 12 | 13 | variable "base_dir" { 14 | type = string 15 | } 16 | 17 | variable "public_assets_bucket" { 18 | type = map(any) 19 | } 20 | 21 | variable "image_optimization_lambda_memory_size" { 22 | type = number 23 | } 24 | 25 | variable "image_optimization_ephemeral_storage_size" { 26 | type = number 27 | } 28 | -------------------------------------------------------------------------------- /modules/image-optimization/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6.3" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.0" 8 | configuration_aliases = [aws.global_region] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /modules/public-assets-hosting/main.tf: -------------------------------------------------------------------------------- 1 | #################################### 2 | ####### public_assets_bucket ####### 3 | #################################### 4 | 5 | locals { 6 | base_dir = "${var.base_dir}public" 7 | 8 | image_widths = [16, 32, 64, 128, 256, 512, 1024] 9 | image_types = ["webp", "jpeg", "png"] 10 | image_combinations = flatten([ 11 | for width in local.image_widths : [ 12 | for type in local.image_types : "${width}/${type}" 13 | ] 14 | ]) 15 | 16 | all_paths = [ 17 | for file in module.public_assets_static_files.files : replace(file.source_path, "${local.base_dir}/", "") 18 | ] 19 | 20 | all_resized_images_paths_list = flatten([ 21 | for path in local.all_paths : [ 22 | for prefix in local.image_combinations : "${prefix}/${path}" 23 | ] 24 | ]) 25 | 26 | all_resized_images_paths_map = { 27 | for idx, path in local.all_resized_images_paths_list : idx => path 28 | } 29 | } 30 | 31 | module "public_assets_bucket" { 32 | source = "terraform-aws-modules/s3-bucket/aws" 33 | version = "3.15.1" 34 | 35 | bucket = "${var.deployment_name}-public-assets" 36 | acl = "private" 37 | force_destroy = true 38 | control_object_ownership = true 39 | object_ownership = "ObjectWriter" 40 | 41 | block_public_acls = true 42 | block_public_policy = true 43 | ignore_public_acls = true 44 | restrict_public_buckets = true 45 | } 46 | 47 | module "public_assets_static_files" { 48 | source = "hashicorp/dir/template" 49 | version = "1.0.2" 50 | 51 | base_dir = local.base_dir 52 | } 53 | 54 | resource "aws_s3_object" "public_assets_files" { 55 | bucket = module.public_assets_bucket.s3_bucket_id 56 | for_each = module.public_assets_static_files.files 57 | 58 | key = "assets/${each.key}" # necessary prefix 59 | source = each.value.source_path 60 | content = each.value.content 61 | content_type = each.value.content_type 62 | etag = each.value.digests.md5 63 | } 64 | 65 | # CloudFront IAM policy 66 | resource "aws_cloudfront_origin_access_identity" "public_assets_oai" { 67 | comment = "public_assets_origin" 68 | } 69 | 70 | data "aws_iam_policy_document" "public_assets_s3_policy" { 71 | statement { 72 | actions = ["s3:GetObject"] 73 | resources = ["${module.public_assets_bucket.s3_bucket_arn}/*"] 74 | 75 | principals { 76 | type = "AWS" 77 | identifiers = [aws_cloudfront_origin_access_identity.public_assets_oai.iam_arn] 78 | } 79 | } 80 | } 81 | 82 | resource "aws_s3_bucket_policy" "public_assets_bucket_policy" { 83 | bucket = module.public_assets_bucket.s3_bucket_id 84 | policy = data.aws_iam_policy_document.public_assets_s3_policy.json 85 | } 86 | -------------------------------------------------------------------------------- /modules/public-assets-hosting/outputs.tf: -------------------------------------------------------------------------------- 1 | output "public_assets_bucket" { 2 | value = module.public_assets_bucket 3 | } 4 | 5 | output "public_assets_oai" { 6 | value = aws_cloudfront_origin_access_identity.public_assets_oai 7 | } 8 | 9 | output "all_resized_images_paths" { 10 | value = local.all_resized_images_paths_map 11 | } 12 | -------------------------------------------------------------------------------- /modules/public-assets-hosting/variables.tf: -------------------------------------------------------------------------------- 1 | variable "deployment_name" { 2 | type = string 3 | } 4 | 5 | variable "base_dir" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /modules/server-side-rendering/main.tf: -------------------------------------------------------------------------------- 1 | #################################### 2 | ########### next_lambda ############ 3 | #################################### 4 | 5 | locals { 6 | lambda_source_object_s3_key = "source-${filemd5("${var.base_dir}deployments/source.zip")}.zip" 7 | lambda_layer_object_s3_key = "layer-${filemd5("${var.base_dir}deployments/layer.zip")}.zip" 8 | 9 | lambda_default_env_vars = { 10 | DEFAULT_SS_PROPS_HANDLER = var.use_default_server_side_props_handler 11 | CUSTOM_IMAGE_TYPES = join(",", var.custom_image_types) 12 | SHOW_DEBUG_LOGS = var.show_debug_logs 13 | } 14 | } 15 | 16 | module "next_lambda_zips_bucket" { 17 | source = "terraform-aws-modules/s3-bucket/aws" 18 | version = "3.15.1" 19 | 20 | bucket = "${var.deployment_name}-next-lambda-zips" 21 | acl = "private" 22 | force_destroy = true 23 | control_object_ownership = true 24 | object_ownership = "ObjectWriter" 25 | 26 | block_public_acls = true 27 | block_public_policy = true 28 | ignore_public_acls = true 29 | restrict_public_buckets = true 30 | } 31 | 32 | resource "aws_s3_object" "lambda_source_object" { 33 | bucket = module.next_lambda_zips_bucket.s3_bucket_id 34 | key = local.lambda_source_object_s3_key 35 | source = "${var.base_dir}deployments/source.zip" 36 | } 37 | 38 | resource "aws_s3_object" "lambda_layer_object" { 39 | bucket = module.next_lambda_zips_bucket.s3_bucket_id 40 | key = local.lambda_layer_object_s3_key 41 | source = "${var.base_dir}deployments/layer.zip" 42 | } 43 | 44 | resource "aws_lambda_layer_version" "server_layer" { 45 | depends_on = [aws_s3_object.lambda_layer_object] 46 | 47 | layer_name = "${var.deployment_name}-layer" 48 | compatible_runtimes = [var.next_lambda_runtime] 49 | 50 | s3_bucket = module.next_lambda_zips_bucket.s3_bucket_id 51 | s3_key = local.lambda_layer_object_s3_key 52 | } 53 | 54 | module "next_lambda" { 55 | source = "terraform-aws-modules/lambda/aws" 56 | version = "6.5.0" 57 | 58 | function_name = "${var.deployment_name}-ssr" 59 | description = "${var.deployment_name} Server" 60 | 61 | lambda_at_edge = false 62 | runtime = var.next_lambda_runtime 63 | memory_size = var.next_lambda_memory_size 64 | ephemeral_storage_size = var.next_lambda_ephemeral_storage_size 65 | timeout = 30 66 | maximum_event_age_in_seconds = 60 67 | maximum_retry_attempts = 0 68 | 69 | create_package = false 70 | handler = "server.handler" 71 | s3_existing_package = { 72 | bucket = module.next_lambda_zips_bucket.s3_bucket_id 73 | key = local.lambda_source_object_s3_key 74 | } 75 | 76 | publish = true 77 | layers = [aws_lambda_layer_version.server_layer.arn] 78 | 79 | cloudwatch_logs_retention_in_days = var.next_lambda_logs_retention 80 | 81 | attach_network_policy = false 82 | 83 | cors = { 84 | allow_credentials = true 85 | allow_origins = ["*"] #TODO: update 86 | allow_methods = ["*"] 87 | } 88 | 89 | environment_variables = merge(local.lambda_default_env_vars, var.next_lambda_env_vars) 90 | 91 | allowed_triggers = { 92 | api_gateway = { 93 | action = "lambda:InvokeFunction" 94 | service = "apigateway" 95 | source_arn = "${module.api_gateway.apigatewayv2_api_execution_arn}/*/*" 96 | } 97 | } 98 | 99 | attach_policy_statements = length(keys(var.next_lambda_policy_statements)) != 0 100 | policy_statements = var.next_lambda_policy_statements 101 | } 102 | 103 | #################################### 104 | ########### api_gateway ############ 105 | #################################### 106 | 107 | module "api_gateway_cloudwatch_log_group" { 108 | source = "terraform-aws-modules/cloudwatch/aws//modules/log-group" 109 | version = "4.3.0" 110 | 111 | name = "${var.deployment_name}-api-gateway-logs" 112 | retention_in_days = var.next_lambda_logs_retention 113 | } 114 | 115 | module "api_gateway" { 116 | source = "terraform-aws-modules/apigateway-v2/aws" 117 | version = "2.2.2" 118 | 119 | name = "${var.deployment_name}-api" 120 | description = "${var.deployment_name} API" 121 | 122 | create_vpc_link = false 123 | create_api_domain_name = false 124 | 125 | default_stage_access_log_destination_arn = module.api_gateway_cloudwatch_log_group.cloudwatch_log_group_arn 126 | default_stage_access_log_format = var.api_gateway_log_format 127 | 128 | cors_configuration = { 129 | allow_headers = ["*"] 130 | allow_origins = ["*"] 131 | allow_methods = ["*"] 132 | } 133 | 134 | integrations = { 135 | "$default" = { 136 | lambda_arn = module.next_lambda.lambda_function_arn 137 | payload_format_version = "2.0" 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /modules/server-side-rendering/outputs.tf: -------------------------------------------------------------------------------- 1 | output "next_lambda" { 2 | value = module.next_lambda 3 | } 4 | 5 | output "api_gateway" { 6 | value = module.api_gateway 7 | } 8 | 9 | output "api_gateway_cloudwatch_log_group" { 10 | value = module.api_gateway_cloudwatch_log_group 11 | } 12 | 13 | output "aws_lambda_layer_version" { 14 | value = aws_lambda_layer_version.server_layer 15 | } 16 | -------------------------------------------------------------------------------- /modules/server-side-rendering/variables.tf: -------------------------------------------------------------------------------- 1 | variable "next_lambda_runtime" { 2 | type = string 3 | } 4 | 5 | variable "next_lambda_memory_size" { 6 | type = number 7 | } 8 | 9 | variable "next_lambda_logs_retention" { 10 | type = number 11 | } 12 | 13 | variable "deployment_name" { 14 | type = string 15 | } 16 | 17 | variable "base_dir" { 18 | type = string 19 | } 20 | 21 | variable "next_lambda_env_vars" { 22 | type = map(any) 23 | } 24 | 25 | variable "custom_image_types" { 26 | type = list(string) 27 | } 28 | 29 | variable "next_lambda_policy_statements" { 30 | type = map(any) 31 | } 32 | 33 | variable "next_lambda_ephemeral_storage_size" { 34 | type = number 35 | } 36 | 37 | variable "api_gateway_log_format" { 38 | type = string 39 | } 40 | 41 | variable "use_default_server_side_props_handler" { 42 | type = bool 43 | } 44 | 45 | variable "show_debug_logs" { 46 | type = bool 47 | } 48 | -------------------------------------------------------------------------------- /modules/static-assets-hosting/main.tf: -------------------------------------------------------------------------------- 1 | #################################### 2 | ####### static_assets_bucket ####### 3 | #################################### 4 | 5 | module "static_assets_bucket" { 6 | source = "terraform-aws-modules/s3-bucket/aws" 7 | version = "3.15.1" 8 | 9 | bucket = "${var.deployment_name}-static-assets" 10 | acl = "private" 11 | force_destroy = true 12 | control_object_ownership = true 13 | object_ownership = "ObjectWriter" 14 | 15 | block_public_acls = true 16 | block_public_policy = true 17 | ignore_public_acls = true 18 | restrict_public_buckets = true 19 | } 20 | 21 | module "static_assets_static_files" { 22 | source = "hashicorp/dir/template" 23 | version = "1.0.2" 24 | 25 | base_dir = "${var.base_dir}standalone/static" 26 | } 27 | 28 | resource "aws_s3_object" "static_assets_files" { 29 | bucket = module.static_assets_bucket.s3_bucket_id 30 | for_each = module.static_assets_static_files.files 31 | 32 | key = each.key 33 | source = each.value.source_path 34 | content = each.value.content 35 | content_type = each.value.content_type 36 | etag = each.value.digests.md5 37 | } 38 | 39 | # CloudFront IAM policy 40 | resource "aws_cloudfront_origin_access_identity" "static_assets_oai" { 41 | comment = "static_assets_origin" 42 | } 43 | 44 | data "aws_iam_policy_document" "static_assets_s3_policy" { 45 | statement { 46 | actions = ["s3:GetObject"] 47 | resources = ["${module.static_assets_bucket.s3_bucket_arn}/*"] 48 | 49 | principals { 50 | type = "AWS" 51 | identifiers = [aws_cloudfront_origin_access_identity.static_assets_oai.iam_arn] 52 | } 53 | } 54 | } 55 | 56 | resource "aws_s3_bucket_policy" "static_assets_bucket_policy" { 57 | bucket = module.static_assets_bucket.s3_bucket_id 58 | policy = data.aws_iam_policy_document.static_assets_s3_policy.json 59 | } 60 | -------------------------------------------------------------------------------- /modules/static-assets-hosting/outputs.tf: -------------------------------------------------------------------------------- 1 | output "static_assets_bucket" { 2 | value = module.static_assets_bucket 3 | } 4 | 5 | output "static_assets_oai" { 6 | value = aws_cloudfront_origin_access_identity.static_assets_oai 7 | } 8 | -------------------------------------------------------------------------------- /modules/static-assets-hosting/variables.tf: -------------------------------------------------------------------------------- 1 | variable "deployment_name" { 2 | type = string 3 | } 4 | 5 | variable "base_dir" { 6 | type = string 7 | } 8 | -------------------------------------------------------------------------------- /original-license.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | the copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor to discuss and improve the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by the Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributors that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, the Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assuming any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2023 Nexode Consulting GmbH 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "static-assets-hosting" { 2 | value = module.static-assets-hosting 3 | description = "Resources created by the static-assets-hosting module" 4 | } 5 | 6 | output "public-assets-hosting" { 7 | value = module.public-assets-hosting 8 | description = "Resources created by the public-assets-hosting module" 9 | } 10 | 11 | output "image-optimization" { 12 | value = module.image-optimization 13 | description = "Resources created by the image-optimization module" 14 | } 15 | 16 | output "server-side-rendering" { 17 | value = module.server-side-rendering 18 | description = "Resources created by the server-side-rendering module" 19 | } 20 | 21 | output "distribution" { 22 | value = module.distribution 23 | description = "Resources created by the distribution module" 24 | } 25 | 26 | output "cloudfront_url" { 27 | value = module.distribution.next_distribution.domain_name 28 | description = "The URL where cloudfront hosts the distribution" 29 | } 30 | -------------------------------------------------------------------------------- /packages/ns-build/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | the copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor to discuss and improve the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by the Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributors that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, the Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assuming any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2024\ 179 | Eleftherios Myriounis 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /packages/ns-build/README.md: -------------------------------------------------------------------------------- 1 | ## ns-build package 2 | 3 | 4 | 1. Add the `output: 'standalone'` option on the next.config.js configuration 5 | 6 | 2. Run `ns-build build` 7 | 8 | 3. Use the [nextjs-serverless](https://registry.terraform.io/modules/emyriounis/nextjs-serverless/aws/latest) terraform module to deploy your next.js app on aws 9 | 10 | 11 | ## Keywords 12 | 13 | - Next.js 14 | - Terraform 15 | - AWS 16 | - Serverless 17 | -------------------------------------------------------------------------------- /packages/ns-build/bin/ns-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # List of packages to copy directly on the lambda or ALL packages, instead of the layer 5 | copyAllPackages=false 6 | for ARGUMENT in "$@" 7 | do 8 | KEY=$(echo $ARGUMENT | cut -f1 -d=) 9 | 10 | if [[ $KEY == --copyAllPackages ]]; then 11 | copyAllPackages=true 12 | elif [[ $KEY == --packages-to-copy* ]]; then 13 | KEY_LENGTH=${#KEY} 14 | VALUE="${ARGUMENT:$KEY_LENGTH+1}" 15 | 16 | IFS=',' read -r -a packages_to_copy <<< "$VALUE" 17 | fi 18 | done 19 | 20 | # Clean-up old builds 21 | rm -r .next 22 | rm -r standalone 23 | rm -r deployments 24 | 25 | # Install necessary packages 26 | npm i -D serverless@3.38.0 serverless-esbuild@1.49.0 esbuild@0.19.7 serverless-http@3.2.0 ns-img-opt@1.8.1 ns-img-rdr@1.8.1 27 | 28 | # Inject code in build, and cleanup 29 | cp -a ./app ./app-backup 30 | find ./app -type f -name 'page.tsx' -exec sh -c 'printf "\nexport const runtime = '\''edge'\'';\n" >> "$0"' {} \; 31 | set -e 32 | npm run build 33 | set +e 34 | rm -r ./app 35 | mv ./app-backup ./app 36 | 37 | # Keep necessary files 38 | cp -a .next/static .next/standalone/.next 39 | cp -a .next/standalone standalone 40 | rm standalone/server.js 41 | cp node_modules/ns-build/server.js standalone 42 | cp next.config.js standalone 43 | 44 | # Prepare deployment 45 | mkdir deployments 46 | mkdir standalone 47 | mkdir nodejs 48 | 49 | # Keeps necessary node modules 50 | cp -a standalone/node_modules nodejs 51 | cp -a node_modules/serverless nodejs/node_modules 52 | cp -a node_modules/serverless-esbuild nodejs/node_modules 53 | cp -a node_modules/esbuild nodejs/node_modules 54 | cp -a node_modules/serverless-http nodejs/node_modules 55 | 56 | # Zip node modules 57 | echo "Generating layer.zip ..." 58 | zip -r -q deployments/layer.zip nodejs 59 | echo "layer.zip generated !" 60 | 61 | # Keep image optimization/redirection source code zips 62 | cd deployments 63 | mkdir ns-img-rdr 64 | mkdir image-optimization 65 | cd .. 66 | cp node_modules/ns-img-rdr/source.zip deployments/ns-img-rdr/ 67 | cp node_modules/ns-img-opt/source.zip deployments/image-optimization/ 68 | 69 | # Keep necessary files 70 | cd standalone 71 | mkdir -p static/_next 72 | cp -a .next/static static/_next 73 | 74 | # Prepare source code 75 | rm -r node_modules 76 | 77 | # optinal: add node_modules 78 | if [[ $copyAllPackages == true ]]; then 79 | cp -a ../.next/standalone/node_modules node_modules 80 | else 81 | mkdir node_modules 82 | for package in "${packages_to_copy[@]}" 83 | do 84 | cp -a ../node_modules/$package node_modules/$package 85 | done 86 | fi 87 | 88 | # zip source code 89 | echo "Generating source.zip ..." 90 | zip -r -q ../deployments/source.zip * .[!.]* 91 | echo "source.zip generated !" 92 | cd .. 93 | 94 | # Clean-up 95 | rm -r .next 96 | # rm -r standalone 97 | rm -r nodejs 98 | 99 | # Remove installed node modules 100 | npm uninstall serverless serverless-esbuild esbuild serverless-http ns-img-opt ns-img-rdr 101 | -------------------------------------------------------------------------------- /packages/ns-build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ns-build", 3 | "version": "1.8.1", 4 | "description": "Bash script to build serverless component for deploying to AWS using Terraform", 5 | "bin": { 6 | "ns-build": "./bin/ns-build.sh" 7 | }, 8 | "license": "Apache-2.0", 9 | "main": "index.js", 10 | "scripts": { 11 | "pre-publish": "tsc" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/emyriounis/terraform-aws-nextjs-serverless.git" 16 | }, 17 | "author": "Eleftherios Myriounis", 18 | "bugs": { 19 | "url": "https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues" 20 | }, 21 | "homepage": "https://github.com/emyriounis/terraform-aws-nextjs-serverless#readme", 22 | "dependencies": { 23 | "serverless-http": "^3.2.0" 24 | }, 25 | "peerDependencies": { 26 | "next": "^13.0.0 || ^14.0.0" 27 | }, 28 | "devDependencies": { 29 | "@types/aws-lambda": "^8.10.147", 30 | "@types/node": "^20.12.7", 31 | "typescript": "^5.4.5" 32 | } 33 | } -------------------------------------------------------------------------------- /packages/ns-build/server.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | var __importDefault = (this && this.__importDefault) || function (mod) { 12 | return (mod && mod.__esModule) ? mod : { "default": mod }; 13 | }; 14 | var _a, _b; 15 | Object.defineProperty(exports, "__esModule", { value: true }); 16 | exports.handler = void 0; 17 | const fs_1 = __importDefault(require("fs")); 18 | const path_1 = __importDefault(require("path")); 19 | const next_server_1 = __importDefault(require("next/dist/server/next-server")); 20 | const serverless_http_1 = __importDefault(require("serverless-http")); 21 | // @ts-ignore 22 | const required_server_files_json_1 = require("./.next/required-server-files.json"); 23 | const imageTypes = (_b = (_a = process.env.CUSTOM_IMAGE_TYPES) === null || _a === void 0 ? void 0 : _a.split(',')) !== null && _b !== void 0 ? _b : [ 24 | 'webp', 25 | 'jpeg', 26 | 'jpg', 27 | 'png', 28 | 'gif', 29 | 'ico', 30 | 'svg', 31 | ]; 32 | const showDebugLogs = process.env.SHOW_DEBUG_LOGS === 'true'; 33 | // Check if the custom server-side props handler should be used. 34 | const useCustomServerSidePropsHandler = (path) => process.env.DEFAULT_SS_PROPS_HANDLER !== 'true' && 35 | path.includes('/_next/data/'); 36 | const parseCookies = (cookies = []) => { 37 | const parsedCookies = {}; 38 | for (const cookie of cookies) { 39 | const parts = cookie.split(';'); 40 | for (const part of parts) { 41 | const [key, value] = part.split('='); 42 | if (key && value) { 43 | parsedCookies[key.trim()] = decodeURIComponent(value.trim()); 44 | } 45 | } 46 | } 47 | return parsedCookies; 48 | }; 49 | // Modify the event object to match the one expected by Next.JS 50 | const parseEvent = (event) => { 51 | const parsedEvent = Object.assign(event); 52 | parsedEvent.path = parsedEvent.rawPath; 53 | parsedEvent.headers.host = parsedEvent.headers['x-forwarded-host']; 54 | return parsedEvent; 55 | }; 56 | // Convert route file path to regex 57 | const routeToRegex = (filePath) => { 58 | const relativePath = filePath 59 | .replace('.next/server/pages', '') 60 | .replace(/\.js$/, ''); 61 | const regexPattern = relativePath 62 | .replace(/\/\[\[\.\.\.(\w+)\]\]/g, '(?:/(.*))?') // Handle [[...param]] correctly (no extra slash) 63 | .replace(/\[\.\.\.(\w+)\]/g, '/(.*)') // Handle [...param] 64 | .replace(/\/?\[(\w+)\]/g, '/([^/]+)'); // Handle [param] 65 | return { 66 | regex: new RegExp('^' + regexPattern + '$'), 67 | paramNames: [ 68 | ...relativePath.matchAll(/\[\[?\.\.\.(\w+)\]\]?|\[(\w+)\]/g), 69 | ].map(m => m[1] || m[2]), 70 | filePath, 71 | }; 72 | }; 73 | const depth = (path) => path.split('/').length; // Count path depth 74 | const dynamicCount = (path) => (path.match(/\[([^\]]+)\]/g) || []).length; // Count dynamic segments 75 | const isCatchAll = (path) => path.includes('[...'); 76 | const isOptionalCatchAll = (path) => path.includes('[[...'); 77 | const compareRoutes = (a, b) => { 78 | // 1. Sort by absolute path depth (more nested = higher priority) 79 | const depthDiff = depth(b) - depth(a); // Reverse order (deeper first) 80 | if (depthDiff !== 0) 81 | return depthDiff; 82 | // 2. Fewer dynamic segments take priority 83 | const dynamicDiff = dynamicCount(a) - dynamicCount(b); 84 | if (dynamicDiff !== 0) 85 | return dynamicDiff; 86 | // 3. Catch-all `[...param]` has lower priority than `[param]` 87 | const aCatchAll = isCatchAll(a); 88 | const bCatchAll = isCatchAll(b); 89 | if (aCatchAll !== bCatchAll) 90 | return aCatchAll ? 1 : -1; 91 | // 4. Optional catch-all `[[...param]]` has the lowest priority 92 | const aOptionalCatchAll = isOptionalCatchAll(a); 93 | const bOptionalCatchAll = isOptionalCatchAll(b); 94 | if (aOptionalCatchAll !== bOptionalCatchAll) 95 | return aOptionalCatchAll ? 1 : -1; 96 | return 0; // Same priority 97 | }; 98 | // Get all Next.js dynamic routes 99 | const getAllNextRoutes = () => { 100 | const dir = './.next/server/pages'; 101 | const allFiles = []; 102 | function traverse(subdir) { 103 | fs_1.default.readdirSync(subdir, { withFileTypes: true }).forEach(file => { 104 | const fullPath = path_1.default.join(subdir, file.name); 105 | if (file.isDirectory()) { 106 | traverse(fullPath); 107 | } 108 | else if (fullPath.endsWith('.js') && fullPath.includes('[')) { 109 | allFiles.push(fullPath); 110 | } 111 | }); 112 | } 113 | traverse(dir); 114 | return allFiles.sort((a, b) => compareRoutes(a, b)).map(routeToRegex); 115 | }; 116 | // Match a request path to a known Next.js dynamic route 117 | const matchRoute = (requestPath) => { 118 | const routes = getAllNextRoutes(); 119 | showDebugLogs && console.debug('Discovered routes:', getAllNextRoutes()); 120 | for (const { regex, paramNames, filePath } of routes) { 121 | showDebugLogs && console.debug({ requestPath, regex, paramNames, filePath }); 122 | const match = requestPath.match(regex); 123 | if (match) { 124 | const params = paramNames.reduce((acc, param, i) => { 125 | const value = match[i + 1] ? match[i + 1].split('/') : undefined; 126 | if (value && value.length === 1) { 127 | acc[param] = value[0]; 128 | } 129 | return acc; 130 | }, {}); 131 | return { filePath, params }; 132 | } 133 | } 134 | return null; 135 | }; 136 | // Load getServerSideProps with fallback to dynamic routes 137 | const loadProps = (importPath) => __awaiter(void 0, void 0, void 0, function* () { 138 | try { 139 | const { getServerSideProps } = yield require(importPath); 140 | return { getServerSideProps }; 141 | } 142 | catch (err) { 143 | showDebugLogs && 144 | console.debug(`Failed to directly load ${importPath}, trying dynamic route match...`, err); 145 | // Extract the request path from the import path 146 | const requestPath = importPath 147 | .replace('./.next/server/pages', '') 148 | .replace(/\.js$/, ''); 149 | // Try to match the request path dynamically 150 | const matchedRoute = matchRoute(requestPath); 151 | if (matchedRoute) { 152 | try { 153 | showDebugLogs && 154 | console.debug(`Matched dynamic route: ${matchedRoute.filePath}`, { 155 | params: matchedRoute.params, 156 | }); 157 | const { getServerSideProps } = yield require(matchedRoute.filePath); 158 | return { getServerSideProps, params: matchedRoute.params }; 159 | } 160 | catch (fallbackErr) { 161 | showDebugLogs && 162 | console.debug(`Fallback failed for ${matchedRoute.filePath}`, fallbackErr); 163 | } 164 | } 165 | showDebugLogs && 166 | console.debug(`Failed to match dynamic route for ${requestPath}`); 167 | return { getServerSideProps: null }; 168 | } 169 | }); 170 | /** 171 | * Dynamically load server-side rendering logic based on the 172 | * requested URL path and returns the page props in a JSON response. 173 | * @param {ParsedEvent} event - An object that contains information 174 | * related to the incoming request triggering this function. 175 | * @returns Returns a response object with a status code of 200 and a body 176 | * containing the `pageProps` extracted from the custom response obtained by calling the 177 | * `getServerSideProps` function dynamically based on the requested URL path. The `pageProps` are 178 | * serialized into a JSON string before being returned. 179 | */ 180 | const getProps = (event) => __awaiter(void 0, void 0, void 0, function* () { 181 | var _c, _d; 182 | const routePath = '/' + 183 | event.rawPath 184 | .replace('/_next/data/', '') 185 | .split('/') 186 | .slice(1) 187 | .join('/') 188 | .replace('.json', ''); 189 | const path = './.next/server/pages' + routePath + '.js'; 190 | const resolvedUrl = routePath.replace('/index', '/'); 191 | showDebugLogs && console.debug({ routePath, path, resolvedUrl }); 192 | /* 193 | * Dynamically import the module from the specified path and 194 | * extracts the `getServerSideProps` function from that module to load 195 | * the server-side rendering logic dynamically based on the requested URL path. 196 | */ 197 | const { getServerSideProps, params } = yield loadProps(path); 198 | if (getServerSideProps === null) { 199 | return { 200 | statusCode: 500, 201 | body: JSON.stringify({ notFound: true }), 202 | }; 203 | } 204 | // Provide a custom server-side rendering context for the server-side rendering. 205 | const customSsrContext = { 206 | req: event, 207 | query: (_c = event.queryStringParameters) !== null && _c !== void 0 ? _c : {}, 208 | params, 209 | resolvedUrl, 210 | }; 211 | showDebugLogs && console.debug({ customSsrContext }); 212 | const customResponse = yield getServerSideProps(customSsrContext); 213 | showDebugLogs && console.debug({ customResponse }); 214 | const redirectDestination = (_d = customResponse.redirect) === null || _d === void 0 ? void 0 : _d.destination; 215 | showDebugLogs && console.debug({ redirectDestination }); 216 | // TODO: fix this 217 | if (redirectDestination) { 218 | return { 219 | statusCode: 500, 220 | body: JSON.stringify({ notFound: true }), 221 | }; 222 | } 223 | const body = JSON.stringify(redirectDestination 224 | ? { __N_REDIRECT: redirectDestination, __N_SSP: true } 225 | : { pageProps: customResponse.props, __N_SSP: true }); 226 | const response = {}; 227 | response.statusCode = 200; 228 | response.body = body; 229 | response.headers = { 230 | 'Cache-Control': 'no-store', 231 | }; 232 | showDebugLogs && console.debug({ response }); 233 | return response; 234 | }); 235 | // Creating the Next.js Server 236 | const nextServer = new next_server_1.default({ 237 | hostname: 'localhost', 238 | port: 3000, 239 | dir: './', 240 | dev: false, 241 | conf: Object.assign({}, required_server_files_json_1.config), 242 | customServer: true, 243 | }); 244 | // Creating the serverless wrapper using the `serverless-http` library. 245 | const main = (0, serverless_http_1.default)(nextServer.getRequestHandler(), { 246 | binary: ['*/*'], 247 | provider: 'aws', 248 | }); 249 | /** 250 | * The handler function processes an event, checks if an image is requested, and either redirects to an 251 | * S3 bucket or calls another function based on custom server-side props. 252 | * @param {APIGatewayProxyEventV2} event - The `event` parameter typically contains information about the HTTP request 253 | * that triggered the Lambda function. This can include details such as headers, query parameters, path 254 | * parameters, request body, and more. In your code snippet, the `event` object is being used to 255 | * extract information like the path and headers of 256 | * @param {Context} context - The `context` parameter in the code snippet you provided is typically used to 257 | * provide information about the execution environment and runtime context of the function. It can 258 | * include details such as the AWS Lambda function name, version, memory limit, request ID, and more. 259 | * This information can be useful for understanding the context 260 | * @param {Callback} callback - The `callback` parameter in the `handler` function is a function that you 261 | * can call to send a response back to the caller. In this case, the response is an HTTP response 262 | * object that includes a status code and headers. When you call `callback(null, response)`, you are 263 | * indicating that 264 | * @returns The code is returning either the result of the `getProps(parsedEvent)` function if 265 | * `useCustomServerSidePropsHandler(parsedEvent.rawPath)` returns true, or the result of the 266 | * `main(parsedEvent, context)` function if `useCustomServerSidePropsHandler(parsedEvent.rawPath)` 267 | * returns false. 268 | */ 269 | const handler = (event, context, callback) => { 270 | showDebugLogs && console.debug({ event }); 271 | showDebugLogs && console.debug({ context }); 272 | const parsedEvent = parseEvent(event); 273 | showDebugLogs && console.debug({ parsedEvent }); 274 | /* If an image is requested, redirect to the corresponding S3 bucket. */ 275 | if (imageTypes.some(type => parsedEvent.path.includes('.' + type))) { 276 | const response = { 277 | statusCode: 302, 278 | headers: { 279 | Location: 'https://' + parsedEvent.headers.host + '/assets' + parsedEvent.path, 280 | }, 281 | }; 282 | return callback(null, response); 283 | } 284 | const shouldUseCustomServerSidePropsHandler = useCustomServerSidePropsHandler(parsedEvent.rawPath); 285 | if (shouldUseCustomServerSidePropsHandler) { 286 | const rawCookies = event.cookies; 287 | Object.defineProperty(parsedEvent, 'cookies', { 288 | get: () => parseCookies(rawCookies), 289 | }); 290 | showDebugLogs && console.debug({ parsedEvent }); 291 | return getProps(parsedEvent); 292 | } 293 | return main(parsedEvent, context); 294 | }; 295 | exports.handler = handler; 296 | -------------------------------------------------------------------------------- /packages/ns-build/server.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import { NextConfig } from 'next' 4 | import { 5 | APIGatewayProxyEventV2, 6 | APIGatewayProxyResultV2, 7 | Callback, 8 | Context, 9 | } from 'aws-lambda' 10 | import NextServer from 'next/dist/server/next-server' 11 | import serverless from 'serverless-http' 12 | // @ts-ignore 13 | import { config } from './.next/required-server-files.json' 14 | 15 | type ParsedEvent = APIGatewayProxyEventV2 & { path: string } 16 | 17 | const imageTypes = process.env.CUSTOM_IMAGE_TYPES?.split(',') ?? [ 18 | 'webp', 19 | 'jpeg', 20 | 'jpg', 21 | 'png', 22 | 'gif', 23 | 'ico', 24 | 'svg', 25 | ] 26 | 27 | const showDebugLogs = process.env.SHOW_DEBUG_LOGS === 'true' 28 | 29 | // Check if the custom server-side props handler should be used. 30 | const useCustomServerSidePropsHandler = (path: string) => 31 | process.env.DEFAULT_SS_PROPS_HANDLER !== 'true' && 32 | path.includes('/_next/data/') 33 | 34 | const parseCookies = (cookies: string[] = []) => { 35 | const parsedCookies: Record = {} 36 | 37 | for (const cookie of cookies) { 38 | const parts = cookie.split(';') 39 | 40 | for (const part of parts) { 41 | const [key, value] = part.split('=') 42 | 43 | if (key && value) { 44 | parsedCookies[key.trim()] = decodeURIComponent(value.trim()) 45 | } 46 | } 47 | } 48 | 49 | return parsedCookies 50 | } 51 | 52 | // Modify the event object to match the one expected by Next.JS 53 | const parseEvent = (event: APIGatewayProxyEventV2): ParsedEvent => { 54 | const parsedEvent: ParsedEvent = Object.assign(event) 55 | 56 | parsedEvent.path = parsedEvent.rawPath 57 | parsedEvent.headers.host = parsedEvent.headers['x-forwarded-host'] 58 | 59 | return parsedEvent 60 | } 61 | 62 | // Convert route file path to regex 63 | const routeToRegex = (filePath: string) => { 64 | const relativePath = filePath 65 | .replace('.next/server/pages', '') 66 | .replace(/\.js$/, '') 67 | const regexPattern = relativePath 68 | .replace(/\/\[\[\.\.\.(\w+)\]\]/g, '(?:/(.*))?') // Handle [[...param]] correctly (no extra slash) 69 | .replace(/\[\.\.\.(\w+)\]/g, '/(.*)') // Handle [...param] 70 | .replace(/\/?\[(\w+)\]/g, '/([^/]+)') // Handle [param] 71 | 72 | return { 73 | regex: new RegExp('^' + regexPattern + '$'), 74 | paramNames: [ 75 | ...relativePath.matchAll(/\[\[?\.\.\.(\w+)\]\]?|\[(\w+)\]/g), 76 | ].map(m => m[1] || m[2]), 77 | filePath, 78 | } 79 | } 80 | 81 | const depth = (path: string) => path.split('/').length // Count path depth 82 | const dynamicCount = (path: string) => 83 | (path.match(/\[([^\]]+)\]/g) || []).length // Count dynamic segments 84 | const isCatchAll = (path: string) => path.includes('[...') 85 | const isOptionalCatchAll = (path: string) => path.includes('[[...') 86 | 87 | const compareRoutes = (a: string, b: string) => { 88 | // 1. Sort by absolute path depth (more nested = higher priority) 89 | const depthDiff = depth(b) - depth(a) // Reverse order (deeper first) 90 | if (depthDiff !== 0) return depthDiff 91 | 92 | // 2. Fewer dynamic segments take priority 93 | const dynamicDiff = dynamicCount(a) - dynamicCount(b) 94 | if (dynamicDiff !== 0) return dynamicDiff 95 | 96 | // 3. Catch-all `[...param]` has lower priority than `[param]` 97 | const aCatchAll = isCatchAll(a) 98 | const bCatchAll = isCatchAll(b) 99 | if (aCatchAll !== bCatchAll) return aCatchAll ? 1 : -1 100 | 101 | // 4. Optional catch-all `[[...param]]` has the lowest priority 102 | const aOptionalCatchAll = isOptionalCatchAll(a) 103 | const bOptionalCatchAll = isOptionalCatchAll(b) 104 | if (aOptionalCatchAll !== bOptionalCatchAll) return aOptionalCatchAll ? 1 : -1 105 | 106 | return 0 // Same priority 107 | } 108 | 109 | // Get all Next.js dynamic routes 110 | const getAllNextRoutes = () => { 111 | const dir = './.next/server/pages' 112 | const allFiles: string[] = [] 113 | 114 | function traverse(subdir: string) { 115 | fs.readdirSync(subdir, { withFileTypes: true }).forEach(file => { 116 | const fullPath = path.join(subdir, file.name) 117 | 118 | if (file.isDirectory()) { 119 | traverse(fullPath) 120 | } else if (fullPath.endsWith('.js') && fullPath.includes('[')) { 121 | allFiles.push(fullPath) 122 | } 123 | }) 124 | } 125 | 126 | traverse(dir) 127 | 128 | return allFiles.sort((a, b) => compareRoutes(a, b)).map(routeToRegex) 129 | } 130 | 131 | // Match a request path to a known Next.js dynamic route 132 | const matchRoute = (requestPath: string) => { 133 | const routes = getAllNextRoutes() 134 | showDebugLogs && console.debug('Discovered routes:', getAllNextRoutes()) 135 | 136 | for (const { regex, paramNames, filePath } of routes) { 137 | showDebugLogs && console.debug({ requestPath, regex, paramNames, filePath }) 138 | const match = requestPath.match(regex) 139 | if (match) { 140 | const params = paramNames.reduce((acc, param, i) => { 141 | const value = match[i + 1] ? match[i + 1].split('/') : undefined 142 | 143 | if (value && value.length === 1) { 144 | acc[param] = value[0] 145 | } 146 | 147 | return acc 148 | }, {} as Record) 149 | 150 | return { filePath, params } 151 | } 152 | } 153 | return null 154 | } 155 | 156 | // Load getServerSideProps with fallback to dynamic routes 157 | const loadProps = async (importPath: string) => { 158 | try { 159 | const { getServerSideProps } = await require(importPath) 160 | return { getServerSideProps } 161 | } catch (err) { 162 | showDebugLogs && 163 | console.debug( 164 | `Failed to directly load ${importPath}, trying dynamic route match...`, 165 | err 166 | ) 167 | // Extract the request path from the import path 168 | const requestPath = importPath 169 | .replace('./.next/server/pages', '') 170 | .replace(/\.js$/, '') 171 | // Try to match the request path dynamically 172 | const matchedRoute = matchRoute(requestPath) 173 | if (matchedRoute) { 174 | try { 175 | showDebugLogs && 176 | console.debug(`Matched dynamic route: ${matchedRoute.filePath}`, { 177 | params: matchedRoute.params, 178 | }) 179 | const { getServerSideProps } = await require(matchedRoute.filePath) 180 | return { getServerSideProps, params: matchedRoute.params } 181 | } catch (fallbackErr) { 182 | showDebugLogs && 183 | console.debug( 184 | `Fallback failed for ${matchedRoute.filePath}`, 185 | fallbackErr 186 | ) 187 | } 188 | } 189 | 190 | showDebugLogs && 191 | console.debug(`Failed to match dynamic route for ${requestPath}`) 192 | return { getServerSideProps: null } 193 | } 194 | } 195 | 196 | /** 197 | * Dynamically load server-side rendering logic based on the 198 | * requested URL path and returns the page props in a JSON response. 199 | * @param {ParsedEvent} event - An object that contains information 200 | * related to the incoming request triggering this function. 201 | * @returns Returns a response object with a status code of 200 and a body 202 | * containing the `pageProps` extracted from the custom response obtained by calling the 203 | * `getServerSideProps` function dynamically based on the requested URL path. The `pageProps` are 204 | * serialized into a JSON string before being returned. 205 | */ 206 | const getProps = async (event: ParsedEvent) => { 207 | const routePath = 208 | '/' + 209 | event.rawPath 210 | .replace('/_next/data/', '') 211 | .split('/') 212 | .slice(1) 213 | .join('/') 214 | .replace('.json', '') 215 | const path = './.next/server/pages' + routePath + '.js' 216 | const resolvedUrl = routePath.replace('/index', '/') 217 | showDebugLogs && console.debug({ routePath, path, resolvedUrl }) 218 | 219 | /* 220 | * Dynamically import the module from the specified path and 221 | * extracts the `getServerSideProps` function from that module to load 222 | * the server-side rendering logic dynamically based on the requested URL path. 223 | */ 224 | const { getServerSideProps, params } = await loadProps(path) 225 | if (getServerSideProps === null) { 226 | return { 227 | statusCode: 500, 228 | body: JSON.stringify({ notFound: true }), 229 | } 230 | } 231 | 232 | // Provide a custom server-side rendering context for the server-side rendering. 233 | const customSsrContext = { 234 | req: event, 235 | query: event.queryStringParameters ?? {}, 236 | params, 237 | resolvedUrl, 238 | } 239 | showDebugLogs && console.debug({ customSsrContext }) 240 | 241 | const customResponse = await getServerSideProps(customSsrContext) 242 | showDebugLogs && console.debug({ customResponse }) 243 | 244 | const redirectDestination = customResponse.redirect?.destination 245 | showDebugLogs && console.debug({ redirectDestination }) 246 | // TODO: fix this 247 | if (redirectDestination) { 248 | return { 249 | statusCode: 500, 250 | body: JSON.stringify({ notFound: true }), 251 | } 252 | } 253 | 254 | const body = JSON.stringify( 255 | redirectDestination 256 | ? { __N_REDIRECT: redirectDestination, __N_SSP: true } 257 | : { pageProps: customResponse.props, __N_SSP: true } 258 | ) 259 | 260 | const response: APIGatewayProxyResultV2 = {} 261 | response.statusCode = 200 262 | response.body = body 263 | response.headers = { 264 | 'Cache-Control': 'no-store', 265 | } 266 | showDebugLogs && console.debug({ response }) 267 | 268 | return response 269 | } 270 | 271 | // Creating the Next.js Server 272 | const nextServer = new NextServer({ 273 | hostname: 'localhost', 274 | port: 3000, 275 | dir: './', 276 | dev: false, 277 | conf: { 278 | ...(config as NextConfig), 279 | }, 280 | customServer: true, 281 | }) 282 | 283 | // Creating the serverless wrapper using the `serverless-http` library. 284 | const main = serverless(nextServer.getRequestHandler(), { 285 | binary: ['*/*'], 286 | provider: 'aws', 287 | }) 288 | 289 | /** 290 | * The handler function processes an event, checks if an image is requested, and either redirects to an 291 | * S3 bucket or calls another function based on custom server-side props. 292 | * @param {APIGatewayProxyEventV2} event - The `event` parameter typically contains information about the HTTP request 293 | * that triggered the Lambda function. This can include details such as headers, query parameters, path 294 | * parameters, request body, and more. In your code snippet, the `event` object is being used to 295 | * extract information like the path and headers of 296 | * @param {Context} context - The `context` parameter in the code snippet you provided is typically used to 297 | * provide information about the execution environment and runtime context of the function. It can 298 | * include details such as the AWS Lambda function name, version, memory limit, request ID, and more. 299 | * This information can be useful for understanding the context 300 | * @param {Callback} callback - The `callback` parameter in the `handler` function is a function that you 301 | * can call to send a response back to the caller. In this case, the response is an HTTP response 302 | * object that includes a status code and headers. When you call `callback(null, response)`, you are 303 | * indicating that 304 | * @returns The code is returning either the result of the `getProps(parsedEvent)` function if 305 | * `useCustomServerSidePropsHandler(parsedEvent.rawPath)` returns true, or the result of the 306 | * `main(parsedEvent, context)` function if `useCustomServerSidePropsHandler(parsedEvent.rawPath)` 307 | * returns false. 308 | */ 309 | export const handler = ( 310 | event: APIGatewayProxyEventV2, 311 | context: Context, 312 | callback: Callback 313 | ) => { 314 | showDebugLogs && console.debug({ event }) 315 | showDebugLogs && console.debug({ context }) 316 | 317 | const parsedEvent = parseEvent(event) 318 | showDebugLogs && console.debug({ parsedEvent }) 319 | 320 | /* If an image is requested, redirect to the corresponding S3 bucket. */ 321 | if (imageTypes.some(type => parsedEvent.path.includes('.' + type))) { 322 | const response = { 323 | statusCode: 302, 324 | headers: { 325 | Location: 326 | 'https://' + parsedEvent.headers.host + '/assets' + parsedEvent.path, 327 | }, 328 | } 329 | 330 | return callback(null, response) 331 | } 332 | 333 | const shouldUseCustomServerSidePropsHandler = useCustomServerSidePropsHandler( 334 | parsedEvent.rawPath 335 | ) 336 | if (shouldUseCustomServerSidePropsHandler) { 337 | const rawCookies = event.cookies 338 | Object.defineProperty(parsedEvent, 'cookies', { 339 | get: () => parseCookies(rawCookies), 340 | }) 341 | showDebugLogs && console.debug({ parsedEvent }) 342 | 343 | return getProps(parsedEvent) 344 | } 345 | 346 | return main(parsedEvent, context) 347 | } 348 | -------------------------------------------------------------------------------- /packages/ns-build/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */ 22 | // "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | 26 | /* Modules */ 27 | "module": "commonjs" /* Specify what module code is generated. */, 28 | // "rootDir": "./", /* Specify the root folder within your source files. */ 29 | // "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ 30 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 31 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 32 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 33 | // "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */ 34 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 35 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 36 | // "resolveJsonModule": true, /* Enable importing .json files */ 37 | // "noResolve": true, /* Disallow `import`s, `require`s or ``s from expanding the number of files TypeScript should add to a project. */ 38 | 39 | /* JavaScript Support */ 40 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */ 41 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 42 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */ 43 | 44 | /* Emit */ 45 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 46 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 47 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 48 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 49 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */ 50 | // "outDir": "./", /* Specify an output folder for all emitted files. */ 51 | // "removeComments": true, /* Disable emitting comments. */ 52 | // "noEmit": true, /* Disable emitting files from a compilation. */ 53 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 54 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */ 55 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 56 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 59 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 60 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 61 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 62 | // "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */ 63 | // "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */ 64 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 65 | // "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ 66 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 67 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 68 | 69 | /* Interop Constraints */ 70 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 71 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 72 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, 73 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 74 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 75 | 76 | /* Type Checking */ 77 | "strict": true /* Enable all strict type-checking options. */, 78 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ 79 | // "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */ 80 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 81 | // "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */ 82 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 83 | // "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */ 84 | // "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */ 85 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 86 | // "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */ 87 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */ 88 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 89 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 90 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 91 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 92 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 93 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */ 94 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 95 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 96 | 97 | /* Completeness */ 98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 99 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/ns-img-opt/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | the copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor to discuss and improve the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by the Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributors that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, the Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assuming any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2024\ 179 | Eleftherios Myriounis 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /packages/ns-img-opt/README.md: -------------------------------------------------------------------------------- 1 | ## image-optimization lambda 2 | 3 | ### Build 4 | 5 | 1. Edit the source code 6 | 7 | 2. Run `npm run prepare-lambda` to build source.zip 8 | -------------------------------------------------------------------------------- /packages/ns-img-opt/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ns-img-opt", 3 | "version": "1.8.1", 4 | "description": "Image Optimization Lambda", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "pre-publish": "bash scripts/pre-publish.sh" 9 | }, 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "@types/node": "^20.8.4", 13 | "ts-node": "^10.9.1", 14 | "typescript": "^5.2.2" 15 | }, 16 | "dependencies": { 17 | "@aws-sdk/client-s3": "^3.433.0", 18 | "sharp": "^0.32.6" 19 | }, 20 | "files": [ 21 | "source.zip" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/emyriounis/terraform-aws-nextjs-serverless.git" 26 | }, 27 | "author": "Eleftherios Myriounis", 28 | "bugs": { 29 | "url": "https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues" 30 | }, 31 | "homepage": "https://github.com/emyriounis/terraform-aws-nextjs-serverless#readme" 32 | } -------------------------------------------------------------------------------- /packages/ns-img-opt/scripts/pre-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build 4 | rm -r build/ 5 | npm run build 6 | 7 | # add node_modules 8 | rm -r node_modules/ 9 | npm install --arch=x64 --platform=linux --omit=dev 10 | cp -r node_modules/ build/node_modules/ 11 | 12 | # zip 13 | cd build/ 14 | rm -r ../source.zip 15 | zip -r -q ../source.zip * 16 | cd .. 17 | 18 | # cleanup 19 | rm -r build/ 20 | npm i 21 | -------------------------------------------------------------------------------- /packages/ns-img-opt/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const defaults = { 2 | width: 256, 3 | quality: 75, 4 | } 5 | -------------------------------------------------------------------------------- /packages/ns-img-opt/src/helpers.ts: -------------------------------------------------------------------------------- 1 | import { HeadObjectCommand, S3Client } from "@aws-sdk/client-s3" 2 | 3 | /** 4 | * The function `redirectTo` is used to create a redirect response with a specified URL. 5 | * @param {string} url - The `url` parameter is a string that represents the URL to which you want to 6 | * redirect the user. 7 | * @param {any} callback - The `callback` parameter is a function that is used to return the response 8 | * to the caller. It takes two arguments: an error object (if any) and the response object. In this 9 | * case, the response object is an HTTP response with a status code of 302 (Redirect) and a ` 10 | * @returns a callback function with two arguments: null and an object representing a response. 11 | */ 12 | export const redirectTo = (url: string, callback: any) => { 13 | const response = { 14 | status: 302, 15 | statusDescription: 'Redirect', 16 | headers: { 17 | location: [ 18 | { 19 | key: 'Location', 20 | value: url, 21 | }, 22 | ], 23 | 'cache-control': [ 24 | { 25 | key: 'Cache-Control', 26 | value: 'public, max-age=600, stale-while-revalidate=2592000', // Serve cached content up to 30 days old while revalidating it after 10 minutes 27 | }, 28 | ], 29 | }, 30 | } 31 | 32 | return callback(null, response) 33 | } 34 | 35 | export const isVersionStored = async (s3Client: S3Client, bucket: string, key: string): Promise => { 36 | try { 37 | const command = new HeadObjectCommand({ 38 | Bucket: bucket, 39 | Key: key, 40 | }) 41 | 42 | // will throw error if it's not found 43 | await s3Client.send(command) 44 | return true 45 | } catch (error) { 46 | console.warn(`${key} is not stored yet`, error); 47 | return false 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/ns-img-opt/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetObjectCommand, 3 | PutObjectCommand, 4 | S3Client, 5 | } from '@aws-sdk/client-s3' 6 | import sharp from 'sharp' 7 | 8 | import { defaults } from './constants' 9 | import { isVersionStored, redirectTo } from './helpers' 10 | 11 | /** 12 | * This TypeScript function is a CloudFront function that resizes and compresses images based on the 13 | * request URI and returns the resized image as a base64 encoded string. 14 | * @param {any} event - The `event` parameter is an object that contains information about the event 15 | * that triggered the Lambda function. In this case, it contains the CloudFront event data, which 16 | * includes details about the request and configuration. 17 | * @param {any} _context - The `_context` parameter is a context object that contains information about 18 | * the execution environment and runtime. It is typically not used in this code snippet, so it can be 19 | * ignored for now. 20 | * @param {any} callback - The `callback` parameter is a function that is used to send the response 21 | * back to the caller. It takes two arguments: an error object (or null if there is no error) and the 22 | * response object. The response object should contain the status code, status description, headers, 23 | * body encoding, and 24 | * @returns The code is returning a response object with the following properties: 25 | */ 26 | export const handler = async (event: any, _context: any, callback: any) => { 27 | try { 28 | /* Extract the `request` and `config` properties. */ 29 | const { request, config } = event?.Records?.[0]?.cf 30 | /* Construct the base URL for the image assets. */ 31 | const baseUrl = `https://${config?.distributionDomainName}/` 32 | 33 | /* The S3 region. */ 34 | const s3Region = 35 | request?.origin?.custom?.customHeaders?.['s3-region']?.[0]?.value 36 | /* The public_assets_bucket name. */ 37 | const publicAssetsBucket = 38 | request?.origin?.custom?.customHeaders?.['public-assets-bucket']?.[0] 39 | ?.value 40 | 41 | /* Extracting the relevant information from the request URI. */ 42 | const queryString = (request?.uri as string) 43 | ?.replace('/_next/image/', '') 44 | ?.split('/') 45 | // Map required info 46 | const width = parseInt(queryString?.[0] || defaults.width.toString()) 47 | const type = queryString?.[1] 48 | const filename = queryString?.slice(2)?.join('/').replace('%2F', '/') 49 | 50 | /* The S3 Client. */ 51 | const s3 = new S3Client({ region: s3Region }) 52 | const resizedImageFilename = `resized-assets/${width}/${type}/${filename}` 53 | 54 | const isVersionAlreadyResized = await isVersionStored(s3, publicAssetsBucket, resizedImageFilename) 55 | if (isVersionAlreadyResized) { 56 | return redirectTo(baseUrl + resizedImageFilename, callback) 57 | } 58 | 59 | // The url where the image is stored 60 | const imageUrl = baseUrl + 'assets/' + filename 61 | // The options for image transformation 62 | const options = { quality: defaults.quality } 63 | 64 | /* Build the s3 command. */ 65 | const s3GetObjectCommand = new GetObjectCommand({ 66 | Bucket: publicAssetsBucket, 67 | Key: 'assets/' + filename, 68 | }) 69 | 70 | /* The body of the S3 object. */ 71 | const { Body } = await s3.send(s3GetObjectCommand) 72 | /* Transforming the body of the S3 object into a byte array. */ 73 | const s3Object = await Body.transformToByteArray() 74 | 75 | /* Resize and compress the image. */ 76 | const resizedImage = sharp(s3Object).resize({ width }) 77 | 78 | let newContentType: string | null = null 79 | /* Apply the corresponding image type transformation. */ 80 | switch (type) { 81 | case 'webp': 82 | resizedImage.webp(options) 83 | newContentType = 'image/webp' 84 | break 85 | case 'jpeg': 86 | resizedImage.jpeg(options) 87 | newContentType = 'image/jpeg' 88 | break 89 | case 'png': 90 | resizedImage.png(options) 91 | newContentType = 'image/png' 92 | break 93 | // case 'gif': 94 | // // resizedImage.gif(options) 95 | // resizedImage.gif() 96 | // newContentType = 'image/gif' 97 | // break 98 | // case 'apng': 99 | // // resizedImage.apng(options) 100 | // resizedImage.png(options) 101 | // newContentType = 'image/apng' 102 | // break 103 | // case 'avif': 104 | // resizedImage.avif(options) 105 | // newContentType = 'image/avif' 106 | // break 107 | // // case 'svg+xml': 108 | // // resizedImage.svg(options) 109 | // // newContentType = 'image/svg+xml' 110 | // // break 111 | 112 | default: 113 | return redirectTo(imageUrl, callback) 114 | } 115 | 116 | /* Converting the resized image into a buffer. */ 117 | const resizedImageBuffer = await resizedImage.toBuffer() 118 | /* Store the resized image */ 119 | const s3PutObjectCommand = new PutObjectCommand({ 120 | Bucket: publicAssetsBucket, 121 | Key: resizedImageFilename, 122 | Body: resizedImageBuffer, 123 | ContentType: newContentType, 124 | }) 125 | await s3.send(s3PutObjectCommand) 126 | 127 | return redirectTo(baseUrl + resizedImageFilename, callback) 128 | } catch (error) { 129 | console.error('An unexpected occured', error) 130 | 131 | return callback(null, { 132 | status: 403, // to not leak data 133 | }) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /packages/ns-img-opt/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "strictNullChecks": false, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./build", 11 | "rootDir": "./src" 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /packages/ns-img-rdr/LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | the copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor to discuss and improve the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by the Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributors that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, the Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assuming any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | Copyright 2024\ 179 | Eleftherios Myriounis 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | http://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. -------------------------------------------------------------------------------- /packages/ns-img-rdr/README.md: -------------------------------------------------------------------------------- 1 | ## image-optimization lambda 2 | 3 | ### Build 4 | 5 | 1. Edit the source code 6 | 7 | 2. Run `npm run prepare-lambda` to build source.zip 8 | -------------------------------------------------------------------------------- /packages/ns-img-rdr/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ns-img-rdr", 3 | "version": "1.8.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "ns-img-rdr", 9 | "version": "1.8.1", 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "@types/node": "^20.8.4", 13 | "ts-node": "^10.9.1", 14 | "typescript": "^5.2.2" 15 | } 16 | }, 17 | "node_modules/@cspotcode/source-map-support": { 18 | "version": "0.8.1", 19 | "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", 20 | "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", 21 | "dev": true, 22 | "dependencies": { 23 | "@jridgewell/trace-mapping": "0.3.9" 24 | }, 25 | "engines": { 26 | "node": ">=12" 27 | } 28 | }, 29 | "node_modules/@jridgewell/resolve-uri": { 30 | "version": "3.1.1", 31 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", 32 | "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", 33 | "dev": true, 34 | "engines": { 35 | "node": ">=6.0.0" 36 | } 37 | }, 38 | "node_modules/@jridgewell/sourcemap-codec": { 39 | "version": "1.4.15", 40 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", 41 | "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", 42 | "dev": true 43 | }, 44 | "node_modules/@jridgewell/trace-mapping": { 45 | "version": "0.3.9", 46 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", 47 | "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", 48 | "dev": true, 49 | "dependencies": { 50 | "@jridgewell/resolve-uri": "^3.0.3", 51 | "@jridgewell/sourcemap-codec": "^1.4.10" 52 | } 53 | }, 54 | "node_modules/@tsconfig/node10": { 55 | "version": "1.0.9", 56 | "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", 57 | "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", 58 | "dev": true 59 | }, 60 | "node_modules/@tsconfig/node12": { 61 | "version": "1.0.11", 62 | "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", 63 | "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", 64 | "dev": true 65 | }, 66 | "node_modules/@tsconfig/node14": { 67 | "version": "1.0.3", 68 | "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", 69 | "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", 70 | "dev": true 71 | }, 72 | "node_modules/@tsconfig/node16": { 73 | "version": "1.0.4", 74 | "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", 75 | "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", 76 | "dev": true 77 | }, 78 | "node_modules/@types/node": { 79 | "version": "20.8.6", 80 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.8.6.tgz", 81 | "integrity": "sha512-eWO4K2Ji70QzKUqRy6oyJWUeB7+g2cRagT3T/nxYibYcT4y2BDL8lqolRXjTHmkZCdJfIPaY73KbJAZmcryxTQ==", 82 | "dev": true, 83 | "dependencies": { 84 | "undici-types": "~5.25.1" 85 | } 86 | }, 87 | "node_modules/acorn": { 88 | "version": "8.10.0", 89 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", 90 | "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", 91 | "dev": true, 92 | "bin": { 93 | "acorn": "bin/acorn" 94 | }, 95 | "engines": { 96 | "node": ">=0.4.0" 97 | } 98 | }, 99 | "node_modules/acorn-walk": { 100 | "version": "8.2.0", 101 | "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", 102 | "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", 103 | "dev": true, 104 | "engines": { 105 | "node": ">=0.4.0" 106 | } 107 | }, 108 | "node_modules/arg": { 109 | "version": "4.1.3", 110 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 111 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 112 | "dev": true 113 | }, 114 | "node_modules/create-require": { 115 | "version": "1.1.1", 116 | "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", 117 | "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", 118 | "dev": true 119 | }, 120 | "node_modules/diff": { 121 | "version": "4.0.2", 122 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 123 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 124 | "dev": true, 125 | "engines": { 126 | "node": ">=0.3.1" 127 | } 128 | }, 129 | "node_modules/make-error": { 130 | "version": "1.3.6", 131 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 132 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 133 | "dev": true 134 | }, 135 | "node_modules/ts-node": { 136 | "version": "10.9.1", 137 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", 138 | "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", 139 | "dev": true, 140 | "dependencies": { 141 | "@cspotcode/source-map-support": "^0.8.0", 142 | "@tsconfig/node10": "^1.0.7", 143 | "@tsconfig/node12": "^1.0.7", 144 | "@tsconfig/node14": "^1.0.0", 145 | "@tsconfig/node16": "^1.0.2", 146 | "acorn": "^8.4.1", 147 | "acorn-walk": "^8.1.1", 148 | "arg": "^4.1.0", 149 | "create-require": "^1.1.0", 150 | "diff": "^4.0.1", 151 | "make-error": "^1.1.1", 152 | "v8-compile-cache-lib": "^3.0.1", 153 | "yn": "3.1.1" 154 | }, 155 | "bin": { 156 | "ts-node": "dist/bin.js", 157 | "ts-node-cwd": "dist/bin-cwd.js", 158 | "ts-node-esm": "dist/bin-esm.js", 159 | "ts-node-script": "dist/bin-script.js", 160 | "ts-node-transpile-only": "dist/bin-transpile.js", 161 | "ts-script": "dist/bin-script-deprecated.js" 162 | }, 163 | "peerDependencies": { 164 | "@swc/core": ">=1.2.50", 165 | "@swc/wasm": ">=1.2.50", 166 | "@types/node": "*", 167 | "typescript": ">=2.7" 168 | }, 169 | "peerDependenciesMeta": { 170 | "@swc/core": { 171 | "optional": true 172 | }, 173 | "@swc/wasm": { 174 | "optional": true 175 | } 176 | } 177 | }, 178 | "node_modules/typescript": { 179 | "version": "5.2.2", 180 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", 181 | "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", 182 | "dev": true, 183 | "bin": { 184 | "tsc": "bin/tsc", 185 | "tsserver": "bin/tsserver" 186 | }, 187 | "engines": { 188 | "node": ">=14.17" 189 | } 190 | }, 191 | "node_modules/undici-types": { 192 | "version": "5.25.3", 193 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.25.3.tgz", 194 | "integrity": "sha512-Ga1jfYwRn7+cP9v8auvEXN1rX3sWqlayd4HP7OKk4mZWylEmu3KzXDUGrQUN6Ol7qo1gPvB2e5gX6udnyEPgdA==", 195 | "dev": true 196 | }, 197 | "node_modules/v8-compile-cache-lib": { 198 | "version": "3.0.1", 199 | "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", 200 | "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", 201 | "dev": true 202 | }, 203 | "node_modules/yn": { 204 | "version": "3.1.1", 205 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 206 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 207 | "dev": true, 208 | "engines": { 209 | "node": ">=6" 210 | } 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /packages/ns-img-rdr/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ns-img-rdr", 3 | "version": "1.8.1", 4 | "description": "Image Redirection Lambda", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc", 8 | "pre-publish": "bash scripts/pre-publish.sh" 9 | }, 10 | "license": "Apache-2.0", 11 | "devDependencies": { 12 | "@types/node": "^20.8.4", 13 | "ts-node": "^10.9.1", 14 | "typescript": "^5.2.2" 15 | }, 16 | "files": [ 17 | "source.zip" 18 | ], 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/emyriounis/terraform-aws-nextjs-serverless.git" 22 | }, 23 | "author": "Eleftherios Myriounis", 24 | "bugs": { 25 | "url": "https://github.com/emyriounis/terraform-aws-nextjs-serverless/issues" 26 | }, 27 | "homepage": "https://github.com/emyriounis/terraform-aws-nextjs-serverless#readme" 28 | } -------------------------------------------------------------------------------- /packages/ns-img-rdr/scripts/pre-publish.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # build 4 | rm -r build/ 5 | npm run build 6 | 7 | # zip 8 | cd build/ 9 | rm -r ../source.zip 10 | zip -r -q ../source.zip * 11 | cd .. 12 | -------------------------------------------------------------------------------- /packages/ns-img-rdr/src/index.ts: -------------------------------------------------------------------------------- 1 | /* A list of supported image types. 2 | It contains the MIME types for various image formats. 3 | These image types are prioritized in the array, with the most preferred format at the beginning. */ 4 | const imageTypes = [ 5 | 'image/webp', 6 | // 'image/avif', 7 | 'image/jpeg', 8 | 'image/png', 9 | // 'image/svg+xml', 10 | // 'image/gif', 11 | // 'image/apng', 12 | ] 13 | 14 | /* A list of specific image widths. These values represent different width 15 | dimensions that are commonly used for images. */ 16 | const imageWidths = [16, 32, 64, 128, 256, 512, 1024] 17 | 18 | /** 19 | * Returns the closest image width from a predefined list that is greater 20 | * than or equal to the requested width, with a maximum limit of 1024. 21 | * @param {number} requestedWidth - The width of an image that a user is requesting. 22 | * @returns The first width in the `imageWidths` array that is greater than or equal to the `requestedWidth`. 23 | * If no such width is found, it will return 1024 as a fallback value to avoid exceeding cloudfront limits. 24 | */ 25 | const mapImageWidth = (requestedWidth: string): string => { 26 | const requestedWidthInt = parseInt(requestedWidth) 27 | 28 | for (const width of imageWidths) { 29 | if (width < requestedWidthInt) continue 30 | 31 | return width.toString() 32 | } 33 | 34 | return '1024' // avoid bigger widths, they'll hit cloudfront limits 35 | } 36 | 37 | /** 38 | * The function `redirectTo` is used to create a redirect response with a specified URL. 39 | * @param {string} url - The `url` parameter is a string that represents the URL to which you want to 40 | * redirect the user. 41 | * @param {any} callback - The `callback` parameter is a function that is used to return the response 42 | * to the caller. It takes two arguments: an error object (if any) and the response object. In this 43 | * case, the response object is an HTTP response with a status code of 302 (Redirect) and a ` 44 | * @returns a callback function with two arguments: null and an object representing a response. 45 | */ 46 | const redirectTo = (url: string, callback: any) => { 47 | const response = { 48 | status: 302, 49 | statusDescription: 'Redirect', 50 | headers: { 51 | location: [ 52 | { 53 | key: 'Location', 54 | value: url, 55 | }, 56 | ], 57 | 'cache-control': [ 58 | { 59 | key: 'Cache-Control', 60 | value: 'public, max-age=600, stale-while-revalidate=2592000', // Serve cached content up to 30 days old while revalidating it after 10 minutes 61 | }, 62 | ], 63 | }, 64 | } 65 | 66 | return callback(null, response) 67 | } 68 | 69 | /** 70 | * This TypeScript function handles image requests and redirects them to the appropriate image URL 71 | * based on the request parameters and headers. 72 | * @param {any} event - The `event` parameter is an object that contains information about the event 73 | * that triggered the Lambda function. In this case, it is expected to have a `Records` property, which 74 | * is an array of records. Each record represents a CloudFront event and contains information about the 75 | * request and configuration. 76 | * @param {any} _context - The `_context` parameter is a context object that contains information about 77 | * the execution environment and runtime. It is typically used to access information such as the AWS 78 | * Lambda function name, version, and memory limit. In this code snippet, the `_context` parameter is 79 | * not used, so it can be safely ignored 80 | * @param {any} callback - The `callback` parameter is a function that you can use to send a response 81 | * back to the caller. It takes two arguments: an error object (or null if there is no error) and a 82 | * response object. The response object should contain the necessary information to return a response 83 | * to the caller, such 84 | * @returns The code is returning a redirect response to a specified URL. 85 | */ 86 | export const handler = async (event: any, _context: any, callback: any) => { 87 | try { 88 | /* Extract the `request` and `config` properties. */ 89 | const { request, config } = event?.Records?.[0]?.cf 90 | 91 | /* This forms the base URL for the redirect. */ 92 | const baseUrl = '/_next/image' 93 | /* Parsing the query string from the request URL and converting it into an object. */ 94 | const query: Record = request?.querystring 95 | ?.split('&') 96 | .map((q: string) => q.split('=')) 97 | .reduce( 98 | (acc: Record, q: string) => ({ 99 | ...acc, 100 | [q[0]]: q[1], 101 | }), 102 | {} 103 | ) 104 | 105 | // Return original image if it's remote image 106 | if (/^(http|https)%3A%2F%2F/.test(query.url)) { 107 | /* The URL for the original image. */ 108 | const imageUrl = query.url.replace(/%3A/g, ':').replace(/%2F/g, '/') 109 | console.log({ imageUrl }) 110 | return redirectTo(imageUrl, callback) 111 | } 112 | 113 | // Return original image if it's static image 114 | if (/_next/.test(query.url)) { 115 | /* The URL for the original image. */ 116 | const imageUrl = 117 | 'https://' + 118 | config?.distributionDomainName + 119 | query.url.replace(/%2F/g, '/') 120 | return redirectTo(imageUrl, callback) 121 | } 122 | 123 | // Return original image if it's image/gif or image/svg+xml 124 | // OR is the feature is disabled 125 | const isFeatureEnabled = 126 | request?.origin?.custom?.customHeaders?.['enable-image-optimization']?.[0]?.value === 'true' 127 | const regex = /\.(gif|svg|xml)$/ 128 | if (regex.test(query.url) || !isFeatureEnabled) { 129 | /* The URL for the original image. */ 130 | const imageUrl = 131 | 'https://' + 132 | config?.distributionDomainName + 133 | '/assets' + 134 | query.url.replace(/%2F/g, '/') 135 | return redirectTo(imageUrl, callback) 136 | } 137 | 138 | /* Extract the value of the "accept" header from the request headers. */ 139 | const acceptHeader: string = request?.headers?.accept?.find( 140 | (item: Record) => item.key === 'accept' 141 | )?.value 142 | /* Create a list with accepted image types. */ 143 | const acceptedTypes = acceptHeader 144 | ?.split(',') 145 | ?.filter((type: string) => type.startsWith('image/')) 146 | 147 | /* Default value in case none of the accepted image types match the supported image types. */ 148 | let requestType = imageTypes[0] 149 | /* Find a prefered type that is accepted */ 150 | for (const type of imageTypes) { 151 | if (acceptedTypes.includes(type)) { 152 | requestType = type 153 | break 154 | } 155 | } 156 | 157 | const { w: requestedWidth, url } = query 158 | const width = mapImageWidth(requestedWidth) 159 | /* Creating a URL string for the redirect. */ 160 | const redirectToUrl = [ 161 | baseUrl, 162 | width, 163 | requestType.replace('image/', ''), 164 | url.replace('%2F', ''), 165 | ] 166 | .join('/') 167 | .replace(/%2F/g, '/') 168 | .replace('/assets', '') 169 | 170 | return redirectTo(redirectToUrl, callback) 171 | } catch (error) { 172 | console.error({ error }) 173 | 174 | return callback(null, { 175 | status: 403, // to not leak data 176 | }) 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /packages/ns-img-rdr/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES6", 4 | "module": "commonjs", 5 | "strict": true, 6 | "strictNullChecks": false, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./build", 11 | "rootDir": "./src" 12 | }, 13 | "include": ["src/**/*.ts"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /variables.tf: -------------------------------------------------------------------------------- 1 | variable "deployment_name" { 2 | description = "The name that will be used in the resources, must be unique. We recommend to use up to 20 characters" 3 | type = string 4 | } 5 | 6 | variable "region" { 7 | description = "The AWS region you wish to deploy your resources (ex. eu-central-1)" 8 | type = string 9 | } 10 | 11 | variable "base_dir" { 12 | description = "The base directory of the next.js app" 13 | type = string 14 | default = "./" 15 | } 16 | 17 | variable "cloudfront_acm_certificate_arn" { 18 | description = "The certificate ARN for the cloudfront_aliases (CloudFront works only with certs stored in us-east-1)" 19 | type = string 20 | default = null 21 | } 22 | 23 | # If you need a wildcard domain(ex: *.example.com), you can add it like this: 24 | # aliases = [var.custom_domain, "*.${var.custom_domain}"] 25 | variable "cloudfront_aliases" { 26 | description = "A list of custom domain for the cloudfront distribution, e.g. www.my-nextjs-app.com" 27 | type = list(string) 28 | default = [] 29 | } 30 | 31 | variable "cloudfront_price_class" { 32 | description = "Price class for the CloudFront distribution. Options: PriceClass_All, PriceClass_200, PriceClass_100" 33 | type = string 34 | default = "PriceClass_100" 35 | } 36 | 37 | # Example: 38 | # next_lambda_env_vars = { 39 | # BACKEND_VIRTUAL_DOMAIN = "backend.example.com" 40 | # NEXT_PUBLIC_RECAPTCHA_KEY = "recaptcha-key" 41 | # } 42 | variable "next_lambda_env_vars" { 43 | description = "Map of environment variables that you want to pass to the lambda" 44 | type = map(any) 45 | default = {} 46 | } 47 | 48 | variable "custom_image_types" { 49 | description = "List of image file extentions that you store in the public/ directory. Defaults to ('webp', 'jpeg', 'jpg', 'png', 'gif', 'ico', 'svg')" 50 | type = list(string) 51 | default = ["webp", "jpeg", "jpg", "png", "gif", "ico", "svg"] 52 | } 53 | 54 | variable "next_lambda_policy_statements" { 55 | description = "Map of dynamic policy statements to attach to Lambda Function role" 56 | type = map(any) 57 | default = {} 58 | } 59 | 60 | variable "next_lambda_memory_size" { 61 | description = "The memory size for the server side rendering Lambda (Set memory to between 128 MB and 10240 MB)" 62 | type = number 63 | default = 4096 64 | } 65 | 66 | variable "next_lambda_runtime" { 67 | description = "The runtime for the next lambda (nodejs16.x or nodejs20.x)" 68 | type = string 69 | default = "nodejs20.x" 70 | } 71 | 72 | variable "next_lambda_logs_retention" { 73 | description = "The number of days that cloudwatch logs of next lambda should be retained (Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653)" 74 | type = number 75 | default = 30 76 | } 77 | 78 | variable "next_lambda_ephemeral_storage_size" { 79 | description = "Amount of ephemeral storage (/tmp) in MB the next lambda can use at runtime. Valid value between 512 MB to 10240 MB" 80 | type = number 81 | default = 512 82 | } 83 | 84 | variable "api_gateway_log_format" { 85 | description = "Default stage's single line format of the access logs of data, as specified by selected $context variables" 86 | type = string 87 | default = "sourceIp: $context.identity.sourceIp, $context.domainName $context.requestTime \"$context.httpMethod $context.path $context.routeKey $context.protocol\" path: $context.customDomain.basePathMatched resp_status: $context.status integrationLatency: $context.integrationLatency responseLatency: $context.responseLatency requestId: $context.requestId Error: $context.integrationErrorMessage rawRequestPayloadSize: $input.body.size() rawRequestPayload: $input.body" # https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-logging-variables.html 88 | } 89 | 90 | variable "enable_image_optimization" { 91 | description = "Boolean to disable image optimization feature" 92 | type = bool 93 | default = true 94 | } 95 | 96 | variable "image_optimization_runtime" { 97 | description = "The runtime for the image optimization Lambdas (nodejs16.x or nodejs20.x)" 98 | type = string 99 | default = "nodejs20.x" 100 | } 101 | 102 | variable "image_optimization_lambda_memory_size" { 103 | description = "The memory size for the image optimization Lambda (Set memory to between 128 MB and 10240 MB)" 104 | type = number 105 | default = 2048 106 | } 107 | 108 | variable "image_optimization_logs_retention" { 109 | description = "The number of days that cloudwatch logs of image optimization lambdas should be retained (Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, and 3653)" 110 | type = number 111 | default = 30 112 | } 113 | 114 | variable "image_optimization_ephemeral_storage_size" { 115 | description = "Amount of ephemeral storage (/tmp) in MB the image optimization lambdas can use at runtime. Valid value between 512 MB to 10240 MB" 116 | type = number 117 | default = 512 118 | } 119 | 120 | variable "cloudfront_cached_paths" { 121 | description = "An object containing a list of paths to cache and min, default and max TTL values" 122 | type = object({ 123 | paths = list(string) 124 | min_ttl = number 125 | default_ttl = number 126 | max_ttl = number 127 | }) 128 | default = { 129 | paths = [] 130 | min_ttl = 0 131 | default_ttl = 0 132 | max_ttl = 0 133 | } 134 | } 135 | 136 | variable "custom_cache_policy_id" { 137 | description = "The ID of CloudFront cache policy" 138 | type = string 139 | default = null 140 | } 141 | 142 | variable "cloudfront_cache_default_ttl" { 143 | description = "Default TTL in seconds for ordered cache behaviors" 144 | type = number 145 | default = 86400 146 | } 147 | 148 | variable "cloudfront_cache_max_ttl" { 149 | description = "Maximum TTL in seconds for ordered cache behaviors" 150 | type = number 151 | default = 31536000 152 | } 153 | 154 | variable "cloudfront_cache_min_ttl" { 155 | description = "Minimum TTL in seconds for ordered cache behaviors" 156 | type = number 157 | default = 1 158 | } 159 | 160 | variable "cloudfront_function_associations" { 161 | description = "List of CloudFront functions, to associate them with the defaulf distribution" 162 | type = list(object({ 163 | event_type = string 164 | function_arn = string 165 | })) 166 | default = [] 167 | } 168 | 169 | variable "create_cloudfront_invalidation" { 170 | description = "Boolean to disable the trigger for cloudfront invalidation after every deployment" 171 | type = bool 172 | default = true 173 | } 174 | 175 | variable "wait_for_distribution_deployment" { 176 | description = "If enabled, the resource will wait for the distribution status to change from `InProgress` to `Deployed`" 177 | type = bool 178 | default = true 179 | } 180 | 181 | variable "use_default_server_side_props_handler" { 182 | description = "Boolean to enabled usage of the default server side props handler, instead of the our custom one" 183 | type = bool 184 | default = false 185 | } 186 | 187 | variable "show_debug_logs" { 188 | description = "Boolean to enabled debug logs" 189 | type = bool 190 | default = false 191 | } 192 | 193 | variable "pre_resize_images" { 194 | description = "Boolean to enabled the resizing of public images, after each deployment. Enabling this might increase the AWS bill" 195 | type = bool 196 | default = false 197 | } 198 | 199 | variable "delete_resized_versions" { 200 | description = "Boolean to disable the trigger for deleting old resized versions of public images" 201 | type = bool 202 | default = true 203 | } 204 | -------------------------------------------------------------------------------- /versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = ">= 1.6.3" 3 | 4 | required_providers { 5 | aws = { 6 | source = "hashicorp/aws" 7 | version = "~> 5.0" 8 | configuration_aliases = [aws.global_region] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /visuals/cache.drawio: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | -------------------------------------------------------------------------------- /visuals/cache.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/visuals/cache.webp -------------------------------------------------------------------------------- /visuals/distribution.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/visuals/distribution.webp -------------------------------------------------------------------------------- /visuals/module.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emyriounis/terraform-aws-nextjs-serverless/f2a298c8509861e9976752cc9a52401a808075fc/visuals/module.webp --------------------------------------------------------------------------------