├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── extensions.json ├── CHANGELOG.md ├── README.md ├── e2e ├── s3-e2e │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── project.json │ ├── tests │ │ └── s3.test.ts │ ├── tsconfig.json │ └── tsconfig.spec.json └── sam-e2e │ ├── CHANGELOG.md │ ├── jest.config.js │ ├── project.json │ ├── tests │ └── sam.test.ts │ ├── tsconfig.json │ └── tsconfig.spec.json ├── jest.config.ts ├── jest.preset.js ├── migrations.json ├── nx.json ├── package.json ├── packages ├── core │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── index.ts │ │ └── lib │ │ │ ├── ImportStackOutput.ts │ │ │ ├── ImportStackOutputs.ts │ │ │ ├── OutputValueRetriever.ts │ │ │ ├── formatStackName.ts │ │ │ ├── getValidatedOptions.ts │ │ │ └── importDotenv.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── s3 │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── builders.json │ ├── collection.json │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ │ ├── builders │ │ │ └── deploy │ │ │ │ ├── deploy.ts │ │ │ │ └── schema.json │ │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json └── sam │ ├── .eslintrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── builders.json │ ├── collection.json │ ├── jest.config.ts │ ├── package.json │ ├── project.json │ ├── src │ ├── builders │ │ ├── build │ │ │ ├── build.ts │ │ │ ├── compat.ts │ │ │ ├── config.ts │ │ │ ├── get-entries-from-cloudformation.ts │ │ │ ├── installNpmModules.ts │ │ │ ├── schema.json │ │ │ └── source-map-install.ts │ │ ├── cloudformation │ │ │ ├── deploy │ │ │ │ ├── CloudFormationDeployOptions.ts │ │ │ │ ├── IParameterOverrides.ts │ │ │ │ ├── deploy.ts │ │ │ │ └── schema.json │ │ │ ├── get-final-template-location.ts │ │ │ ├── package │ │ │ │ ├── isContentfulString.ts │ │ │ │ ├── mapRelativePathsToAbsolute.ts │ │ │ │ ├── package.ts │ │ │ │ ├── replaceProjectReferenceWithPath.ts │ │ │ │ ├── schema.json │ │ │ │ └── updateCloudFormationTemplate.ts │ │ │ └── run-cloudformation-command.ts │ │ └── execute │ │ │ ├── execute.ts │ │ │ ├── options.ts │ │ │ ├── run-sam.ts │ │ │ └── schema.json │ ├── executors │ │ └── layer │ │ │ ├── executor.ts │ │ │ └── schema.json │ ├── index.ts │ ├── lambda │ │ ├── index.ts │ │ ├── lambda.ts │ │ └── parseEnvironmentVariables.ts │ ├── schematics │ │ ├── application │ │ │ ├── application.spec.ts │ │ │ ├── application.ts │ │ │ ├── files │ │ │ │ ├── app │ │ │ │ │ └── hello │ │ │ │ │ │ └── hello.ts__tmpl__ │ │ │ │ └── template.yaml__tmpl__ │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ ├── init │ │ │ ├── init.spec.ts │ │ │ ├── init.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ └── update-lambdas │ │ │ ├── getUpdates.spec.ts │ │ │ ├── getUpdates.ts │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ ├── updateLambdas.spec.ts │ │ │ └── updateLambdas.ts │ └── utils │ │ ├── SamFunctionProperties.ts │ │ ├── dumpCloudformationTemplate.ts │ │ ├── getLambdaSourcePath.ts │ │ ├── getParameterOverrides.ts │ │ ├── getTag.ts │ │ ├── guards │ │ ├── getOwnStringProperties.ts │ │ ├── getOwnStringProperty.ts │ │ ├── hasOwnProperties.ts │ │ ├── hasOwnProperty.ts │ │ ├── hasOwnStringProperties.ts │ │ └── index.ts │ │ ├── load-cloud-formation-template.ts │ │ ├── loadEnvFromStack.ts │ │ ├── loadEnvironmentVariablesForStackLambdas.ts │ │ └── testing.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── project.json ├── tools ├── generators │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.base.json ├── tsconfig.node.json ├── wallaby.js ├── workspace.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 4 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nrwl/nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "parserOptions": { 9 | "project": ["./tsconfig.*?.json"] 10 | }, 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 15 | "rules": { 16 | "@nrwl/nx/enforce-module-boundaries": [ 17 | "error", 18 | { 19 | "enforceBuildableLibDependency": true, 20 | "allow": [], 21 | "depConstraints": [ 22 | { 23 | "sourceTag": "*", 24 | "onlyDependOnLibsWithTags": ["*"] 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | }, 31 | { 32 | "files": ["*.ts", "*.tsx"], 33 | "extends": ["plugin:@nrwl/nx/typescript"], 34 | "rules": {} 35 | }, 36 | { 37 | "files": ["*.js", "*.jsx"], 38 | "extends": ["plugin:@nrwl/nx/javascript"], 39 | "rules": {} 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [14.x] 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - name: Use Node.js ${{ matrix.node-version }} 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: ${{ matrix.node-version }} 19 | - name: yarn install, build, and test 20 | run: | 21 | yarn 22 | yarn build 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | serverless-output.yaml 8 | *.tgz 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events.json 15 | speed-measure-plugin.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | .env 44 | 45 | # System Files 46 | .DS_Store 47 | Thumbs.db 48 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict = true 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "printWidth": 80 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "ms-vscode.vscode-typescript-tslint-plugin", 5 | "esbenp.prettier-vscode", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.15.2](https://github.com/studds/nx-aws/compare/v0.15.1...v0.15.2) (2023-01-18) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * changing path for nx utils under v15 ([6fb21da](https://github.com/studds/nx-aws/commit/6fb21dac88c3e6b9eecdfab72c042fab9858a3ad)) 11 | 12 | 13 | 14 | ## [0.15.1](https://github.com/studds/nx-aws/compare/v0.15.0...v0.15.1) (2022-11-08) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * correcting dependencies ([3d0f543](https://github.com/studds/nx-aws/commit/3d0f5432401d6a4986987e5a7ca50e0371aaa5e0)) 20 | 21 | 22 | 23 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * restore generate after v14 upgrade ([06d99a9](https://github.com/studds/nx-aws/commit/06d99a9b36b0201b80ddf75e8742357b963779ef)) 29 | * restore installNpmModules ([95f2b10](https://github.com/studds/nx-aws/commit/95f2b10b412f09ba7a2667f2ffb02a92b7aef70e)) 30 | 31 | 32 | ### Features 33 | 34 | * adding experimental layer executor ([d44b39c](https://github.com/studds/nx-aws/commit/d44b39cd79da9d3341fc3fe93f0ecbe56452f28e)) 35 | * **sam:** webpack build: remove non-external deps from generated package.json ([5e8f653](https://github.com/studds/nx-aws/commit/5e8f65362db099f7be77170af26fe9fcb822684a)) 36 | 37 | 38 | 39 | ## [0.14.1](https://github.com/studds/nx-aws/compare/v0.14.0...v0.14.1) (2022-11-02) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * resolve webpack config path ([f940a78](https://github.com/studds/nx-aws/commit/f940a78cee2d9c50ad1bf1f4e4dd6a0a8052fb03)) 45 | 46 | 47 | 48 | # [0.14.0](https://github.com/studds/nx-aws/compare/v0.13.0...v0.14.0) (2022-10-31) 49 | 50 | 51 | 52 | # [0.13.0](https://github.com/studds/nx-aws/compare/v0.12.2...v0.13.0) (2021-08-22) 53 | 54 | ### Features 55 | 56 | - update to nx 12.1.1 ([074eb6a](https://github.com/studds/nx-aws/commit/074eb6a3c0b8e232c34f1355047a8e800124a331)) 57 | - updating to nx 12.7 ([d165230](https://github.com/studds/nx-aws/commit/d165230b2538c422c4834fe686fb49f9f98929d6)) 58 | 59 | ## [0.12.2](https://github.com/studds/nx-aws/compare/v0.12.1...v0.12.2) (2021-04-24) 60 | 61 | ### Bug Fixes 62 | 63 | - importStackOutputs must derive stackname from target config ([56b4fb1](https://github.com/studds/nx-aws/commit/56b4fb1e4115779ae9ba6756c9550d7ff4f57d32)) 64 | 65 | ## [0.12.1](https://github.com/studds/nx-aws/compare/v0.12.0...v0.12.1) (2021-04-07) 66 | 67 | ### Bug Fixes 68 | 69 | - **sam:** Use a single build with multiple entries by default [#69](https://github.com/studds/nx-aws/issues/69) ([c39b777](https://github.com/studds/nx-aws/commit/c39b7775e04868a42318c74b5980e9e1bd5e59d4)) 70 | 71 | # [0.12.0](https://github.com/studds/nx-aws/compare/v0.11.1...v0.12.0) (2021-03-17) 72 | 73 | ### Bug Fixes 74 | 75 | - **sam:** fix [#66](https://github.com/studds/nx-aws/issues/66) generated WebBucketPolicy ([4775f04](https://github.com/studds/nx-aws/commit/4775f04ddc372cd3cb46d4043d511a7cbc46f458)) 76 | - **sam:** fix [#67](https://github.com/studds/nx-aws/issues/67) s3 origin regional domain names ([a9f2646](https://github.com/studds/nx-aws/commit/a9f26469693f1a02e0974af15be8053c7da89509)) 77 | - **sam:** handle spaces in parameter overrides closes [#63](https://github.com/studds/nx-aws/issues/63) ([45a5d35](https://github.com/studds/nx-aws/commit/45a5d3556755e0b61e9639a0744260f3b2f8a486)) 78 | 79 | ### Features 80 | 81 | - **s3:** add buildOutputPath and buildTarget options to s3 deploy closes [#70](https://github.com/studds/nx-aws/issues/70) ([453b4a4](https://github.com/studds/nx-aws/commit/453b4a497be037618708dc51d533f00837be3fd4)) 82 | 83 | ## [0.11.1](https://github.com/studds/nx-aws/compare/v0.11.0...v0.11.1) (2021-03-03) 84 | 85 | ### Bug Fixes 86 | 87 | - **sam:** correct required properties in build schema ([8aa684a](https://github.com/studds/nx-aws/commit/8aa684a5e154d5eb5198bfa79f8c90e165845e52)) 88 | 89 | # [0.11.0](https://github.com/studds/nx-aws/compare/v0.10.0...v0.11.0) (2021-03-01) 90 | 91 | ### Features 92 | 93 | - **sam/build:** install npm modules if generatePackageJson is on ([a93e230](https://github.com/studds/nx-aws/commit/a93e23066e7c1fae58ad840565cf727b58ee8647)) 94 | - copy & paste the latest @nrwl/node:build ([773e5ce](https://github.com/studds/nx-aws/commit/773e5ce1085c25d64b6fb62b8ad2a40dc40a59a9)) 95 | 96 | # [0.10.0](https://github.com/studds/nx-aws/compare/v0.9.1...v0.10.0) (2021-02-24) 97 | 98 | ### Bug Fixes 99 | 100 | - get out of the way of how sam-cli handles layers [#55](https://github.com/studds/nx-aws/issues/55) ([6632316](https://github.com/studds/nx-aws/commit/6632316ad0283b5aeffa80912b083e0d3b66ef24)) 101 | 102 | ### Features 103 | 104 | - **sam:** add support for parameter overrides to execute ([7d9bfbf](https://github.com/studds/nx-aws/commit/7d9bfbf7441b48b26441589e7038e25fb71c7274)) 105 | 106 | ## [0.9.1](https://github.com/studds/nx-aws/compare/v0.9.0...v0.9.1) (2021-02-24) 107 | 108 | ### Bug Fixes 109 | 110 | - reverting to simple handler ([48d3625](https://github.com/studds/nx-aws/commit/48d36251988053fe9982f0fad08d3883ffcf80f8)) 111 | - updating node builder for latest nx ([defdcbc](https://github.com/studds/nx-aws/commit/defdcbcb3b02b4f4a9995de2094f8dfae0b9ed45)) 112 | 113 | # [0.9.0](https://github.com/studds/nx-aws/compare/v0.8.3...v0.9.0) (2021-02-14) 114 | 115 | ### Features 116 | 117 | - enabling tree shaking ([f9a7b60](https://github.com/studds/nx-aws/commit/f9a7b605e78425f1a1c7b9dbc017fbfdc56fd6d2)) 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # @nx-aws/sam 2 | 3 | [Angular CLI](https://cli.angular.io) Builders for [AWS SAM](https://aws.amazon.com/serverless/sam/) projects, 4 | designed for use alongside [nx](https://nx.dev) 5 | 6 | ## Why 7 | 8 | nx superpowers the angular CLI, to add support for a range of backend project types. 9 | 10 | However, what if your backend uses SAM? 11 | 12 | This project includes builders for that! 13 | 14 | - @nx-aws/sam:build - builds your functions 15 | - @nx-aws/sam:package - packages your SAM (ie. CloudFormation) template ready for deployment 16 | (including resolving AWS::Serverless::Application references to other apps in your monorepo) 17 | - @nx-aws/sam:deploy - deploys your CloudFormation template 18 | 19 | ## Get started 20 | 21 | **NB: nx-aws 0.10.0 and higher require @nrwl/nx v11 and @angular-devkit/core v11.** 22 | 23 | 1. Open your existing workspace or run `npx create-nx-workspace` to create a new workspace 24 | 1. `npm install @nx-aws/sam` or `yarn add @nx-aws/sam` 25 | 1. `nx g @nx-aws/sam:app api [--frontendProject sample]` 26 | 1. Create a bucket in AWS to store deploy artifacts (via the [console](https://console.aws.amazon.com) or [AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-bucket.html) using `aws s3api create-bucket --bucket ${my-nx-deploy-artifacts} --region us-east-1`) 27 | 1. Update your `workspace.json` or `angular.json` to include the key `s3Bucket` under both the `package` and `deploy` targets (see details below). 28 | 29 | ## @nx-aws/sam:build 30 | 31 | Add the following to your `angular.json` 32 | 33 | ```json 34 | { 35 | "api": { 36 | "root": "apps/api", 37 | "sourceRoot": "apps/api/src", 38 | "projectType": "application", 39 | "prefix": "api", 40 | "schematics": {}, 41 | "architect": { 42 | "build": { 43 | "builder": "@nx-aws/sam:build", 44 | "options": { 45 | "outputPath": "dist/apps/api", 46 | "template": "apps/api/template.yaml", 47 | "tsConfig": "apps/api/tsconfig.app.json" 48 | }, 49 | ... 50 | } 51 | } 52 | } 53 | } 54 | ``` 55 | 56 | The builder will search through your CloudFormation template at `apps/api/template.yaml` 57 | and find any `AWS::Serverless::Function` and trigger appropriate builds. 58 | 59 | (All the other options are the same as for nrwl's node builder.) 60 | 61 | ### Functions 62 | 63 | Given this code in your `template.yaml`: 64 | 65 | ```yaml 66 | Resources: 67 | MyFunction: 68 | Type: 'AWS::Serverless::Function' 69 | Properties: 70 | # CodeUri should be the directory, relative to template.yaml, where the handler file is found 71 | CodeUri: src/my-function 72 | # This is the name of the handler file and then the name of the exported handler function 73 | # (standard SAM approach) 74 | Handler: handler-file.handlerFn 75 | ``` 76 | 77 | The builder will run a webpack build for `src/my-function/handler-file`. 78 | 79 | ### Lambda Layers 80 | 81 | #### Building lambda layers - experimental 82 | 83 | There's an experimental builder added for lambda layers, `@nx-aws/sam:layer`. It wraps the [tsc executor](https://nx.dev/packages/js/executors/tsc#@nrwl/js:tsc), 84 | with all the same options. 85 | 86 | After the tsc executor has run, it creates a package.json file, runs `npm install` with appropriate flags for AWS Lambda, and then zips the result. You can then deploy this 87 | using the `package` and `deploy` executors. 88 | 89 | This is the resource you'll need in your `template.yaml`: 90 | 91 | ```yaml 92 | AirmailLayer: 93 | Type: AWS::Serverless::LayerVersion 94 | Description: Airmail layer 95 | Properties: 96 | ContentUri: ./nodejs.zip 97 | CompatibleRuntimes: 98 | - nodejs14.x 99 | - nodejs16.x 100 | ``` 101 | 102 | **Note:** At the moment, the ContentUri ./nodejs.zip is essentially hard-coded. The assumption is, essentially, that the layer is the only resource in this template. 103 | 104 | My preferred way to use the layer is via the `importStackOutputs` on another stack. 105 | 106 | #### Using lambda layers in functions 107 | 108 | Lambda layers defined in your template should Just Work - however, the way sam-cli treats layers is broken, 109 | so they won't: https://github.com/aws/aws-sam-cli/issues/2222. 110 | 111 | That said, if you've got a layer defined like this: 112 | 113 | ``` 114 | TestLayer: 115 | Type: AWS::Serverless::LayerVersion 116 | Description: Test layer 117 | Properties: 118 | ContentUri: ./src/test-layer 119 | CompatibleRuntimes: 120 | - nodejs10.x 121 | - nodejs12.x 122 | Metadata: 123 | BuildMethod: nodejs12.x 124 | ``` 125 | 126 | Then during `serve` or `build` nx-aws will simply map the `ContentUri` to an absolute path. Assuming you've got a 127 | layer at that location that `sam-cli` is happy with, then you're good to go. 128 | 129 | ## @nx-aws/sam:package 130 | 131 | Add the following to your `angular.json`: 132 | 133 | ```json 134 | { 135 | "api": { 136 | "root": "apps/api", 137 | "sourceRoot": "apps/api/src", 138 | "projectType": "application", 139 | "prefix": "api", 140 | "schematics": {}, 141 | "architect": { 142 | "package": { 143 | "builder": "@nx-aws/sam:package", 144 | "options": { 145 | "templateFile": "apps/api/template.yaml", 146 | "outputTemplateFile": "dist/apps/api/serverless-output.yaml", 147 | "s3Prefix": "api", 148 | "s3Bucket": "my-artefacts-bucket" 149 | }, 150 | "configurations": { 151 | "production": {} 152 | } 153 | } 154 | } 155 | } 156 | } 157 | ``` 158 | 159 | **NB: sam:package requires an S3 bucket to store deploy artefacts - you need to create a bucket and add the `s3Bucket` option to your project configuration** 160 | 161 | For the most part, this simply wraps the `aws cloudformation package` command, but it will also 162 | rewrite the `Location` property of `AWS::Serverless::Application` resources, if they refer to 163 | another project. 164 | 165 | ### AWS::Serverless::Application 166 | 167 | The package builder will attempt to resolve a reference to another CloudFormation stack, defined 168 | in a _different_ project in `angular.json`. 169 | 170 | If the package builder finds an `AWS::Serverless::Application` in `template.yaml`, eg: 171 | 172 | ```yaml 173 | Resources: 174 | MySubStack: 175 | Type: AWS::Serverless::Application 176 | Properties: 177 | Location: my-sub-stack 178 | ``` 179 | 180 | it will attempt to: 181 | 182 | 1. Find an project in `angular.json` that matches the `Location` property, ie. `my-sub-stack`. 183 | 2. If it finds such a project, it will look for the `package` target. 184 | 3. If it finds the `package` target, it will replace `my-sub-stack` with the absolute path to 185 | the `outputTemplateFile` from that target. 186 | 187 | ## deploy 188 | 189 | Add the following to `angular.json`: 190 | 191 | ```json 192 | { 193 | ... 194 | "api": { 195 | "root": "apps/api", 196 | "sourceRoot": "apps/api/src", 197 | "projectType": "application", 198 | "prefix": "api", 199 | "schematics": {}, 200 | "architect": { 201 | ... 202 | "deploy": { 203 | "builder": "@nx-aws/sam:deploy", 204 | "options": { 205 | "templateFile": "dist/apps/api/serverless-output.yaml", 206 | "s3Prefix": "api", 207 | "capabilities": ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"], 208 | "s3Bucket": "my-artefacts-bucket", 209 | "stackNameFormat": "api-$ENVIRONMENT" 210 | }, 211 | "configurations": { 212 | "production": {} 213 | } 214 | } 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | **NB: sam:deploy requires an S3 bucket to store deploy artefacts - you need to create a bucket and add the `s3Bucket` option to your project configuration - this must be the same as used for sam:package** 221 | 222 | This wraps the `aws cloudformation deploy` command. The one nice thing it does is pull 223 | any parameters defined in your `template.yaml` from environment variables, and pass them 224 | in as parameter overrides. For example, if you have in your `template.yaml`: 225 | 226 | ```yaml 227 | Parameters: 228 | MyParameter: 229 | Type: String 230 | Description: An example parameter 231 | ``` 232 | 233 | The the deploy builder will look for an environment variable MY_PARAMETER and pass it in as 234 | a parameter overrides. 235 | 236 | ## faq 237 | 238 | ### How do I resolve the error "Required property 's3Bucket' is missing"? 239 | 240 | The SAM package and deploy steps require an s3 bucket to store and retrieve deployment artefacts. 241 | 242 | You need to create and s3 bucket to store your deployment artefacts and then include that bucket in your project configuration. 243 | 244 | 1. Create a bucket in AWS to store deploy artifacts (via the [console](https://console.aws.amazon.com) or [AWS CLI](https://docs.aws.amazon.com/cli/latest/reference/s3api/create-bucket.html) using `aws s3api create-bucket --bucket ${my-nx-deploy-artifacts} --region us-east-1`) 245 | 1. Update your `workspace.json` or `angular.json` to include the key `s3Bucket` under both the `package` and `deploy` targets (see details above for the package and deploy steps). 246 | 247 | ## contributing 248 | 249 | PRs and contributions are very very welcome! 250 | 251 | ### Building & testing locally 252 | 253 | To build, run `yarn build`. 254 | 255 | `yarn link` doesn't work to test locally, due to the way npm resolves dependencies. The best 256 | workflow I've found is to copy across the files as the change. There's a script to do 257 | this: `yarn pack:copy --projectPath ../test-nx-aws/` - just change `../test-nx-aws` to your 258 | local test project. 259 | -------------------------------------------------------------------------------- /e2e/s3-e2e/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.15.2](https://github.com/studds/nx-aws/compare/v0.15.1...v0.15.2) (2023-01-18) 6 | 7 | 8 | 9 | ## [0.15.1](https://github.com/studds/nx-aws/compare/v0.15.0...v0.15.1) (2022-11-08) 10 | 11 | 12 | 13 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 14 | 15 | 16 | 17 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 18 | 19 | 20 | 21 | ## [0.14.1](https://github.com/studds/nx-aws/compare/v0.14.0...v0.14.1) (2022-11-02) 22 | 23 | 24 | 25 | # [0.14.0](https://github.com/studds/nx-aws/compare/v0.13.0...v0.14.0) (2022-10-31) 26 | 27 | 28 | 29 | # [0.13.0](https://github.com/studds/nx-aws/compare/v0.12.2...v0.13.0) (2021-08-22) 30 | 31 | ### Features 32 | 33 | - update to nx 12.1.1 ([074eb6a](https://github.com/studds/nx-aws/commit/074eb6a3c0b8e232c34f1355047a8e800124a331)) 34 | 35 | ## [0.12.2](https://github.com/studds/nx-aws/compare/v0.12.1...v0.12.2) (2021-04-24) 36 | 37 | ## [0.12.1](https://github.com/studds/nx-aws/compare/v0.12.0...v0.12.1) (2021-04-07) 38 | 39 | # [0.12.0](https://github.com/studds/nx-aws/compare/v0.11.1...v0.12.0) (2021-03-17) 40 | 41 | ## [0.11.1](https://github.com/studds/nx-aws/compare/v0.11.0...v0.11.1) (2021-03-03) 42 | 43 | # [0.11.0](https://github.com/studds/nx-aws/compare/v0.10.0...v0.11.0) (2021-03-01) 44 | 45 | # [0.10.0](https://github.com/studds/nx-aws/compare/v0.9.1...v0.10.0) (2021-02-24) 46 | 47 | ## [0.9.1](https://github.com/studds/nx-aws/compare/v0.9.0...v0.9.1) (2021-02-24) 48 | 49 | # [0.9.0](https://github.com/studds/nx-aws/compare/v0.8.3...v0.9.0) (2021-02-14) 50 | -------------------------------------------------------------------------------- /e2e/s3-e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 's3-e2e', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 13 | coverageDirectory: '../../coverage/e2e/s3-e2e', 14 | }; 15 | -------------------------------------------------------------------------------- /e2e/s3-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "e2e/s3-e2e/src", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nrwl/nx-plugin:e2e", 9 | "options": { 10 | "target": "s3:build", 11 | "npmPackageName": "@nx-aws/s3", 12 | "pluginOutputPath": "dist/packages/s3", 13 | "jestConfig": "e2e/s3-e2e/jest.config.js" 14 | } 15 | } 16 | }, 17 | "tags": [], 18 | "implicitDependencies": ["s3"] 19 | } 20 | -------------------------------------------------------------------------------- /e2e/s3-e2e/tests/s3.test.ts: -------------------------------------------------------------------------------- 1 | import { ensureNxProject } from '@nrwl/nx-plugin/testing'; 2 | describe('s3 e2e', () => { 3 | it('should create s3', async (done) => { 4 | ensureNxProject('@nx-aws/s3', 'dist/packages/s3'); 5 | 6 | // no op at the moment.... 7 | 8 | done(); 9 | }); 10 | }); 11 | -------------------------------------------------------------------------------- /e2e/s3-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.e2e.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /e2e/s3-e2e/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /e2e/sam-e2e/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.15.2](https://github.com/studds/nx-aws/compare/v0.15.1...v0.15.2) (2023-01-18) 6 | 7 | 8 | 9 | ## [0.15.1](https://github.com/studds/nx-aws/compare/v0.15.0...v0.15.1) (2022-11-08) 10 | 11 | 12 | 13 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 14 | 15 | 16 | 17 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 18 | 19 | 20 | 21 | ## [0.14.1](https://github.com/studds/nx-aws/compare/v0.14.0...v0.14.1) (2022-11-02) 22 | 23 | 24 | 25 | # [0.14.0](https://github.com/studds/nx-aws/compare/v0.13.0...v0.14.0) (2022-10-31) 26 | 27 | 28 | 29 | # [0.13.0](https://github.com/studds/nx-aws/compare/v0.12.2...v0.13.0) (2021-08-22) 30 | 31 | ### Features 32 | 33 | - update to nx 12.1.1 ([074eb6a](https://github.com/studds/nx-aws/commit/074eb6a3c0b8e232c34f1355047a8e800124a331)) 34 | 35 | ## [0.12.2](https://github.com/studds/nx-aws/compare/v0.12.1...v0.12.2) (2021-04-24) 36 | 37 | ## [0.12.1](https://github.com/studds/nx-aws/compare/v0.12.0...v0.12.1) (2021-04-07) 38 | 39 | # [0.12.0](https://github.com/studds/nx-aws/compare/v0.11.1...v0.12.0) (2021-03-17) 40 | 41 | ## [0.11.1](https://github.com/studds/nx-aws/compare/v0.11.0...v0.11.1) (2021-03-03) 42 | 43 | # [0.11.0](https://github.com/studds/nx-aws/compare/v0.10.0...v0.11.0) (2021-03-01) 44 | 45 | # [0.10.0](https://github.com/studds/nx-aws/compare/v0.9.1...v0.10.0) (2021-02-24) 46 | 47 | ## [0.9.1](https://github.com/studds/nx-aws/compare/v0.9.0...v0.9.1) (2021-02-24) 48 | 49 | # [0.9.0](https://github.com/studds/nx-aws/compare/v0.8.3...v0.9.0) (2021-02-14) 50 | -------------------------------------------------------------------------------- /e2e/sam-e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'sam-e2e', 3 | preset: '../../jest.preset.js', 4 | globals: { 5 | 'ts-jest': { 6 | tsconfig: '/tsconfig.spec.json', 7 | }, 8 | }, 9 | transform: { 10 | '^.+\\.[tj]sx?$': 'ts-jest', 11 | }, 12 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 13 | coverageDirectory: '../../coverage/e2e/sam-e2e', 14 | testTimeout: 900000, 15 | }; 16 | -------------------------------------------------------------------------------- /e2e/sam-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sam-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "e2e/sam-e2e/src", 6 | "targets": { 7 | "e2e": { 8 | "executor": "@nrwl/nx-plugin:e2e", 9 | "options": { 10 | "target": "sam:build", 11 | "npmPackageName": "@nx-aws/sam", 12 | "pluginOutputPath": "dist/packages/sam", 13 | "jestConfig": "e2e/sam-e2e/jest.config.js" 14 | } 15 | } 16 | }, 17 | "tags": [], 18 | "implicitDependencies": ["sam"] 19 | } 20 | -------------------------------------------------------------------------------- /e2e/sam-e2e/tests/sam.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | // checkFilesExist, 3 | ensureNxProject, 4 | // readJson, 5 | runNxCommandAsync, 6 | uniq, 7 | } from '@nrwl/nx-plugin/testing'; 8 | describe('sam e2e', () => { 9 | it('should create sam', async (done) => { 10 | const plugin = uniq('sam'); 11 | ensureNxProject('@nx-aws/sam', 'dist/packages/sam'); 12 | await runNxCommandAsync(`generate @nx-aws/sam:app ${plugin}`); 13 | 14 | const result = await runNxCommandAsync(`build ${plugin}`); 15 | expect(result.stdout).toContain('chunk {app/hello/hello}'); 16 | 17 | done(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /e2e/sam-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.spec.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /e2e/sam-e2e/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nrwl/jest'); 2 | 3 | export default { 4 | projects: [ 5 | ...getJestProjects(), 6 | '/e2e/sam-e2e', 7 | '/e2e/core-e2e', 8 | '/e2e/s3-e2e', 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nrwl/jest/preset').default; 2 | module.exports = { 3 | ...nxPreset, 4 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], 5 | transform: { 6 | '^.+\\.(ts|js|html)$': 'ts-jest', 7 | }, 8 | resolver: '@nrwl/jest/plugins/resolver', 9 | moduleFileExtensions: ['ts', 'js', 'html'], 10 | coverageReporters: ['html'], 11 | }; 12 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "cli": "nx", 5 | "version": "15.0.12-beta.1", 6 | "description": "Set project names in project.json files", 7 | "implementation": "./src/migrations/update-15-1-0/set-project-names", 8 | "package": "nx", 9 | "name": "15.1.0-set-project-names" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "nx-aws", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "tasksRunnerOptions": { 7 | "default": { 8 | "runner": "@nrwl/nx-cloud", 9 | "options": { 10 | "cacheableOperations": ["build", "lint", "test", "e2e"], 11 | "parallel": 3, 12 | "accessToken": "YTJhMWY1ODQtNjZkYi00ODY3LWJkZDYtMWFlZGQwNzUxNmY2fHJlYWQtd3JpdGU=" 13 | } 14 | } 15 | }, 16 | "workspaceLayout": { 17 | "appsDir": "e2e", 18 | "libsDir": "packages" 19 | }, 20 | "targetDefaults": { 21 | "build": { 22 | "dependsOn": ["^build"], 23 | "inputs": ["production", "^production"] 24 | }, 25 | "publish": { 26 | "dependsOn": ["build"], 27 | "inputs": ["production", "^production"] 28 | } 29 | }, 30 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 31 | "namedInputs": { 32 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 33 | "sharedGlobals": [ 34 | "{workspaceRoot}/workspace.json", 35 | "{workspaceRoot}/tsconfig.base.json", 36 | "{workspaceRoot}/tslint.json", 37 | "{workspaceRoot}/nx.json" 38 | ], 39 | "production": ["default"] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-aws", 3 | "version": "0.15.2", 4 | "description": "AWS plugins for nx", 5 | "keywords": [ 6 | "AWS", 7 | "SAM", 8 | "Serverless", 9 | "Angular CLI", 10 | "Workspace", 11 | "Nx", 12 | "Monorepo", 13 | "s3" 14 | ], 15 | "scripts": { 16 | "nx": "nx", 17 | "ts-node": "TS_NODE_PROJECT=tsconfig.node.json cross-dotenv ts-node -r tsconfig-paths/register", 18 | "start": "nx serve", 19 | "build": "nx run-many --target=build --all --exclude=workspace", 20 | "pack:all": "nx run-many --target=pack --all --exclude=workspace", 21 | "semver:dryrun": "yarn semver --dry-run", 22 | "semver": "nx run workspace:semver", 23 | "publish:latest": "NPM_TAG=latest nx run-many --target=publish --all", 24 | "publish:next": "NPM_TAG=next nx run-many --target=publish --all", 25 | "test": "nx run-many --target=test --all", 26 | "lint": "nx workspace-lint && nx run-many --target=lint --all", 27 | "e2e": "nx e2e", 28 | "affected:apps": "nx affected:apps", 29 | "affected:libs": "nx affected:libs", 30 | "affected:build": "nx affected:build", 31 | "affected:e2e": "nx affected:e2e", 32 | "affected:test": "nx affected:test", 33 | "affected:lint": "nx affected:lint", 34 | "affected:dep-graph": "nx affected:dep-graph", 35 | "affected": "nx affected", 36 | "format": "nx format:write", 37 | "format:write": "nx format:write", 38 | "format:check": "nx format:check", 39 | "update": "nx migrate latest", 40 | "dep-graph": "nx dep-graph", 41 | "help": "nx help", 42 | "workspace-generator": "nx workspace-generator" 43 | }, 44 | "author": "studds@fastmail.com", 45 | "license": "MIT", 46 | "dependencies": { 47 | "@aws-sdk/client-cloudformation": "^3.252.0", 48 | "@aws-sdk/client-lambda": "^3.252.0", 49 | "@types/aws-lambda": "^8.10.109", 50 | "cloudform-types": "^7.3.0", 51 | "cloudformation-js-yaml-schema": "^0.4.2", 52 | "cross-spawn": "^7.0.3", 53 | "cross-zip": "^4.0.0", 54 | "js-yaml": "^4.1.0", 55 | "mkdirp": "^2.1.3", 56 | "rxjs": "6", 57 | "source-map-support": "^0.5.21", 58 | "strip-json-comments": "^5.0.0", 59 | "ts-essentials": "^9.3.0", 60 | "webpack-merge": "^5.8.0" 61 | }, 62 | "devDependencies": { 63 | "@angular-devkit/architect": "0.1501.1", 64 | "@angular-devkit/build-angular": "15.1.1", 65 | "@angular-devkit/core": "15.1.1", 66 | "@angular-devkit/schematics": "15.1.1", 67 | "@angular/compiler": "15.1.0", 68 | "@angular/compiler-cli": "15.1.0", 69 | "@babel/core": "^7.20.12", 70 | "@jscutlery/semver": "2.29.3", 71 | "@nrwl/cli": "15.5.2", 72 | "@nrwl/devkit": "15.5.2", 73 | "@nrwl/eslint-plugin-nx": "15.5.2", 74 | "@nrwl/jest": "15.5.2", 75 | "@nrwl/js": "15.5.2", 76 | "@nrwl/node": "15.5.2", 77 | "@nrwl/nx-cloud": "15.0.2", 78 | "@nrwl/nx-plugin": "15.5.2", 79 | "@nrwl/webpack": "15.5.2", 80 | "@nrwl/workspace": "15.5.2", 81 | "@swc-node/register": "^1.5.5", 82 | "@swc/core": "^1.3.27", 83 | "@types/cpx": "^1.5.1", 84 | "@types/cross-spawn": "^6.0.2", 85 | "@types/cross-zip": "^4.0.0", 86 | "@types/jest": "28.1.8", 87 | "@types/js-yaml": "^4.0.5", 88 | "@types/mkdirp": "^1.0.2", 89 | "@types/node": "18.11.18", 90 | "@types/webpack": "5.28.0", 91 | "@types/webpack-dev-server": "^4.7.2", 92 | "@types/yargs": "^17.0.19", 93 | "@typescript-eslint/eslint-plugin": "~5.48.2", 94 | "@typescript-eslint/parser": "~5.48.2", 95 | "dotenv": "16.0.3", 96 | "eslint": "8.32.0", 97 | "eslint-config-prettier": "8.6.0", 98 | "eslint-plugin-prettier": "^4.2.1", 99 | "jest": "28.1.3", 100 | "npm-run-all": "^4.1.5", 101 | "nx": "15.5.2", 102 | "prettier": "2.8.3", 103 | "prettier-eslint": "^15.0.1", 104 | "ts-jest": "28.0.8", 105 | "ts-node": "10.9.1", 106 | "tsconfig-paths": "^4.1.2", 107 | "tslib": "^2.4.1", 108 | "typescript": "4.8.4", 109 | "webpack": "^5.75.0", 110 | "yargs": "^17.6.2" 111 | }, 112 | "repository": "https://github.com/studds/nx-aws", 113 | "publishConfig": { 114 | "registry": "https://registry.npmjs.org/" 115 | }, 116 | "engines": { 117 | "npm": "please-use-yarn", 118 | "yarn": ">= 1.22.10", 119 | "node": ">= 14.6.1" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /packages/core/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "rules": {}, 4 | "ignorePatterns": ["!**/*"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "parserOptions": { 9 | "project": ["packages/core/tsconfig.*?.json"] 10 | }, 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.ts", "*.tsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.js", "*.jsx"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.15.2](https://github.com/studds/nx-aws/compare/v0.15.1...v0.15.2) (2023-01-18) 6 | 7 | 8 | 9 | ## [0.15.1](https://github.com/studds/nx-aws/compare/v0.15.0...v0.15.1) (2022-11-08) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * correcting dependencies ([3d0f543](https://github.com/studds/nx-aws/commit/3d0f5432401d6a4986987e5a7ca50e0371aaa5e0)) 15 | 16 | 17 | 18 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 19 | 20 | 21 | 22 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 23 | 24 | 25 | 26 | ## [0.14.1](https://github.com/studds/nx-aws/compare/v0.14.0...v0.14.1) (2022-11-02) 27 | 28 | 29 | 30 | # [0.14.0](https://github.com/studds/nx-aws/compare/v0.13.0...v0.14.0) (2022-10-31) 31 | 32 | 33 | 34 | # [0.13.0](https://github.com/studds/nx-aws/compare/v0.12.2...v0.13.0) (2021-08-22) 35 | 36 | ### Features 37 | 38 | - update to nx 12.1.1 ([074eb6a](https://github.com/studds/nx-aws/commit/074eb6a3c0b8e232c34f1355047a8e800124a331)) 39 | - updating to nx 12.7 ([d165230](https://github.com/studds/nx-aws/commit/d165230b2538c422c4834fe686fb49f9f98929d6)) 40 | 41 | ## [0.12.2](https://github.com/studds/nx-aws/compare/v0.12.1...v0.12.2) (2021-04-24) 42 | 43 | ### Bug Fixes 44 | 45 | - importStackOutputs must derive stackname from target config ([56b4fb1](https://github.com/studds/nx-aws/commit/56b4fb1e4115779ae9ba6756c9550d7ff4f57d32)) 46 | 47 | ## [0.12.1](https://github.com/studds/nx-aws/compare/v0.12.0...v0.12.1) (2021-04-07) 48 | 49 | # [0.12.0](https://github.com/studds/nx-aws/compare/v0.11.1...v0.12.0) (2021-03-17) 50 | 51 | ## [0.11.1](https://github.com/studds/nx-aws/compare/v0.11.0...v0.11.1) (2021-03-03) 52 | 53 | # [0.11.0](https://github.com/studds/nx-aws/compare/v0.10.0...v0.11.0) (2021-03-01) 54 | 55 | # [0.10.0](https://github.com/studds/nx-aws/compare/v0.9.1...v0.10.0) (2021-02-24) 56 | 57 | ## [0.9.1](https://github.com/studds/nx-aws/compare/v0.9.0...v0.9.1) (2021-02-24) 58 | 59 | # [0.9.0](https://github.com/studds/nx-aws/compare/v0.8.3...v0.9.0) (2021-02-14) 60 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # core 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test core` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /packages/core/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* eslint-disable */ 3 | export default { 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 14 | coverageDirectory: '../../coverage/packages/core', 15 | displayName: 'core', 16 | testEnvironment: 'node', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nx-aws/core", 3 | "description": "Core of @nx-aws", 4 | "keywords": [ 5 | "AWS", 6 | "Serverless", 7 | "Angular CLI", 8 | "Workspace", 9 | "Nx", 10 | "Monorepo" 11 | ], 12 | "main": "src/index.js", 13 | "version": "0.15.2", 14 | "publishConfig": { 15 | "registry": "https://registry.npmjs.org/" 16 | }, 17 | "dependencies": { 18 | "rxjs": "~6.6.0", 19 | "ts-essentials": "~9.3.0", 20 | "@aws-sdk/client-cloudformation": "^3.200.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/core/src", 5 | "projectType": "library", 6 | "generators": {}, 7 | "targets": { 8 | "lint": { 9 | "executor": "@nrwl/linter:eslint", 10 | "options": { 11 | "lintFilePatterns": [ 12 | "packages/core/**/*.ts", 13 | "packages/core/**/*.spec.ts", 14 | "packages/core/**/*.spec.tsx", 15 | "packages/core/**/*.spec.js", 16 | "packages/core/**/*.spec.jsx", 17 | "packages/core/**/*.d.ts" 18 | ] 19 | } 20 | }, 21 | "test": { 22 | "executor": "@nrwl/jest:jest", 23 | "options": { 24 | "jestConfig": "packages/core/jest.config.ts", 25 | "passWithNoTests": true 26 | }, 27 | "outputs": ["{workspaceRoot}/coverage/packages/core"] 28 | }, 29 | "build": { 30 | "executor": "@nrwl/js:tsc", 31 | "options": { 32 | "outputPath": "dist/packages/core", 33 | "tsConfig": "packages/core/tsconfig.lib.json", 34 | "packageJson": "packages/core/package.json", 35 | "main": "packages/core/src/index.ts", 36 | "assets": [ 37 | "packages/core/*.md", 38 | { 39 | "input": "./packages/core/src", 40 | "glob": "**/*.!(ts)", 41 | "output": "./src" 42 | }, 43 | { 44 | "input": "./packages/core", 45 | "glob": "collection.json", 46 | "output": "." 47 | }, 48 | { 49 | "input": "./packages/core", 50 | "glob": "builders.json", 51 | "output": "." 52 | } 53 | ], 54 | "srcRootForCompilationRoot": "packages/core" 55 | }, 56 | "outputs": ["{options.outputPath}"] 57 | }, 58 | "publish": { 59 | "executor": "nx:run-commands", 60 | "options": { 61 | "parallel": false, 62 | "commands": [ 63 | { 64 | "command": "npm publish dist/packages/core --access public --tag $NPM_TAG" 65 | } 66 | ] 67 | } 68 | }, 69 | "pack": { 70 | "executor": "nx:run-commands", 71 | "options": { 72 | "parallel": false, 73 | "commands": [ 74 | { 75 | "command": "npm pack dist/packages/core" 76 | } 77 | ] 78 | } 79 | } 80 | }, 81 | "tags": [] 82 | } 83 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lib/ImportStackOutput'; 2 | export * from './lib/ImportStackOutputs'; 3 | export * from './lib/OutputValueRetriever'; 4 | export * from './lib/formatStackName'; 5 | export * from './lib/getValidatedOptions'; 6 | export * from './lib/importDotenv'; 7 | -------------------------------------------------------------------------------- /packages/core/src/lib/ImportStackOutput.ts: -------------------------------------------------------------------------------- 1 | export interface ImportStackOutput { 2 | targetName: string; 3 | outputName: string; 4 | } 5 | -------------------------------------------------------------------------------- /packages/core/src/lib/ImportStackOutputs.ts: -------------------------------------------------------------------------------- 1 | import { ImportStackOutput } from './ImportStackOutput'; 2 | 3 | export interface ImportStackOutputs { 4 | [key: string]: ImportStackOutput; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/lib/OutputValueRetriever.ts: -------------------------------------------------------------------------------- 1 | import { formatStackName } from './formatStackName'; 2 | import { getValidatedOptions } from './getValidatedOptions'; 3 | import { BuilderContext } from '@angular-devkit/architect'; 4 | import { ImportStackOutputs } from './ImportStackOutputs'; 5 | import { JsonObject } from '@angular-devkit/core'; 6 | import { assert } from 'ts-essentials/dist/functions'; 7 | import { 8 | CloudFormationClient, 9 | DescribeStacksCommand, 10 | Output, 11 | } from '@aws-sdk/client-cloudformation'; 12 | 13 | // force AWS SDK to load config, in case region is set there 14 | process.env.AWS_SDK_LOAD_CONFIG = '1'; 15 | 16 | export class OutputValueRetriever { 17 | private cfCache: Record = {}; 18 | private outputCache: Record = {}; 19 | private optionsByTarget: Record = {}; 20 | 21 | async getOutputValues( 22 | importStackOutputs: ImportStackOutputs, 23 | context: BuilderContext, 24 | stackSuffix: string | undefined 25 | ) { 26 | const values: Record = {}; 27 | const keys = Object.keys(importStackOutputs); 28 | for (const key of keys) { 29 | const element = importStackOutputs[key]; 30 | const { targetName, outputName } = element; 31 | const value = await this.getOutputValue( 32 | targetName, 33 | outputName, 34 | context, 35 | stackSuffix 36 | ); 37 | values[key] = value; 38 | } 39 | return values; 40 | } 41 | 42 | private async getOutputValue( 43 | targetName: string, 44 | outputName: string, 45 | context: BuilderContext, 46 | stackSuffix: string | undefined 47 | ) { 48 | const otherStackName = await this.getStackNameForTarget( 49 | targetName, 50 | context, 51 | stackSuffix 52 | ); 53 | const outputs = await this.getStackOutputs( 54 | otherStackName, 55 | targetName, 56 | context 57 | ); 58 | const output = outputs.find( 59 | (o) => 60 | o.OutputKey && 61 | o.OutputKey.toLowerCase() === outputName.toLowerCase() 62 | ); 63 | if (!output) { 64 | throw new Error( 65 | `Stack ${otherStackName} did not have an output called ${outputName}` 66 | ); 67 | } 68 | const value = output.OutputValue; 69 | if (!value) { 70 | throw new Error( 71 | `Stack ${otherStackName} did not have a value for output called ${outputName}` 72 | ); 73 | } 74 | return value; 75 | } 76 | 77 | private async getStackOutputs( 78 | otherStackName: string, 79 | targetName: string, 80 | context: BuilderContext 81 | ) { 82 | if (this.outputCache[otherStackName]) { 83 | return this.outputCache[otherStackName]; 84 | } 85 | context.logger.info(`Retrieving outputs for ${otherStackName}...`); 86 | const cloudFormation = await this.getCfForProject(targetName, context); 87 | const describeStacksResult = await cloudFormation.send( 88 | new DescribeStacksCommand({ StackName: otherStackName }) 89 | ); 90 | const stacks = describeStacksResult.Stacks; 91 | if (!stacks) { 92 | throw new Error( 93 | `Could not find the source stack ${otherStackName} for project ${targetName}` 94 | ); 95 | } 96 | const outputs = stacks[0].Outputs; 97 | if (!outputs) { 98 | throw new Error(`Stack ${otherStackName} did not have any outputs`); 99 | } 100 | this.outputCache[otherStackName] = outputs; 101 | return outputs; 102 | } 103 | 104 | private async getCfForProject(targetName: string, context: BuilderContext) { 105 | const region = await this.getRegionForProject(targetName, context); 106 | return this.getCfForRegion(region); 107 | } 108 | 109 | private async getRegionForProject( 110 | targetName: string, 111 | context: BuilderContext 112 | ) { 113 | const options = await this.getOptionsForTarget(targetName, context); 114 | const region = 115 | typeof options.region === 'string' ? options.region : undefined; 116 | return region; 117 | } 118 | 119 | private async getStackNameForTarget( 120 | targetName: string, 121 | context: BuilderContext, 122 | currentStackSuffix: string | undefined 123 | ) { 124 | const [sourceProjectName, sourceTargetName, explicitConfig] = 125 | targetName.split(':'); 126 | 127 | assert( 128 | sourceProjectName && sourceTargetName, 129 | `Malformed target ${targetName}: must be in format {projectName}:{targetName}[:{configName}]` 130 | ); 131 | 132 | const targetNameParts = [sourceProjectName, sourceTargetName]; 133 | if (explicitConfig) { 134 | targetNameParts.push(explicitConfig); 135 | } else if (context.target && context.target.configuration) { 136 | targetNameParts.push(context.target.configuration); 137 | } 138 | const resolvedTargetName = targetNameParts.join(':'); 139 | 140 | const options = await this.getOptionsForTarget( 141 | resolvedTargetName, 142 | context 143 | ); 144 | if (!options.stackSuffix) { 145 | console.warn( 146 | `Expected to get a stackSuffix from ${resolvedTargetName}: defaulting to ${ 147 | currentStackSuffix ?? 'dev' 148 | }` 149 | ); 150 | } 151 | const stackSuffix = 152 | typeof options.stackSuffix === 'string' 153 | ? options.stackSuffix 154 | : currentStackSuffix; 155 | const targetStackName = formatStackName( 156 | sourceProjectName, 157 | undefined, 158 | stackSuffix 159 | ); 160 | return targetStackName; 161 | } 162 | 163 | private async getOptionsForTarget( 164 | targetName: string, 165 | context: BuilderContext 166 | ) { 167 | if (!this.optionsByTarget[targetName]) { 168 | const options = await getValidatedOptions( 169 | targetName, 170 | context, 171 | false 172 | ).toPromise(); 173 | this.optionsByTarget[targetName] = options; 174 | } 175 | return this.optionsByTarget[targetName]; 176 | } 177 | 178 | private getCfForRegion(region: string | undefined) { 179 | const key = region || ''; 180 | if (!this.cfCache[key]) { 181 | this.cfCache[key] = new CloudFormationClient({ region }); 182 | } 183 | return this.cfCache[key]; 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /packages/core/src/lib/formatStackName.ts: -------------------------------------------------------------------------------- 1 | export function formatStackName( 2 | projectName: string, 3 | stackNameFormat = '$PROJECT-$ENVIRONMENT', 4 | environmentName = 'dev' 5 | ): string { 6 | const env = { 7 | PROJECT: projectName, 8 | ENVIRONMENT: environmentName, 9 | ...process.env, 10 | }; 11 | type EnvKey = keyof typeof env; 12 | // sort by descending length to prevent partial replacement 13 | const envKeys: EnvKey[] = Object.keys(env).sort( 14 | (a, b) => b.length - a.length 15 | ) as EnvKey[]; 16 | envKeys.forEach((key) => { 17 | const value = env[key]; 18 | if (!value) { 19 | return; 20 | } 21 | const regex = new RegExp(`\\$${key}|%${key}%`, 'ig'); 22 | stackNameFormat = stackNameFormat.replace(regex, value); 23 | }); 24 | return stackNameFormat; 25 | } 26 | -------------------------------------------------------------------------------- /packages/core/src/lib/getValidatedOptions.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BuilderContext, 3 | targetFromTargetString, 4 | } from '@angular-devkit/architect'; 5 | import { Observable, from } from 'rxjs'; 6 | import { JsonObject } from '@angular-devkit/core'; 7 | export function getValidatedOptions( 8 | targetName: string, 9 | context: BuilderContext, 10 | validate = true 11 | ): Observable { 12 | const target = targetFromTargetString(targetName); 13 | return from( 14 | // First we get the build options and make sure they are valid 15 | Promise.all([ 16 | context.getTargetOptions(target), 17 | context.getBuilderNameForTarget(target), 18 | ]).then(async ([targetOptions, builderName]) => { 19 | if (!validate) { 20 | return targetOptions; 21 | } 22 | const validatedBuilderOptions = await context.validateOptions( 23 | targetOptions, 24 | builderName 25 | ); 26 | return validatedBuilderOptions; 27 | }) 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/lib/importDotenv.ts: -------------------------------------------------------------------------------- 1 | export function importDotenv() { 2 | try { 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const dotEnvResult = require('dotenv').config({ silent: true }); 5 | console.log(`Loaded .env file with values`, dotEnvResult.parsed); 6 | // eslint-disable-next-line no-empty 7 | } catch (err) {} 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/core/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/core/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/s3/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "rules": {}, 4 | "ignorePatterns": ["!**/*"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "parserOptions": { 9 | "project": ["packages/s3/tsconfig.*?.json"] 10 | }, 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.ts", "*.tsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.js", "*.jsx"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/s3/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.15.2](https://github.com/studds/nx-aws/compare/v0.15.1...v0.15.2) (2023-01-18) 6 | 7 | 8 | 9 | ## [0.15.1](https://github.com/studds/nx-aws/compare/v0.15.0...v0.15.1) (2022-11-08) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * correcting dependencies ([3d0f543](https://github.com/studds/nx-aws/commit/3d0f5432401d6a4986987e5a7ca50e0371aaa5e0)) 15 | 16 | 17 | 18 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 19 | 20 | 21 | 22 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 23 | 24 | 25 | 26 | ## [0.14.1](https://github.com/studds/nx-aws/compare/v0.14.0...v0.14.1) (2022-11-02) 27 | 28 | 29 | 30 | # [0.14.0](https://github.com/studds/nx-aws/compare/v0.13.0...v0.14.0) (2022-10-31) 31 | 32 | 33 | 34 | # [0.13.0](https://github.com/studds/nx-aws/compare/v0.12.2...v0.13.0) (2021-08-22) 35 | 36 | ### Features 37 | 38 | - update to nx 12.1.1 ([074eb6a](https://github.com/studds/nx-aws/commit/074eb6a3c0b8e232c34f1355047a8e800124a331)) 39 | - updating to nx 12.7 ([d165230](https://github.com/studds/nx-aws/commit/d165230b2538c422c4834fe686fb49f9f98929d6)) 40 | 41 | ## [0.12.2](https://github.com/studds/nx-aws/compare/v0.12.1...v0.12.2) (2021-04-24) 42 | 43 | ## [0.12.1](https://github.com/studds/nx-aws/compare/v0.12.0...v0.12.1) (2021-04-07) 44 | 45 | # [0.12.0](https://github.com/studds/nx-aws/compare/v0.11.1...v0.12.0) (2021-03-17) 46 | 47 | ### Features 48 | 49 | - **s3:** add buildOutputPath and buildTarget options to s3 deploy closes [#70](https://github.com/studds/nx-aws/issues/70) ([453b4a4](https://github.com/studds/nx-aws/commit/453b4a497be037618708dc51d533f00837be3fd4)) 50 | 51 | ## [0.11.1](https://github.com/studds/nx-aws/compare/v0.11.0...v0.11.1) (2021-03-03) 52 | 53 | # [0.11.0](https://github.com/studds/nx-aws/compare/v0.10.0...v0.11.0) (2021-03-01) 54 | 55 | # [0.10.0](https://github.com/studds/nx-aws/compare/v0.9.1...v0.10.0) (2021-02-24) 56 | 57 | ## [0.9.1](https://github.com/studds/nx-aws/compare/v0.9.0...v0.9.1) (2021-02-24) 58 | 59 | # [0.9.0](https://github.com/studds/nx-aws/compare/v0.8.3...v0.9.0) (2021-02-14) 60 | -------------------------------------------------------------------------------- /packages/s3/README.md: -------------------------------------------------------------------------------- 1 | # s3 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test s3` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /packages/s3/builders.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular-devkit/architect/src/builders-schema.json", 3 | "builders": { 4 | "deploy": { 5 | "implementation": "./src/builders/deploy/deploy", 6 | "schema": "./src/builders/deploy/schema.json", 7 | "description": "Deploy a static website to S3" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/s3/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "name": "s3", 4 | "version": "0.0.1", 5 | "schematics": { 6 | "s3": { 7 | "factory": "./src/schematics/s3/schematic", 8 | "schema": "./src/schematics/s3/schema.json", 9 | "description": "s3 schematic" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/s3/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* eslint-disable */ 3 | export default { 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 14 | coverageDirectory: '../../coverage/packages/s3', 15 | displayName: 's3', 16 | testEnvironment: 'node', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/s3/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nx-aws/s3", 3 | "version": "0.15.2", 4 | "description": "AWS S3 plugin for nx - for deploying static resources to S3", 5 | "keywords": [ 6 | "AWS", 7 | "S3", 8 | "Serverless", 9 | "Angular CLI", 10 | "Workspace", 11 | "Nx", 12 | "Monorepo" 13 | ], 14 | "schematics": "./collection.json", 15 | "builders": "./builders.json", 16 | "publishConfig": { 17 | "registry": "https://registry.npmjs.org/" 18 | }, 19 | "dependencies": { 20 | "rxjs": "~6.6.0", 21 | "@nx-aws/core": "0.15.0", 22 | "ts-essentials": "~9.3.0", 23 | "@aws-sdk/client-cloudformation": "^3.200.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/s3/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "s3", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/s3/src", 5 | "projectType": "library", 6 | "generators": {}, 7 | "targets": { 8 | "lint": { 9 | "executor": "@nrwl/linter:eslint", 10 | "options": { 11 | "lintFilePatterns": [ 12 | "packages/s3/**/*.ts", 13 | "packages/s3/**/*.spec.ts", 14 | "packages/s3/**/*.spec.tsx", 15 | "packages/s3/**/*.spec.js", 16 | "packages/s3/**/*.spec.jsx", 17 | "packages/s3/**/*.d.ts" 18 | ] 19 | } 20 | }, 21 | "test": { 22 | "executor": "@nrwl/jest:jest", 23 | "options": { 24 | "jestConfig": "packages/s3/jest.config.ts", 25 | "passWithNoTests": true 26 | }, 27 | "outputs": ["{workspaceRoot}/coverage/packages/s3"] 28 | }, 29 | "build": { 30 | "executor": "@nrwl/js:tsc", 31 | "options": { 32 | "outputPath": "dist/packages/s3", 33 | "tsConfig": "packages/s3/tsconfig.lib.json", 34 | "packageJson": "packages/s3/package.json", 35 | "main": "packages/s3/src/index.ts", 36 | "assets": [ 37 | "packages/s3/*.md", 38 | { 39 | "input": "./packages/s3/src", 40 | "glob": "**/*.!(ts)", 41 | "output": "./src" 42 | }, 43 | { 44 | "input": "./packages/s3", 45 | "glob": "collection.json", 46 | "output": "." 47 | }, 48 | { 49 | "input": "./packages/s3", 50 | "glob": "builders.json", 51 | "output": "." 52 | } 53 | ], 54 | "srcRootForCompilationRoot": "packages/s3" 55 | }, 56 | "outputs": ["{options.outputPath}"] 57 | }, 58 | "publish": { 59 | "executor": "nx:run-commands", 60 | "options": { 61 | "parallel": false, 62 | "commands": [ 63 | { 64 | "command": "npm publish dist/packages/s3 --access public --tag $NPM_TAG" 65 | } 66 | ] 67 | } 68 | }, 69 | "pack": { 70 | "executor": "nx:run-commands", 71 | "options": { 72 | "parallel": false, 73 | "commands": [ 74 | { 75 | "command": "npm pack dist/packages/s3" 76 | } 77 | ] 78 | } 79 | } 80 | }, 81 | "tags": [] 82 | } 83 | -------------------------------------------------------------------------------- /packages/s3/src/builders/deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | import { createBuilder, BuilderContext } from '@angular-devkit/architect'; 2 | import { JsonObject } from '@angular-devkit/core'; 3 | import { from } from 'rxjs'; 4 | import { switchMap } from 'rxjs/operators'; 5 | import { 6 | OutputValueRetriever, 7 | ImportStackOutput, 8 | ImportStackOutputs, 9 | importDotenv, 10 | } from '@nx-aws/core'; 11 | import { writeFileSync } from 'fs'; 12 | import { resolve } from 'path'; 13 | 14 | importDotenv(); 15 | 16 | export interface S3DeployOptionsResource extends JsonObject { 17 | include: string[]; 18 | cacheControl: string; 19 | invalidate: boolean; 20 | } 21 | 22 | export interface S3DeployOptions extends JsonObject { 23 | buildOutputPath: string | null; 24 | buildTarget: string | null; 25 | resources: S3DeployOptionsResource[] | null; 26 | bucket: ImportStackOutput & JsonObject; 27 | distribution: (ImportStackOutput & JsonObject) | null; 28 | destPrefix: string | null; 29 | stackSuffix: string | null; 30 | config: { 31 | importStackOutputs: ImportStackOutputs & JsonObject; 32 | configFileName: string; 33 | } | null; 34 | configValues: Record | null; 35 | } 36 | 37 | const outputValueRetriever = new OutputValueRetriever(); 38 | 39 | export default createBuilder((options, context) => { 40 | const config = options.config; 41 | const resources = normaliseResources( 42 | options.resources, 43 | config?.configFileName 44 | ); 45 | 46 | return from(getBuildOutputPath(options, context)).pipe( 47 | switchMap(async (outputDir) => { 48 | const map = config?.importStackOutputs || {}; 49 | map.bucket = options.bucket; 50 | if (options.distribution) { 51 | map.distribution = options.distribution; 52 | } 53 | const stackSuffix = options.stackSuffix || undefined; 54 | const { 55 | bucket, 56 | distribution, 57 | // any remaining output values will be those specified in config.importStackOutputs 58 | ...configFile 59 | } = await outputValueRetriever.getOutputValues( 60 | map, 61 | context, 62 | stackSuffix 63 | ); 64 | if (config && config.configFileName) { 65 | if (options.configValues) { 66 | Object.assign(configFile, options.configValues); 67 | } 68 | // write any values imported for config to the output directory 69 | writeFileSync( 70 | resolve(outputDir, config.configFileName), 71 | JSON.stringify(configFile), 72 | { encoding: 'utf-8' } 73 | ); 74 | } 75 | const destPrefix = options.destPrefix || ''; 76 | const commands = resourceToS3Command( 77 | resources, 78 | outputDir, 79 | bucket, 80 | destPrefix, 81 | distribution 82 | ); 83 | // disable less for AWS CLI 84 | process.env.AWS_PAGER = ''; 85 | return context.scheduleBuilder('@nrwl/workspace:run-commands', { 86 | parallel: false, 87 | commands, 88 | }); 89 | }), 90 | switchMap((run) => run.output) 91 | ); 92 | }); 93 | 94 | async function getBuildOutputPath( 95 | options: S3DeployOptions, 96 | context: BuilderContext 97 | ): Promise { 98 | if (options.buildOutputPath) { 99 | return options.buildOutputPath; 100 | } 101 | const currentProject = context.target && context.target.project; 102 | const buildTarget = options.buildTarget || `${currentProject}:build`; 103 | const [project, target] = buildTarget.split(':'); 104 | if (!project || !target) { 105 | throw new Error( 106 | `Could not find project name for ${options.buildTarget} - invalid input` 107 | ); 108 | } 109 | const buildOptions = await context.getTargetOptions({ 110 | project, 111 | target, 112 | }); 113 | const outputDir = buildOptions.outputPath; 114 | if (typeof outputDir !== 'string') { 115 | throw new Error( 116 | `Expected to get outputPath on target ${options.buildTarget}` 117 | ); 118 | } 119 | return outputDir; 120 | } 121 | 122 | function normaliseResources( 123 | resources: S3DeployOptionsResource[] | null, 124 | configFileName: string | undefined 125 | ): S3DeployOptionsResource[] { 126 | if (resources && resources.length > 0) { 127 | return resources; 128 | } 129 | const dynamicResources = ['index.html']; 130 | if (configFileName) { 131 | dynamicResources.push(configFileName); 132 | } 133 | return [ 134 | { 135 | include: dynamicResources, 136 | cacheControl: 'no-cache, stale-while-revalidate=300', 137 | invalidate: true, 138 | }, 139 | { 140 | include: ['*.js', '*.css', '*.woff'], 141 | cacheControl: 'public, max-age=315360000, immutable', 142 | invalidate: false, 143 | }, 144 | { 145 | include: [], 146 | cacheControl: 147 | 'public, max-age=86400, stale-while-revalidate=315360000', 148 | invalidate: false, 149 | }, 150 | ]; 151 | } 152 | 153 | function resourceToS3Command( 154 | resources: S3DeployOptionsResource[], 155 | outputDir: string, 156 | bucketName: string, 157 | destPrefix: string, 158 | distribution: string 159 | ) { 160 | const allIncludes = resources.reduce((acc, resource) => { 161 | return [...acc, ...resource.include]; 162 | }, []); 163 | const s3Path = destPrefix ? `${bucketName}/${destPrefix}` : `${bucketName}`; 164 | const commands = resources.map(({ include, cacheControl }) => { 165 | const includePhrases = include.map((i) => `--include "${i}"`).join(' '); 166 | const excludePhrases = 167 | include.length > 0 168 | ? `--exclude "*"` 169 | : allIncludes 170 | .filter((i) => include.includes(i)) 171 | .map((i) => `--exclude "${i}"`); 172 | const command = `aws s3 sync --delete ${excludePhrases} ${includePhrases} ${outputDir} s3://${s3Path} --cache-control "${cacheControl}"`; 173 | return { command }; 174 | }); 175 | appendInvalidationCommand(distribution, resources, commands); 176 | return commands; 177 | } 178 | 179 | function appendInvalidationCommand( 180 | distribution: string, 181 | resources: S3DeployOptionsResource[], 182 | commands: { command: string }[] 183 | ) { 184 | if (distribution) { 185 | const resourcesToInvalidate = resources.filter( 186 | (needle) => needle.invalidate 187 | ); 188 | if (resourcesToInvalidate.length > 1) { 189 | throw new Error( 190 | `Unsupported: invalidating more than a single resource set` 191 | ); 192 | } 193 | const paths = resourcesToInvalidate[0].include 194 | .map((path) => `"/${path}"`) 195 | .join(' '); 196 | commands.push({ 197 | command: `aws cloudfront create-invalidation --distribution-id ${distribution} --paths ${paths}`, 198 | }); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /packages/s3/src/builders/deploy/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "outputCapture": "direct-nodejs", 4 | "title": "Schema for deploying static sites to AWS s3", 5 | "description": "AWS S3 deploy options", 6 | "type": "object", 7 | "properties": { 8 | "buildOutputPath": { 9 | "type": "string", 10 | "description": "Explicit path to the built output that will be uploaded to S3" 11 | }, 12 | "buildTarget": { 13 | "type": "string", 14 | "description": "Target to retrieve outputPath from - by default, will use the build target on the current project" 15 | }, 16 | "resources": { 17 | "type": "array", 18 | "items": { 19 | "type": "object", 20 | "properties": { 21 | "include": { 22 | "type": "array", 23 | "items": { 24 | "type": "string" 25 | } 26 | }, 27 | "cacheControl": { 28 | "type": "string" 29 | } 30 | } 31 | } 32 | }, 33 | "destPrefix": { 34 | "type": "string" 35 | }, 36 | "stackSuffix": { 37 | "type": "string" 38 | }, 39 | "bucket": { 40 | "type": "object", 41 | "properties": { 42 | "targetName": { 43 | "type": "string" 44 | }, 45 | "outputName": { 46 | "type": "string" 47 | } 48 | }, 49 | "required": ["targetName", "outputName"] 50 | }, 51 | "distribution": { 52 | "type": "object", 53 | "properties": { 54 | "targetName": { 55 | "type": "string" 56 | }, 57 | "outputName": { 58 | "type": "string" 59 | } 60 | }, 61 | "required": ["targetName", "outputName"] 62 | }, 63 | "config": { 64 | "type": "object", 65 | "properties": { 66 | "configFileName": { 67 | "type": "string" 68 | }, 69 | "importStackOutputs": { 70 | "description": "Map of values to import from stacks defined by other projects. Values are in the format {project-name}:{target-name}.{output-name}. Note this is not on the basis of exported values.", 71 | "type": "object", 72 | "additionalProperties": { 73 | "type": "object", 74 | "properties": { 75 | "targetName": { 76 | "type": "string" 77 | }, 78 | "outputName": { 79 | "type": "string" 80 | } 81 | } 82 | } 83 | } 84 | }, 85 | "required": ["configFileName", "importStackOutputs"] 86 | }, 87 | "configValues": { 88 | "description": "Map of config values - separate from config to allow easy overrides", 89 | "type": "object", 90 | "additionalProperties": { 91 | "type": "string" 92 | } 93 | } 94 | }, 95 | "additionalProperties": false, 96 | "required": ["bucket"] 97 | } 98 | -------------------------------------------------------------------------------- /packages/s3/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/studds/nx-aws/e188ca4af7e56a81118bff58c26eee805e44695b/packages/s3/src/index.ts -------------------------------------------------------------------------------- /packages/s3/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/s3/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/s3/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /packages/sam/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../.eslintrc", 3 | "rules": {}, 4 | "ignorePatterns": ["!**/*"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "parserOptions": { 9 | "project": ["packages/sam/tsconfig.*?.json"] 10 | }, 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.ts", "*.tsx"], 15 | "rules": {} 16 | }, 17 | { 18 | "files": ["*.js", "*.jsx"], 19 | "rules": {} 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /packages/sam/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | This file was generated using [@jscutlery/semver](https://github.com/jscutlery/semver). 4 | 5 | ## [0.15.2](https://github.com/studds/nx-aws/compare/v0.15.1...v0.15.2) (2023-01-18) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * changing path for nx utils under v15 ([6fb21da](https://github.com/studds/nx-aws/commit/6fb21dac88c3e6b9eecdfab72c042fab9858a3ad)) 11 | 12 | 13 | 14 | ## [0.15.1](https://github.com/studds/nx-aws/compare/v0.15.0...v0.15.1) (2022-11-08) 15 | 16 | 17 | ### Bug Fixes 18 | 19 | * correcting dependencies ([3d0f543](https://github.com/studds/nx-aws/commit/3d0f5432401d6a4986987e5a7ca50e0371aaa5e0)) 20 | 21 | 22 | 23 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 24 | 25 | 26 | ### Bug Fixes 27 | 28 | * restore generate after v14 upgrade ([06d99a9](https://github.com/studds/nx-aws/commit/06d99a9b36b0201b80ddf75e8742357b963779ef)) 29 | * restore installNpmModules ([95f2b10](https://github.com/studds/nx-aws/commit/95f2b10b412f09ba7a2667f2ffb02a92b7aef70e)) 30 | 31 | 32 | ### Features 33 | 34 | * adding experimental layer executor ([d44b39c](https://github.com/studds/nx-aws/commit/d44b39cd79da9d3341fc3fe93f0ecbe56452f28e)) 35 | * **sam:** webpack build: remove non-external deps from generated package.json ([5e8f653](https://github.com/studds/nx-aws/commit/5e8f65362db099f7be77170af26fe9fcb822684a)) 36 | 37 | 38 | 39 | # [0.15.0](https://github.com/studds/nx-aws/compare/v0.14.1...v0.15.0) (2022-11-08) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * restore generate after v14 upgrade ([06d99a9](https://github.com/studds/nx-aws/commit/06d99a9b36b0201b80ddf75e8742357b963779ef)) 45 | * restore installNpmModules ([95f2b10](https://github.com/studds/nx-aws/commit/95f2b10b412f09ba7a2667f2ffb02a92b7aef70e)) 46 | 47 | 48 | ### Features 49 | 50 | * adding experimental layer executor ([d44b39c](https://github.com/studds/nx-aws/commit/d44b39cd79da9d3341fc3fe93f0ecbe56452f28e)) 51 | * **sam:** webpack build: remove non-external deps from generated package.json ([5e8f653](https://github.com/studds/nx-aws/commit/5e8f65362db099f7be77170af26fe9fcb822684a)) 52 | 53 | 54 | 55 | ## [0.14.1](https://github.com/studds/nx-aws/compare/v0.14.0...v0.14.1) (2022-11-02) 56 | 57 | 58 | ### Bug Fixes 59 | 60 | * resolve webpack config path ([f940a78](https://github.com/studds/nx-aws/commit/f940a78cee2d9c50ad1bf1f4e4dd6a0a8052fb03)) 61 | 62 | 63 | 64 | # [0.14.0](https://github.com/studds/nx-aws/compare/v0.13.0...v0.14.0) (2022-10-31) 65 | 66 | 67 | 68 | # [0.13.0](https://github.com/studds/nx-aws/compare/v0.12.2...v0.13.0) (2021-08-22) 69 | 70 | ### Features 71 | 72 | - update to nx 12.1.1 ([074eb6a](https://github.com/studds/nx-aws/commit/074eb6a3c0b8e232c34f1355047a8e800124a331)) 73 | - updating to nx 12.7 ([d165230](https://github.com/studds/nx-aws/commit/d165230b2538c422c4834fe686fb49f9f98929d6)) 74 | 75 | ## [0.12.2](https://github.com/studds/nx-aws/compare/v0.12.1...v0.12.2) (2021-04-24) 76 | 77 | ## [0.12.1](https://github.com/studds/nx-aws/compare/v0.12.0...v0.12.1) (2021-04-07) 78 | 79 | ### Bug Fixes 80 | 81 | - **sam:** Use a single build with multiple entries by default [#69](https://github.com/studds/nx-aws/issues/69) ([c39b777](https://github.com/studds/nx-aws/commit/c39b7775e04868a42318c74b5980e9e1bd5e59d4)) 82 | 83 | # [0.12.0](https://github.com/studds/nx-aws/compare/v0.11.1...v0.12.0) (2021-03-17) 84 | 85 | ### Bug Fixes 86 | 87 | - **sam:** fix [#66](https://github.com/studds/nx-aws/issues/66) generated WebBucketPolicy ([4775f04](https://github.com/studds/nx-aws/commit/4775f04ddc372cd3cb46d4043d511a7cbc46f458)) 88 | - **sam:** fix [#67](https://github.com/studds/nx-aws/issues/67) s3 origin regional domain names ([a9f2646](https://github.com/studds/nx-aws/commit/a9f26469693f1a02e0974af15be8053c7da89509)) 89 | - **sam:** handle spaces in parameter overrides closes [#63](https://github.com/studds/nx-aws/issues/63) ([45a5d35](https://github.com/studds/nx-aws/commit/45a5d3556755e0b61e9639a0744260f3b2f8a486)) 90 | 91 | ## [0.11.1](https://github.com/studds/nx-aws/compare/v0.11.0...v0.11.1) (2021-03-03) 92 | 93 | ### Bug Fixes 94 | 95 | - **sam:** correct required properties in build schema ([8aa684a](https://github.com/studds/nx-aws/commit/8aa684a5e154d5eb5198bfa79f8c90e165845e52)) 96 | 97 | # [0.11.0](https://github.com/studds/nx-aws/compare/v0.10.0...v0.11.0) (2021-03-01) 98 | 99 | ### Features 100 | 101 | - **sam/build:** install npm modules if generatePackageJson is on ([a93e230](https://github.com/studds/nx-aws/commit/a93e23066e7c1fae58ad840565cf727b58ee8647)) 102 | - copy & paste the latest @nrwl/node:build ([773e5ce](https://github.com/studds/nx-aws/commit/773e5ce1085c25d64b6fb62b8ad2a40dc40a59a9)) 103 | 104 | # [0.10.0](https://github.com/studds/nx-aws/compare/v0.9.1...v0.10.0) (2021-02-24) 105 | 106 | ### Bug Fixes 107 | 108 | - get out of the way of how sam-cli handles layers [#55](https://github.com/studds/nx-aws/issues/55) ([6632316](https://github.com/studds/nx-aws/commit/6632316ad0283b5aeffa80912b083e0d3b66ef24)) 109 | 110 | ### Features 111 | 112 | - **sam:** add support for parameter overrides to execute ([7d9bfbf](https://github.com/studds/nx-aws/commit/7d9bfbf7441b48b26441589e7038e25fb71c7274)) 113 | 114 | ## [0.9.1](https://github.com/studds/nx-aws/compare/v0.9.0...v0.9.1) (2021-02-24) 115 | 116 | ### Bug Fixes 117 | 118 | - reverting to simple handler ([48d3625](https://github.com/studds/nx-aws/commit/48d36251988053fe9982f0fad08d3883ffcf80f8)) 119 | - updating node builder for latest nx ([defdcbc](https://github.com/studds/nx-aws/commit/defdcbcb3b02b4f4a9995de2094f8dfae0b9ed45)) 120 | 121 | # [0.9.0](https://github.com/studds/nx-aws/compare/v0.8.3...v0.9.0) (2021-02-14) 122 | 123 | ### Features 124 | 125 | - enabling tree shaking ([f9a7b60](https://github.com/studds/nx-aws/commit/f9a7b605e78425f1a1c7b9dbc017fbfdc56fd6d2)) 126 | -------------------------------------------------------------------------------- /packages/sam/README.md: -------------------------------------------------------------------------------- 1 | # sam 2 | 3 | This library was generated with [Nx](https://nx.dev). 4 | 5 | ## Running unit tests 6 | 7 | Run `nx test sam` to execute the unit tests via [Jest](https://jestjs.io). 8 | -------------------------------------------------------------------------------- /packages/sam/builders.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular-devkit/architect/src/builders-schema.json", 3 | "executors": { 4 | "build": { 5 | "implementation": "./src/builders/build/build", 6 | "schema": "./src/builders/build/schema.json", 7 | "description": "Build a SAM application" 8 | }, 9 | "layer": { 10 | "implementation": "./src/executors/layer/executor", 11 | "schema": "./src/executors/layer/schema.json", 12 | "description": "layer executor" 13 | } 14 | }, 15 | "builders": { 16 | "build": { 17 | "implementation": "./src/builders/build/compat", 18 | "schema": "./src/builders/build/schema.json", 19 | "description": "Build a SAM application" 20 | }, 21 | "execute": { 22 | "implementation": "./src/builders/execute/execute", 23 | "schema": "./src/builders/execute/schema.json", 24 | "description": "Execute a SAM application" 25 | }, 26 | "package": { 27 | "implementation": "./src/builders/cloudformation/package/package", 28 | "schema": "./src/builders/cloudformation/package/schema.json", 29 | "description": "Package a SAM application" 30 | }, 31 | "deploy": { 32 | "implementation": "./src/builders/cloudformation/deploy/deploy", 33 | "schema": "./src/builders/cloudformation/deploy/schema.json", 34 | "description": "Deploy a SAM application" 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/sam/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "name": "sam", 4 | "version": "0.0.1", 5 | "schematics": { 6 | "init": { 7 | "factory": "./src/schematics/init/init", 8 | "schema": "./src/schematics/init/schema.json", 9 | "description": "Initialize the @nx-aws/sam plugin", 10 | "aliases": ["ng-add"], 11 | "hidden": true 12 | }, 13 | "application": { 14 | "factory": "./src/schematics/application/application", 15 | "schema": "./src/schematics/application/schema.json", 16 | "aliases": ["app"], 17 | "description": "Create an AWS SAM application" 18 | }, 19 | "update-lambdas": { 20 | "factory": "./src/schematics/update-lambdas/updateLambdas", 21 | "schema": "./src/schematics/update-lambdas/schema.json", 22 | "description": "Parse the SAM template and update lambda functions to match" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/sam/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* eslint-disable */ 3 | export default { 4 | preset: '../../jest.preset.js', 5 | globals: { 6 | 'ts-jest': { 7 | tsconfig: '/tsconfig.spec.json', 8 | }, 9 | }, 10 | transform: { 11 | '^.+\\.[tj]sx?$': 'ts-jest', 12 | }, 13 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 14 | coverageDirectory: '../../coverage/packages/sam', 15 | displayName: 'sam', 16 | testEnvironment: 'node', 17 | }; 18 | -------------------------------------------------------------------------------- /packages/sam/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nx-aws/sam", 3 | "version": "0.15.2", 4 | "main": "src/index.js", 5 | "schematics": "./collection.json", 6 | "builders": "./builders.json", 7 | "dependencies": { 8 | "ts-essentials": "~9.3.0", 9 | "webpack-merge": "^5.8.0", 10 | "terser-webpack-plugin": "5.3.6", 11 | "cloudform-types": "~7.3.0", 12 | "source-map-support": "^0.5.13", 13 | "@nx-aws/core": "0.15.0", 14 | "rxjs": "~6.6.0", 15 | "@aws-sdk/client-cloudformation": "^3.200.0", 16 | "mkdirp": "~1.0.4", 17 | "cross-spawn": "~6.0.5", 18 | "cross-zip": "~4.0.0", 19 | "js-yaml": "~4.1.0", 20 | "cloudformation-js-yaml-schema": "~0.4.2", 21 | "@aws-sdk/client-lambda": "^3.200.0" 22 | }, 23 | "publishConfig": { 24 | "registry": "https://registry.npmjs.org/" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /packages/sam/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sam", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "packages/sam/src", 5 | "projectType": "library", 6 | "generators": {}, 7 | "targets": { 8 | "lint": { 9 | "executor": "@nrwl/linter:eslint", 10 | "options": { 11 | "lintFilePatterns": [ 12 | "packages/sam/**/*.ts", 13 | "packages/sam/**/*.spec.ts", 14 | "packages/sam/**/*.spec.tsx", 15 | "packages/sam/**/*.spec.js", 16 | "packages/sam/**/*.spec.jsx", 17 | "packages/sam/**/*.d.ts" 18 | ] 19 | } 20 | }, 21 | "test": { 22 | "executor": "@nrwl/jest:jest", 23 | "options": { 24 | "jestConfig": "packages/sam/jest.config.ts", 25 | "passWithNoTests": true 26 | }, 27 | "outputs": ["{workspaceRoot}/coverage/packages/sam"] 28 | }, 29 | "build": { 30 | "executor": "@nrwl/js:tsc", 31 | "options": { 32 | "outputPath": "dist/packages/sam", 33 | "tsConfig": "packages/sam/tsconfig.lib.json", 34 | "packageJson": "packages/sam/package.json", 35 | "main": "packages/sam/src/index.ts", 36 | "assets": [ 37 | "packages/sam/*.md", 38 | { 39 | "input": "./packages/sam/src", 40 | "glob": "**/*.!(ts)", 41 | "output": "./src" 42 | }, 43 | { 44 | "input": "./packages/sam", 45 | "glob": "collection.json", 46 | "output": "." 47 | }, 48 | { 49 | "input": "./packages/sam", 50 | "glob": "builders.json", 51 | "output": "." 52 | } 53 | ], 54 | "srcRootForCompilationRoot": "packages/sam" 55 | }, 56 | "outputs": ["{options.outputPath}"] 57 | }, 58 | "publish": { 59 | "executor": "nx:run-commands", 60 | "options": { 61 | "parallel": false, 62 | "commands": [ 63 | { 64 | "command": "npm publish dist/packages/sam --access public --tag $NPM_TAG" 65 | } 66 | ] 67 | } 68 | }, 69 | "pack": { 70 | "executor": "nx:run-commands", 71 | "options": { 72 | "parallel": false, 73 | "commands": [ 74 | { 75 | "command": "npm pack dist/packages/sam" 76 | } 77 | ] 78 | } 79 | } 80 | }, 81 | "tags": [] 82 | } 83 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/build.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from '@angular-devkit/core'; 2 | 3 | import { join, resolve } from 'path'; 4 | import { getEntriesFromCloudFormation } from './get-entries-from-cloudformation'; 5 | import { EntryObject } from 'webpack'; 6 | import { loadCloudFormationTemplate } from '../../utils/load-cloud-formation-template'; 7 | import { assert } from 'ts-essentials'; 8 | import { WebpackExecutorOptions } from '@nrwl/webpack/src/executors/webpack/schema'; 9 | import { webpackExecutor } from '@nrwl/node/src/executors/webpack/webpack.impl'; 10 | import { ExecutorContext } from '@nrwl/devkit'; 11 | import { installNpmModules } from './installNpmModules'; 12 | import { createPackageJson as generatePackageJson } from 'nx/src/utils/create-package-json'; 13 | import { writeFileSync } from 'fs'; 14 | 15 | export interface ExtendedBuildBuilderOptions extends WebpackExecutorOptions { 16 | originalWebpackConfig?: string; 17 | template: string; 18 | entry: string | EntryObject; 19 | buildPerFunction?: boolean; 20 | } 21 | export default cfBuilder; 22 | 23 | /** 24 | * Custom build function for CloudFormation templates. 25 | * 26 | * Actual build is handled by nrwl's node builder. This just inspects a CloudFormation template 27 | * and finds things to build. Right now, it handles AWS::Serverless::Function 28 | * 29 | */ 30 | export async function* cfBuilder( 31 | options: ExtendedBuildBuilderOptions & JsonObject, 32 | context: ExecutorContext 33 | ) { 34 | normaliseOptions(options, context); 35 | 36 | // we inspect the CloudFormation template and return webpack entries for the functions 37 | // we want to build. 38 | // NB: there's one entry per function. This gives us more flexibility when it comes to 39 | // optimising the package for each function. 40 | const cf = loadCloudFormationTemplate(options.template); 41 | const entriesPerFn = getEntriesFromCloudFormation(options, cf); 42 | 43 | if (entriesPerFn.length === 0) { 44 | console.log(`Didn't find anything to build in CloudFormation template`); 45 | yield { emittedFiles: [], success: true }; 46 | return; 47 | } 48 | 49 | assert( 50 | !options.buildPerFunction, 51 | `Had to disable build per function for nx 12, honestly not that useful` 52 | ); 53 | 54 | // in watch mode, we only want a single build for everything. 55 | const combinedEntry: EntryObject = {}; 56 | entriesPerFn.forEach((entry) => { 57 | Object.keys(entry).forEach((key) => { 58 | combinedEntry[key] = entry[key]; 59 | }); 60 | }); 61 | 62 | // we customise the build itself by passing a webpack config customising function to nrwl's builder 63 | addOurCustomWebpackConfig(options, context); 64 | 65 | // and now... to run the build 66 | 67 | // we have to have something here to keep the validation on nrwl's builder happy. 68 | options.main = 'placeholder to keep validation happy'; 69 | // in our custom webpack config, we use this instead of nrwl's entry. Slightly 70 | // dirty, but gives us more control for very little cost :-) 71 | options.entry = combinedEntry; 72 | // kick off the build itself; 73 | 74 | yield* webpackExecutor(options, context); 75 | 76 | if (options.generatePackageJson) { 77 | assert(context.projectName, `Missing project name from target`); 78 | assert(context.projectGraph, `Missing project graph from target`); 79 | const packageJson = generatePackageJson( 80 | context.projectName, 81 | context.projectGraph, 82 | { 83 | root: context.root, 84 | } 85 | ); 86 | const externalDependencies = options.externalDependencies; 87 | if (Array.isArray(externalDependencies) && packageJson.dependencies) { 88 | Object.keys(packageJson.dependencies).forEach((key) => { 89 | if ( 90 | !externalDependencies.includes(key) && 91 | packageJson.dependencies 92 | ) { 93 | delete packageJson.dependencies[key]; 94 | } 95 | }); 96 | } 97 | writeFileSync( 98 | join(options.outputPath, 'package.json'), 99 | JSON.stringify(packageJson, null, 4), 100 | { encoding: 'utf-8' } 101 | ); 102 | installNpmModules(options); 103 | } 104 | } 105 | 106 | /** 107 | * 108 | * nrwl's node builder has an option `webpackConfig` which takes a path to a function to 109 | * customise the final webpack config. We're using that to actually implement our changes to the webpack config. 110 | * 111 | */ 112 | function addOurCustomWebpackConfig( 113 | options: ExtendedBuildBuilderOptions & JsonObject, 114 | context: ExecutorContext 115 | ) { 116 | const webpackConfigPath = resolve(__dirname, 'config.js'); 117 | if (options.webpackConfig) { 118 | options.originalWebpackConfig = resolve( 119 | context.root, 120 | options.webpackConfig 121 | ); 122 | } 123 | options.webpackConfig = webpackConfigPath; 124 | } 125 | 126 | /** 127 | * 128 | * This only normalises our options - we're piggy-backing on nrwl's node building, and that normalises 129 | * it's own options. 130 | * 131 | */ 132 | function normaliseOptions( 133 | options: ExtendedBuildBuilderOptions & JsonObject, 134 | context: ExecutorContext 135 | ) { 136 | // normalise the path to the template 137 | const originalTemplatePath = options.template; 138 | options.template = resolve(context.root, originalTemplatePath); 139 | } 140 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/compat.ts: -------------------------------------------------------------------------------- 1 | import { convertNxExecutor } from '@nrwl/devkit'; 2 | 3 | import { cfBuilder } from './build'; 4 | 5 | export default convertNxExecutor(cfBuilder); 6 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration } from 'webpack'; 2 | import { merge } from 'webpack-merge'; 3 | import { ExtendedBuildBuilderOptions } from './build'; 4 | import TerserPlugin from 'terser-webpack-plugin'; 5 | 6 | /** 7 | * This entry-point is called by nrwl's builder, just before it calls out to webpack. 8 | * It allows us to customise the webpack build, while building on top of everything that 9 | * the nrwl building is already doing. 10 | * 11 | * We trigger this by setting options.webpackConfig to the path of this file in the builder. 12 | * 13 | */ 14 | export = ( 15 | configFromNrwlNodeBuilder: Configuration, 16 | options: { 17 | options: ExtendedBuildBuilderOptions; 18 | configuration: string; 19 | } 20 | ) => { 21 | const config = merge(configFromNrwlNodeBuilder, getCustomWebpack()); 22 | // override the entry with the entry determined in the builder 23 | config.entry = options.options.entry; 24 | // if the end-consumer provided their own function to customise the webpack config, run it 25 | const webpackConfig = options.options.originalWebpackConfig; 26 | if (webpackConfig) { 27 | // eslint-disable-next-line @typescript-eslint/no-var-requires 28 | const configFn = require(webpackConfig); 29 | return configFn(config, options); 30 | } 31 | return config; 32 | }; 33 | 34 | function getCustomWebpack(): Configuration { 35 | return { 36 | output: { 37 | libraryTarget: 'commonjs', 38 | // we create each chunk in it's own directory: this makes it easy to upload independent packages 39 | filename: '[name].js', 40 | }, 41 | // exclude the aws-sdk 42 | externals: [/^aws-sdk/], 43 | optimization: { 44 | minimize: true, 45 | minimizer: [ 46 | new TerserPlugin({ 47 | terserOptions: { 48 | compress: { 49 | defaults: false, 50 | unused: true, 51 | }, 52 | mangle: false, 53 | }, 54 | }), 55 | ], 56 | }, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/get-entries-from-cloudformation.ts: -------------------------------------------------------------------------------- 1 | import Resource from 'cloudform-types/types/resource'; 2 | import { resolve, relative } from 'path'; 3 | import { ExtendedBuildBuilderOptions } from './build'; 4 | import { EntryObject } from 'webpack'; 5 | import { getLambdaSourcePath } from '../../utils/getLambdaSourcePath'; 6 | import { 7 | Globals, 8 | ParsedSamTemplate, 9 | } from '../../utils/load-cloud-formation-template'; 10 | 11 | /** 12 | * Read the CloudFormation template yaml, and use it to identify our input files. 13 | */ 14 | export function getEntriesFromCloudFormation( 15 | options: ExtendedBuildBuilderOptions, 16 | cf: ParsedSamTemplate 17 | ): Array { 18 | const globals = cf.Globals; 19 | const resources = cf.Resources; 20 | if (!resources) { 21 | throw new Error("CloudFormation template didn't contain any resources"); 22 | } 23 | return Object.keys(resources) 24 | .map((name) => { 25 | return getEntry(name, resources[name], options, globals); 26 | }) 27 | .filter((s): s is EntryObject => !!s); 28 | } 29 | 30 | function getEntry( 31 | resourceName: string, 32 | resource: Resource, 33 | options: ExtendedBuildBuilderOptions, 34 | globalProperties?: Globals 35 | ): EntryObject | undefined { 36 | const properties = resource.Properties; 37 | if (!properties) { 38 | return; 39 | } 40 | 41 | // we add source-map-install to all entries - it's nice to get mapped error messages 42 | // from your lambdas 43 | 44 | const srcMapInstall = resolve(__dirname, 'source-map-install.js'); 45 | if (resource.Type === 'AWS::Serverless::Function') { 46 | return getEntryForFunction( 47 | resourceName, 48 | properties, 49 | options, 50 | srcMapInstall, 51 | globalProperties?.Function 52 | ); 53 | } else { 54 | return; 55 | } 56 | } 57 | function getEntryForFunction( 58 | resourceName: string, 59 | properties: { [key: string]: any }, 60 | options: ExtendedBuildBuilderOptions, 61 | srcMapInstall: string, 62 | globalProperties?: { [key: string]: any } 63 | ) { 64 | const { src, dir } = getLambdaSourcePath( 65 | options.template, 66 | resourceName, 67 | properties, 68 | globalProperties 69 | ); 70 | const entryName = relative(dir, src).replace('.ts', ''); 71 | return { [entryName]: [srcMapInstall, src] }; 72 | } 73 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/installNpmModules.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { execSync } from 'child_process'; 3 | import { readFileSync, writeFileSync } from 'fs'; 4 | import { logger } from '@nrwl/devkit'; 5 | 6 | export function installNpmModules(normalizedOptions: { outputPath: string }) { 7 | const packageJsonPath = join(normalizedOptions.outputPath, 'package.json'); 8 | const packagejson = JSON.parse( 9 | readFileSync(packageJsonPath, { encoding: 'utf-8' }) 10 | ); 11 | packagejson.dependencies['source-map-support'] = '*'; 12 | writeFileSync(packageJsonPath, JSON.stringify(packagejson, null, 4), { 13 | encoding: 'utf-8', 14 | }); 15 | logger.info(`Installing packages in ${normalizedOptions.outputPath}`); 16 | execSync( 17 | 'npm install --target=12.2.0 --target_arch=x64 --target_platform=linux --target_libc=glibc', 18 | { 19 | cwd: normalizedOptions.outputPath, 20 | stdio: [0, 1, 2], 21 | } 22 | ); 23 | logger.info(`Packages installed`); 24 | } 25 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "outputCapture": "direct-nodejs", 4 | "title": "Node Application Build Target", 5 | "description": "Node application build target options for Build Facade", 6 | "type": "object", 7 | "properties": { 8 | "template": { 9 | "type": "string", 10 | "description": "The SAM template." 11 | }, 12 | "tsConfig": { 13 | "type": "string", 14 | "description": "The name of the Typescript configuration file." 15 | }, 16 | "outputPath": { 17 | "type": "string", 18 | "description": "The output path of the generated files." 19 | }, 20 | "watch": { 21 | "type": "boolean", 22 | "description": "Run build when files change.", 23 | "default": false 24 | }, 25 | "poll": { 26 | "type": "number", 27 | "description": "Frequency of file watcher in ms." 28 | }, 29 | "sourceMap": { 30 | "type": "boolean", 31 | "description": "Produce source maps.", 32 | "default": true 33 | }, 34 | "progress": { 35 | "type": "boolean", 36 | "description": "Log progress to the console while building.", 37 | "default": false 38 | }, 39 | "assets": { 40 | "type": "array", 41 | "description": "List of static application assets.", 42 | "default": [], 43 | "items": { 44 | "$ref": "#/definitions/assetPattern" 45 | } 46 | }, 47 | "externalDependencies": { 48 | "oneOf": [ 49 | { 50 | "type": "string", 51 | "enum": ["none", "all"] 52 | }, 53 | { 54 | "type": "array", 55 | "items": { 56 | "type": "string" 57 | } 58 | } 59 | ], 60 | "description": "Dependencies to keep external to the bundle. (\"all\" (default), \"none\", or an array of module names)", 61 | "default": "none" 62 | }, 63 | "statsJson": { 64 | "type": "boolean", 65 | "description": "Generates a 'stats.json' file which can be analyzed using tools such as: #webpack-bundle-analyzer' or https: //webpack.github.io/analyse.", 66 | "default": false 67 | }, 68 | "verbose": { 69 | "type": "boolean", 70 | "description": "Emits verbose output", 71 | "default": false 72 | }, 73 | "extractLicenses": { 74 | "type": "boolean", 75 | "description": "Extract all licenses in a separate file, in the case of production builds only.", 76 | "default": false 77 | }, 78 | "optimization": { 79 | "type": "boolean", 80 | "description": "Defines the optimization level of the build.", 81 | "default": false 82 | }, 83 | "showCircularDependencies": { 84 | "type": "boolean", 85 | "description": "Show circular dependency warnings on builds.", 86 | "default": true 87 | }, 88 | "maxWorkers": { 89 | "type": "number", 90 | "description": "Number of workers to use for type checking. (defaults to # of CPUS - 2)" 91 | }, 92 | "memoryLimit": { 93 | "type": "number", 94 | "description": "Memory limit for type checking service process in MB. (defaults to 2048)" 95 | }, 96 | "fileReplacements": { 97 | "description": "Replace files with other files in the build.", 98 | "type": "array", 99 | "items": { 100 | "type": "object", 101 | "properties": { 102 | "replace": { 103 | "type": "string" 104 | }, 105 | "with": { 106 | "type": "string" 107 | } 108 | }, 109 | "additionalProperties": false, 110 | "required": ["replace", "with"] 111 | }, 112 | "default": [] 113 | }, 114 | "webpackConfig": { 115 | "type": "string", 116 | "description": "Path to a function which takes a webpack config, context and returns the resulting webpack config" 117 | }, 118 | "buildLibsFromSource": { 119 | "type": "boolean", 120 | "description": "Read buildable libraries from source instead of building them separately.", 121 | "default": true 122 | }, 123 | "generatePackageJson": { 124 | "type": "boolean", 125 | "description": "Generates a package.json file with the project's node_module dependencies populated for installing in a container. If a package.json exists in the project's directory, it will be reused with dependencies populated.", 126 | "default": false 127 | }, 128 | "buildPerFunction": { 129 | "type": "boolean", 130 | "description": "Runs a new webpack build for each function, rather than running one build for all functions.", 131 | "default": false 132 | } 133 | }, 134 | "required": ["tsConfig", "template"], 135 | "definitions": { 136 | "assetPattern": { 137 | "oneOf": [ 138 | { 139 | "type": "object", 140 | "properties": { 141 | "glob": { 142 | "type": "string", 143 | "description": "The pattern to match." 144 | }, 145 | "input": { 146 | "type": "string", 147 | "description": "The input directory path in which to apply 'glob'. Defaults to the project root." 148 | }, 149 | "ignore": { 150 | "description": "An array of globs to ignore.", 151 | "type": "array", 152 | "items": { 153 | "type": "string" 154 | } 155 | }, 156 | "output": { 157 | "type": "string", 158 | "description": "Absolute path within the output." 159 | } 160 | }, 161 | "additionalProperties": false, 162 | "required": ["glob", "input", "output"] 163 | }, 164 | { 165 | "type": "string" 166 | } 167 | ] 168 | } 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /packages/sam/src/builders/build/source-map-install.ts: -------------------------------------------------------------------------------- 1 | import 'source-map-support/register'; 2 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/deploy/CloudFormationDeployOptions.ts: -------------------------------------------------------------------------------- 1 | import { Capability } from './deploy'; 2 | import { IParameterOverrides } from './IParameterOverrides'; 3 | export interface CloudFormationDeployOptions { 4 | parameterOverrides: IParameterOverrides; 5 | noFailOnEmptyChangeset: true; 6 | region: string | null; 7 | capabilities: Capability[] | null; 8 | templateFile: string; 9 | stackName: string | null; 10 | s3Bucket: string; 11 | s3Prefix: string | null; 12 | } 13 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/deploy/IParameterOverrides.ts: -------------------------------------------------------------------------------- 1 | export interface IParameterOverrides { 2 | [key: string]: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/deploy/deploy.ts: -------------------------------------------------------------------------------- 1 | import { createBuilder } from '@angular-devkit/architect'; 2 | import { JsonObject } from '@angular-devkit/core'; 3 | import { runCloudformationCommand } from '../run-cloudformation-command'; 4 | import { CloudFormationDeployOptions } from './CloudFormationDeployOptions'; 5 | import { 6 | ImportStackOutputs, 7 | formatStackName, 8 | importDotenv, 9 | } from '@nx-aws/core'; 10 | import { IParameterOverrides } from './IParameterOverrides'; 11 | import { getParameterOverrides } from '../../../utils/getParameterOverrides'; 12 | 13 | export type Capability = 14 | | 'CAPABILITY_IAM' 15 | | 'CAPABILITY_NAMED_IAM' 16 | | 'CAPABILITY_AUTO_EXPAND'; 17 | 18 | // todo: allow overriding some / all of these with environment variables 19 | interface IDeployOptions extends JsonObject { 20 | /** 21 | * The path where your AWS CloudFormation template is located. 22 | */ 23 | templateFile: string; 24 | /** 25 | * Only set internally - not a valid option 26 | */ 27 | stackName: string | null; 28 | /** 29 | * 30 | */ 31 | stackNameFormat: string | null; 32 | /** 33 | * 34 | * The name of the S3 bucket where this command uploads the artefacts that are referenced in your template. 35 | */ 36 | s3Bucket: string; 37 | /** 38 | * A prefix name that the command adds to the artifacts' name when it uploads them to the S3 bucket. The 39 | * prefix name is a path name (folder name) for the S3 bucket. 40 | */ 41 | s3Prefix: string | null; 42 | /** 43 | * A list of capabilities that you must specify before AWS Cloudformation can create certain stacks. 44 | * Some stack templates might include resources that can affect permissions in your AWS account, 45 | * for example, by creating new AWS Identity and Access Management (IAM) users. For those stacks, 46 | * you must explicitly acknowledge their capabilities by specifying this parameter. The only valid 47 | * values are CAPABILITY_IAM and CAPABILITY_NAMED_IAM. If you have IAM resources, you can specify 48 | * either capability. If you have IAM resources with custom names, you must specify CAPABILITY_NAMED_IAM. 49 | * If you don't specify this parameter, this action returns an InsufficientCapabilities error. 50 | */ 51 | capabilities: Capability[] | null; 52 | /** 53 | * the region to deploy this stack 54 | */ 55 | region: string | null; 56 | importStackOutputs: (ImportStackOutputs & JsonObject) | null; 57 | parameterOverrides: IParameterOverrides | null; 58 | stackSuffix: string | null; 59 | } 60 | 61 | importDotenv(); 62 | 63 | export default createBuilder(async (options, context) => { 64 | const { capabilities, region, s3Bucket, s3Prefix, templateFile } = options; 65 | 66 | const stackSuffix = options.stackSuffix || undefined; 67 | 68 | const project = context.target && context.target.project; 69 | if (!project) { 70 | throw new Error(`Could not find project name for target`); 71 | } 72 | const stackName = formatStackName( 73 | project, 74 | undefined, 75 | stackSuffix 76 | ).toLowerCase(); 77 | 78 | const parameterOverrides = await getParameterOverrides( 79 | options, 80 | context, 81 | stackSuffix 82 | ); 83 | 84 | const cfOptions: CloudFormationDeployOptions = { 85 | capabilities, 86 | noFailOnEmptyChangeset: true, 87 | parameterOverrides, 88 | s3Bucket, 89 | stackName, 90 | templateFile, 91 | s3Prefix, 92 | region, 93 | }; 94 | return runCloudformationCommand(cfOptions, context, 'deploy'); 95 | }); 96 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/deploy/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "outputCapture": "direct-nodejs", 4 | "title": "SAM Application Deploy Target", 5 | "description": "Deploys the SAM Application using the AWS CLI", 6 | "type": "object", 7 | "properties": { 8 | "templateFile": { 9 | "description": "The path where your AWS CloudFormation template is located.", 10 | "type": "string", 11 | "title": "templateFile" 12 | }, 13 | "s3Bucket": { 14 | "description": "The name of the S3 bucket where this command uploads the artefacts that are referenced in your template.", 15 | "type": "string", 16 | "title": "s3Bucket" 17 | }, 18 | "s3Prefix": { 19 | "description": "A prefix name that the command adds to the artifacts' name when it uploads them to the S3 bucket. The\nprefix name is a path name (folder name) for the S3 bucket.", 20 | "type": "string", 21 | "title": "s3Prefix" 22 | }, 23 | "capabilities": { 24 | "description": "A list of capabilities that you must specify before AWS Cloudformation can create certain stacks.\nSome stack templates might include resources that can affect permissions in your AWS account,\nfor example, by creating new AWS Identity and Access Management (IAM) users. For those stacks,\n you must explicitly acknowledge their capabilities by specifying this parameter. The only valid\n values are CAPABILITY_IAM and CAPABILITY_NAMED_IAM. If you have IAM resources, you can specify\n either capability. If you have IAM resources with custom names, you must specify CAPABILITY_NAMED_IAM.\n If you don't specify this parameter, this action returns an InsufficientCapabilities error.", 25 | "type": "array", 26 | "items": { 27 | "enum": [ 28 | "CAPABILITY_AUTO_EXPAND", 29 | "CAPABILITY_IAM", 30 | "CAPABILITY_NAMED_IAM" 31 | ], 32 | "type": "string" 33 | }, 34 | "title": "capabilities" 35 | }, 36 | "region": { 37 | "description": "The region to deploy this stack", 38 | "type": "string", 39 | "title": "region" 40 | }, 41 | "importStackOutputs": { 42 | "description": "Map of values to import from stacks defined by other projects. Values are in the format {project-name}:{target-name}.{output-name}. Note this is not on the basis of exported values.", 43 | "type": "object", 44 | "additionalProperties": { 45 | "type": "object", 46 | "properties": { 47 | "targetName": { 48 | "type": "string" 49 | }, 50 | "outputName": { 51 | "type": "string" 52 | } 53 | }, 54 | "required": ["targetName", "outputName"] 55 | } 56 | }, 57 | "stackSuffix": { 58 | "type": "string" 59 | }, 60 | "parameterOverrides": { 61 | "description": "Map of parameter overrides", 62 | "type": "object", 63 | "additionalProperties": { 64 | "type": "string" 65 | } 66 | } 67 | }, 68 | "required": ["s3Bucket", "templateFile"], 69 | "definitions": {} 70 | } 71 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/get-final-template-location.ts: -------------------------------------------------------------------------------- 1 | import { sync as mkdirpSync } from 'mkdirp'; 2 | import { parse } from 'path'; 3 | import { resolve } from 'path'; 4 | /** 5 | * 6 | * Get the destination where we'll copy the template 7 | * 8 | * @param outputTemplateFile 9 | * @param templateFile 10 | */ 11 | export function getFinalTemplateLocation( 12 | outputTemplateFile: string, 13 | templateFile: string 14 | ): string { 15 | const dir = parse(outputTemplateFile).dir; 16 | mkdirpSync(dir); 17 | const base = parse(templateFile).base; 18 | const finalTemplateLocation = resolve(dir, base); 19 | return finalTemplateLocation; 20 | } 21 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/package/isContentfulString.ts: -------------------------------------------------------------------------------- 1 | export function isContentfulString(s: unknown): s is string { 2 | return typeof s === 'string' && !!s; 3 | } 4 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/package/mapRelativePathsToAbsolute.ts: -------------------------------------------------------------------------------- 1 | import Template from 'cloudform-types/types/template'; 2 | import { resolve } from 'path'; 3 | import { existsSync } from 'fs'; 4 | 5 | interface ResourcePropertiesToMap { 6 | properties: string[]; 7 | skip: boolean; 8 | } 9 | 10 | const MAPPINGS: Record = { 11 | 'AWS::ApiGateway::RestApi': { properties: ['BodyS3Location'], skip: false }, 12 | 'AWS::Lambda::Function': { properties: ['Code'], skip: true }, 13 | 'AWS::Serverless::Function': { properties: ['CodeUri'], skip: true }, 14 | 'AWS::Serverless::LayerVersion': { 15 | properties: ['ContentUri'], 16 | skip: false, 17 | }, 18 | 'AWS::AppSync::GraphQLSchema': { 19 | properties: ['DefinitionS3Location'], 20 | skip: false, 21 | }, 22 | 'AWS::AppSync::Resolver': { 23 | properties: [ 24 | 'RequestMappingTemplateS3Location', 25 | 'ResponseMappingTemplateS3Location', 26 | ], 27 | skip: false, 28 | }, 29 | 'AWS::Serverless::Api': { properties: ['DefinitionUri'], skip: false }, 30 | 'AWS::Include': { properties: ['Location'], skip: false }, 31 | 'AWS::ElasticBeanstalk::ApplicationVersion': { 32 | properties: ['SourceBundle'], 33 | skip: false, 34 | }, 35 | 'AWS::CloudFormation::Stack': { properties: ['TemplateURL'], skip: false }, 36 | 'AWS::Serverless::Application': { properties: ['Location'], skip: false }, 37 | 'AWS::Serverless::StateMachine': { 38 | properties: ['DefinitionUri'], 39 | skip: false, 40 | }, 41 | 'AWS::Glue::Job': { properties: ['Command.ScriptLocation'], skip: false }, 42 | }; 43 | 44 | export function mapRelativePathsToAbsolute( 45 | template: Template, 46 | inputDir: string 47 | ): void { 48 | const resources = template.Resources; 49 | if (!resources) { 50 | return; 51 | } 52 | for (const name in resources) { 53 | if (Object.prototype.hasOwnProperty.call(resources, name)) { 54 | const resource = resources[name]; 55 | const properties = resource.Properties; 56 | const mapping = MAPPINGS[resource.Type]; 57 | if (mapping && properties && !mapping.skip) { 58 | mapping.properties.forEach((property) => { 59 | const value = properties[property]; 60 | if (typeof value === 'string') { 61 | const path = resolve(inputDir, value); 62 | if (existsSync(path)) { 63 | console.log( 64 | `Remapping ${property} for ${name} to ${path}` 65 | ); 66 | properties[property] = path; 67 | } 68 | } 69 | }); 70 | } 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/package/package.ts: -------------------------------------------------------------------------------- 1 | import { createBuilder, BuilderContext } from '@angular-devkit/architect'; 2 | import { JsonObject } from '@angular-devkit/core'; 3 | import { runCloudformationCommand } from '../run-cloudformation-command'; 4 | import { from } from 'rxjs'; 5 | import { switchMap } from 'rxjs/operators'; 6 | import { writeFileSync } from 'fs'; 7 | import { getFinalTemplateLocation } from '../get-final-template-location'; 8 | import { loadCloudFormationTemplate } from '../../../utils/load-cloud-formation-template'; 9 | import { dumpCloudformationTemplate } from '../../../utils/dumpCloudformationTemplate'; 10 | import { updateCloudFormationTemplate } from './updateCloudFormationTemplate'; 11 | import { importDotenv } from '@nx-aws/core'; 12 | 13 | // todo: allow overriding some / all of these with environment variables 14 | export interface IPackageOptions extends JsonObject { 15 | /** 16 | * The path where your AWS CloudFormation template is located. 17 | */ 18 | templateFile: string; 19 | /** 20 | * 21 | */ 22 | outputTemplateFile: string; 23 | /** 24 | * 25 | * The name of the S3 bucket where this command uploads the artefacts that are referenced in your template. 26 | */ 27 | s3Bucket: string; 28 | /** 29 | * A prefix name that the command adds to the artefacts' name when it uploads them to the S3 bucket. The 30 | * prefix name is a path name (folder name) for the S3 bucket. 31 | */ 32 | s3Prefix: string | null; 33 | /** 34 | * If true, we skip the aws package command, which is unnecessary for a sub stack 35 | */ 36 | subStackOnly: boolean; 37 | /** 38 | * The region to upload resources 39 | */ 40 | region: string | null; 41 | } 42 | 43 | importDotenv(); 44 | 45 | export default createBuilder( 46 | (options: IPackageOptions, context: BuilderContext) => { 47 | const cloudFormation = loadCloudFormationTemplate(options.templateFile); 48 | return from( 49 | updateCloudFormationTemplate(cloudFormation, context, options) 50 | ).pipe( 51 | switchMap(async () => { 52 | const updatedTemplateFile = getFinalTemplateLocation( 53 | options.outputTemplateFile, 54 | options.templateFile 55 | ); 56 | // todo: why are we rewriting this? 57 | options.templateFile = updatedTemplateFile; 58 | writeFileSync( 59 | updatedTemplateFile, 60 | dumpCloudformationTemplate(cloudFormation), 61 | { 62 | encoding: 'utf-8', 63 | } 64 | ); 65 | if (options.subStackOnly) { 66 | // if this is a sub-stack only, we don't need to run package, as the aws cli already 67 | // handles nested stacks. 68 | return { success: true }; 69 | } 70 | // todo: probably should use nrwl's command builder (whatever that's called?) 71 | return runCloudformationCommand(options, context, 'package'); 72 | }) 73 | ); 74 | } 75 | ); 76 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/package/replaceProjectReferenceWithPath.ts: -------------------------------------------------------------------------------- 1 | import { BuilderContext } from '@angular-devkit/architect'; 2 | import Template from 'cloudform-types/types/template'; 3 | import Resource from 'cloudform-types/types/resource'; 4 | import { getFinalTemplateLocation } from '../get-final-template-location'; 5 | import { isContentfulString } from './isContentfulString'; 6 | 7 | /** 8 | * 9 | * We allow AWS::Serverless::Application resources to point to an nx project instead of a file path. 10 | * 11 | * This is probably a bad idea, but it is convenient. 12 | * 13 | * @param cloudFormation 14 | * @param context 15 | */ 16 | export async function replaceProjectReferenceWithPath( 17 | cloudFormation: Template, 18 | context: BuilderContext 19 | ): Promise { 20 | const resources = cloudFormation.Resources; 21 | if (resources) { 22 | for (const key in resources) { 23 | if (Object.prototype.hasOwnProperty.call(resources, key)) { 24 | const resource = resources[key]; 25 | if (resource.Type === 'AWS::Serverless::Application') { 26 | await updateTemplate(resource, context, key); 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | async function updateTemplate( 34 | resource: Resource, 35 | context: BuilderContext, 36 | key: string 37 | ): Promise { 38 | const properties = resource.Properties; 39 | if (properties) { 40 | const location = properties.Location; 41 | try { 42 | const applicationOptions = await context.getTargetOptions({ 43 | project: location, 44 | target: 'package', 45 | }); 46 | const outputTemplateFile = applicationOptions.outputTemplateFile; 47 | const templateFile = applicationOptions.templateFile; 48 | if ( 49 | isContentfulString(outputTemplateFile) && 50 | isContentfulString(templateFile) 51 | ) { 52 | // we map the location to the 53 | const finalTemplateLocation = getFinalTemplateLocation( 54 | outputTemplateFile, 55 | templateFile 56 | ); 57 | context.logger.info( 58 | `Remapping ${key} location to ${finalTemplateLocation} for referenced project ${location}` 59 | ); 60 | properties.Location = finalTemplateLocation; 61 | } 62 | } catch (err) { 63 | // ignore error - it's not a project reference 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/package/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "outputCapture": "direct-nodejs", 4 | "title": "SAM Application Package Target", 5 | "description": "Packages the SAM Application using the AWS CLI", 6 | "type": "object", 7 | "properties": { 8 | "templateFile": { 9 | "description": "The path where your AWS CloudFormation template is located.", 10 | "type": "string", 11 | "title": "templateFile" 12 | }, 13 | "outputTemplateFile": { 14 | "type": "string", 15 | "title": "outputTemplateFile" 16 | }, 17 | "s3Bucket": { 18 | "description": "The name of the S3 bucket where this command uploads the artefacts that are referenced in your template.", 19 | "type": "string", 20 | "title": "s3Bucket" 21 | }, 22 | "s3Prefix": { 23 | "description": "A prefix name that the command adds to the artefacts' name when it uploads them to the S3 bucket. The\nprefix name is a path name (folder name) for the S3 bucket.", 24 | "type": "string", 25 | "title": "s3Prefix" 26 | }, 27 | "subStackOnly": { 28 | "description": "If true, we skip the aws package command, which is unnecessary for a sub stack", 29 | "type": "boolean", 30 | "title": "subStackOnly" 31 | }, 32 | "region": { 33 | "description": "The region to upload resources", 34 | "type": "string", 35 | "title": "region" 36 | } 37 | }, 38 | "required": ["outputTemplateFile", "s3Bucket", "templateFile"], 39 | "definitions": {} 40 | } 41 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/package/updateCloudFormationTemplate.ts: -------------------------------------------------------------------------------- 1 | import { BuilderContext } from '@angular-devkit/architect'; 2 | import Template from 'cloudform-types/types/template'; 3 | import { parse } from 'path'; 4 | import { mapRelativePathsToAbsolute } from './mapRelativePathsToAbsolute'; 5 | import { IPackageOptions } from './package'; 6 | import { replaceProjectReferenceWithPath } from './replaceProjectReferenceWithPath'; 7 | 8 | export async function updateCloudFormationTemplate( 9 | cloudFormation: Template, 10 | context: BuilderContext, 11 | options: Pick 12 | ): Promise { 13 | await replaceProjectReferenceWithPath(cloudFormation, context); 14 | const inputPath = parse(options.templateFile).dir; 15 | mapRelativePathsToAbsolute(cloudFormation, inputPath); 16 | } 17 | -------------------------------------------------------------------------------- /packages/sam/src/builders/cloudformation/run-cloudformation-command.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from '@angular-devkit/core'; 2 | import { BuilderOutput, BuilderContext } from '@angular-devkit/architect'; 3 | import { spawn } from 'cross-spawn'; 4 | import { dasherize } from '@angular-devkit/core/src/utils/strings'; 5 | import { CloudFormationDeployOptions } from './deploy/CloudFormationDeployOptions'; 6 | 7 | export function runCloudformationCommand( 8 | options: JsonObject | CloudFormationDeployOptions, 9 | context: BuilderContext, 10 | subcommand: string | string[] 11 | ) { 12 | return new Promise((resolve) => { 13 | const args: string[] = Array.isArray(subcommand) 14 | ? subcommand 15 | : [subcommand]; 16 | Object.keys(options).forEach((arg) => { 17 | const value = (options as any)[arg]; 18 | if (value) { 19 | if (Array.isArray(value)) { 20 | if (arg === 'args') { 21 | args.push(...value); 22 | } else { 23 | args.push(`--${dasherize(arg)}`); 24 | // todo: avoid this cast to Array 25 | args.push(...(value as Array)); 26 | } 27 | } else if (typeof value === 'object') { 28 | const keys = Object.keys(value); 29 | if (keys.length > 0) { 30 | args.push(`--${dasherize(arg)}`); 31 | keys.forEach((key) => { 32 | // todo: avoid this cast to any 33 | args.push(`${key}="${(value as any)[key]}"`); 34 | }); 35 | } 36 | } else if (typeof value === 'boolean') { 37 | args.push(`--${dasherize(arg)}`); 38 | // do nothing - just including the flag is all that's required 39 | } else { 40 | args.push(`--${dasherize(arg)}`); 41 | args.push(`${value}`); 42 | } 43 | } 44 | }); 45 | const command = `sam`; 46 | 47 | context.logger.log( 48 | 'info', 49 | `Executing "${command} ${args.join(' ')}"...` 50 | ); 51 | context.reportStatus(`Executing "${command} ${args.join(' ')}"...`); 52 | const child = spawn(command, args, { 53 | stdio: 'inherit', 54 | env: process.env, 55 | }); 56 | 57 | context.reportStatus(`Done.`); 58 | child.on('close', (code) => { 59 | resolve({ success: code === 0 }); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /packages/sam/src/builders/execute/execute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BuilderContext, 3 | createBuilder, 4 | BuilderOutput, 5 | targetFromTargetString, 6 | scheduleTargetAndForget, 7 | Target, 8 | } from '@angular-devkit/architect'; 9 | 10 | import { Observable, of, combineLatest, from } from 'rxjs'; 11 | import { concatMap, map, tap, switchMap } from 'rxjs/operators'; 12 | 13 | import { stripIndents } from '@angular-devkit/core/src/utils/literals'; 14 | import { SamExecuteBuilderOptions } from './options'; 15 | import { runSam } from './run-sam'; 16 | import { JsonObject } from '@angular-devkit/core'; 17 | import { getFinalTemplateLocation } from '../cloudformation/get-final-template-location'; 18 | import { watch, writeFileSync } from 'fs'; 19 | import { getValidatedOptions, importDotenv } from '@nx-aws/core'; 20 | import { loadEnvFromStack } from '../../utils/loadEnvFromStack'; 21 | import { updateCloudFormationTemplate } from '../cloudformation/package/updateCloudFormationTemplate'; 22 | import { loadCloudFormationTemplate } from '../../utils/load-cloud-formation-template'; 23 | import { dumpCloudformationTemplate } from '../../utils/dumpCloudformationTemplate'; 24 | import { getParameterOverrides } from '../../utils/getParameterOverrides'; 25 | 26 | importDotenv(); 27 | 28 | export const enum InspectType { 29 | Inspect = 'inspect', 30 | InspectBrk = 'inspect-brk', 31 | } 32 | 33 | export default createBuilder( 34 | nodeExecuteBuilderHandler 35 | ); 36 | 37 | export function nodeExecuteBuilderHandler( 38 | options: SamExecuteBuilderOptions, 39 | context: BuilderContext 40 | ): Observable { 41 | const project = context.target?.project; 42 | return loadEnvFromStack(options.mimicEnv, project).pipe( 43 | switchMap(() => runWaitUntilTargets(options)), 44 | concatMap((v): Observable => { 45 | if (!v.success) { 46 | context.logger.error( 47 | `One of the tasks specified in waitUntilTargets failed` 48 | ); 49 | return of({ success: false }); 50 | } 51 | return startBuild(options, context); 52 | }) 53 | ); 54 | } 55 | 56 | function startBuild( 57 | options: SamExecuteBuilderOptions, 58 | context: BuilderContext 59 | ): Observable { 60 | const buildTarget = targetFromTargetString(options.buildTarget); 61 | return getBuilderOptions(options, context).pipe( 62 | concatMap((builderOptions): Observable => { 63 | const template = builderOptions.template; 64 | if (typeof template !== 'string') { 65 | throw new Error( 66 | 'Builder options was missing template property' 67 | ); 68 | } 69 | return copyTemplate(options, context, template).pipe( 70 | switchMap((finalTemplateLocation) => 71 | from( 72 | getParameterOverrides( 73 | { ...options, templateFile: template }, 74 | context, 75 | undefined 76 | ) 77 | ).pipe( 78 | map((parameterOverrides) => ({ 79 | finalTemplateLocation, 80 | parameterOverrides, 81 | })) 82 | ) 83 | ), 84 | switchMap(({ finalTemplateLocation, parameterOverrides }) => { 85 | return startBuildImpl( 86 | { ...options, parameterOverrides }, 87 | context, 88 | finalTemplateLocation, 89 | buildTarget 90 | ); 91 | }) 92 | ); 93 | }) 94 | ); 95 | } 96 | 97 | function startBuildImpl( 98 | options: SamExecuteBuilderOptions, 99 | context: BuilderContext, 100 | template: string, 101 | buildTarget: Target 102 | ) { 103 | const sam$ = runSam(options, context, template); 104 | // todo: it would be nice to wait until the first successful completion of build$ before triggering sam$ 105 | const build$ = scheduleTargetAndForget(context, buildTarget, { 106 | watch: true, 107 | }); 108 | return combineLatest([sam$, build$]).pipe( 109 | map(([samResult, buildResult]): BuilderOutput => { 110 | if (!samResult.success || !buildResult.success) { 111 | context.logger.error( 112 | 'There was an error with the build. See above.' 113 | ); 114 | return { success: false }; 115 | } 116 | return { success: true }; 117 | }) 118 | ); 119 | } 120 | 121 | function getBuilderOptions( 122 | options: SamExecuteBuilderOptions, 123 | context: BuilderContext 124 | ) { 125 | const targetName = options.buildTarget; 126 | const validateOptions = getValidatedOptions(targetName, context); 127 | return validateOptions.pipe( 128 | tap((builderOptions) => { 129 | if (builderOptions.optimization) { 130 | context.logger.warn(stripIndents` 131 | ************************************************ 132 | This is a simple process manager for use in 133 | testing or debugging Node applications locally. 134 | DO NOT USE IT FOR PRODUCTION! 135 | You should look into proper means of deploying 136 | your node application to production. 137 | ************************************************`); 138 | } 139 | }) 140 | ); 141 | } 142 | 143 | function getPackageOptions( 144 | options: SamExecuteBuilderOptions, 145 | context: BuilderContext 146 | ): Observable { 147 | return getValidatedOptions(options.packageTarget, context, false); 148 | } 149 | 150 | function copyTemplate( 151 | options: SamExecuteBuilderOptions, 152 | context: BuilderContext, 153 | templateFile: string 154 | ): Observable { 155 | return watchTemplate(context, templateFile).pipe( 156 | switchMap(() => { 157 | return getDestinationTemplatePath( 158 | options, 159 | context, 160 | templateFile 161 | ).pipe( 162 | switchMap((finalTemplateLocation) => { 163 | return from( 164 | updateTemplate( 165 | templateFile, 166 | context, 167 | finalTemplateLocation 168 | ) 169 | ); 170 | }) 171 | ); 172 | }) 173 | ); 174 | } 175 | 176 | async function updateTemplate( 177 | templateFile: string, 178 | context: BuilderContext, 179 | finalTemplateLocation: string 180 | ): Promise { 181 | const template = loadCloudFormationTemplate(templateFile); 182 | await updateCloudFormationTemplate(template, context, { 183 | templateFile, 184 | }); 185 | writeFileSync(finalTemplateLocation, dumpCloudformationTemplate(template), { 186 | encoding: 'utf-8', 187 | }); 188 | return finalTemplateLocation; 189 | } 190 | 191 | function watchTemplate( 192 | context: BuilderContext, 193 | templateFile: string 194 | ): Observable { 195 | return new Observable((subscriber) => { 196 | // initial emit to get everything moving 197 | subscriber.next(); 198 | const listener = () => { 199 | context.logger.info(`${templateFile} changed, restarting build`); 200 | return subscriber.next(); 201 | }; 202 | const watcher = watch(templateFile, listener); 203 | () => { 204 | watcher.close(); 205 | }; 206 | }); 207 | } 208 | 209 | function getDestinationTemplatePath( 210 | options: SamExecuteBuilderOptions, 211 | context: BuilderContext, 212 | templateFile: string 213 | ): Observable { 214 | return getPackageOptions(options, context).pipe( 215 | map((packageOptions: JsonObject): string => { 216 | if (typeof packageOptions.outputTemplateFile !== 'string') { 217 | throw new Error( 218 | 'Package options were missing outputTemplateFile' 219 | ); 220 | } 221 | return getFinalTemplateLocation( 222 | packageOptions.outputTemplateFile, 223 | templateFile 224 | ); 225 | }) 226 | ); 227 | } 228 | 229 | function runWaitUntilTargets( 230 | options: SamExecuteBuilderOptions 231 | ): Observable { 232 | if (!options.waitUntilTargets || options.waitUntilTargets.length === 0) { 233 | return of({ success: true }); 234 | } 235 | 236 | throw new Error('Unimplemented - need to get the updated way to do this.'); 237 | } 238 | -------------------------------------------------------------------------------- /packages/sam/src/builders/execute/options.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from '@angular-devkit/core'; 2 | import { ImportStackOutputs } from '@nx-aws/core'; 3 | import { IParameterOverrides } from '../cloudformation/deploy/IParameterOverrides'; 4 | 5 | export interface SamExecuteBuilderOptions extends JsonObject { 6 | args: string[]; 7 | waitUntilTargets: string[]; 8 | buildTarget: string; 9 | packageTarget: string; 10 | host: string; 11 | port: number; 12 | mimicEnv: string; 13 | importStackOutputs: (ImportStackOutputs & JsonObject) | null; 14 | parameterOverrides: IParameterOverrides | null; 15 | } 16 | -------------------------------------------------------------------------------- /packages/sam/src/builders/execute/run-sam.ts: -------------------------------------------------------------------------------- 1 | import { BuilderOutput, BuilderContext } from '@angular-devkit/architect'; 2 | import { SamExecuteBuilderOptions } from './options'; 3 | import { from, Observable } from 'rxjs'; 4 | import { runCloudformationCommand } from '../cloudformation/run-cloudformation-command'; 5 | 6 | export function runSam( 7 | options: SamExecuteBuilderOptions, 8 | context: BuilderContext, 9 | templatePath: string 10 | ): Observable { 11 | return from( 12 | runCloudformationCommand( 13 | { 14 | template: templatePath, 15 | host: options.host, 16 | port: options.port, 17 | parameterOverrides: options.parameterOverrides, 18 | args: options.args, 19 | }, 20 | context, 21 | ['local', 'start-api'] 22 | ) 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /packages/sam/src/builders/execute/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "outputCapture": "direct-nodejs", 4 | "title": "Schema for Executing NodeJS apps", 5 | "description": "NodeJS execution options", 6 | "type": "object", 7 | "properties": { 8 | "buildTarget": { 9 | "type": "string", 10 | "description": "The target to run to build you the app" 11 | }, 12 | "packageTarget": { 13 | "type": "string", 14 | "description": "The target to run to package you the app" 15 | }, 16 | "waitUntilTargets": { 17 | "type": "array", 18 | "description": "The targets to run to before starting the node app", 19 | "default": [], 20 | "items": { 21 | "type": "string" 22 | } 23 | }, 24 | "host": { 25 | "type": "string", 26 | "default": "localhost", 27 | "description": "The host to inspect the process on" 28 | }, 29 | "port": { 30 | "type": "number", 31 | "default": 7777, 32 | "description": "The port to inspect the process on. Setting port to 0 will assign random free ports to all forked processes." 33 | }, 34 | "args": { 35 | "type": "array", 36 | "description": "Extra args when starting the app", 37 | "default": [], 38 | "items": { 39 | "type": "string" 40 | } 41 | }, 42 | "mimicEnv": { 43 | "type": "string", 44 | "description": "The name of an environment to grab environment variables from" 45 | }, 46 | "importStackOutputs": { 47 | "description": "Map of values to import from stacks defined by other projects. Values are in the format {project-name}:{target-name}.{output-name}. Note this is not on the basis of exported values.", 48 | "type": "object", 49 | "additionalProperties": { 50 | "type": "object", 51 | "properties": { 52 | "targetName": { 53 | "type": "string" 54 | }, 55 | "outputName": { 56 | "type": "string" 57 | } 58 | }, 59 | "required": ["targetName", "outputName"] 60 | } 61 | }, 62 | "parameterOverrides": { 63 | "description": "Map of parameter overrides", 64 | "type": "object", 65 | "additionalProperties": { 66 | "type": "string" 67 | } 68 | } 69 | }, 70 | "additionalProperties": false, 71 | "required": ["buildTarget", "packageTarget"] 72 | } 73 | -------------------------------------------------------------------------------- /packages/sam/src/executors/layer/executor.ts: -------------------------------------------------------------------------------- 1 | import { ExecutorOptions } from '@nrwl/js/src/utils/schema'; 2 | import { tscExecutor } from '@nrwl/js/src/executors/tsc/tsc.impl'; 3 | import { ExecutorContext } from '@nrwl/devkit'; 4 | import { basename, join } from 'path'; 5 | import { installNpmModules } from '../../builders/build/installNpmModules'; 6 | import { createPackageJson } from 'nx/src/utils/create-package-json'; 7 | import { TypescriptCompilationResult } from '@nrwl/js/src/utils/typescript/compile-typescript-files'; 8 | import { assert } from 'ts-essentials/dist/functions'; 9 | import { writeFileSync } from 'fs'; 10 | import { zipSync } from 'cross-zip'; 11 | 12 | interface Options extends ExecutorOptions { 13 | templatePath?: string; 14 | } 15 | 16 | export default async function* runExecutor( 17 | options: Options, 18 | context: ExecutorContext 19 | ) { 20 | options.outputPath = join(options.outputPath, 'nodejs'); 21 | 22 | assert(context.projectName, `Missing project name from context`); 23 | 24 | // build with typescript 25 | const tsc = tscExecutor(options, context); 26 | let executionResult: TypescriptCompilationResult | undefined = undefined; 27 | for await (const emission of tsc) { 28 | executionResult = emission; 29 | yield executionResult; 30 | } 31 | 32 | assert( 33 | executionResult, 34 | `Expected execution result to be defined at this point but it was not.` 35 | ); 36 | 37 | if (!executionResult.success) { 38 | // build failed; bail out 39 | return; 40 | } 41 | 42 | try { 43 | assert(context.projectGraph, `Missing project graph from context`); 44 | 45 | // generate and write package.json 46 | const packageJson = createPackageJson( 47 | context.projectName, 48 | context.projectGraph, 49 | { 50 | root: context.root, 51 | } 52 | ); 53 | writeFileSync( 54 | join(options.outputPath, 'package.json'), 55 | JSON.stringify(packageJson, null, 4), 56 | { encoding: 'utf-8' } 57 | ); 58 | 59 | // install npm modules 60 | installNpmModules(options); 61 | 62 | // zip 63 | const outputPath = `${basename(options.outputPath)}.zip`; 64 | zipSync(options.outputPath, outputPath); 65 | 66 | yield executionResult; 67 | } catch (err) { 68 | console.error(err); 69 | yield { 70 | ...executionResult, 71 | success: false, 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/sam/src/executors/layer/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "outputCapture": "direct-nodejs", 4 | "title": "Typescript Build Target", 5 | "description": "Builds using TypeScript.", 6 | "cli": "nx", 7 | "type": "object", 8 | "properties": { 9 | "main": { 10 | "type": "string", 11 | "description": "The name of the main entry-point file.", 12 | "x-completion-type": "file", 13 | "x-completion-glob": "main@(.js|.ts|.jsx|.tsx)" 14 | }, 15 | "rootDir": { 16 | "type": "string", 17 | "description": "Sets the rootDir for TypeScript compilation. When not defined, it uses the root of project." 18 | }, 19 | "outputPath": { 20 | "type": "string", 21 | "description": "The output path of the generated files.", 22 | "x-completion-type": "directory" 23 | }, 24 | "tsConfig": { 25 | "type": "string", 26 | "description": "The path to the Typescript configuration file.", 27 | "x-completion-type": "file", 28 | "x-completion-glob": "tsconfig.*.json" 29 | }, 30 | "packageJson": { 31 | "type": "string", 32 | "description": "The path to the package.json file.", 33 | "x-completion-type": "file", 34 | "x-completion-glob": "package.json" 35 | }, 36 | "templatePath": { 37 | "type": "string", 38 | "description": "The path to the cloudformation template.yaml file.", 39 | "x-completion-type": "file", 40 | "x-completion-glob": "template.yaml" 41 | }, 42 | "assets": { 43 | "type": "array", 44 | "description": "List of static assets.", 45 | "default": [], 46 | "items": { 47 | "$ref": "#/definitions/assetPattern" 48 | } 49 | }, 50 | "clean": { 51 | "type": "boolean", 52 | "description": "Remove previous output before build.", 53 | "default": true 54 | }, 55 | "transformers": { 56 | "type": "array", 57 | "description": "List of TypeScript Transformer Plugins.", 58 | "default": [], 59 | "items": { 60 | "$ref": "#/definitions/transformerPattern" 61 | } 62 | }, 63 | "updateBuildableProjectDepsInPackageJson": { 64 | "type": "boolean", 65 | "description": "Whether to update the buildable project dependencies in `package.json`.", 66 | "default": true 67 | }, 68 | "buildableProjectDepsInPackageJsonType": { 69 | "type": "string", 70 | "description": "When `updateBuildableProjectDepsInPackageJson` is `true`, this adds dependencies to either `peerDependencies` or `dependencies`.", 71 | "enum": ["dependencies", "peerDependencies"], 72 | "default": "peerDependencies" 73 | }, 74 | "external": { 75 | "description": "A list projects to be treated as external. This feature is experimental", 76 | "oneOf": [ 77 | { 78 | "type": "string", 79 | "enum": ["all", "none"] 80 | }, 81 | { 82 | "type": "array", 83 | "items": { 84 | "type": "string" 85 | } 86 | } 87 | ] 88 | }, 89 | "externalBuildTargets": { 90 | "type": "array", 91 | "items": { 92 | "type": "string" 93 | }, 94 | "description": "List of target names that annotate a build target for a project", 95 | "default": ["build"] 96 | } 97 | }, 98 | "required": ["main", "outputPath", "tsConfig"], 99 | "definitions": { 100 | "assetPattern": { 101 | "oneOf": [ 102 | { 103 | "type": "object", 104 | "properties": { 105 | "glob": { 106 | "type": "string", 107 | "description": "The pattern to match." 108 | }, 109 | "input": { 110 | "type": "string", 111 | "description": "The input directory path in which to apply 'glob'. Defaults to the project root." 112 | }, 113 | "ignore": { 114 | "description": "An array of globs to ignore.", 115 | "type": "array", 116 | "items": { 117 | "type": "string" 118 | } 119 | }, 120 | "output": { 121 | "type": "string", 122 | "description": "Absolute path within the output." 123 | } 124 | }, 125 | "additionalProperties": false, 126 | "required": ["glob", "input", "output"] 127 | }, 128 | { 129 | "type": "string" 130 | } 131 | ] 132 | }, 133 | "transformerPattern": { 134 | "oneOf": [ 135 | { 136 | "type": "string" 137 | }, 138 | { 139 | "type": "object", 140 | "properties": { 141 | "name": { 142 | "type": "string" 143 | }, 144 | "options": { 145 | "type": "object", 146 | "additionalProperties": true 147 | } 148 | }, 149 | "additionalProperties": false, 150 | "required": ["name"] 151 | } 152 | ] 153 | } 154 | }, 155 | "examplesFile": "../../../docs/tsc-examples.md" 156 | } 157 | -------------------------------------------------------------------------------- /packages/sam/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/load-cloud-formation-template'; 2 | export * from './utils/loadEnvironmentVariablesForStackLambdas'; 3 | export * from './utils/loadEnvFromStack'; 4 | export * from './lambda'; 5 | -------------------------------------------------------------------------------- /packages/sam/src/lambda/index.ts: -------------------------------------------------------------------------------- 1 | export * from './lambda'; 2 | -------------------------------------------------------------------------------- /packages/sam/src/lambda/lambda.ts: -------------------------------------------------------------------------------- 1 | import { 2 | LambdaEnvironmentVariables, 3 | parseEnvironmentVariables, 4 | } from './parseEnvironmentVariables'; 5 | import { CloudWatchLogsEvent } from 'aws-lambda'; 6 | 7 | export interface BaseLambdaConfig { 8 | environmentVariables: readonly EV[]; 9 | } 10 | 11 | interface BaseLambdaParameters { 12 | env: LambdaEnvironmentVariables; 13 | } 14 | 15 | interface CloudWatchLogsEventLambdaParams 16 | extends BaseLambdaParameters { 17 | event: CloudWatchLogsEvent; 18 | } 19 | 20 | export interface CloudWatchLogsEventLambdaConfig 21 | extends BaseLambdaConfig { 22 | type: 'CloudWatchLogs'; 23 | handler: (params: CloudWatchLogsEventLambdaParams) => Promise; 24 | } 25 | 26 | type LambdaConfig = CloudWatchLogsEventLambdaConfig; 27 | 28 | export const lambda = (config: LambdaConfig) => { 29 | const env = parseEnvironmentVariables(config.environmentVariables); 30 | return (event: CloudWatchLogsEvent) => { 31 | return config.handler({ event, env }); 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /packages/sam/src/lambda/parseEnvironmentVariables.ts: -------------------------------------------------------------------------------- 1 | import { RequiredKeys, OptionalKeys } from 'ts-essentials'; 2 | import { getOwnStringProperties } from '../utils/guards'; 3 | 4 | /** 5 | * see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html 6 | */ 7 | interface ReserverLambdaEnvironmentVariables { 8 | /** 9 | * The handler location configured on the function. 10 | */ 11 | _HANDLER: string; 12 | /** 13 | * The AWS Region where the Lambda function is executed. 14 | */ 15 | AWS_REGION: string; 16 | /** 17 | * The runtime identifier, prefixed by AWS_Lambda_—for example, AWS_Lambda_java8. 18 | */ 19 | AWS_EXECUTION_ENV: string; 20 | /** 21 | * The name of the function. 22 | */ 23 | AWS_LAMBDA_FUNCTION_NAME: string; 24 | /** 25 | * The amount of memory available to the function in MB. 26 | */ 27 | AWS_LAMBDA_FUNCTION_MEMORY_SIZE: string; 28 | /** 29 | * The version of the function being executed. 30 | */ 31 | AWS_LAMBDA_FUNCTION_VERSION: string; 32 | /** 33 | * The name of the Amazon CloudWatch Logs group for the function. 34 | */ 35 | AWS_LAMBDA_LOG_GROUP_NAME: string; 36 | /** 37 | * The name of the Amazon CloudWatch Logs stream for the function. 38 | */ 39 | AWS_LAMBDA_LOG_STREAM_NAME: string; 40 | /** 41 | * The access keys obtained from the function's execution role. 42 | */ 43 | AWS_ACCESS_KEY_ID: string; 44 | /** 45 | * The access keys obtained from the function's execution role. 46 | */ 47 | AWS_SECRET_ACCESS_KEY: string; 48 | /** 49 | * The access keys obtained from the function's execution role. 50 | */ 51 | AWS_SESSION_TOKEN: string; 52 | /** 53 | * (Custom runtime) The host and port of the runtime API. 54 | */ 55 | AWS_LAMBDA_RUNTIME_API?: string; 56 | /** 57 | * The path to your Lambda function code. 58 | */ 59 | LAMBDA_TASK_ROOT: string; 60 | /** 61 | * The path to runtime libraries. 62 | */ 63 | LAMBDA_RUNTIME_DIR: string; 64 | /** 65 | * The environment's time zone (UTC). The execution environment uses NTP to synchronize the system clock. 66 | */ 67 | TZ: string; 68 | } 69 | 70 | /** 71 | * see https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html 72 | */ 73 | interface UnreservedLambdaEnvironmentVariables { 74 | /** 75 | * The locale of the runtime (en_US.UTF-8). 76 | */ 77 | LANG: string; 78 | /** 79 | * The execution path (/usr/local/bin:/usr/bin/:/bin:/opt/bin). 80 | */ 81 | PATH: string; 82 | /** 83 | * The system library path (/lib64:/usr/lib64:$LAMBDA_RUNTIME_DIR:$LAMBDA_RUNTIME_DIR/lib:$LAMBDA_TASK_ROOT:$LAMBDA_TASK_ROOT/lib:/opt/lib). 84 | */ 85 | LD_LIBRARY_PATH: string; 86 | /** 87 | * (Node.js) The Node.js library path (/opt/nodejs/node12/node_modules/:/opt/nodejs/node_modules:$LAMBDA_RUNTIME_DIR/node_modules). 88 | */ 89 | NODE_PATH: string; 90 | /** 91 | * The X-Ray tracing header. 92 | */ 93 | _X_AMZN_TRACE_ID?: string; 94 | /** 95 | * For X-Ray tracing, Lambda sets this to LOG_ERROR to avoid throwing runtime errors from the X-Ray SDK. 96 | */ 97 | AWS_XRAY_CONTEXT_MISSING?: string; 98 | /** 99 | * For X-Ray tracing, the IP address and port of the X-Ray daemon. 100 | */ 101 | AWS_XRAY_DAEMON_ADDRESS?: string; 102 | } 103 | 104 | /** 105 | * see: https://docs.aws.amazon.com/lambda/latest/dg/configuration-envvars.html 106 | * note: only includes node env variables, not eg. python / ruby 107 | */ 108 | type CoreLambdaEnvironmentVariables = ReserverLambdaEnvironmentVariables & 109 | UnreservedLambdaEnvironmentVariables; 110 | 111 | type RequiredEnvVariableName = RequiredKeys; 112 | type OptionalEnvVariableName = OptionalKeys; 113 | 114 | export type LambdaEnvironmentVariables = 115 | CoreLambdaEnvironmentVariables & { [key in EV]: string }; 116 | 117 | const requiredEnvVariables = getRequiredKeys(); 118 | const optionalEnvVariables = getOptionalKeys(); 119 | 120 | export function parseEnvironmentVariables( 121 | keys: readonly K[] 122 | ): LambdaEnvironmentVariables { 123 | return getOwnStringProperties({ 124 | keys: [...requiredEnvVariables, ...keys], 125 | optionalKeys: optionalEnvVariables, 126 | object: process.env, 127 | }); 128 | } 129 | 130 | function getRequiredKeys(): RequiredEnvVariableName[] { 131 | const dummy: Record = { 132 | AWS_ACCESS_KEY_ID: true, 133 | AWS_EXECUTION_ENV: true, 134 | AWS_LAMBDA_FUNCTION_MEMORY_SIZE: true, 135 | AWS_LAMBDA_FUNCTION_NAME: true, 136 | AWS_LAMBDA_FUNCTION_VERSION: true, 137 | AWS_LAMBDA_LOG_GROUP_NAME: true, 138 | AWS_LAMBDA_LOG_STREAM_NAME: true, 139 | AWS_REGION: true, 140 | AWS_SECRET_ACCESS_KEY: true, 141 | AWS_SESSION_TOKEN: true, 142 | LAMBDA_RUNTIME_DIR: true, 143 | LAMBDA_TASK_ROOT: true, 144 | LANG: true, 145 | LD_LIBRARY_PATH: true, 146 | NODE_PATH: true, 147 | PATH: true, 148 | TZ: true, 149 | _HANDLER: true, 150 | }; 151 | return Object.getOwnPropertyNames(dummy) as RequiredEnvVariableName[]; 152 | } 153 | function getOptionalKeys(): OptionalEnvVariableName[] { 154 | const dummy: Record = { 155 | AWS_LAMBDA_RUNTIME_API: true, 156 | AWS_XRAY_CONTEXT_MISSING: true, 157 | AWS_XRAY_DAEMON_ADDRESS: true, 158 | _X_AMZN_TRACE_ID: true, 159 | }; 160 | return Object.getOwnPropertyNames(dummy) as OptionalEnvVariableName[]; 161 | } 162 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/application/application.spec.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { createEmptyWorkspace } from '@nrwl/workspace/testing'; 3 | import { runSchematic } from '../../utils/testing'; 4 | import { readJsonInTree } from '@nrwl/workspace'; 5 | 6 | describe('app', () => { 7 | let appTree: Tree; 8 | 9 | beforeEach(() => { 10 | appTree = Tree.empty(); 11 | appTree = createEmptyWorkspace(appTree); 12 | }); 13 | 14 | it('should generate files', async () => { 15 | const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree); 16 | expect( 17 | tree.readContent('apps/my-node-app/src/app/hello/hello.ts') 18 | ).toContain(`export const handler = lambda`); 19 | expect(tree.exists('apps/my-node-app/src/template.yaml')).toBeTruthy(); 20 | }); 21 | 22 | it('should have es2018 as the tsconfig target', async () => { 23 | const tree = await runSchematic('app', { name: 'myNodeApp' }, appTree); 24 | const tsconfig = readJsonInTree( 25 | tree, 26 | 'apps/my-node-app/tsconfig.app.json' 27 | ); 28 | expect(tsconfig.compilerOptions.target).toBe('es2018'); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/application/application.ts: -------------------------------------------------------------------------------- 1 | import { 2 | apply, 3 | chain, 4 | externalSchematic, 5 | mergeWith, 6 | move, 7 | Rule, 8 | SchematicContext, 9 | template, 10 | Tree, 11 | url, 12 | } from '@angular-devkit/schematics'; 13 | import { join, normalize, Path } from '@angular-devkit/core'; 14 | import { Schema } from './schema'; 15 | import { formatFiles, names, updateJsonInTree } from '@nrwl/workspace'; 16 | import init from '../init/init'; 17 | import { 18 | appsDir, 19 | updateWorkspaceInTree, 20 | } from '@nrwl/workspace/src/utils/ast-utils'; 21 | // there's an implicit dependency on @nrwl/node 22 | import '@nrwl/node'; 23 | 24 | interface NormalizedSchema extends Schema { 25 | appProjectRoot: Path; 26 | } 27 | type BuilderConfig = { 28 | builder: string; 29 | options: Record; 30 | configurations?: Record; 31 | }; 32 | 33 | type Architect = Record; 34 | 35 | function updateWorkspaceJson(options: NormalizedSchema): Rule { 36 | return updateWorkspaceInTree((workspaceJson) => { 37 | const project: { architect: Architect } = 38 | workspaceJson.projects[options.name]; 39 | 40 | project.architect.build.builder = '@nx-aws/sam:build'; 41 | project.architect.build.options = { 42 | ...project.architect.build.options, 43 | template: `${options.appProjectRoot}/src/template.yaml`, 44 | sourceMap: true, 45 | maxWorkers: 1, 46 | }; 47 | 48 | project.architect.serve = { 49 | builder: '@nx-aws/sam:execute', 50 | options: { 51 | buildTarget: `${options.name}:build`, 52 | packageTarget: `${options.name}:package`, 53 | }, 54 | }; 55 | 56 | project.architect.package = { 57 | builder: '@nx-aws/sam:package', 58 | options: { 59 | templateFile: `${options.appProjectRoot}/src/template.yaml`, 60 | outputTemplateFile: `dist/${options.appProjectRoot}/serverless-output.yaml`, 61 | s3Prefix: `${options.appProjectRoot}`, 62 | }, 63 | configurations: { 64 | production: {}, 65 | }, 66 | }; 67 | 68 | project.architect.deploy = { 69 | builder: '@nx-aws/sam:deploy', 70 | options: { 71 | templateFile: `dist/${options.appProjectRoot}/serverless-output.yaml`, 72 | s3Prefix: `${options.appProjectRoot}`, 73 | capabilities: ['CAPABILITY_IAM', 'CAPABILITY_AUTO_EXPAND'], 74 | }, 75 | configurations: { 76 | production: { 77 | stackSuffix: 'prod', 78 | }, 79 | }, 80 | }; 81 | 82 | return workspaceJson; 83 | }); 84 | } 85 | 86 | function removeMainFile(options: NormalizedSchema): Rule { 87 | return (host: Tree) => { 88 | host.delete(join(options.appProjectRoot, 'src/main.ts')); 89 | }; 90 | } 91 | 92 | function addAppFiles(options: NormalizedSchema): Rule { 93 | return mergeWith( 94 | apply(url(`./files`), [ 95 | template({ 96 | tmpl: '', 97 | name: options.name, 98 | root: options.appProjectRoot, 99 | }), 100 | move(join(options.appProjectRoot, 'src')), 101 | ]) 102 | ); 103 | } 104 | 105 | export default function (schema: Schema): Rule { 106 | return (host: Tree, context: SchematicContext) => { 107 | const options = normalizeOptions(host, schema); 108 | return chain([ 109 | init({ 110 | ...options, 111 | skipFormat: true, 112 | }), 113 | externalSchematic('@nrwl/node', 'application', schema), 114 | updateWorkspaceJson(options), 115 | removeMainFile(options), 116 | addAppFiles(options), 117 | updateJsonInTree( 118 | join(options.appProjectRoot, 'tsconfig.app.json'), 119 | (json) => { 120 | json.compilerOptions.target = 'es2018'; 121 | return json; 122 | } 123 | ), 124 | formatFiles(options), 125 | ])(host, context); 126 | }; 127 | } 128 | 129 | function normalizeOptions(host: Tree, options: Schema): NormalizedSchema { 130 | const appDirectory = options.directory 131 | ? `${names(options.directory).fileName}/${names(options.name).fileName}` 132 | : names(options.name).fileName; 133 | const appProjectRoot = join(normalize(appsDir(host)), appDirectory); 134 | const appProjectName = names( 135 | appDirectory.replace(new RegExp('/', 'g'), '-') 136 | ).fileName; 137 | 138 | return { 139 | ...options, 140 | appProjectRoot, 141 | name: appProjectName, 142 | }; 143 | } 144 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/application/files/app/hello/hello.ts__tmpl__: -------------------------------------------------------------------------------- 1 | export const handler = async () => { 2 | return { 3 | statusCode: 200, 4 | body: JSON.stringify({ message: 'Welcome to <%= name %>!' }), 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/application/files/template.yaml__tmpl__: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Backend for <%= name %> 4 | 5 | Globals: 6 | Function: 7 | Runtime: nodejs12.x 8 | Timeout: 5 9 | MemorySize: 128 10 | 11 | Resources: 12 | # We use CloudFront to "unite" the statically hosted website and with the backend, so that they appear to be served 13 | # from a single site 14 | WebsiteCDN: 15 | Type: 'AWS::CloudFront::Distribution' 16 | Properties: 17 | DistributionConfig: 18 | Comment: !Sub '${AWS::StackName}' 19 | Enabled: 'true' 20 | 21 | # By default, serve from s3 22 | DefaultCacheBehavior: 23 | Compress: true 24 | ForwardedValues: 25 | QueryString: false 26 | TargetOriginId: protected-bucket-public 27 | ViewerProtocolPolicy: redirect-to-https 28 | 29 | # Add a behaviour to /api/* to forward requests to API Gateway 30 | CacheBehaviors: 31 | # allow all method for the backend to implement 32 | # NB: only certain combinations are possible here. To allow POST, we must allow all. 33 | - AllowedMethods: 34 | [HEAD, DELETE, POST, GET, OPTIONS, PUT, PATCH] 35 | Compress: true 36 | ForwardedValues: 37 | Headers: #define explicit headers, since API Gateway doesn't work otherwise 38 | - Accept 39 | - Referer 40 | - Authorization 41 | - Content-Type 42 | QueryString: true #to transfer get parameters to the gateway 43 | PathPattern: '/api/*' #path pattern after the Gateway stage identifier. 44 | TargetOriginId: api-origin #id of the orignin 45 | ViewerProtocolPolicy: https-only #API Gateway only support https 46 | MaxTTL: 0 47 | MinTTL: 0 48 | DefaultTTL: 0 49 | 50 | DefaultRootObject: index.html 51 | 52 | Origins: 53 | # S3 - public subdir - we keep this separate for ease of deploying additional ambient state elsewhere in the bucket 54 | - DomainName: !GetAtt WebBucket.RegionalDomainName 55 | Id: protected-bucket-public 56 | OriginPath: /public 57 | S3OriginConfig: 58 | OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${CloudFrontOriginIdentity}' 59 | 60 | # API Gateway 61 | - DomainName: #define the API Gateway origin 62 | Fn::Join: 63 | - '' 64 | - - Ref: Api 65 | - '.execute-api.' 66 | - Ref: AWS::Region 67 | - '.amazonaws.com' 68 | Id: api-origin 69 | CustomOriginConfig: 70 | OriginProtocolPolicy: https-only #again API-Gateway only supports https 71 | OriginPath: /Prod #name of the deployed stage 72 | 73 | CustomErrorResponses: 74 | - ErrorCode: 404 75 | ResponseCode: 200 76 | ResponsePagePath: /index.html 77 | 78 | HttpVersion: http2 79 | 80 | # CF identity which we can hang the private bucket access permissions on 81 | CloudFrontOriginIdentity: 82 | Type: AWS::CloudFront::CloudFrontOriginAccessIdentity 83 | Properties: 84 | CloudFrontOriginAccessIdentityConfig: 85 | Comment: !Sub 'Cloudfront OAI for ${AWS::StackName}:WebBucket' 86 | 87 | # Policy to allow CF to access the private webbucket 88 | WebBucketPolicy: 89 | Type: AWS::S3::BucketPolicy 90 | Properties: 91 | Bucket: 92 | Ref: WebBucket 93 | PolicyDocument: 94 | Version: '2012-10-17' 95 | Statement: 96 | - Effect: Allow 97 | Principal: 98 | CanonicalUser: !GetAtt CloudFrontOriginIdentity.S3CanonicalUserId 99 | Action: 100 | - 's3:GetObject' 101 | # ListBucket is required so that a 404 is returned on misses 102 | - 's3:ListBucket' 103 | Resource: 104 | - !Sub '${WebBucket.Arn}/*' 105 | - !Sub '${WebBucket.Arn}' 106 | 107 | # The WebBucket is where we store our SPA 108 | WebBucket: 109 | Type: 'AWS::S3::Bucket' 110 | 111 | Api: 112 | Type: AWS::Serverless::Api 113 | Properties: 114 | Name: 115 | Ref: AWS::StackName 116 | StageName: Prod 117 | EndpointConfiguration: REGIONAL 118 | 119 | Hello: 120 | # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction 121 | Type: AWS::Serverless::Function 122 | Properties: 123 | CodeUri: app/hello 124 | Handler: hello.handler 125 | Events: 126 | Hello: 127 | Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api 128 | Properties: 129 | Path: /api/hello 130 | Method: get 131 | RestApiId: 132 | Ref: Api 133 | 134 | Outputs: 135 | WebBucket: 136 | Value: !Ref WebBucket 137 | 138 | DistributionId: 139 | Value: !Ref WebsiteCDN 140 | 141 | Url: 142 | Value: !Sub 'https://${WebsiteCDN.DomainName}' 143 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/application/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { UnitTestRunner } from '../../utils/test-runners'; 2 | 3 | export interface Schema { 4 | name: string; 5 | skipFormat: boolean; 6 | skipPackageJson: boolean; 7 | directory?: string; 8 | unitTestRunner: UnitTestRunner; 9 | tags?: string; 10 | linter: 'eslint' | 'tslint'; 11 | frontendProject?: string; 12 | } 13 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/application/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "SchematicsNxAwsSamApp", 4 | "title": "Nx Application Options Schema", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "description": "The name of the application.", 9 | "type": "string", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "What name would you like to use for the node application?" 15 | }, 16 | "directory": { 17 | "description": "The directory of the new application.", 18 | "type": "string" 19 | }, 20 | "skipFormat": { 21 | "description": "Skip formatting files", 22 | "type": "boolean", 23 | "default": false 24 | }, 25 | "skipPackageJson": { 26 | "type": "boolean", 27 | "default": false, 28 | "description": "Do not add dependencies to package.json." 29 | }, 30 | "linter": { 31 | "description": "The tool to use for running lint checks.", 32 | "type": "string", 33 | "enum": ["eslint", "tslint"], 34 | "default": "eslint" 35 | }, 36 | "unitTestRunner": { 37 | "type": "string", 38 | "enum": ["jest", "none"], 39 | "description": "Test runner to use for unit tests", 40 | "default": "jest" 41 | }, 42 | "tags": { 43 | "type": "string", 44 | "description": "Add tags to the application (used for linting)" 45 | }, 46 | "frontendProject": { 47 | "type": "string", 48 | "description": "Frontend project that needs to access this application. This sets up proxy configuration." 49 | } 50 | }, 51 | "required": [] 52 | } 53 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/init/init.spec.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { createEmptyWorkspace } from '@nrwl/workspace/testing'; 3 | import { readJsonInTree } from '@nrwl/workspace'; 4 | import { runSchematic } from '../../utils/testing'; 5 | 6 | describe('init', () => { 7 | let tree: Tree; 8 | 9 | beforeEach(() => { 10 | tree = Tree.empty(); 11 | tree = createEmptyWorkspace(tree); 12 | }); 13 | 14 | it('should add dependencies', async () => { 15 | const result = await runSchematic('init', {}, tree); 16 | const packageJson = readJsonInTree(result, 'package.json'); 17 | 18 | expect(packageJson.dependencies['@nx-aws/sam']).toBeUndefined(); 19 | expect(packageJson.devDependencies['@nx-aws/sam']).toBeDefined(); 20 | }); 21 | 22 | describe('defaultCollection', () => { 23 | it('should be set if none was set before', async () => { 24 | const result = await runSchematic('init', {}, tree); 25 | const workspaceJson = readJsonInTree(result, 'workspace.json'); 26 | expect(workspaceJson.cli.defaultCollection).toEqual('@nx-aws/sam'); 27 | }); 28 | }); 29 | 30 | it('should not add jest config if unitTestRunner is none', async () => { 31 | const result = await runSchematic( 32 | 'init', 33 | { 34 | unitTestRunner: 'none', 35 | }, 36 | tree 37 | ); 38 | expect(result.exists('jest.config.js')).toEqual(false); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/init/init.ts: -------------------------------------------------------------------------------- 1 | import { chain, noop, Rule } from '@angular-devkit/schematics'; 2 | import { 3 | addDepsToPackageJson, 4 | addPackageWithInit, 5 | formatFiles, 6 | updateJsonInTree, 7 | } from '@nrwl/workspace'; 8 | import { Schema } from './schema'; 9 | 10 | export const updateDependencies = addDepsToPackageJson( 11 | {}, 12 | { 13 | '@nx-aws/sam': '*', 14 | } 15 | ); 16 | 17 | function moveDependency(): Rule { 18 | return updateJsonInTree('package.json', (json) => { 19 | json.dependencies = json.dependencies || {}; 20 | delete json.dependencies['@nx-aws/sam']; 21 | return json; 22 | }); 23 | } 24 | 25 | export default function (schema: Schema): Rule { 26 | return chain([ 27 | addPackageWithInit('@nrwl/node', schema), 28 | schema.unitTestRunner === 'jest' 29 | ? addPackageWithInit('@nrwl/jest') 30 | : noop(), 31 | updateDependencies, 32 | moveDependency(), 33 | formatFiles(schema), 34 | ]); 35 | } 36 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/init/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface Schema { 2 | unitTestRunner: 'jest' | 'none'; 3 | skipFormat: boolean; 4 | } 5 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/init/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "NxAwsSamInit", 4 | "title": "Add Nx Nest Schematics", 5 | "type": "object", 6 | "properties": { 7 | "unitTestRunner": { 8 | "description": "Adds the specified unit test runner", 9 | "type": "string", 10 | "enum": ["jest", "none"], 11 | "default": "jest" 12 | }, 13 | "e2eTestRunner": { 14 | "description": "Adds the specified e2e test runner", 15 | "type": "string", 16 | "enum": ["cypress", "none"], 17 | "default": "cypress" 18 | }, 19 | "skipFormat": { 20 | "description": "Skip formatting files", 21 | "type": "boolean", 22 | "default": false 23 | } 24 | }, 25 | "required": [] 26 | } 27 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/update-lambdas/getUpdates.spec.ts: -------------------------------------------------------------------------------- 1 | import { getUpdates } from './getUpdates'; 2 | 3 | describe('getUpdates', () => { 4 | it('should return the correct replacement for environment variables', () => { 5 | const sourceText = `export const demo = lambda({environmentVariables: []});`; 6 | const actual = getUpdates({ 7 | fileName: 'meow', 8 | functionName: 'demo', 9 | sourceText: sourceText, 10 | targetConfig: { environmentVariables: ['GOETH'] }, 11 | }); 12 | const start = sourceText.indexOf('[]'); 13 | const end = start + 2; 14 | const replacement = `['GOETH']`; 15 | expect(actual).toEqual([{ start, end, replacement }]); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/update-lambdas/getUpdates.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ArrayLiteralExpression, 3 | createSourceFile, 4 | Declaration, 5 | getCombinedModifierFlags, 6 | isArrayLiteralExpression, 7 | isCallExpression, 8 | isIdentifier, 9 | isObjectLiteralExpression, 10 | isPropertyAssignment, 11 | isVariableStatement, 12 | ModifierFlags, 13 | Node, 14 | ObjectLiteralExpression, 15 | ScriptTarget, 16 | SourceFile, 17 | VariableDeclaration, 18 | } from 'typescript'; 19 | 20 | export interface Replacement { 21 | start: number; 22 | end: number; 23 | replacement: string; 24 | } 25 | 26 | interface GetUpdatesInput { 27 | fileName: string; 28 | sourceText: string; 29 | functionName: string; 30 | targetConfig: { 31 | environmentVariables: string[]; 32 | }; 33 | } 34 | 35 | export function getUpdates({ 36 | fileName, 37 | sourceText, 38 | functionName, 39 | targetConfig, 40 | }: GetUpdatesInput): Replacement[] { 41 | // Build a program using the set of root file names in fileNames 42 | const sourceFile = createSourceFile( 43 | fileName, 44 | sourceText, 45 | ScriptTarget.Latest 46 | ); 47 | const config = getConfigObject(sourceFile, functionName); 48 | const environmentVariables = getEnvironmentVariablesArray( 49 | config, 50 | functionName 51 | ); 52 | return [ 53 | { 54 | start: environmentVariables.getStart(sourceFile), 55 | end: environmentVariables.getEnd(), 56 | replacement: `[${targetConfig.environmentVariables 57 | .map((e) => `'${e}'`) 58 | .join(',')}]`, 59 | }, 60 | ]; 61 | } 62 | 63 | function getEnvironmentVariablesArray( 64 | config: ObjectLiteralExpression, 65 | lambdaName: string 66 | ) { 67 | let environmentVariables: ArrayLiteralExpression | undefined; 68 | config.properties.forEach((prop) => { 69 | if ( 70 | prop.name && 71 | isIdentifier(prop.name) && 72 | prop.name.text === 'environmentVariables' && 73 | isPropertyAssignment(prop) 74 | ) { 75 | if (isArrayLiteralExpression(prop.initializer)) { 76 | environmentVariables = prop.initializer; 77 | } 78 | } 79 | }); 80 | if (!environmentVariables) { 81 | throw new Error( 82 | `Did not find environmentVariables array on config for ${lambdaName}` 83 | ); 84 | } 85 | return environmentVariables; 86 | } 87 | 88 | function getConfigObject(sourceFile: SourceFile, lambdaName: string) { 89 | const exportedVariable = findExportedVariable(sourceFile, lambdaName); //? 90 | if (!exportedVariable) { 91 | throw new Error( 92 | `Expected to find an exported variable with name ${lambdaName}` 93 | ); 94 | } 95 | const lambdaCall = exportedVariable.initializer; 96 | if (!lambdaCall || !isCallExpression(lambdaCall)) { 97 | throw new Error(`${lambdaName} should be a call expression to lambda`); 98 | } 99 | const config = lambdaCall.arguments[0]; 100 | if (!isObjectLiteralExpression(config)) { 101 | throw new Error( 102 | `First argument of ${lambdaName} should be the lambda configuration object literal` 103 | ); 104 | } 105 | return config; 106 | } 107 | 108 | function findExportedVariable(sourceFile: SourceFile, name: string) { 109 | let variableDeclaration: VariableDeclaration | undefined; 110 | sourceFile.forEachChild((needle) => { 111 | if ( 112 | isNodeExported(needle) && 113 | isVariableStatement(needle) && 114 | isIdentifier(needle.declarationList.declarations[0].name) && 115 | needle.declarationList.declarations[0].name.text === name 116 | ) { 117 | variableDeclaration = needle.declarationList.declarations[0]; 118 | } 119 | }); 120 | return variableDeclaration; 121 | } 122 | 123 | /** True if this is visible outside this file, false otherwise */ 124 | function isNodeExported(node: Node): boolean { 125 | return ( 126 | (getCombinedModifierFlags(node as Declaration) & 127 | ModifierFlags.Export) !== 128 | 0 129 | ); 130 | } 131 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/update-lambdas/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateLambdasSchematicSchema { 2 | projectName: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/update-lambdas/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "UpdateLambdas", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "projectName": { 8 | "type": "string", 9 | "description": "", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "Which project should we update lambdas for?" 15 | } 16 | }, 17 | "required": ["projectName"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/update-lambdas/updateLambdas.spec.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { createEmptyWorkspace } from '@nrwl/workspace/testing'; 3 | import { loadYamlFile, runSchematic } from '../../utils/testing'; 4 | import { dumpCloudformationTemplate } from '../../utils/dumpCloudformationTemplate'; 5 | 6 | import { UpdateLambdasSchematicSchema } from './schema'; 7 | 8 | describe('update-lambdas schematic', () => { 9 | let appTree: Tree; 10 | const options: UpdateLambdasSchematicSchema = { projectName: 'test' }; 11 | 12 | beforeEach(async () => { 13 | appTree = createEmptyWorkspace(Tree.empty()); 14 | appTree = await runSchematic( 15 | 'application', 16 | { 17 | name: 'test', 18 | linter: 'eslint', 19 | skipFormat: false, 20 | skipPackageJson: false, 21 | unitTestRunner: 'jest', 22 | }, 23 | appTree 24 | ); 25 | const template = loadYamlFile(appTree, 'apps/test/src/template.yaml'); 26 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 27 | template.Resources!.Hello!.Properties!.Environment = { 28 | Variables: { HELLO: 'World' }, 29 | }; 30 | appTree.overwrite( 31 | 'apps/test/src/template.yaml', 32 | dumpCloudformationTemplate(template) 33 | ); 34 | }); 35 | 36 | it('should run successfully', async () => { 37 | await expect( 38 | runSchematic('update-lambdas', options, appTree) 39 | ).resolves.not.toThrowError(); 40 | expect( 41 | appTree.read('apps/test/src/app/hello/hello.ts')?.toString('utf-8') 42 | ).toContain("['HELLO']"); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /packages/sam/src/schematics/update-lambdas/updateLambdas.ts: -------------------------------------------------------------------------------- 1 | import { chain, Rule, Tree } from '@angular-devkit/schematics'; 2 | import { formatFiles, getProjectConfig } from '@nrwl/workspace'; 3 | import Resource from 'cloudform-types/types/resource'; 4 | import { getLambdaSourcePath } from '../../utils/getLambdaSourcePath'; 5 | import { SamFunctionProperties } from '../../utils/SamFunctionProperties'; 6 | import { loadYamlFile } from '../../utils/testing'; 7 | import { getUpdates } from './getUpdates'; 8 | import { UpdateLambdasSchematicSchema } from './schema'; 9 | 10 | export default function (options: UpdateLambdasSchematicSchema): Rule { 11 | return chain([ 12 | updateLambdasInTree(options), 13 | formatFiles({ skipFormat: false }), 14 | ]); 15 | } 16 | 17 | function updateLambdasInTree(options: UpdateLambdasSchematicSchema) { 18 | return (tree: Tree): void => { 19 | const project = getProjectConfig(tree, options.projectName); 20 | const templateFile: string | undefined = 21 | project?.architect?.package?.options?.templateFile; 22 | if (!templateFile) { 23 | throw new Error( 24 | `Couldn't find templateFile option on ${project}:package` 25 | ); 26 | } 27 | const template = loadYamlFile(tree, templateFile); 28 | const resources = template.Resources; 29 | if (!resources) { 30 | return; 31 | } 32 | updateLambdasFromResources(tree, resources, templateFile); 33 | }; 34 | } 35 | 36 | function updateLambdasFromResources( 37 | tree: Tree, 38 | resources: Record, 39 | templatePath: string 40 | ) { 41 | for (const resourceName in resources) { 42 | if (Object.prototype.hasOwnProperty.call(resources, resourceName)) { 43 | const resource = resources[resourceName]; 44 | if (resource.Type.endsWith('Function')) { 45 | updateFunction(resource, tree, resourceName, templatePath); 46 | } 47 | } 48 | } 49 | } 50 | 51 | function updateFunction( 52 | resource: Resource, 53 | tree: Tree, 54 | resourceName: string, 55 | templatePath: string 56 | ) { 57 | const properties: Partial | undefined = 58 | resource.Properties; 59 | if (!properties) { 60 | console.warn(`No properties on function ${resourceName}`); 61 | return; 62 | } 63 | const { sourcePath, functionName, sourceText } = getSourceText( 64 | properties, 65 | resourceName, 66 | templatePath, 67 | tree 68 | ); 69 | const envVarsObject = properties?.Environment?.Variables; 70 | const environmentVariables = envVarsObject 71 | ? Object.keys(envVarsObject) 72 | : []; 73 | const updates = getUpdates({ 74 | fileName: sourcePath, 75 | functionName, 76 | sourceText, 77 | targetConfig: { environmentVariables }, 78 | }); 79 | const recorder = tree.beginUpdate(sourcePath); 80 | updates.forEach((update) => { 81 | recorder.remove(update.start, update.end - update.start); 82 | recorder.insertLeft(update.start, update.replacement); 83 | }); 84 | tree.commitUpdate(recorder); 85 | } 86 | 87 | function getSourceText( 88 | properties: Partial, 89 | resourceName: string, 90 | templatePath: string, 91 | tree: Tree 92 | ) { 93 | const { src: sourcePath, functionName } = getLambdaSourcePath( 94 | templatePath, 95 | resourceName, 96 | properties 97 | ); 98 | const sourceText = tree.read(sourcePath)?.toString('utf-8'); 99 | if (!sourceText) { 100 | throw new Error(`${sourcePath} was empty`); 101 | } 102 | return { sourceText, functionName, sourcePath }; 103 | } 104 | -------------------------------------------------------------------------------- /packages/sam/src/utils/SamFunctionProperties.ts: -------------------------------------------------------------------------------- 1 | import { FunctionProperties } from 'cloudform-types/types/lambda/function'; 2 | 3 | export interface SamFunctionProperties extends FunctionProperties { 4 | CodeUri?: string; 5 | } 6 | -------------------------------------------------------------------------------- /packages/sam/src/utils/dumpCloudformationTemplate.ts: -------------------------------------------------------------------------------- 1 | import { dump } from 'js-yaml'; 2 | import { CLOUDFORMATION_SCHEMA } from 'cloudformation-js-yaml-schema'; 3 | import { ParsedSamTemplate } from './load-cloud-formation-template'; 4 | 5 | export function dumpCloudformationTemplate( 6 | cloudFormation: ParsedSamTemplate 7 | ): string { 8 | return dump(cloudFormation, { schema: CLOUDFORMATION_SCHEMA }); 9 | } 10 | -------------------------------------------------------------------------------- /packages/sam/src/utils/getLambdaSourcePath.ts: -------------------------------------------------------------------------------- 1 | import { parse, join } from 'path'; 2 | import { SamFunctionProperties } from './SamFunctionProperties'; 3 | 4 | export function getLambdaSourcePath( 5 | templatePath: string, 6 | resourceName: string, 7 | properties: Partial, 8 | globalProperties?: Pick< 9 | Partial, 10 | 'CodeUri' | 'Handler' 11 | > 12 | ): { src: string; dir: string; functionName: string } { 13 | const { dir } = parse(templatePath); 14 | // fallback to global CodeUri if function doesn't specify one 15 | const codeUri = properties.CodeUri || globalProperties?.CodeUri; 16 | // fallback to global Handler if function doesn't specify one 17 | const handler = properties.Handler || globalProperties?.Handler; 18 | 19 | if (!codeUri) { 20 | throw new Error(`Property CodeUri was missing on ${resourceName}`); 21 | } 22 | if (typeof handler !== 'string') { 23 | throw new Error( 24 | `Property Handler was missing or malformed on ${resourceName}` 25 | ); 26 | } 27 | const handlerParts = handler.split('.'); 28 | const functionName = handlerParts.pop(); 29 | if (!functionName) { 30 | throw new Error( 31 | `Property Handler did not contain the function name: ${handler}` 32 | ); 33 | } 34 | const fileName = [...handlerParts, 'ts'].join('.'); 35 | const filePath = join(codeUri, fileName); 36 | const src = join(dir, filePath); 37 | return { src, dir, functionName }; 38 | } 39 | -------------------------------------------------------------------------------- /packages/sam/src/utils/getParameterOverrides.ts: -------------------------------------------------------------------------------- 1 | import { BuilderContext } from '@angular-devkit/architect'; 2 | import { JsonObject } from '@angular-devkit/core'; 3 | import { underscore } from '@angular-devkit/core/src/utils/strings'; 4 | import { ImportStackOutputs, OutputValueRetriever } from '@nx-aws/core'; 5 | import { IParameterOverrides } from '../builders/cloudformation/deploy/IParameterOverrides'; 6 | import { loadCloudFormationTemplate } from './load-cloud-formation-template'; 7 | 8 | export async function getParameterOverrides( 9 | options: { 10 | templateFile: string; 11 | importStackOutputs: (ImportStackOutputs & JsonObject) | null; 12 | parameterOverrides: IParameterOverrides | null; 13 | }, 14 | context: BuilderContext, 15 | stackSuffix: string | undefined 16 | ): Promise { 17 | const cf = loadCloudFormationTemplate(options.templateFile); 18 | const parameters = cf.Parameters; 19 | if (!parameters) { 20 | return {}; 21 | } 22 | const importStackOutputs = options.importStackOutputs; 23 | const overrides: IParameterOverrides = {}; 24 | if (importStackOutputs) { 25 | const outputValueRetriever = new OutputValueRetriever(); 26 | // retrieve the values from the other projects 27 | const values = await outputValueRetriever.getOutputValues( 28 | importStackOutputs, 29 | context, 30 | stackSuffix 31 | ); 32 | options.parameterOverrides = { 33 | ...(options.parameterOverrides || {}), 34 | ...values, 35 | }; 36 | } 37 | for (const key in parameters) { 38 | if (Object.prototype.hasOwnProperty.call(parameters, key)) { 39 | if ( 40 | options.parameterOverrides && 41 | Object.prototype.hasOwnProperty.call( 42 | options.parameterOverrides, 43 | key 44 | ) 45 | ) { 46 | overrides[key] = options.parameterOverrides[key]; 47 | } else { 48 | const envKey = underscore(key).toUpperCase(); 49 | const value = process.env[envKey]; 50 | if (value) { 51 | context.logger.info( 52 | `Retrieved parameter override ${key} from environment variable ${envKey}` 53 | ); 54 | overrides[key] = value; 55 | } else if (!parameters[key].Default) { 56 | context.logger.fatal( 57 | `Missing parameter override ${key}; deploy will likely fail` 58 | ); 59 | } 60 | } 61 | } 62 | } 63 | return overrides; 64 | } 65 | -------------------------------------------------------------------------------- /packages/sam/src/utils/getTag.ts: -------------------------------------------------------------------------------- 1 | import { CLOUDFORMATION_SCHEMA } from 'cloudformation-js-yaml-schema'; 2 | import { Type } from 'js-yaml'; 3 | export function getTag(name: string, value: any): Type { 4 | const types: Array< 5 | Type & { 6 | tag: string; 7 | } 8 | > = CLOUDFORMATION_SCHEMA.explicit; 9 | const tag = types.filter((t) => t.tag === `!${name}`)[0]; 10 | if (!tag) { 11 | throw new Error(`Could not find tag called ${name}`); 12 | } 13 | return tag.construct(value); 14 | } 15 | -------------------------------------------------------------------------------- /packages/sam/src/utils/guards/getOwnStringProperties.ts: -------------------------------------------------------------------------------- 1 | import { hasOwnStringProperties } from './hasOwnStringProperties'; 2 | 3 | export function getOwnStringProperties({ 4 | keys, 5 | optionalKeys = [], 6 | object, 7 | }: { 8 | keys: K[]; 9 | optionalKeys?: O[]; 10 | object: unknown; 11 | }): { [key in K]: string } & { [key in O]?: string } { 12 | if (!hasOwnStringProperties(keys, optionalKeys, object)) { 13 | throw new Error( 14 | `Exepected object to have a string at key ${keys.join( 15 | ', ' 16 | )} but it did not` 17 | ); 18 | } 19 | return object; 20 | } 21 | -------------------------------------------------------------------------------- /packages/sam/src/utils/guards/getOwnStringProperty.ts: -------------------------------------------------------------------------------- 1 | import { hasOwnStringProperties } from './hasOwnStringProperties'; 2 | 3 | export function getOwnStringProperty( 4 | key: K, 5 | object: unknown 6 | ): string { 7 | if (!hasOwnStringProperties([key], [], object)) { 8 | throw new Error( 9 | `Exepected object to have a string at key ${key} but it did not` 10 | ); 11 | } 12 | return object[key]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/sam/src/utils/guards/hasOwnProperties.ts: -------------------------------------------------------------------------------- 1 | export function hasOwnProperties( 2 | keys: K[], 3 | _optionalKeys: OK[], 4 | object: unknown 5 | ): object is Record { 6 | if (typeof object !== 'object' || !object) { 7 | return false; 8 | } 9 | return keys.every((key) => 10 | Object.prototype.hasOwnProperty.call(object, key) 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /packages/sam/src/utils/guards/hasOwnProperty.ts: -------------------------------------------------------------------------------- 1 | export function hasOwnProperty( 2 | key: K, 3 | object: unknown 4 | ): object is Record { 5 | if (typeof object !== 'object' || !object) { 6 | return false; 7 | } 8 | return Object.prototype.hasOwnProperty.call(object, key); 9 | } 10 | -------------------------------------------------------------------------------- /packages/sam/src/utils/guards/hasOwnStringProperties.ts: -------------------------------------------------------------------------------- 1 | import { hasOwnProperties } from './hasOwnProperties'; 2 | 3 | export function hasOwnStringProperties( 4 | keys: K[], 5 | optionalKeys: OK[], 6 | object: unknown 7 | ): object is Record & Record { 8 | if (!hasOwnProperties(keys, optionalKeys, object)) { 9 | return false; 10 | } 11 | const mandatory = keys.every((key) => typeof key === 'string'); 12 | if (!mandatory) { 13 | return false; 14 | } 15 | return ( 16 | optionalKeys.length === 0 || 17 | optionalKeys.every( 18 | (key) => typeof key === 'undefined' || typeof key === 'string' 19 | ) 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/sam/src/utils/guards/index.ts: -------------------------------------------------------------------------------- 1 | export * from './hasOwnProperties'; 2 | export * from './hasOwnProperty'; 3 | export * from './hasOwnStringProperties'; 4 | export * from './getOwnStringProperty'; 5 | export * from './getOwnStringProperties'; 6 | -------------------------------------------------------------------------------- /packages/sam/src/utils/load-cloud-formation-template.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { load } from 'js-yaml'; 3 | import { CLOUDFORMATION_SCHEMA } from 'cloudformation-js-yaml-schema'; 4 | import Template from 'cloudform-types/types/template'; 5 | 6 | /** 7 | * Partial list of supported properties for the Globals property 8 | * @since 0.7.0 9 | */ 10 | export interface Globals { 11 | Function?: FunctionResource; 12 | } 13 | 14 | /** 15 | * Partial list of supported values the Function attribute of the Globals property 16 | * 17 | * @since 0.7.0 18 | */ 19 | export interface FunctionResource { 20 | Handler?: string; 21 | CodeUri?: 22 | | string 23 | | { 24 | Bucket: string; 25 | Key: string; 26 | Version: string; 27 | }; 28 | } 29 | 30 | export type ParsedSamTemplate = Template & { 31 | Globals: Globals; 32 | }; 33 | 34 | export function loadCloudFormationTemplate( 35 | templatePath: string 36 | ): ParsedSamTemplate { 37 | const yaml = readFileSync(templatePath, { encoding: 'utf-8' }); 38 | const cf: ParsedSamTemplate = parseCloudFormationTemplate(yaml); 39 | return cf; 40 | } 41 | 42 | export function parseCloudFormationTemplate(yaml: string): ParsedSamTemplate { 43 | return load(yaml, { 44 | schema: CLOUDFORMATION_SCHEMA, 45 | }) as ParsedSamTemplate; 46 | } 47 | -------------------------------------------------------------------------------- /packages/sam/src/utils/loadEnvFromStack.ts: -------------------------------------------------------------------------------- 1 | import { Observable, of, from } from 'rxjs'; 2 | import { formatStackName } from '@nx-aws/core'; 3 | import { loadEnvironmentVariablesForStackLambdas } from './loadEnvironmentVariablesForStackLambdas'; 4 | 5 | export function loadEnvFromStack( 6 | mimicEnv: string | undefined, 7 | project: string | undefined 8 | ): Observable> { 9 | if (mimicEnv && project) { 10 | const stackName = formatStackName(project, undefined, mimicEnv); 11 | console.log(`Getting environment variables for ${stackName}`); 12 | return from(loadEnvironmentVariablesForStackLambdas(stackName)); 13 | } 14 | return of({}); 15 | } 16 | -------------------------------------------------------------------------------- /packages/sam/src/utils/loadEnvironmentVariablesForStackLambdas.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CloudFormationClient, 3 | DescribeStackResourcesCommand, 4 | } from '@aws-sdk/client-cloudformation'; 5 | import { GetFunctionCommand, LambdaClient } from '@aws-sdk/client-lambda'; 6 | 7 | // force AWS SDK to load config, in case region is set there 8 | process.env.AWS_SDK_LOAD_CONFIG = '1'; 9 | 10 | export async function loadEnvironmentVariablesForStackLambdas( 11 | stackName: string 12 | ) { 13 | const cf = new CloudFormationClient({}); 14 | const lambda = new LambdaClient({}); 15 | 16 | const stackDesc = await cf.send( 17 | new DescribeStackResourcesCommand({ StackName: stackName }) 18 | ); 19 | 20 | const stackResources = stackDesc.StackResources; 21 | if (!stackResources) { 22 | return {}; 23 | } 24 | 25 | const envVars: Record = {}; 26 | 27 | for (const resource of stackResources) { 28 | if (resource.ResourceType === 'AWS::Lambda::Function') { 29 | const id = resource.PhysicalResourceId; 30 | if (id) { 31 | const fndesc = await lambda.send( 32 | new GetFunctionCommand({ FunctionName: id }) 33 | ); 34 | const variables = fndesc.Configuration?.Environment?.Variables; 35 | Object.assign(envVars, variables); 36 | } 37 | } 38 | } 39 | 40 | process.env = { ...envVars, ...process.env }; 41 | 42 | return envVars; 43 | } 44 | -------------------------------------------------------------------------------- /packages/sam/src/utils/testing.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path'; 2 | import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; 3 | import { Rule, Tree } from '@angular-devkit/schematics'; 4 | import { 5 | parseCloudFormationTemplate, 6 | ParsedSamTemplate, 7 | } from './load-cloud-formation-template'; 8 | 9 | const testRunner = new SchematicTestRunner( 10 | '@nrwl/nest', 11 | join(__dirname, '../../collection.json') 12 | ); 13 | 14 | export function runSchematic(schematicName: string, options: any, tree: Tree) { 15 | return testRunner 16 | .runSchematicAsync(schematicName, options, tree) 17 | .toPromise(); 18 | } 19 | 20 | export function callRule(rule: Rule, tree: Tree) { 21 | return testRunner.callRule(rule, tree).toPromise(); 22 | } 23 | 24 | export function loadYamlFile( 25 | tree: Tree, 26 | templateFile: string 27 | ): ParsedSamTemplate { 28 | const templateYaml = tree.read(templateFile)?.toString('utf-8'); 29 | if (!templateYaml) { 30 | throw new Error( 31 | `Couldn't find could not read template from ${templateFile}` 32 | ); 33 | } 34 | const template = parseCloudFormationTemplate(templateYaml); 35 | return template; 36 | } 37 | -------------------------------------------------------------------------------- /packages/sam/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /packages/sam/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "types": ["node"] 8 | }, 9 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/sam/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.test.ts", 11 | "**/*.spec.tsx", 12 | "**/*.test.tsx", 13 | "**/*.spec.js", 14 | "**/*.test.js", 15 | "**/*.spec.jsx", 16 | "**/*.test.jsx", 17 | "**/*.d.ts", 18 | "jest.config.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workspace", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "semver": { 6 | "executor": "@jscutlery/semver:version", 7 | "options": { 8 | "syncVersions": true 9 | } 10 | } 11 | }, 12 | "tags": [] 13 | } 14 | -------------------------------------------------------------------------------- /tools/generators/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/studds/nx-aws/e188ca4af7e56a81118bff58c26eee805e44695b/tools/generators/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2018", 12 | "module": "esnext", 13 | "typeRoots": ["node_modules/@types"], 14 | "lib": ["es2018"], 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noImplicitReturns": true, 22 | "noFallthroughCasesInSwitch": true, 23 | "esModuleInterop": true, 24 | "paths": { 25 | "@nx-aws/core": ["packages/core/src/index.ts"], 26 | "@nx-aws/s3": ["packages/s3/src/index.ts"], 27 | "@nx-aws/sam": ["packages/sam/src/index.ts"], 28 | "@nx-aws/test": ["packages/test/src/index.ts"] 29 | } 30 | }, 31 | "exclude": ["node_modules", "tmp"] 32 | } 33 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = () => { 2 | return { 3 | autoDetect: true, 4 | }; 5 | }; 6 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "projects": { 4 | "core": "packages/core", 5 | "s3": "packages/s3", 6 | "s3-e2e": "e2e/s3-e2e", 7 | "sam": "packages/sam", 8 | "sam-e2e": "e2e/sam-e2e", 9 | "workspace": "." 10 | }, 11 | "$schema": "./node_modules/nx/schemas/workspace-schema.json" 12 | } 13 | --------------------------------------------------------------------------------