├── .circleci └── config.yml ├── .editorconfig ├── .eslintrc ├── .github └── ISSUE_TEMPLATE │ ├── bug_report_nx-deploy-it.md │ └── feature_request_nx-deploy-it.md ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── apps ├── .gitkeep └── nx-deploy-it-e2e │ ├── jest.config.js │ ├── tests │ └── nx-deploy-it.test.ts │ ├── tsconfig.json │ └── tsconfig.spec.json ├── jest.config.js ├── libs ├── .gitkeep └── nx-deploy-it │ ├── .eslintrc │ ├── README.md │ ├── builders.json │ ├── collection.json │ ├── docs │ └── nx-deploy-it-aws.gif │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── adapter │ │ ├── angular-universal │ │ │ ├── angular-universal.adapter.ts │ │ │ └── deployment-type.enum.ts │ │ ├── base.adapter.model.ts │ │ ├── base.adapter.ts │ │ ├── express │ │ │ └── express.adapter.ts │ │ ├── nestjs │ │ │ └── nestjs.adapter.ts │ │ └── webapp │ │ │ └── webapp.adapter.ts │ ├── builders │ │ ├── deploy │ │ │ ├── __snapshots__ │ │ │ │ └── builder.spec.ts.snap │ │ │ ├── builder.spec.ts │ │ │ ├── builder.ts │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ └── target-options.ts │ │ └── destroy │ │ │ ├── __snapshots__ │ │ │ └── builder.spec.ts.snap │ │ │ ├── builder.spec.ts │ │ │ ├── builder.ts │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ └── target-options.ts │ ├── index.ts │ ├── schematics │ │ ├── init │ │ │ ├── __snapshots__ │ │ │ │ └── schematic.spec.ts.snap │ │ │ ├── architect-options.ts │ │ │ ├── files │ │ │ │ ├── aws │ │ │ │ │ ├── angular-universal │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ │ ├── cdn.ts.template │ │ │ │ │ │ │ ├── certificate.ts.template │ │ │ │ │ │ │ ├── functions │ │ │ │ │ │ │ └── main │ │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ │ ├── server-side-rendering.ts.template │ │ │ │ │ │ │ ├── tsconfig.json.template │ │ │ │ │ │ │ └── utils.ts.template │ │ │ │ │ ├── express │ │ │ │ │ │ ├── __rootDir__ │ │ │ │ │ │ │ └── main.aws.ts.template │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ │ ├── .gitignore.template │ │ │ │ │ │ │ ├── functions │ │ │ │ │ │ │ └── main │ │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ ├── nestjs │ │ │ │ │ │ ├── __rootDir__ │ │ │ │ │ │ │ └── main.aws.ts.template │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ │ ├── .gitignore.template │ │ │ │ │ │ │ ├── functions │ │ │ │ │ │ │ └── main │ │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ └── webapp │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ ├── cdn.ts.template │ │ │ │ │ │ ├── certificate.ts.template │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ └── utils.ts.template │ │ │ │ ├── azure │ │ │ │ │ ├── angular-universal │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ │ ├── cdnCustomDomain.ts.template │ │ │ │ │ │ │ ├── functions │ │ │ │ │ │ │ ├── host.json.template │ │ │ │ │ │ │ ├── local.settings.json.template │ │ │ │ │ │ │ ├── main │ │ │ │ │ │ │ │ ├── function.json.template │ │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ │ └── proxies.json.template │ │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ │ ├── server-side-rendering.ts.template │ │ │ │ │ │ │ ├── static-website.resource.ts.template │ │ │ │ │ │ │ ├── tsconfig.json.template │ │ │ │ │ │ │ └── utils.ts.template │ │ │ │ │ ├── express │ │ │ │ │ │ ├── __rootDir__ │ │ │ │ │ │ │ └── main.azure.ts.template │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ │ ├── .gitignore.template │ │ │ │ │ │ │ ├── functions │ │ │ │ │ │ │ ├── host.json.template │ │ │ │ │ │ │ ├── local.settings.json.template │ │ │ │ │ │ │ ├── main │ │ │ │ │ │ │ │ ├── function.json.template │ │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ │ └── proxies.json.template │ │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ ├── nestjs │ │ │ │ │ │ ├── __rootDir__ │ │ │ │ │ │ │ └── main.azure.ts.template │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ │ ├── .gitignore.template │ │ │ │ │ │ │ ├── functions │ │ │ │ │ │ │ ├── host.json.template │ │ │ │ │ │ │ ├── local.settings.json.template │ │ │ │ │ │ │ ├── main │ │ │ │ │ │ │ │ ├── function.json.template │ │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ │ └── proxies.json.template │ │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ └── webapp │ │ │ │ │ │ └── infrastructure │ │ │ │ │ │ ├── cdnCustomDomain.ts.template │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ ├── static-website.resource.ts.template │ │ │ │ │ │ └── utils.ts.template │ │ │ │ └── gcp │ │ │ │ │ ├── angular-universal │ │ │ │ │ └── infrastructure │ │ │ │ │ │ ├── functions │ │ │ │ │ │ └── main │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ ├── server-side-rendering.ts.template │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ ├── express │ │ │ │ │ ├── __rootDir__ │ │ │ │ │ │ └── main.gcp.ts.template │ │ │ │ │ └── infrastructure │ │ │ │ │ │ ├── functions │ │ │ │ │ │ └── main │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ ├── nestjs │ │ │ │ │ ├── __rootDir__ │ │ │ │ │ │ └── main.gcp.ts.template │ │ │ │ │ └── infrastructure │ │ │ │ │ │ ├── functions │ │ │ │ │ │ └── main │ │ │ │ │ │ │ └── index.ts.template │ │ │ │ │ │ ├── index.ts.template │ │ │ │ │ │ └── tsconfig.json.template │ │ │ │ │ └── webapp │ │ │ │ │ └── infrastructure │ │ │ │ │ └── index.ts.template │ │ │ ├── schema.d.ts │ │ │ ├── schema.json │ │ │ ├── schematic.spec.ts │ │ │ └── schematic.ts │ │ └── scan │ │ │ ├── __snapshots__ │ │ │ └── schematic.spec.ts.snap │ │ │ ├── schema.json │ │ │ ├── schematic.spec.ts │ │ │ └── schematic.ts │ ├── utils-test │ │ ├── app.utils.ts │ │ ├── builders.utils.ts │ │ ├── enquirer.utils.ts │ │ ├── logger.utils.ts │ │ └── pulumi.mock.ts │ └── utils │ │ ├── application-type.ts │ │ ├── ats.utils.ts │ │ ├── provider.ts │ │ ├── questions.ts │ │ ├── versions.ts │ │ └── workspace.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── nx.json ├── package-lock.json ├── package.json ├── tools ├── schematics │ └── .gitkeep └── tsconfig.tools.json ├── tsconfig.json └── workspace.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | executors: 4 | default: 5 | working_directory: ~/repo 6 | docker: 7 | - image: circleci/node:12-browsers 8 | 9 | commands: 10 | npm_install: 11 | description: 'Install Dependencies' 12 | steps: 13 | - run: npm install 14 | - save_cache: 15 | key: node-cache-node12-{{ checksum "package-lock.json" }} 16 | paths: 17 | - node_modules 18 | restore_npm_cache: 19 | description: 'Restore Cached Dependencies' 20 | steps: 21 | - restore_cache: 22 | keys: 23 | - node-cache-node12-{{ checksum "package-lock.json" }} 24 | - node-cache-node12- 25 | setup: 26 | description: 'Setup Executor' 27 | steps: 28 | - checkout 29 | - restore_npm_cache 30 | - npm_install 31 | 32 | jobs: 33 | build-master: 34 | executor: default 35 | steps: 36 | - setup 37 | - run: npx nx affected --target=lint --base=origin/master~1 --parallel 38 | - run: npx nx affected --target=test --base=origin/master~1 39 | - run: npx nx affected --target=build --base=origin/master~1 40 | build-pr: 41 | executor: default 42 | steps: 43 | - setup 44 | - run: npx nx affected --target=lint --base=origin/master --parallel 45 | - run: npx nx affected --target=test --base=origin/master 46 | - run: npx nx affected --target=build --base=origin/master 47 | 48 | workflows: 49 | version: 2.1 50 | default_workflow: 51 | jobs: 52 | - build-master: 53 | filters: 54 | branches: 55 | only: 56 | - master 57 | - build-pr: 58 | filters: 59 | branches: 60 | ignore: 61 | - master 62 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 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: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module", 7 | "project": "./tsconfig.json" 8 | }, 9 | "ignorePatterns": ["**/*"], 10 | "plugins": ["@typescript-eslint", "@nrwl/nx"], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/eslint-recommended", 14 | "plugin:@typescript-eslint/recommended", 15 | "prettier", 16 | "prettier/@typescript-eslint" 17 | ], 18 | "rules": { 19 | "@typescript-eslint/explicit-member-accessibility": "off", 20 | "@typescript-eslint/explicit-function-return-type": "off", 21 | "@typescript-eslint/no-parameter-properties": "off", 22 | "@nrwl/nx/enforce-module-boundaries": [ 23 | "error", 24 | { 25 | "enforceBuildableLibDependency": true, 26 | "allow": [], 27 | "depConstraints": [ 28 | { "sourceTag": "*", "onlyDependOnLibsWithTags": ["*"] } 29 | ] 30 | } 31 | ] 32 | }, 33 | "overrides": [ 34 | { 35 | "files": ["*.tsx"], 36 | "rules": { 37 | "@typescript-eslint/no-unused-vars": "off" 38 | } 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_nx-deploy-it.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Logs** 20 | If applicable, add logs to help explain your problem. 21 | 22 | **Application type:** 23 | [] Angular 24 | [] NestJS 25 | 26 | **Provider:** 27 | [] AWS 28 | [] Azure 29 | [] Google Cloud Platform 30 | 31 | **nx-deploy-it version** 32 | 0.2.2 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_nx-deploy-it.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Check which provider is affected:** 20 | [] Azure 21 | [] AWS 22 | [] Google Cloud Platform 23 | 24 | **Additional context** 25 | Add any other context or screenshots about the feature request here. 26 | -------------------------------------------------------------------------------- /.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 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | /connect.lock 30 | /coverage 31 | /libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | /typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#65c89b", 4 | "activityBar.activeBackground": "#65c89b", 5 | "activityBar.activeBorder": "#945bc4", 6 | "activityBar.foreground": "#15202b", 7 | "activityBar.inactiveForeground": "#15202b99", 8 | "activityBarBadge.background": "#945bc4", 9 | "activityBarBadge.foreground": "#e7e7e7", 10 | "titleBar.activeBackground": "#42b883", 11 | "titleBar.inactiveBackground": "#42b88399", 12 | "titleBar.activeForeground": "#15202b", 13 | "titleBar.inactiveForeground": "#15202b99", 14 | "statusBar.background": "#42b883", 15 | "statusBarItem.hoverBackground": "#359268", 16 | "statusBar.foreground": "#15202b" 17 | }, 18 | "peacock.color": "#42b883", 19 | "typescript.tsdk": "node_modules/typescript/lib", 20 | "jestrunner.jestPath": "npm test --" 21 | } 22 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 7 | 8 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to a positive environment for our community include: 13 | 14 | * Demonstrating empathy and kindness toward other people 15 | * Being respectful of differing opinions, viewpoints, and experiences 16 | * Giving and gracefully accepting constructive feedback 17 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 18 | * Focusing on what is best not just for us as individuals, but for the overall community 19 | 20 | Examples of unacceptable behavior include: 21 | 22 | * The use of sexualized language or imagery, and sexual attention or 23 | advances of any kind 24 | * Trolling, insulting or derogatory comments, and personal or political attacks 25 | * Public or private harassment 26 | * Publishing others' private information, such as a physical or email 27 | address, without their explicit permission 28 | * Other conduct which could reasonably be considered inappropriate in a 29 | professional setting 30 | 31 | ## Enforcement Responsibilities 32 | 33 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 34 | 35 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 40 | 41 | ## Enforcement 42 | 43 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [INSERT CONTACT METHOD]. All complaints will be reviewed and investigated promptly and fairly. 44 | 45 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 46 | 47 | ## Enforcement Guidelines 48 | 49 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 50 | 51 | ### 1. Correction 52 | 53 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 54 | 55 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 56 | 57 | ### 2. Warning 58 | 59 | **Community Impact**: A violation through a single incident or series of actions. 60 | 61 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 62 | 63 | ### 3. Temporary Ban 64 | 65 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 66 | 67 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 68 | 69 | ### 4. Permanent Ban 70 | 71 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 72 | 73 | **Consequence**: A permanent ban from any sort of public interaction within the community. 74 | 75 | ## Attribution 76 | 77 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 78 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 79 | 80 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 81 | 82 | [homepage]: https://www.contributor-covenant.org 83 | 84 | For answers to common questions about this code of conduct, see the FAQ at 85 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 86 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Dev Thought 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dev Thought - Nx Plugins 2 | 3 | _ng-deploy-it was refactored to be a nx plugin and with this change this repository will contain all nx plugins provided by Dev Thought._ 4 | 5 | _The migration process for old ng-deploy-it can be fonund in the plugin documentation._ 6 | 7 | ## Welcome to our nx plugins repository 8 | 9 | You can find here a collection (currently just one but who knows what the future brings :P) of usefull plugins which are usable in nx and the angular cli. 10 | 11 | ### [nx-deploy-it](libs/nx-deploy-it) 12 | 13 | You are done with your application! But without deploying somewhere, nobody can enjoy it! 14 | 15 | With **[NxDeployIt](libs/nx-deploy-it)** your live gets easier! Use it to deploy your applications to your favorite cloud provider! It can even autodetect the supported applications in your nx workspace :heart: 16 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/nx-deploy-it-e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'nx-deploy-it-e2e', 3 | preset: '../../jest.config.js', 4 | coverageDirectory: '../../coverage/apps/nx-deploy-it-e2e' 5 | }; 6 | -------------------------------------------------------------------------------- /apps/nx-deploy-it-e2e/tests/nx-deploy-it.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ensureNxProject, 3 | runNxCommandAsync, 4 | uniq, 5 | runCommandAsync 6 | } from '@nrwl/nx-plugin/testing'; 7 | 8 | jest.setTimeout(500000); 9 | 10 | xdescribe('nx-deploy-it e2e', () => { 11 | let project: string; 12 | beforeAll(async done => { 13 | console.time('create-nx-workspace'); 14 | ensureNxProject('@dev-thought/nx-deploy-it', 'dist/libs/nx-deploy-it'); 15 | console.timeEnd('create-nx-workspace'); 16 | done(); 17 | }); 18 | 19 | it('should create a workspace with nestjs and angular', async done => { 20 | console.time('nest'); 21 | const projectNestJs = uniq('nestjs'); 22 | await runNxCommandAsync(`generate @nrwl/nest:application ${projectNestJs}`); 23 | console.timeEnd('nest'); 24 | 25 | console.time('angular'); 26 | const projectAngular = uniq('angular'); 27 | await runNxCommandAsync(`generate @nrwl/angular:app ${projectAngular}`); 28 | console.timeEnd('angular'); 29 | 30 | done(); 31 | }); 32 | 33 | fit('should create a workspace with angular and angular universal', async done => { 34 | const projectAngular = uniq('angular'); 35 | console.time('setup angular'); 36 | await runNxCommandAsync(`generate @nrwl/angular:app ${projectAngular}`); 37 | console.timeEnd('setup angular'); 38 | 39 | console.time('install @nguniversal/express-engine'); 40 | await runCommandAsync(`npx yarn add @nguniversal/express-engine`); 41 | console.timeEnd('install @nguniversal/express-engine'); 42 | 43 | console.time('setup install @nguniversal/express-engine'); 44 | await runNxCommandAsync( 45 | `generate @nguniversal/express-engine:ng-add --clientProject ${projectAngular}` 46 | ); 47 | console.timeEnd('setup install @nguniversal/express-engine'); 48 | 49 | done(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /apps/nx-deploy-it-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"] 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /apps/nx-deploy-it-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.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testMatch: ['**/+(*.)+(spec|test).+(ts|js)?(x)'], 3 | transform: { 4 | '^.+\\.(ts|js|html)$': 'ts-jest' 5 | }, 6 | resolver: '@nrwl/jest/plugins/resolver', 7 | moduleFileExtensions: ['ts', 'js', 'html'], 8 | coverageReporters: ['html'], 9 | collectCoverage: true 10 | }; 11 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dev-Thought/nx-plugins/4f34f6cba6fd7d7330b68407f528bff83ebfd06e/libs/.gitkeep -------------------------------------------------------------------------------- /libs/nx-deploy-it/.eslintrc: -------------------------------------------------------------------------------- 1 | { "extends": "../../.eslintrc", "rules": {}, "ignorePatterns": ["!**/*"] } 2 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/README.md: -------------------------------------------------------------------------------- 1 | # @dev-thought/nx-deploy-it 2 | 3 | [![npm version](https://badge.fury.io/js/%40dev-thought%2Fnx-deploy-it.svg)](https://www.npmjs.com/package/@dev-thought/nx-deploy-it) 4 | [![The MIT License](https://img.shields.io/badge/license-MIT-orange.svg?color=blue&style=flat-square)](http://opensource.org/licenses/MIT) 5 | 6 | # Version compatibility 7 | 8 | | NX Version | Nx Deploy It | 9 | | ---------- | :----------: | 10 | | >= 11.x.x | 2.x.x | 11 | | < 11 | 1.x.x | 12 | 13 | **Deploy applications in nx / angular workspaces to the cloud using a provider of your Choice (Azure, AWS, Google Cloud Platform)** 14 | 15 | ![AWS example](./docs/nx-deploy-it-aws.gif?raw=true) 16 | 17 | We are using under the hood the code as infrastructure tool [Pulumi](https://www.pulumi.com/). It gives you the possibility to have every piece of code under your control. You can extend it with your requirements (VPN, ...) and still able to use the schematics for deployment. 18 | 19 | ## Quick-start 20 | 21 | 1. Create a new project with the nx cli. 22 | 23 | ```sh 24 | npx create-nx-workspace@latest hello-world --preset="angular-nest" --appName="hello-world" --style="scss" 25 | cd hello-world 26 | ``` 27 | 28 | 1. Add `nx-deploy-it` to your project. The tool will search for supported applications and ask you which one of them you want to setup. You may be prompted to answer some questions for the setup. 29 | 30 | ```sh 31 | npx ng add @dev-thought/nx-deploy-it 32 | ``` 33 | 34 | 1. Switch to local state management. 35 | 36 | ```sh 37 | npx pulumi login --local 38 | ``` 39 | 40 | 1. Deploy your project to your cloud provider. 41 | 42 | ```sh 43 | npx ng run hello-world:deploy 44 | ``` 45 | 46 | The project will be built with the development configuration. 47 | In development you will be asked to confirm the changes of your infrastructure 48 | 49 | 1. Everything is done and you want to remove your whole infrastructure. No problem ;) Just do it with 50 | 51 | ```sh 52 | npx ng run hello-world:destroy 53 | ``` 54 | 55 | You can initialize any time infrastructure as code for your project if you skipped the setup on ng add. 56 | 57 | ```sh 58 | npx ng g @dev-thought/nx-deploy-it:init 59 | ``` 60 | 61 | ## Requirements 62 | 63 | You will need the Angular CLI, an Angular project, and an Azure Subscription to deploy to Azure. Details of these requirements are in this section. 64 | 65 | ## :bangbang: Cloud provider setup 66 | 67 | nx-deploy-it is only working with already configured cloud providers. If you need to know how to set up them, please follow the first steps in the [Pulumi Quickstart](https://www.pulumi.com/docs/get-started/) for your provider. 68 | 69 | ## Infrastructure as code and their state 70 | 71 | As many things in development, infrastructure as code needs to hold a state somewhere. This is how the tools can check if something has changed and to do only updates where it is necessary. Pulumi provides different ways to hold the state. 72 | The simplest way at the beginning is to hold it local. It's perfect for your local development. Since you want to share it with multiple colleagues or to feel better if it is not only on your disk, you might think about a persistent solution in the cloud with your provider, which you can choose here or with Pulumi self. You can read more about it [here](https://www.pulumi.com/docs/reference/cli/pulumi_login/). 73 | nx-deploy-it installs pulumi as binary in your node_modules folder so you can use it with `npx` easy. 74 | 75 | ### Azure 76 | 77 | If you don't have an Azure subscription, [create your Azure free account from this link](https://azure.microsoft.com/en-us/free/?WT.mc_id=ng_deploy_azure-github-cxa). 78 | https://www.pulumi.com/docs/intro/cloud-providers/azure/setup/ 79 | 80 | ### AWS 81 | 82 | https://www.pulumi.com/docs/intro/cloud-providers/aws/setup/ 83 | 84 | ### Google cloud platform 85 | 86 | https://www.pulumi.com/docs/intro/cloud-providers/gcp/setup/ 87 | 88 | ## Application / Feature Lists 89 | 90 | Legend 91 | 92 | - :white_check_mark: is implemented 93 | - :soon: in development 94 | - :calendar: in planning 95 | - :x: is not supported 96 | 97 | ### Workspaces 98 | 99 | | Nx workspace (native & angular) | angular | 100 | | :-----------------------------: | :-----: | 101 | | Nx | Ng | 102 | 103 | ### Angular / React Application 104 | 105 | | Feature | Azure | AWS | GCP | Workspace | activated in dev (default) | activated in prod (default) | 106 | | -------------- | :----------------: | :----------------: | :------------------------------------------: | :-------: | :------------------------: | :-------------------------: | 107 | | static hosting | :white_check_mark: | :white_check_mark: | :white_check_mark: (only with custom domain) | Nx, Ng | yes | yes | 108 | | cdn | :white_check_mark: | :white_check_mark: | :white_check_mark: | Nx, Ng | no | yes | 109 | | custom domain | :white_check_mark: | :white_check_mark: | :white_check_mark: | Nx, Ng | no (GCP yes) | no (GCP yes) | 110 | 111 | Custom domains need some manual configuration step. You need to verify them for the providers. 112 | 113 | #### Azure custom domain setup 114 | 115 | To verify your custom domain you need to create a CNAME record in your DNS settings. You can read about more about it [here](https://docs.microsoft.com/en-us/azure/cdn/cdn-map-content-to-custom-domain#map-the-permanent-custom-domain). 116 | Azure only allows a set of characters. So the `.` in your custom domain name will be replaced with a `-`. If your custom domain is `www.example.com` then your cdn name will be `www-example-com.azureedge.net`. 117 | 118 | HINT: Current limitation: domain name must have maximum 50 characters 119 | 120 | #### GCP custom domain setup 121 | 122 | Verify owner: Google makes it really easy. You can use the [google webmaster](https://www.google.com/webmasters/verification/home). 123 | Add CNAME entry: https://cloud.google.com/storage/docs/hosting-static-website 124 | 125 | #### AWS custom domain setup 126 | 127 | For AWS we need to create first a hosted zone with the domain name e.g.: if your domain is `www.my-domain.com` than use the name `my-domain.com` for the hosted zone. After this is done you get name servers. Now you can replace the name servers from your domain with the one from aws and you have everything under conrtol via AWS route 53. The rest will be done by nx-deploy-it. It will create the ssl certification and validates if you are the owner of the domain. 128 | You can create the hosted zone in the [Route53](https://console.aws.amazon.com/route53/home#hosted-zones:) Service 129 | 130 | ### Angular Universal Application 131 | 132 | Only supports native @nguniversal 133 | 134 | | Feature | Azure | AWS | GCP | Workspace | Supported integration | 135 | | --------------- | :----------------: | :----------------: | :----------------: | :--------: | :-------------------------: | 136 | | serverless | :white_check_mark: | :white_check_mark: | :white_check_mark: | nx | @nguniversal/express-engine | 137 | | server instance | :calendar: | :calendar: | :calendar: | :calendar: | | 138 | 139 | ### NestJS & ExpressJS 140 | 141 | | Feature | Azure | AWS | GCP | Workspace | 142 | | --------------- | :----------------: | :----------------: | :----------------: | :--------: | 143 | | serverless | :white_check_mark: | :white_check_mark: | :white_check_mark: | Nx | 144 | | server instance | :calendar: | :calendar: | :calendar: | :calendar: | 145 | 146 | If you use the nx workspace or angular workspace with other types of applications and you want to have them supported by nx-deploy-it, please feel free and create a new Issue and of course ;) -> Contributions are welcome! 147 | 148 | ### Migration from old ng-deploy-it 149 | 150 | 1. Remove the old package `npm uninstall @dev-thought/ng-deploy-it` from your package.json 151 | 152 | 2. Install the new package `npm i @dev-thought/nx-deploy-it -D` and skip the auto scan of the applications by not selecting an application 153 | 154 | 3. Rename everything in your repository from `ng-deploy-it` to `nx-deploy-it`. 155 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/builders.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular-devkit/architect/src/builders-schema.json", 3 | "builders": { 4 | "deploy": { 5 | "implementation": "./src/builders/deploy/builder", 6 | "schema": "./src/builders/deploy/schema.json", 7 | "description": "deploy infrastructure" 8 | }, 9 | "destroy": { 10 | "implementation": "./src/builders/destroy/builder", 11 | "schema": "./src/builders/destroy/schema.json", 12 | "description": "Destroy infrstructure" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../../node_modules/@angular-devkit/schematics/collection-schema.json", 3 | "name": "nx-deploy-it", 4 | "version": "0.0.1", 5 | "schematics": { 6 | "scan": { 7 | "factory": "./src/schematics/scan/schematic", 8 | "schema": "./src/schematics/scan/schema.json", 9 | "description": "scan schematic", 10 | "aliases": ["ng-add"] 11 | }, 12 | "init": { 13 | "factory": "./src/schematics/init/schematic", 14 | "schema": "./src/schematics/init/schema.json", 15 | "description": "init schematic" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/docs/nx-deploy-it-aws.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dev-Thought/nx-plugins/4f34f6cba6fd7d7330b68407f528bff83ebfd06e/libs/nx-deploy-it/docs/nx-deploy-it-aws.gif -------------------------------------------------------------------------------- /libs/nx-deploy-it/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'nx-deploy-it', 3 | preset: '../../jest.config.js', 4 | transform: { 5 | '^.+\\.[tj]sx?$': 'ts-jest' 6 | }, 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'html'], 8 | coverageDirectory: '../../coverage/libs/nx-deploy-it', 9 | coverageThreshold: { 10 | global: { 11 | statements: 76, 12 | branches: 53, 13 | lines: 75, 14 | functions: 76 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@dev-thought/nx-deploy-it", 3 | "version": "2.0.0", 4 | "main": "src/index.js", 5 | "schematics": "./collection.json", 6 | "builders": "./builders.json", 7 | "homepage": "https://www.npmjs.com/package/nx-deploy-it", 8 | "repository": "https://github.com/dev-thought/nx-plugins", 9 | "bugs": { 10 | "url": "https://github.com/dev-thought/nx-plugins/issues", 11 | "email": "mitko@dev-thought.cool" 12 | }, 13 | "author": "Mitko Tschimev ", 14 | "license": "MIT", 15 | "keywords": [ 16 | "pulumi", 17 | "Infrastructure as code", 18 | "infrastructure-as-code", 19 | "IaC", 20 | "terraform", 21 | "schematics", 22 | "ng-deploy", 23 | "nx", 24 | "angular", 25 | "nestjs" 26 | ], 27 | "dependencies": { 28 | "@dev-thought/pulumi-npm": "^1.5.1", 29 | "@vercel/ncc": "^0.25.1", 30 | "enquirer": "^2.3.4" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/angular-universal/angular-universal.adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../base.adapter'; 2 | import { PROVIDER } from '../../utils/provider'; 3 | import { prompt } from 'enquirer'; 4 | import { Rule, applyTemplates } from '@angular-devkit/schematics'; 5 | import { TargetDefinition } from '@angular-devkit/core/src/workspace'; 6 | import { join, resolve } from 'path'; 7 | import { JsonObject } from '@angular-devkit/core'; 8 | import { QUESTIONS } from '../../utils/questions'; 9 | import { BuilderOutput, BuilderContext } from '@angular-devkit/architect'; 10 | import { Observable, from, of } from 'rxjs'; 11 | import { switchMap } from 'rxjs/operators'; 12 | import { NxDeployItInitSchematicSchema } from '../../schematics/init/schema'; 13 | import { getDistributionPath, getProjectConfig } from '../../utils/workspace'; 14 | import { NxDeployItDeployBuilderSchema } from '../../builders/deploy/schema'; 15 | import { ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE } from './deployment-type.enum'; 16 | import { copyFileSync } from 'fs'; 17 | 18 | export class AngularUniversalAdapter extends BaseAdapter { 19 | async extendOptionsByUserInput() { 20 | await super.extendOptionsByUserInput(); 21 | const options = this.options as NxDeployItInitSchematicSchema; 22 | const questions: any[] = []; 23 | 24 | questions.push(QUESTIONS.angularUniversal); 25 | 26 | if (options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM) { 27 | if (!options.customDomainName) questions.push(QUESTIONS.customDomainName); 28 | if (!options['gcp:region']) 29 | questions.push(QUESTIONS.gcpRegionCloudFunctions); 30 | } 31 | 32 | const anwsers = await prompt(questions); 33 | this.options = { 34 | ...options, 35 | ...anwsers 36 | }; 37 | } 38 | 39 | addRequiredDependencies() { 40 | const dependencies = super.addRequiredDependencies(); 41 | 42 | dependencies.push({ name: 'mime', version: '2.4.4' }); 43 | 44 | if (this.options.provider === PROVIDER.AWS) { 45 | dependencies.push({ 46 | name: 'aws-serverless-express', 47 | version: '^3.3.6' 48 | }); 49 | } 50 | 51 | if (this.options.provider === PROVIDER.AZURE) { 52 | dependencies.push( 53 | { name: '@azure/arm-cdn', version: '^4.2.0' }, 54 | { 55 | name: 'azure-aws-serverless-express', 56 | version: '^0.1.5' 57 | } 58 | ); 59 | } 60 | 61 | return dependencies; 62 | } 63 | 64 | getApplicationTypeTemplate(): Rule { 65 | const buildTarget = this.project.targets.get('build') as TargetDefinition; 66 | return applyTemplates({ 67 | getRootDirectory: () => '', 68 | buildPath: join( 69 | `../../../${(buildTarget.options as JsonObject).outputPath}` 70 | ), 71 | projectName: this.options.project 72 | }); 73 | } 74 | 75 | getApplicationTemplatePath() { 76 | return `${super.getApplicationTemplatePath()}/angular-universal/`; 77 | } 78 | 79 | getDeployActionConfiguration(): any { 80 | const config = super.getDeployActionConfiguration(); 81 | 82 | config.options.pulumi.useCdn = false; 83 | config.configurations = { 84 | production: { pulumi: { useCdn: true } } 85 | }; 86 | return config; 87 | } 88 | 89 | getDestroyActionConfiguration(): any { 90 | const config = super.getDestroyActionConfiguration(); 91 | return config; 92 | } 93 | 94 | deploy( 95 | context: BuilderContext, 96 | cwd: string, 97 | options: NxDeployItDeployBuilderSchema, 98 | configuration: string, 99 | targetOptions: any 100 | ): Observable { 101 | const distributationPath = getDistributionPath(context); 102 | const project = getProjectConfig(context); 103 | const infrastructureFolder = resolve( 104 | context.workspaceRoot, 105 | project.root, 106 | 'infrastructure' 107 | ); 108 | const deploymentType: ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE = 109 | targetOptions.pulumi.angularUniversalDeploymentType; 110 | 111 | let baseHref = '/'; 112 | switch (this.options.provider) { 113 | case PROVIDER.AWS: 114 | baseHref = `/${context.target.configuration || 'dev'}/`; 115 | break; 116 | 117 | default: 118 | break; 119 | } 120 | 121 | let build$: Observable; 122 | 123 | switch (deploymentType) { 124 | case ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE.PRERENDERING: 125 | build$ = from( 126 | context 127 | .scheduleTarget({ 128 | target: 'prerender', 129 | project: context.target.project, 130 | configuration: context.target.configuration || undefined 131 | }) 132 | .then(build => build.result) 133 | ); 134 | break; 135 | 136 | case ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE.SERVER_SIDE_RENDERING: 137 | build$ = from( 138 | Promise.all([ 139 | context.scheduleTarget( 140 | { 141 | target: 'build', 142 | project: context.target.project, 143 | configuration: context.target.configuration || undefined 144 | }, 145 | { 146 | baseHref 147 | } 148 | ), 149 | context.scheduleTarget( 150 | { 151 | target: 'server', 152 | project: context.target.project, 153 | configuration: context.target.configuration || undefined 154 | }, 155 | { 156 | main: resolve(infrastructureFolder, 'functions/main/index.ts'), 157 | tsConfig: resolve(infrastructureFolder, 'tsconfig.json') 158 | } 159 | ) 160 | ]).then(([build, server]) => 161 | Promise.all([build.result, server.result]) 162 | ) 163 | ).pipe( 164 | switchMap(() => { 165 | if (this.options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM) { 166 | copyFileSync( 167 | join( 168 | context.workspaceRoot, 169 | `${project.architect.server.options.outputPath}/main.js` 170 | ), 171 | join( 172 | context.workspaceRoot, 173 | `${project.architect.server.options.outputPath}/index.js` 174 | ) 175 | ); 176 | } 177 | return of({ success: true }); 178 | }) 179 | ); 180 | break; 181 | 182 | default: 183 | throw new Error( 184 | 'Unknown deployment type! Supported types are: ["prerendering", "ssr"]' 185 | ); 186 | } 187 | 188 | return build$.pipe( 189 | switchMap(() => 190 | this.up( 191 | cwd, 192 | options, 193 | configuration, 194 | targetOptions, 195 | distributationPath, 196 | context.target.project 197 | ) 198 | ) 199 | ); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/angular-universal/deployment-type.enum.ts: -------------------------------------------------------------------------------- 1 | export enum ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE { 2 | PRERENDERING = 'prerendering', 3 | SERVER_SIDE_RENDERING = 'ssr' 4 | } 5 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/base.adapter.model.ts: -------------------------------------------------------------------------------- 1 | import { PROVIDER } from '../utils/provider'; 2 | import { JsonObject } from '@angular-devkit/core'; 3 | 4 | export interface NxDeployItBaseOptions extends JsonObject { 5 | provider: PROVIDER; 6 | project: string; 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/base.adapter.ts: -------------------------------------------------------------------------------- 1 | import { ProjectDefinition } from '@angular-devkit/core/src/workspace'; 2 | import { Rule } from '@angular-devkit/schematics'; 3 | import { ApplicationType } from '../utils/application-type'; 4 | import { ArchitectOptions } from '../schematics/init/architect-options'; 5 | import { join } from 'path'; 6 | import { PROVIDER } from '../utils/provider'; 7 | import { QUESTIONS } from '../utils/questions'; 8 | import { prompt } from 'enquirer'; 9 | import { BuilderOutput, BuilderContext } from '@angular-devkit/architect'; 10 | import { Observable, from, of } from 'rxjs'; 11 | import { switchMap } from 'rxjs/operators'; 12 | import { NxDeployItBaseOptions } from './base.adapter.model'; 13 | import { NxDeployItInitSchematicSchema } from '../schematics/init/schema'; 14 | import { NxDeployItDeployBuilderSchema } from '../builders/deploy/schema'; 15 | import { getDistributionPath, getPulumiBinaryPath } from '../utils/workspace'; 16 | import { DeployTargetOptions } from '../builders/deploy/target-options'; 17 | import { spawnSync } from 'child_process'; 18 | 19 | export class BaseAdapter { 20 | constructor( 21 | public project: ProjectDefinition, 22 | public options: NxDeployItBaseOptions, 23 | public applicationType: ApplicationType 24 | ) {} 25 | 26 | async extendOptionsByUserInput(): Promise { 27 | const options = this.options as NxDeployItInitSchematicSchema; 28 | const questions: any[] = []; 29 | 30 | if (options.provider === PROVIDER.AWS) { 31 | if (!options['aws:region']) { 32 | questions.push(QUESTIONS.awsRegion); 33 | } 34 | if (!options['aws:profile']) { 35 | questions.push(QUESTIONS.awsProfile); 36 | } 37 | } 38 | 39 | if (options.provider === PROVIDER.AZURE && !options['azure:location']) { 40 | questions.push(QUESTIONS.azureLocation); 41 | } 42 | 43 | if ( 44 | options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM && 45 | !options['gcp:project'] 46 | ) { 47 | questions.push(QUESTIONS.gcpProjectId); 48 | } 49 | 50 | const anwsers = await prompt(questions); 51 | this.options = { 52 | ...options, 53 | ...anwsers 54 | }; 55 | } 56 | 57 | addRequiredDependencies(): { name: string; version: string }[] { 58 | const dependencies: { name: string; version: string }[] = []; 59 | 60 | return dependencies; 61 | } 62 | 63 | getApplicationTypeTemplate(): Rule { 64 | throw new Error('implement me'); 65 | } 66 | 67 | getApplicationTemplatePath(): string { 68 | return `${this.options.provider}/`; 69 | } 70 | 71 | getDeployActionConfiguration(): any { 72 | const architectOptions: ArchitectOptions = { 73 | main: join(this.project.root, 'infrastructure', 'index.ts'), 74 | provider: this.options.provider 75 | }; 76 | 77 | const mergeOptions = { ...this.options }; 78 | delete mergeOptions.project; 79 | delete mergeOptions.provider; 80 | 81 | return { 82 | builder: '@dev-thought/nx-deploy-it:deploy', 83 | options: { 84 | ...architectOptions, 85 | pulumi: mergeOptions 86 | }, 87 | configurations: {} 88 | }; 89 | } 90 | 91 | getDestroyActionConfiguration(): any { 92 | const architectOptions: ArchitectOptions = { 93 | main: join(this.project.root, 'infrastructure', 'index.ts'), 94 | provider: this.options.provider 95 | }; 96 | 97 | return { 98 | builder: '@dev-thought/nx-deploy-it:destroy', 99 | options: { 100 | ...architectOptions 101 | }, 102 | configurations: {} 103 | }; 104 | } 105 | 106 | deploy( 107 | context: BuilderContext, 108 | cwd: string, 109 | options: NxDeployItDeployBuilderSchema, 110 | configuration: string, 111 | targetOptions: any 112 | ): Observable { 113 | const distributationPath = getDistributionPath(context); 114 | 115 | const build$: Observable = from( 116 | context 117 | .scheduleTarget({ 118 | target: 'build', 119 | project: context.target.project, 120 | configuration: context.target.configuration || '' 121 | }) 122 | .then(target => target.result) 123 | ); 124 | 125 | return build$.pipe( 126 | switchMap(() => 127 | this.up( 128 | cwd, 129 | options, 130 | configuration, 131 | targetOptions, 132 | distributationPath, 133 | context.target.project 134 | ) 135 | ) 136 | ); 137 | } 138 | 139 | up( 140 | cwd: string, 141 | options: NxDeployItDeployBuilderSchema, 142 | configuration: string, 143 | targetOptions: DeployTargetOptions, 144 | distPath: string, 145 | projectName: string, 146 | additionArgs: string[] = [] 147 | ) { 148 | const args = [ 149 | 'up', 150 | '--cwd', 151 | cwd, 152 | '--stack', 153 | `${configuration}-${projectName}` 154 | ]; 155 | if (options.nonInteractive) { 156 | args.push('--non-interactive', '--yes'); 157 | } 158 | 159 | if (targetOptions.pulumi) { 160 | for (const key in targetOptions.pulumi) { 161 | const value = targetOptions.pulumi[key]; 162 | if (value) { 163 | args.push('-c', `${key}=${value}`); 164 | } 165 | } 166 | } 167 | args.push('-c', `distPath=${distPath}`); 168 | args.push('-c', `projectName=${projectName}`); 169 | 170 | args.push(...additionArgs); 171 | 172 | const up = spawnSync(getPulumiBinaryPath(), args, { 173 | env: { ...process.env, PULUMI_SKIP_UPDATE_CHECK: '1' }, 174 | stdio: 'inherit' 175 | }); 176 | 177 | if (up.error) { 178 | return of({ success: false, error: up.error.message }); 179 | } 180 | 181 | return of({ success: true }); 182 | } 183 | 184 | getStackOutput(cwd: string, configuration: string, projectName: string) { 185 | const args = [ 186 | 'stack', 187 | 'output', 188 | '--cwd', 189 | cwd, 190 | '--stack', 191 | `${configuration}-${projectName}`, 192 | '--json' 193 | ]; 194 | 195 | const output = spawnSync(getPulumiBinaryPath(), args, { 196 | env: { ...process.env, PULUMI_SKIP_UPDATE_CHECK: '1' } 197 | }); 198 | 199 | return JSON.parse(output.stdout.toString()); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/express/express.adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../base.adapter'; 2 | import { PROVIDER } from '../../utils/provider'; 3 | import { prompt } from 'enquirer'; 4 | import { Rule, applyTemplates } from '@angular-devkit/schematics'; 5 | import { QUESTIONS } from '../../utils/questions'; 6 | import { NxDeployItInitSchematicSchema } from '../../schematics/init/schema'; 7 | import { getDistributionPath, getProjectConfig } from '../../utils/workspace'; 8 | import { NxDeployItDeployBuilderSchema } from '../../builders/deploy/schema'; 9 | import { BuilderOutput, BuilderContext } from '@angular-devkit/architect'; 10 | import { Observable, from } from 'rxjs'; 11 | import { switchMap, map } from 'rxjs/operators'; 12 | import { resolve } from 'path'; 13 | import * as ncc from '@vercel/ncc'; 14 | import { ensureDirSync, ensureFileSync } from 'fs-extra'; 15 | import { writeFileSync } from 'fs'; 16 | 17 | export class ExpressAdapter extends BaseAdapter { 18 | async extendOptionsByUserInput() { 19 | await super.extendOptionsByUserInput(); 20 | const options = this.options as NxDeployItInitSchematicSchema; 21 | const questions: any[] = []; 22 | 23 | if ( 24 | options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM && 25 | !options['gcp:region'] 26 | ) { 27 | questions.push(QUESTIONS.gcpRegionCloudFunctions); 28 | } 29 | 30 | const anwsers = await prompt(questions); 31 | this.options = { 32 | ...options, 33 | ...anwsers 34 | }; 35 | } 36 | 37 | addRequiredDependencies() { 38 | const dependencies = super.addRequiredDependencies(); 39 | 40 | if (this.options.provider === PROVIDER.AZURE) { 41 | dependencies.push({ 42 | name: 'azure-aws-serverless-express', 43 | version: '^0.1.5' 44 | }); 45 | } 46 | if (this.options.provider === PROVIDER.AWS) { 47 | dependencies.push({ 48 | name: 'aws-serverless-express', 49 | version: '^3.3.6' 50 | }); 51 | } 52 | return dependencies; 53 | } 54 | 55 | getApplicationTypeTemplate(): Rule { 56 | return applyTemplates({ 57 | rootDir: 'src', 58 | getRootDirectory: () => 'src', 59 | stripTsExtension: (s: string) => s.replace(/\.ts$/, ''), 60 | projectName: this.options.project 61 | }); 62 | } 63 | 64 | getApplicationTemplatePath() { 65 | return `${super.getApplicationTemplatePath()}/express/`; 66 | } 67 | 68 | getDeployActionConfiguration(): any { 69 | const config = super.getDeployActionConfiguration(); 70 | return config; 71 | } 72 | 73 | getDestroyActionConfiguration(): any { 74 | const config = super.getDestroyActionConfiguration(); 75 | return config; 76 | } 77 | 78 | deploy( 79 | context: BuilderContext, 80 | cwd: string, 81 | options: NxDeployItDeployBuilderSchema, 82 | configuration: string, 83 | targetOptions: any 84 | ): Observable { 85 | const distributationPath = getDistributionPath(context); 86 | 87 | const project = getProjectConfig(context); 88 | const infrastructureFolder = resolve( 89 | context.workspaceRoot, 90 | project.root, 91 | 'infrastructure' 92 | ); 93 | 94 | const processCwd = process.cwd(); 95 | process.chdir(infrastructureFolder); 96 | 97 | const build$: Observable = from( 98 | ncc(resolve(infrastructureFolder, 'functions/main/index.ts'), { 99 | cache: resolve(infrastructureFolder, 'buildcache') 100 | }) 101 | ).pipe( 102 | map( 103 | (buildResult: { 104 | code: string; 105 | asset: { [index: string]: { source: string } }; 106 | }) => { 107 | process.chdir(processCwd); 108 | ensureDirSync(resolve(infrastructureFolder, 'functions/dist/main')); 109 | // compiled javascript 110 | writeFileSync( 111 | resolve(infrastructureFolder, 'functions/dist/main/index.js'), 112 | buildResult.code 113 | ); 114 | // assets 115 | for (const file in buildResult.asset) { 116 | const content = buildResult.asset[file]; 117 | ensureFileSync( 118 | resolve(infrastructureFolder, `functions/dist/main/${file}`) 119 | ); 120 | writeFileSync( 121 | resolve(infrastructureFolder, `functions/dist/main/${file}`), 122 | content.source.toString() 123 | ); 124 | } 125 | return { success: true }; 126 | } 127 | ) 128 | ); 129 | 130 | return build$.pipe( 131 | switchMap(() => 132 | this.up( 133 | cwd, 134 | options, 135 | configuration, 136 | targetOptions, 137 | distributationPath, 138 | context.target.project 139 | ) 140 | ) 141 | ); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/nestjs/nestjs.adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../base.adapter'; 2 | import { PROVIDER } from '../../utils/provider'; 3 | import { prompt } from 'enquirer'; 4 | import { Rule, applyTemplates } from '@angular-devkit/schematics'; 5 | import { QUESTIONS } from '../../utils/questions'; 6 | import { NxDeployItInitSchematicSchema } from '../../schematics/init/schema'; 7 | import { getDistributionPath, getProjectConfig } from '../../utils/workspace'; 8 | import { NxDeployItDeployBuilderSchema } from '../../builders/deploy/schema'; 9 | import { BuilderOutput, BuilderContext } from '@angular-devkit/architect'; 10 | import { Observable, from } from 'rxjs'; 11 | import { switchMap, map } from 'rxjs/operators'; 12 | import { resolve } from 'path'; 13 | import * as ncc from '@vercel/ncc'; 14 | import { ensureDirSync, ensureFileSync } from 'fs-extra'; 15 | import { writeFileSync } from 'fs'; 16 | 17 | export class NestJSAdapter extends BaseAdapter { 18 | async extendOptionsByUserInput() { 19 | await super.extendOptionsByUserInput(); 20 | const options = this.options as NxDeployItInitSchematicSchema; 21 | const questions: any[] = []; 22 | 23 | if ( 24 | options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM && 25 | !options['gcp:region'] 26 | ) { 27 | questions.push(QUESTIONS.gcpRegionCloudFunctions); 28 | } 29 | 30 | const anwsers = await prompt(questions); 31 | this.options = { 32 | ...options, 33 | ...anwsers 34 | }; 35 | } 36 | 37 | addRequiredDependencies() { 38 | const dependencies = super.addRequiredDependencies(); 39 | 40 | if (this.options.provider === PROVIDER.AZURE) { 41 | dependencies.push( 42 | { 43 | name: '@nestjs/azure-func-http', 44 | version: '^0.4.2' 45 | }, 46 | { 47 | name: '@azure/functions', 48 | version: '^1.2.0' 49 | } 50 | ); 51 | } 52 | if (this.options.provider === PROVIDER.AWS) { 53 | dependencies.push({ 54 | name: 'aws-serverless-express', 55 | version: '^3.3.6' 56 | }); 57 | } 58 | return dependencies; 59 | } 60 | 61 | getApplicationTypeTemplate(): Rule { 62 | return applyTemplates({ 63 | rootDir: 'src', 64 | getRootDirectory: () => 'src', 65 | stripTsExtension: (s: string) => s.replace(/\.ts$/, ''), 66 | getRootModuleName: () => 'AppModule', 67 | getRootModulePath: () => 'app/app.module', 68 | projectName: this.options.project 69 | }); 70 | } 71 | 72 | getApplicationTemplatePath() { 73 | return `${super.getApplicationTemplatePath()}/nestjs/`; 74 | } 75 | 76 | getDeployActionConfiguration(): any { 77 | const config = super.getDeployActionConfiguration(); 78 | 79 | // TODO: use in deploy & destroy via angular.json config 80 | // if (options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM && options.region) { 81 | // args.push('-c', `gcp:region=${options.region}`); 82 | // } 83 | 84 | return config; 85 | } 86 | 87 | getDestroyActionConfiguration(): any { 88 | const config = super.getDestroyActionConfiguration(); 89 | return config; 90 | } 91 | 92 | deploy( 93 | context: BuilderContext, 94 | cwd: string, 95 | options: NxDeployItDeployBuilderSchema, 96 | configuration: string, 97 | targetOptions: any 98 | ): Observable { 99 | const distributationPath = getDistributionPath(context); 100 | 101 | const project = getProjectConfig(context); 102 | const infrastructureFolder = resolve( 103 | context.workspaceRoot, 104 | project.root, 105 | 'infrastructure' 106 | ); 107 | 108 | const processCwd = process.cwd(); 109 | process.chdir(infrastructureFolder); 110 | 111 | const build$: Observable = from( 112 | ncc(resolve(infrastructureFolder, 'functions/main/index.ts'), { 113 | cache: resolve(infrastructureFolder, 'buildcache') 114 | }) 115 | ).pipe( 116 | map( 117 | (buildResult: { 118 | code: string; 119 | asset: { [index: string]: { source: string } }; 120 | }) => { 121 | process.chdir(processCwd); 122 | ensureDirSync(resolve(infrastructureFolder, 'functions/dist/main')); 123 | // compiled javascript 124 | writeFileSync( 125 | resolve(infrastructureFolder, 'functions/dist/main/index.js'), 126 | buildResult.code 127 | ); 128 | // assets 129 | for (const file in buildResult.asset) { 130 | const content = buildResult.asset[file]; 131 | ensureFileSync( 132 | resolve(infrastructureFolder, `functions/dist/main/${file}`) 133 | ); 134 | writeFileSync( 135 | resolve(infrastructureFolder, `functions/dist/main/${file}`), 136 | content.source.toString() 137 | ); 138 | } 139 | return { success: true }; 140 | } 141 | ) 142 | ); 143 | 144 | return build$.pipe( 145 | switchMap(() => 146 | this.up( 147 | cwd, 148 | options, 149 | configuration, 150 | targetOptions, 151 | distributationPath, 152 | context.target.project 153 | ) 154 | ) 155 | ); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/adapter/webapp/webapp.adapter.ts: -------------------------------------------------------------------------------- 1 | import { BaseAdapter } from '../base.adapter'; 2 | import { PROVIDER } from '../../utils/provider'; 3 | import { prompt } from 'enquirer'; 4 | import { Rule, applyTemplates } from '@angular-devkit/schematics'; 5 | import { TargetDefinition } from '@angular-devkit/core/src/workspace'; 6 | import { join } from 'path'; 7 | import { JsonObject } from '@angular-devkit/core'; 8 | import { QUESTIONS } from '../../utils/questions'; 9 | import { NxDeployItInitSchematicSchema } from '../../schematics/init/schema'; 10 | 11 | export class WebappAdapter extends BaseAdapter { 12 | async extendOptionsByUserInput() { 13 | const options = this.options as NxDeployItInitSchematicSchema; 14 | await super.extendOptionsByUserInput(); 15 | const questions: any[] = []; 16 | 17 | if ( 18 | options.provider === PROVIDER.GOOGLE_CLOUD_PLATFORM && 19 | !options.customDomainName 20 | ) { 21 | questions.push(QUESTIONS.customDomainName); 22 | } 23 | 24 | const anwsers = await prompt(questions); 25 | this.options = { 26 | ...options, 27 | ...anwsers 28 | }; 29 | } 30 | 31 | addRequiredDependencies() { 32 | const dependencies = super.addRequiredDependencies(); 33 | 34 | dependencies.push({ name: 'mime', version: '2.4.4' }); 35 | 36 | if (this.options.provider === PROVIDER.AZURE) { 37 | dependencies.push({ name: '@azure/arm-cdn', version: '^4.2.0' }); 38 | } 39 | return dependencies; 40 | } 41 | 42 | getApplicationTypeTemplate(): Rule { 43 | const buildTarget = this.project.targets.get('build') as TargetDefinition; 44 | return applyTemplates({ 45 | buildPath: join( 46 | `../../../${(buildTarget.options as JsonObject).outputPath}` 47 | ), 48 | projectName: this.options.project 49 | }); 50 | } 51 | 52 | getApplicationTemplatePath() { 53 | return `${super.getApplicationTemplatePath()}/webapp/`; 54 | } 55 | 56 | getDeployActionConfiguration(): any { 57 | const config = super.getDeployActionConfiguration(); 58 | 59 | config.options.pulumi.useCdn = false; 60 | config.configurations = { 61 | production: { pulumi: { useCdn: true } } 62 | }; 63 | return config; 64 | } 65 | 66 | getDestroyActionConfiguration(): any { 67 | const config = super.getDestroyActionConfiguration(); 68 | return config; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/deploy/__snapshots__/builder.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Command Runner Builder - Deploy should run deploy for a react app: Result of the pulumi script 1`] = ` 4 | Object { 5 | "success": true, 6 | } 7 | `; 8 | 9 | exports[`Command Runner Builder - Deploy should run deploy for a react app: build schedule target 1`] = ` 10 | Array [ 11 | Object { 12 | "configuration": "dev", 13 | "project": "project-mock", 14 | "target": "build", 15 | }, 16 | ] 17 | `; 18 | 19 | exports[`Command Runner Builder - Deploy should run deploy for a react app: create stack if not exists 1`] = ` 20 | Array [ 21 | "stack", 22 | "--stack", 23 | "dev-project-mock", 24 | "--cwd", 25 | "/root", 26 | ] 27 | `; 28 | 29 | exports[`Command Runner Builder - Deploy should run deploy for a react app: deploy with pulumi 1`] = ` 30 | Array [ 31 | "up", 32 | "--cwd", 33 | "/root", 34 | "--stack", 35 | "dev-project-mock", 36 | "-c", 37 | "distPath=/root/dist/apps/project-mock", 38 | "-c", 39 | "projectName=project-mock", 40 | ] 41 | `; 42 | 43 | exports[`Command Runner Builder - Deploy should update pulumi properties: deploy with pulumi 1`] = ` 44 | Array [ 45 | "up", 46 | "--cwd", 47 | "/root", 48 | "--stack", 49 | "dev-project-mock", 50 | "-c", 51 | "aws:region=eu-central-1", 52 | "-c", 53 | "distPath=/root/dist/apps/project-mock", 54 | "-c", 55 | "projectName=project-mock", 56 | ] 57 | `; 58 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/deploy/builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { NxDeployItDeployBuilderSchema } from './schema'; 2 | import { MockBuilderContext } from '@nrwl/workspace/testing'; 3 | import * as childProcess from 'child_process'; 4 | import { getMockContext } from '../../utils-test/builders.utils'; 5 | import { runBuilder } from './builder'; 6 | import { DeployTargetOptions } from './target-options'; 7 | import * as nrwlWorkspce from '@nrwl/workspace'; 8 | import * as utils from '../../utils/application-type'; 9 | import * as ncc from '@vercel/ncc'; 10 | import * as fsExtra from 'fs-extra'; 11 | import * as fs from 'fs'; 12 | import { PROVIDER } from '../../utils/provider'; 13 | jest.mock('@vercel/ncc'); 14 | 15 | describe('Command Runner Builder - Deploy', () => { 16 | let context: MockBuilderContext; 17 | let options: NxDeployItDeployBuilderSchema; 18 | const spawnSync = jest.spyOn(childProcess, 'spawnSync'); 19 | 20 | beforeEach(async () => { 21 | context = await getMockContext(); 22 | 23 | options = { 24 | project: 'project-mock', 25 | provider: PROVIDER.AWS 26 | }; 27 | 28 | spawnSync.mockReturnValue({ 29 | pid: null, 30 | output: [''], 31 | error: null, 32 | signal: null, 33 | status: null, 34 | stderr: null, 35 | stdout: null 36 | }); 37 | 38 | await context.addTarget( 39 | { project: 'project-mock', configuration: 'dev', target: 'deploy' }, 40 | 'deploy' 41 | ); 42 | context.target = { 43 | project: 'project-mock', 44 | configuration: 'dev', 45 | target: 'deploy' 46 | }; 47 | }); 48 | 49 | afterEach(() => { 50 | jest.clearAllMocks(); 51 | }); 52 | 53 | it('should run deploy for a react app', async () => { 54 | jest.spyOn(context, 'getTargetOptions').mockResolvedValue({ 55 | main: 'somewhere.ts' 56 | } as DeployTargetOptions); 57 | const scheduleTargetSpy = jest 58 | .spyOn(context, 'scheduleTarget') 59 | .mockResolvedValue({ 60 | result: new Promise(resolve => resolve({ success: true })) 61 | } as any); 62 | jest.spyOn(nrwlWorkspce, 'readWorkspaceConfigPath').mockReturnValue({ 63 | projects: { 64 | 'project-mock': { 65 | architect: { 66 | build: { 67 | builder: '@nrwl/web:build', 68 | options: { 69 | main: 'apps/project-mock/src/main.tsx', 70 | webpackConfig: '@nrwl/react/plugins/webpack', 71 | outputPath: 'dist/apps/project-mock' 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }); 78 | const result = await runBuilder(options, context).toPromise(); 79 | 80 | expect(scheduleTargetSpy.mock.calls[0]).toMatchSnapshot( 81 | 'build schedule target' 82 | ); 83 | expect(spawnSync.mock.calls[0][1]).toMatchSnapshot( 84 | 'create stack if not exists' 85 | ); 86 | expect(spawnSync.mock.calls[1][1]).toMatchSnapshot('deploy with pulumi'); 87 | expect(result).toMatchSnapshot('Result of the pulumi script'); 88 | }); 89 | 90 | it('should update pulumi properties', async () => { 91 | jest.spyOn(context, 'getTargetOptions').mockResolvedValue({ 92 | main: 'somewhere.ts', 93 | pulumi: { 94 | 'aws:region': 'eu-central-1' 95 | } 96 | } as DeployTargetOptions); 97 | jest.spyOn(context, 'scheduleTarget').mockResolvedValue({ 98 | result: new Promise(resolve => resolve({ success: true })) 99 | } as any); 100 | jest.spyOn(nrwlWorkspce, 'readWorkspaceConfigPath').mockReturnValue({ 101 | projects: { 102 | 'project-mock': { 103 | architect: { 104 | build: { 105 | builder: '@nrwl/web:build', 106 | options: { 107 | main: 'apps/project-mock/src/main.tsx', 108 | webpackConfig: '@nrwl/react/plugins/webpack', 109 | outputPath: 'dist/apps/project-mock' 110 | } 111 | } 112 | } 113 | } 114 | } 115 | }); 116 | 117 | await runBuilder(options, context).toPromise(); 118 | 119 | expect(spawnSync.mock.calls[1][1]).toMatchSnapshot('deploy with pulumi'); 120 | }); 121 | 122 | it('should deploy with custom build (ncc)', async () => { 123 | jest.spyOn(context, 'getTargetOptions').mockResolvedValue({ 124 | main: 'somewhere.ts', 125 | pulumi: { 126 | 'aws:region': 'eu-central-1' 127 | } 128 | } as DeployTargetOptions); 129 | jest.spyOn(context, 'scheduleTarget').mockResolvedValue({ 130 | result: new Promise(resolve => resolve({ success: true })) 131 | } as any); 132 | jest 133 | .spyOn(utils, 'getApplicationType') 134 | .mockReturnValueOnce(utils.ApplicationType.NESTJS); 135 | jest.spyOn(nrwlWorkspce, 'readWorkspaceConfigPath').mockReturnValue({ 136 | projects: { 137 | 'project-mock': { 138 | architect: { 139 | build: { 140 | builder: '@nrwl/node:build', 141 | options: { 142 | outputPath: 'dist/apps/api', 143 | main: 'apps/api/src/main.ts', 144 | tsConfig: 'apps/api/tsconfig.app.json', 145 | assets: ['apps/api/src/assets'] 146 | } 147 | } 148 | }, 149 | root: 'apps/api' 150 | } 151 | } 152 | }); 153 | jest.spyOn(process, 'chdir').mockReturnValue(); 154 | jest.spyOn(process, 'cwd').mockReturnValue('mockProcessCwd'); 155 | (ncc as jest.SpyInstance).mockResolvedValueOnce({ 156 | code: 'Some Code', 157 | asset: { 'asset1.png': { source: 'code of asset 1' } } 158 | }); 159 | jest.spyOn(fsExtra, 'ensureDirSync').mockReturnThis(); 160 | jest.spyOn(fsExtra, 'ensureFileSync').mockReturnThis(); 161 | jest.spyOn(fs, 'writeFileSync').mockReturnThis(); 162 | 163 | await runBuilder(options, context).toPromise(); 164 | 165 | expect(process.cwd).toHaveBeenCalled(); 166 | expect(process.chdir).toHaveBeenCalledWith('/root/apps/api/infrastructure'); 167 | expect(process.chdir).toHaveBeenCalledWith('mockProcessCwd'); 168 | expect( 169 | ncc 170 | ).toHaveBeenCalledWith( 171 | '/root/apps/api/infrastructure/functions/main/index.ts', 172 | { cache: '/root/apps/api/infrastructure/buildcache' } 173 | ); 174 | 175 | // check if build was written 176 | expect(fsExtra.ensureDirSync).toHaveBeenCalledWith( 177 | '/root/apps/api/infrastructure/functions/dist/main' 178 | ); 179 | expect(fs.writeFileSync).toHaveBeenCalledWith( 180 | '/root/apps/api/infrastructure/functions/dist/main/index.js', 181 | 'Some Code' 182 | ); 183 | 184 | // check if assets were written 185 | expect(fsExtra.ensureFileSync).toHaveBeenCalledWith( 186 | '/root/apps/api/infrastructure/functions/dist/main/asset1.png' 187 | ); 188 | expect(fs.writeFileSync).toHaveBeenCalledWith( 189 | '/root/apps/api/infrastructure/functions/dist/main/asset1.png', 190 | 'code of asset 1' 191 | ); 192 | }); 193 | }); 194 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/deploy/builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BuilderContext, 3 | BuilderOutput, 4 | createBuilder, 5 | } from '@angular-devkit/architect'; 6 | import { Observable, from, of } from 'rxjs'; 7 | import { switchMap } from 'rxjs/operators'; 8 | import { NxDeployItDeployBuilderSchema } from './schema'; 9 | import { getApplicationType } from '../../utils/application-type'; 10 | import { resolve, dirname } from 'path'; 11 | import { DeployTargetOptions } from './target-options'; 12 | import { spawnSync } from 'child_process'; 13 | import { 14 | getPulumiBinaryPath, 15 | getProjectConfig, 16 | getAdapterByApplicationType, 17 | } from '../../utils/workspace'; 18 | 19 | function spawnStack( 20 | cwd: string, 21 | configuration: string, 22 | projectName: string, 23 | withInit = false 24 | ) { 25 | const args = [ 26 | 'stack', 27 | '--stack', 28 | `${configuration}-${projectName}`, 29 | '--cwd', 30 | cwd, 31 | ]; 32 | if (withInit) { 33 | args.splice(1, 0, 'init'); 34 | } 35 | 36 | return spawnSync(getPulumiBinaryPath(), args, { 37 | env: process.env, 38 | }); 39 | } 40 | 41 | function createStackIfNotExist( 42 | cwd: string, 43 | configuration: string, 44 | projectName: string 45 | ) { 46 | const result = spawnStack(cwd, configuration, projectName); 47 | if (result.stderr && result.stderr.toString().includes('no stack named')) { 48 | spawnStack(cwd, configuration, projectName, true); 49 | } 50 | } 51 | 52 | export function runBuilder( 53 | options: NxDeployItDeployBuilderSchema, 54 | context: BuilderContext 55 | ): Observable { 56 | if (!context?.target?.project) { 57 | return of({ success: false }); 58 | } 59 | const configuration = context.target.configuration || 'dev'; 60 | 61 | const project = getProjectConfig(context); 62 | const applicationType = getApplicationType(project.architect); 63 | 64 | return from(context.getTargetOptions(context.target)).pipe( 65 | switchMap((targetOptions: DeployTargetOptions) => { 66 | const cwd = dirname( 67 | resolve(context.workspaceRoot, targetOptions.main as string) 68 | ); 69 | 70 | createStackIfNotExist(cwd, configuration, context.target.project); 71 | 72 | const adapter = getAdapterByApplicationType( 73 | applicationType, 74 | project, 75 | options 76 | ); 77 | 78 | return adapter.deploy( 79 | context, 80 | cwd, 81 | options, 82 | configuration, 83 | targetOptions 84 | ); 85 | }) 86 | ); 87 | } 88 | 89 | export default createBuilder(runBuilder); 90 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/deploy/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { NxDeployItBaseOptions } from '../../adapter/base.adapter.model'; 2 | 3 | export interface NxDeployItDeployBuilderSchema extends NxDeployItBaseOptions { 4 | nonInteractive?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/deploy/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema", 3 | "$id": "https://json-schema.org/draft-07/schema", 4 | "title": "NxDeployIt Deploy infrastructure", 5 | "description": "Deploy the infrastructure for the app", 6 | "type": "object", 7 | "properties": { 8 | "nonInteractive": { 9 | "type": "boolean", 10 | "default": false, 11 | "description": "Changes will apply without user input" 12 | } 13 | }, 14 | "required": [] 15 | } 16 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/deploy/target-options.ts: -------------------------------------------------------------------------------- 1 | import { PROVIDER } from '../../utils/provider'; 2 | import { JsonObject } from '@angular-devkit/core'; 3 | 4 | export interface DeployTargetOptions extends JsonObject { 5 | main: string; 6 | provider: PROVIDER; 7 | pulumi: any; 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/destroy/__snapshots__/builder.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Command Runner Builder - Destroy should fail if no target exists 1`] = ` 4 | Object { 5 | "success": false, 6 | } 7 | `; 8 | 9 | exports[`Command Runner Builder - Destroy should run destroy and return success: false if pulumi fails: match result 1`] = ` 10 | Object { 11 | "error": "Pulumi failed", 12 | "success": false, 13 | } 14 | `; 15 | 16 | exports[`Command Runner Builder - Destroy should run destroy: Result of the pulumi script 1`] = ` 17 | Object { 18 | "success": true, 19 | } 20 | `; 21 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/destroy/builder.spec.ts: -------------------------------------------------------------------------------- 1 | import { NxDeployItDestroyBuilderSchema } from './schema'; 2 | import { MockBuilderContext } from '@nrwl/workspace/testing'; 3 | import { runBuilder } from './builder'; 4 | import { getMockContext } from '../../utils-test/builders.utils'; 5 | import { DestroyTargetOptions } from './target-options'; 6 | import * as childProcess from 'child_process'; 7 | 8 | describe('Command Runner Builder - Destroy', () => { 9 | let context: MockBuilderContext; 10 | let options: NxDeployItDestroyBuilderSchema; 11 | const spawnSync = jest.spyOn(childProcess, 'spawnSync'); 12 | 13 | beforeEach(async () => { 14 | context = await getMockContext(); 15 | 16 | options = { 17 | project: 'project-mock' 18 | }; 19 | }); 20 | 21 | afterEach(() => { 22 | jest.clearAllMocks(); 23 | }); 24 | 25 | it('should fail if no target exists', async () => { 26 | const result = await runBuilder(options, context).toPromise(); 27 | 28 | expect(result).toMatchSnapshot(); 29 | }); 30 | 31 | it('should run destroy', async () => { 32 | jest.spyOn(context, 'getTargetOptions').mockResolvedValue({ 33 | main: 'somewhere.ts' 34 | } as DestroyTargetOptions); 35 | spawnSync.mockReturnValue({ 36 | pid: null, 37 | output: [''], 38 | error: null, 39 | signal: null, 40 | status: null, 41 | stderr: null, 42 | stdout: null 43 | }); 44 | await context.addTarget( 45 | { project: 'project-mock', configuration: 'dev', target: 'destroy' }, 46 | 'destroy' 47 | ); 48 | context.target = { 49 | project: 'project-mock', 50 | configuration: 'dev', 51 | target: 'destroy' 52 | }; 53 | const result = await runBuilder(options, context).toPromise(); 54 | 55 | // expect(spawnSync.mock.calls[0][1]).toMatchSnapshot('Pulumi arguments'); 56 | expect(result).toMatchSnapshot('Result of the pulumi script'); 57 | }); 58 | 59 | it('should run destroy and return success: false if pulumi fails', async () => { 60 | jest.spyOn(context, 'getTargetOptions').mockResolvedValue({ 61 | main: 'somewhere.ts' 62 | } as DestroyTargetOptions); 63 | spawnSync.mockReturnValue({ 64 | pid: null, 65 | output: [''], 66 | error: new Error('Pulumi failed'), 67 | signal: null, 68 | status: null, 69 | stderr: null, 70 | stdout: null 71 | }); 72 | await context.addTarget( 73 | { project: 'project-mock', configuration: 'dev', target: 'destroy' }, 74 | 'destroy' 75 | ); 76 | context.target = { 77 | project: 'project-mock', 78 | configuration: 'dev', 79 | target: 'destroy' 80 | }; 81 | 82 | const result = await runBuilder(options, context).toPromise(); 83 | expect(result).toMatchSnapshot('match result'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/destroy/builder.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BuilderContext, 3 | BuilderOutput, 4 | createBuilder 5 | } from '@angular-devkit/architect'; 6 | import { Observable, from, of } from 'rxjs'; 7 | import { switchMap } from 'rxjs/operators'; 8 | import { NxDeployItDestroyBuilderSchema } from './schema'; 9 | import { spawnSync } from 'child_process'; 10 | import { getPulumiBinaryPath } from '../../utils/workspace'; 11 | import { DestroyTargetOptions } from './target-options'; 12 | import { dirname, resolve } from 'path'; 13 | 14 | function down( 15 | cwd: string, 16 | options: NxDeployItDestroyBuilderSchema, 17 | configuration: string, 18 | projectName: string 19 | ): Observable { 20 | const args = [ 21 | 'destroy', 22 | '--cwd', 23 | cwd, 24 | '--stack', 25 | `${configuration}-${projectName}` 26 | ]; 27 | if (options.nonInteractive) { 28 | args.push('--non-interactive', '--yes'); 29 | } 30 | const up = spawnSync(getPulumiBinaryPath(), args, { 31 | env: { ...process.env, PULUMI_SKIP_UPDATE_CHECK: '1' }, 32 | stdio: 'inherit' 33 | }); 34 | 35 | if (up.error) { 36 | return of({ success: false, error: up.error.message }); 37 | } 38 | 39 | return of({ success: true }); 40 | } 41 | 42 | export function runBuilder( 43 | options: NxDeployItDestroyBuilderSchema, 44 | context: BuilderContext 45 | ): Observable { 46 | if (!context?.target?.target) { 47 | return of({ success: false }); 48 | } 49 | 50 | const configuration = context.target.configuration || 'dev'; 51 | 52 | return from(context.getTargetOptions(context.target)).pipe( 53 | switchMap((targetOptions: DestroyTargetOptions) => { 54 | const cwd = dirname( 55 | resolve(context.workspaceRoot, targetOptions.main as string) 56 | ); 57 | 58 | return down(cwd, options, configuration, context.target.project); 59 | }) 60 | ); 61 | } 62 | 63 | export default createBuilder(runBuilder); 64 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/destroy/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from '@angular-devkit/core'; 2 | 3 | export interface NxDeployItDestroyBuilderSchema extends JsonObject { 4 | nonInteractive?: boolean; 5 | } 6 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/destroy/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft-07/schema", 3 | "$id": "https://json-schema.org/draft-07/schema", 4 | "title": "NxDeployIt Destroy infrastructure", 5 | "description": "Destroy the infrastructure for the app", 6 | "type": "object", 7 | "properties": { 8 | "nonInteractive": { 9 | "type": "boolean", 10 | "default": false, 11 | "description": "Changes will apply without user input" 12 | } 13 | }, 14 | "required": [] 15 | } 16 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/builders/destroy/target-options.ts: -------------------------------------------------------------------------------- 1 | import { JsonObject } from '@angular-devkit/core'; 2 | 3 | export interface DestroyTargetOptions extends JsonObject { 4 | main: string; 5 | } 6 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dev-Thought/nx-plugins/4f34f6cba6fd7d7330b68407f528bff83ebfd06e/libs/nx-deploy-it/src/index.ts -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/__snapshots__/schematic.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`init schematic aws provider should extend the project configuration options with aws profile 1`] = ` 4 | Object { 5 | "builder": "@dev-thought/nx-deploy-it:deploy", 6 | "options": Object { 7 | "main": "apps/mock-project/infrastructure/index.ts", 8 | "provider": "aws", 9 | "pulumi": Object { 10 | "aws:profile": "my-aws-profile", 11 | "aws:region": "eu-central-1", 12 | }, 13 | }, 14 | } 15 | `; 16 | 17 | exports[`init schematic aws provider should extend the project configuration options with aws profile 2`] = ` 18 | Object { 19 | "builder": "@dev-thought/nx-deploy-it:destroy", 20 | "options": Object { 21 | "main": "apps/mock-project/infrastructure/index.ts", 22 | "provider": "aws", 23 | }, 24 | } 25 | `; 26 | 27 | exports[`init schematic aws provider should extend the project default configuration options: Deploy Action 1`] = ` 28 | Object { 29 | "builder": "@dev-thought/nx-deploy-it:deploy", 30 | "options": Object { 31 | "main": "apps/mock-project/infrastructure/index.ts", 32 | "provider": "aws", 33 | "pulumi": Object { 34 | "aws:profile": "", 35 | "aws:region": "eu-central-1", 36 | }, 37 | }, 38 | } 39 | `; 40 | 41 | exports[`init schematic aws provider should extend the project default configuration options: Destroy Action 1`] = ` 42 | Object { 43 | "builder": "@dev-thought/nx-deploy-it:destroy", 44 | "options": Object { 45 | "main": "apps/mock-project/infrastructure/index.ts", 46 | "provider": "aws", 47 | }, 48 | } 49 | `; 50 | 51 | exports[`init schematic azure provider should extend the project default configuration options: Deploy Action 1`] = ` 52 | Object { 53 | "builder": "@dev-thought/nx-deploy-it:deploy", 54 | "options": Object { 55 | "main": "apps/mock-project/infrastructure/index.ts", 56 | "provider": "azure", 57 | "pulumi": Object { 58 | "azure:location": "eastasia", 59 | }, 60 | }, 61 | } 62 | `; 63 | 64 | exports[`init schematic azure provider should extend the project default configuration options: Destroy Action 1`] = ` 65 | Object { 66 | "builder": "@dev-thought/nx-deploy-it:destroy", 67 | "options": Object { 68 | "main": "apps/mock-project/infrastructure/index.ts", 69 | "provider": "azure", 70 | }, 71 | } 72 | `; 73 | 74 | exports[`init schematic google cloud platform provider should extend the project default configuration options: Deploy Action 1`] = ` 75 | Object { 76 | "builder": "@dev-thought/nx-deploy-it:deploy", 77 | "options": Object { 78 | "main": "apps/mock-project/infrastructure/index.ts", 79 | "provider": "gcp", 80 | "pulumi": Object { 81 | "gcp:project": "my-google-project-id", 82 | "gcp:region": "europe-west1", 83 | }, 84 | }, 85 | } 86 | `; 87 | 88 | exports[`init schematic google cloud platform provider should extend the project default configuration options: Destroy Action 1`] = ` 89 | Object { 90 | "builder": "@dev-thought/nx-deploy-it:destroy", 91 | "options": Object { 92 | "main": "apps/mock-project/infrastructure/index.ts", 93 | "provider": "gcp", 94 | }, 95 | } 96 | `; 97 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/architect-options.ts: -------------------------------------------------------------------------------- 1 | import { PROVIDER } from '../../utils/provider'; 2 | 3 | export interface ArchitectOptions { 4 | main: string; 5 | provider: PROVIDER; 6 | useCdn?: boolean; 7 | customDomainName?: string; 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/cdn.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | 3 | import { Output } from '@pulumi/pulumi'; 4 | import { Bucket } from '@pulumi/aws/s3'; 5 | import { Distribution } from '@pulumi/aws/cloudfront'; 6 | import { createAliasRecord } from './utils'; 7 | 8 | export function createCdn( 9 | config: any, 10 | contentBucket: Bucket, 11 | certificateArn?: Output 12 | ): Distribution { 13 | // logsBucket is an S3 bucket that will contain the CDN's request logs. 14 | const logsBucket = new Bucket(`${config.projectName}-requestLogs`, { 15 | acl: 'private', 16 | forceDestroy: true 17 | }); 18 | 19 | const tenMinutes = 60 * 10; 20 | 21 | // distributionArgs configures the CloudFront distribution. Relevant documentation: 22 | // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html 23 | // https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html 24 | const distributionArgs: aws.cloudfront.DistributionArgs = { 25 | enabled: true, 26 | // Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net. 27 | // Required if you want to access the distribution via config.customDomainName as well. 28 | aliases: config.customDomainName ? [config.customDomainName] : undefined, 29 | 30 | // We only specify one origin for this distribution, the S3 content bucket. 31 | origins: [ 32 | { 33 | originId: contentBucket.arn, 34 | domainName: contentBucket.websiteEndpoint, 35 | customOriginConfig: { 36 | // Amazon S3 doesn't support HTTPS connections when using an S3 bucket configured as a website endpoint. 37 | // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy 38 | originProtocolPolicy: 'http-only', 39 | httpPort: 80, 40 | httpsPort: 443, 41 | originSslProtocols: ['TLSv1.2'] 42 | } 43 | } 44 | ], 45 | 46 | defaultRootObject: 'index.html', 47 | 48 | // A CloudFront distribution can configure different cache behaviors based on the request path. 49 | // Here we just specify a single, default cache behavior which is just read-only requests to S3. 50 | defaultCacheBehavior: { 51 | targetOriginId: contentBucket.arn, 52 | 53 | viewerProtocolPolicy: 'redirect-to-https', 54 | allowedMethods: ['GET', 'HEAD', 'OPTIONS'], 55 | cachedMethods: ['GET', 'HEAD', 'OPTIONS'], 56 | 57 | forwardedValues: { 58 | cookies: { forward: 'none' }, 59 | queryString: false 60 | }, 61 | 62 | minTtl: 0, 63 | defaultTtl: tenMinutes, 64 | maxTtl: tenMinutes 65 | }, 66 | 67 | // "All" is the most broad distribution, and also the most expensive. 68 | // "100" is the least broad, and also the least expensive. 69 | priceClass: 'PriceClass_100', 70 | 71 | // You can customize error responses. When CloudFront recieves an error from the origin (e.g. S3 or some other 72 | // web service) it can return a different error code, and return the response for a different resource. 73 | customErrorResponses: [ 74 | { errorCode: 404, responseCode: 200, responsePagePath: '/index.html' } 75 | ], 76 | 77 | restrictions: { 78 | geoRestriction: { 79 | restrictionType: 'none' 80 | } 81 | }, 82 | 83 | viewerCertificate: { 84 | acmCertificateArn: certificateArn, // Per AWS, ACM certificate must be in the us-east-1 region. 85 | cloudfrontDefaultCertificate: certificateArn ? false : true, 86 | sslSupportMethod: 'sni-only' 87 | }, 88 | 89 | loggingConfig: { 90 | bucket: logsBucket.bucketDomainName, 91 | includeCookies: false, 92 | prefix: config.customDomainName ? `${config.customDomainName}/` : '' 93 | } 94 | }; 95 | 96 | const cdn = new Distribution(`${config.projectName}-cdn`, distributionArgs); 97 | 98 | if (config.customDomainName) { 99 | const aliasRecord = createAliasRecord(config.customDomainName, cdn); 100 | } 101 | 102 | return cdn; 103 | } 104 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/certificate.ts.template: -------------------------------------------------------------------------------- 1 | import { Provider, config, route53 } from '@pulumi/aws'; 2 | import { Certificate, CertificateValidation } from '@pulumi/aws/acm'; 3 | import { Record } from '@pulumi/aws/route53'; 4 | import { Output } from '@pulumi/pulumi'; 5 | import { getDomainAndSubdomain } from './utils'; 6 | 7 | export function createCertificate(customDomainName: string): Output { 8 | const tenMinutes = 60 * 10; 9 | 10 | const eastRegion = new Provider('east', { 11 | region: 'us-east-1', // Per AWS, ACM certificate must be in the us-east-1 region. 12 | profile: config.profile 13 | }); 14 | 15 | const certificate = new Certificate( 16 | 'certificate', 17 | { 18 | domainName: customDomainName, 19 | validationMethod: 'DNS' 20 | }, 21 | { provider: eastRegion } 22 | ); 23 | 24 | const domainParts = getDomainAndSubdomain(customDomainName); 25 | const zoneId = route53 26 | .getZone({ name: domainParts.parentDomain }, { async: true }) 27 | .then(zone => zone.zoneId); 28 | 29 | // /** 30 | // * Create a DNS record to prove that we _own_ the domain we're requesting a certificate for. 31 | // * See https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html for more info. 32 | // */ 33 | const certificateValidationDomain = new Record( 34 | `${customDomainName}-validation`, 35 | { 36 | name: certificate.domainValidationOptions[0].resourceRecordName, 37 | zoneId, 38 | type: certificate.domainValidationOptions[0].resourceRecordType, 39 | records: [certificate.domainValidationOptions[0].resourceRecordValue], 40 | ttl: tenMinutes 41 | } 42 | ); 43 | 44 | /** 45 | * This is a _special_ resource that waits for ACM to complete validation via the DNS record 46 | * checking for a status of "ISSUED" on the certificate itself. No actual resources are 47 | * created (or updated or deleted). 48 | * 49 | * See https://www.terraform.io/docs/providers/aws/r/acm_certificate_validation.html for slightly more detail 50 | * and https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_acm_certificate_validation.go 51 | * for the actual implementation. 52 | */ 53 | const certificateValidation = new CertificateValidation( 54 | 'certificateValidation', 55 | { 56 | certificateArn: certificate.arn, 57 | validationRecordFqdns: [certificateValidationDomain.fqdn] 58 | }, 59 | { provider: eastRegion, parent: certificate } 60 | ); 61 | 62 | return certificateValidation.certificateArn; 63 | } 64 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as serverless from 'aws-serverless-express'; 2 | import { Server } from 'http'; 3 | import { app } from '../../../<%= getRootDirectory() %>server'; 4 | 5 | let cachedServer: Server; 6 | 7 | async function bootstrapServer(): Promise { 8 | return serverless.createServer(app()); 9 | } 10 | 11 | export const handler = (event, context) => { 12 | if (!cachedServer) { 13 | bootstrapServer().then(server => { 14 | cachedServer = server; 15 | serverless.proxy(server, event, context); 16 | }); 17 | } else { 18 | serverless.proxy(cachedServer, event, context); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | import * as mime from 'mime'; 4 | 5 | import { createCdn } from './cdn'; 6 | import { createCertificate } from './certificate'; 7 | import { crawlDirectory } from './utils'; 8 | import { createLambda } from './server-side-rendering'; 9 | 10 | const stackConfig = new pulumi.Config(); 11 | const config = { 12 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 13 | projectName: stackConfig.get('projectName'), 14 | distPath: stackConfig.get('distPath'), 15 | useCdn: stackConfig.getBoolean('useCdn'), 16 | customDomainName: stackConfig.get('customDomainName'), 17 | angularUniversalDeploymentType: stackConfig.get( 18 | 'angularUniversalDeploymentType' 19 | ) 20 | // ===== END ====== 21 | }; 22 | const projectName = config.projectName; 23 | const stageName = pulumi.getStack().split('-')[0]; 24 | const region = aws.config.requireRegion(); 25 | 26 | let lambda: { endpoint: pulumi.Output }; 27 | let contentBucket: aws.s3.Bucket; 28 | let cdn: aws.cloudfront.Distribution; 29 | if (config.angularUniversalDeploymentType === 'ssr') { 30 | lambda = createLambda(projectName, stageName, region); 31 | } else { 32 | // contentBucket is the S3 bucket that the website's contents will be stored in. 33 | contentBucket = new aws.s3.Bucket(`${projectName}-contentBucket`, { 34 | acl: 'public-read', 35 | // Configure S3 to serve bucket contents as a website. This way S3 will automatically convert 36 | // requests for "foo/" to "foo/index.html". 37 | website: { 38 | indexDocument: 'index.html', 39 | errorDocument: 'index.html' 40 | }, 41 | forceDestroy: true 42 | }); 43 | 44 | // Sync the contents of the source directory with the S3 bucket, which will in-turn show up on the CDN. 45 | crawlDirectory(config.distPath, (filePath: string) => { 46 | const relativeFilePath = filePath.replace(config.distPath + '/', ''); 47 | const contentFile = new aws.s3.BucketObject( 48 | relativeFilePath, 49 | { 50 | key: relativeFilePath, 51 | 52 | acl: 'public-read', 53 | bucket: contentBucket, 54 | contentType: mime.getType(filePath) || undefined, 55 | source: new pulumi.asset.FileAsset(filePath) 56 | }, 57 | { 58 | parent: contentBucket 59 | } 60 | ); 61 | }); 62 | 63 | if (config.useCdn) { 64 | let certificateArn: pulumi.Output; 65 | if (config.customDomainName) { 66 | certificateArn = createCertificate(config.customDomainName); 67 | } 68 | 69 | cdn = createCdn(config, contentBucket, certificateArn); 70 | } 71 | } 72 | 73 | // Export properties from this stack. This prints them at the end of `pulumi up` and 74 | // makes them easier to access from the pulumi.com. 75 | export const staticEndpoint = contentBucket && contentBucket.websiteEndpoint; 76 | export const cdnEndpoint = cdn && cdn.domainName; 77 | export const cdnCustomDomain = 78 | config.customDomainName && 79 | pulumi.interpolate`https://${config.customDomainName}`; 80 | export const anuglarUniversalEndpoint = lambda && lambda.endpoint; 81 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/server-side-rendering.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | export function createLambda( 5 | projectName: string, 6 | stageName: string, 7 | region: string 8 | ) { 9 | /////////////////// 10 | // Lambda Function 11 | /////////////////// 12 | 13 | const role = new aws.iam.Role(`${projectName}-lambda-role`, { 14 | assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ 15 | Service: 'lambda.amazonaws.com' 16 | }) 17 | }); 18 | 19 | const policy = new aws.iam.RolePolicy(`${projectName}-lambda-policy`, { 20 | role, 21 | policy: pulumi.output({ 22 | Version: '2012-10-17', 23 | Statement: [ 24 | { 25 | Action: ['logs:*', 'cloudwatch:*'], 26 | Resource: '*', 27 | Effect: 'Allow' 28 | } 29 | ] 30 | }) 31 | }); 32 | 33 | const lambda = new aws.lambda.Function( 34 | `${projectName}-function`, 35 | { 36 | memorySize: 128, 37 | code: new pulumi.asset.AssetArchive({ 38 | [`dist/${projectName}/browser`]: new pulumi.asset.FileArchive( 39 | `../../../dist/${projectName}/browser` 40 | ), 41 | 'server/': new pulumi.asset.FileArchive( 42 | `../../../dist/${projectName}/server` 43 | ) 44 | }), 45 | runtime: 'nodejs12.x', 46 | handler: `server/main.handler`, 47 | role: role.arn 48 | }, 49 | { dependsOn: [policy] } 50 | ); 51 | 52 | /////////////////// 53 | // APIGateway RestAPI 54 | /////////////////// 55 | 56 | function lambdaArn(arn: string) { 57 | return `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`; 58 | } 59 | 60 | // Create the API Gateway Rest API, using a swagger spec. 61 | const restApi = new aws.apigateway.RestApi( 62 | `${projectName}-restapi`, 63 | {}, 64 | { dependsOn: [lambda] } 65 | ); 66 | 67 | const rootApigatewayMethod = new aws.apigateway.Method( 68 | `${projectName}-root-apigateway-method`, 69 | { 70 | restApi: restApi, 71 | resourceId: restApi.rootResourceId, 72 | httpMethod: 'ANY', 73 | authorization: 'NONE' 74 | } 75 | ); 76 | const rootApigatewayIntegration = new aws.apigateway.Integration( 77 | `${projectName}-root-apigateway-integration`, 78 | { 79 | restApi, 80 | resourceId: restApi.rootResourceId, 81 | httpMethod: rootApigatewayMethod.httpMethod, 82 | integrationHttpMethod: 'POST', 83 | type: 'AWS_PROXY', 84 | uri: lambda.arn.apply(lambdaArn) 85 | } 86 | ); 87 | 88 | const proxyResource = new aws.apigateway.Resource( 89 | `${projectName}-proxy-resource`, 90 | { 91 | restApi, 92 | parentId: restApi.rootResourceId, 93 | pathPart: '{proxy+}' 94 | } 95 | ); 96 | 97 | const proxyMethod = new aws.apigateway.Method(`${projectName}-proxy-method`, { 98 | restApi: restApi, 99 | resourceId: proxyResource.id, 100 | httpMethod: 'ANY', 101 | authorization: 'NONE' 102 | }); 103 | 104 | const proxyIntegration = new aws.apigateway.Integration( 105 | `${projectName}-proxy-integration`, 106 | { 107 | restApi, 108 | resourceId: proxyResource.id, 109 | httpMethod: proxyMethod.httpMethod, 110 | integrationHttpMethod: 'POST', 111 | type: 'AWS_PROXY', 112 | uri: lambda.arn.apply(lambdaArn) 113 | } 114 | ); 115 | 116 | // Create a deployment of the Rest API. 117 | const deployment = new aws.apigateway.Deployment( 118 | `${projectName}-restapi-deployment>`, 119 | { 120 | restApi: restApi, 121 | // Note: Set to empty to avoid creating an implicit stage, we'll create it explicitly below instead. 122 | stageName: '' 123 | }, 124 | { dependsOn: [rootApigatewayIntegration, proxyIntegration] } 125 | ); 126 | 127 | // Create a stage, which is an addressable instance of the Rest API. Set it to point at the latest deployment. 128 | const stage = new aws.apigateway.Stage(`${projectName}-restapi-stage`, { 129 | restApi: restApi, 130 | deployment: deployment, 131 | stageName: stageName 132 | }); 133 | 134 | // Give permissions from API Gateway to invoke the Lambda 135 | const invokePermission = new aws.lambda.Permission( 136 | `${projectName}-restapi-lambda-permission`, 137 | { 138 | action: 'lambda:invokeFunction', 139 | function: lambda, 140 | principal: 'apigateway.amazonaws.com', 141 | sourceArn: pulumi.interpolate`${deployment.executionArn}*/*` 142 | } 143 | ); 144 | 145 | return { 146 | endpoint: pulumi.interpolate`${deployment.invokeUrl}${stageName}` 147 | }; 148 | } 149 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.server.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | }, 8 | "files": ["functions/main/index.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/angular-universal/infrastructure/utils.ts.template: -------------------------------------------------------------------------------- 1 | import { Distribution } from '@pulumi/aws/cloudfront'; 2 | import { Record } from '@pulumi/aws/route53'; 3 | import { route53 } from '@pulumi/aws'; 4 | import { readdirSync, statSync } from 'fs'; 5 | 6 | export function getDomainAndSubdomain( 7 | domain: string 8 | ): { subdomain: string; parentDomain: string } { 9 | const parts = domain.split('.'); 10 | if (parts.length < 2) { 11 | throw new Error(`No TLD found on ${domain}`); 12 | } 13 | if (parts.length === 2) { 14 | return { subdomain: '', parentDomain: domain }; 15 | } 16 | 17 | const subdomain = parts[0]; 18 | parts.shift(); 19 | return { 20 | subdomain, 21 | // Trailing "." to canonicalize domain. 22 | parentDomain: parts.join('.') + '.' 23 | }; 24 | } 25 | 26 | // Creates a new Route53 DNS record pointing the domain to the CloudFront distribution. 27 | export function createAliasRecord( 28 | targetDomain: string, 29 | distribution: Distribution 30 | ): Record { 31 | const domainParts = getDomainAndSubdomain(targetDomain); 32 | const hostedZoneId = route53 33 | .getZone({ name: domainParts.parentDomain }, { async: true }) 34 | .then(zone => zone.zoneId); 35 | return new Record(targetDomain, { 36 | name: domainParts.subdomain, 37 | zoneId: hostedZoneId, 38 | type: 'A', 39 | aliases: [ 40 | { 41 | name: distribution.domainName, 42 | zoneId: distribution.hostedZoneId, 43 | evaluateTargetHealth: true 44 | } 45 | ] 46 | }); 47 | } 48 | 49 | export function crawlDirectory(dir: string, f: (_: string) => void) { 50 | const files = readdirSync(dir); 51 | for (const file of files) { 52 | const filePath = `${dir}/${file}`; 53 | const stat = statSync(filePath); 54 | if (stat.isDirectory()) { 55 | crawlDirectory(filePath, f); 56 | } 57 | if (stat.isFile()) { 58 | f(filePath); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/express/__rootDir__/main.aws.ts.template: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | const app = express(); 4 | 5 | app.get('/', (req, res) => { 6 | res.send({ message: 'Welcome to your deployed app!' }); 7 | }); 8 | 9 | // export your express application as expressApp 10 | export const expressApp = app; 11 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/express/infrastructure/.gitignore.template: -------------------------------------------------------------------------------- 1 | functions/dist 2 | buildcache 3 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/express/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as serverless from 'aws-serverless-express'; 2 | import { Server } from 'http'; 3 | import { expressApp } from '../../../<%= getRootDirectory() %>/main.aws'; 4 | 5 | let cachedServer: Server; 6 | 7 | async function bootstrapServer(): Promise { 8 | return serverless.createServer(expressApp); 9 | } 10 | 11 | export const handler = (event, context) => { 12 | if (!cachedServer) { 13 | bootstrapServer().then(server => { 14 | cachedServer = server; 15 | serverless.proxy(server, event, context); 16 | }); 17 | } else { 18 | serverless.proxy(cachedServer, event, context); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/express/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | const stackConfig = new pulumi.Config(); 5 | const config = { 6 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 7 | projectName: stackConfig.get('projectName') 8 | // ===== END ====== 9 | }; 10 | const projectName = config.projectName; 11 | const stageName = pulumi.getStack().split('-')[0]; 12 | const region = aws.config.requireRegion(); 13 | 14 | /////////////////// 15 | // Lambda Function 16 | /////////////////// 17 | 18 | const role = new aws.iam.Role(`${projectName}-lambda-role`, { 19 | assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ 20 | Service: 'lambda.amazonaws.com' 21 | }) 22 | }); 23 | 24 | const policy = new aws.iam.RolePolicy(`${projectName}-lambda-policy`, { 25 | role, 26 | policy: pulumi.output({ 27 | Version: '2012-10-17', 28 | Statement: [ 29 | { 30 | Action: ['logs:*', 'cloudwatch:*'], 31 | Resource: '*', 32 | Effect: 'Allow' 33 | } 34 | ] 35 | }) 36 | }); 37 | 38 | const lambda = new aws.lambda.Function( 39 | `${projectName}-function`, 40 | { 41 | memorySize: 128, 42 | code: new pulumi.asset.FileArchive('./functions/dist/main'), 43 | runtime: 'nodejs12.x', 44 | handler: 'index.handler', 45 | role: role.arn 46 | }, 47 | { dependsOn: [policy] } 48 | ); 49 | 50 | /////////////////// 51 | // APIGateway RestAPI 52 | /////////////////// 53 | 54 | function lambdaArn(arn: string) { 55 | return `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`; 56 | } 57 | 58 | // Create the API Gateway Rest API, using a swagger spec. 59 | const restApi = new aws.apigateway.RestApi( 60 | `${projectName}-restapi`, 61 | {}, 62 | { dependsOn: [lambda] } 63 | ); 64 | 65 | const rootApigatewayMethod = new aws.apigateway.Method( 66 | `${projectName}-root-apigateway-method`, 67 | { 68 | restApi: restApi, 69 | resourceId: restApi.rootResourceId, 70 | httpMethod: 'ANY', 71 | authorization: 'NONE' 72 | } 73 | ); 74 | const rootApigatewayIntegration = new aws.apigateway.Integration( 75 | `${projectName}-root-apigateway-integration`, 76 | { 77 | restApi, 78 | resourceId: restApi.rootResourceId, 79 | httpMethod: rootApigatewayMethod.httpMethod, 80 | integrationHttpMethod: 'POST', 81 | type: 'AWS_PROXY', 82 | uri: lambda.arn.apply(lambdaArn) 83 | } 84 | ); 85 | 86 | const proxyResource = new aws.apigateway.Resource( 87 | `${projectName}-proxy-resource`, 88 | { 89 | restApi, 90 | parentId: restApi.rootResourceId, 91 | pathPart: '{proxy+}' 92 | } 93 | ); 94 | 95 | const proxyMethod = new aws.apigateway.Method(`${projectName}-proxy-method`, { 96 | restApi: restApi, 97 | resourceId: proxyResource.id, 98 | httpMethod: 'ANY', 99 | authorization: 'NONE' 100 | }); 101 | 102 | const proxyIntegration = new aws.apigateway.Integration( 103 | `${projectName}-proxy-integration`, 104 | { 105 | restApi, 106 | resourceId: proxyResource.id, 107 | httpMethod: proxyMethod.httpMethod, 108 | integrationHttpMethod: 'POST', 109 | type: 'AWS_PROXY', 110 | uri: lambda.arn.apply(lambdaArn) 111 | } 112 | ); 113 | 114 | // Create a deployment of the Rest API. 115 | const deployment = new aws.apigateway.Deployment( 116 | `${projectName}-restapi-deployment>`, 117 | { 118 | restApi: restApi, 119 | // Note: Set to empty to avoid creating an implicit stage, we'll create it explicitly below instead. 120 | stageName: '' 121 | }, 122 | { dependsOn: [rootApigatewayIntegration, proxyIntegration] } 123 | ); 124 | 125 | // Create a stage, which is an addressable instance of the Rest API. Set it to point at the latest deployment. 126 | const stage = new aws.apigateway.Stage(`${projectName}-restapi-stage`, { 127 | restApi: restApi, 128 | deployment: deployment, 129 | stageName: stageName 130 | }); 131 | 132 | // Give permissions from API Gateway to invoke the Lambda 133 | const invokePermission = new aws.lambda.Permission( 134 | `${projectName}-restapi-lambda-permission`, 135 | { 136 | action: 'lambda:invokeFunction', 137 | function: lambda, 138 | principal: 'apigateway.amazonaws.com', 139 | sourceArn: pulumi.interpolate`${deployment.executionArn}*/*` 140 | } 141 | ); 142 | 143 | exports.endpoint = pulumi.interpolate`${deployment.invokeUrl}${stageName}`; 144 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/express/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.app.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/nestjs/__rootDir__/main.aws.ts.template: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { ExpressAdapter } from '@nestjs/platform-express'; 4 | import { <%= getRootModuleName() %> } from './<%= getRootModulePath() %>'; 5 | 6 | export async function createApp( 7 | expressAdapter: ExpressAdapter 8 | ): Promise { 9 | const app = await NestFactory.create(<%= getRootModuleName() %>, expressAdapter); 10 | 11 | app.enableCors(); 12 | await app.init(); 13 | return app; 14 | } -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/nestjs/infrastructure/.gitignore.template: -------------------------------------------------------------------------------- 1 | functions/dist 2 | buildcache 3 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/nestjs/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import { ExpressAdapter } from '@nestjs/platform-express'; 2 | import * as serverless from 'aws-serverless-express'; 3 | import * as express from 'express'; 4 | import { Server } from 'http'; 5 | import { createApp } from '../../../<%= getRootDirectory() %>/main.aws'; 6 | 7 | let cachedServer: Server; 8 | 9 | async function bootstrapServer(): Promise { 10 | const expressServer = express(); 11 | await createApp(new ExpressAdapter(expressServer)); 12 | 13 | return serverless.createServer(expressServer); 14 | } 15 | 16 | export const handler = (event, context) => { 17 | if (!cachedServer) { 18 | bootstrapServer().then(server => { 19 | cachedServer = server; 20 | serverless.proxy(server, event, context); 21 | }); 22 | } else { 23 | serverless.proxy(cachedServer, event, context); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/nestjs/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | const stackConfig = new pulumi.Config(); 5 | const config = { 6 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 7 | projectName: stackConfig.get('projectName') 8 | // ===== END ====== 9 | }; 10 | const projectName = config.projectName; 11 | const stageName = pulumi.getStack().split('-')[0]; 12 | const region = aws.config.requireRegion(); 13 | 14 | /////////////////// 15 | // Lambda Function 16 | /////////////////// 17 | 18 | const role = new aws.iam.Role(`${projectName}-lambda-role`, { 19 | assumeRolePolicy: aws.iam.assumeRolePolicyForPrincipal({ 20 | Service: 'lambda.amazonaws.com' 21 | }) 22 | }); 23 | 24 | const policy = new aws.iam.RolePolicy(`${projectName}-lambda-policy`, { 25 | role, 26 | policy: pulumi.output({ 27 | Version: '2012-10-17', 28 | Statement: [ 29 | { 30 | Action: ['logs:*', 'cloudwatch:*'], 31 | Resource: '*', 32 | Effect: 'Allow' 33 | } 34 | ] 35 | }) 36 | }); 37 | 38 | const lambda = new aws.lambda.Function( 39 | `${projectName}-function`, 40 | { 41 | memorySize: 128, 42 | code: new pulumi.asset.FileArchive('./functions/dist/main'), 43 | runtime: 'nodejs12.x', 44 | handler: 'index.handler', 45 | role: role.arn 46 | }, 47 | { dependsOn: [policy] } 48 | ); 49 | 50 | /////////////////// 51 | // APIGateway RestAPI 52 | /////////////////// 53 | 54 | function lambdaArn(arn: string) { 55 | return `arn:aws:apigateway:${region}:lambda:path/2015-03-31/functions/${arn}/invocations`; 56 | } 57 | 58 | // Create the API Gateway Rest API, using a swagger spec. 59 | const restApi = new aws.apigateway.RestApi( 60 | `${projectName}-restapi`, 61 | {}, 62 | { dependsOn: [lambda] } 63 | ); 64 | 65 | const rootApigatewayMethod = new aws.apigateway.Method( 66 | `${projectName}-root-apigateway-method`, 67 | { 68 | restApi: restApi, 69 | resourceId: restApi.rootResourceId, 70 | httpMethod: 'ANY', 71 | authorization: 'NONE' 72 | } 73 | ); 74 | const rootApigatewayIntegration = new aws.apigateway.Integration( 75 | `${projectName}-root-apigateway-integration`, 76 | { 77 | restApi, 78 | resourceId: restApi.rootResourceId, 79 | httpMethod: rootApigatewayMethod.httpMethod, 80 | integrationHttpMethod: 'POST', 81 | type: 'AWS_PROXY', 82 | uri: lambda.arn.apply(lambdaArn) 83 | } 84 | ); 85 | 86 | const proxyResource = new aws.apigateway.Resource( 87 | `${projectName}-proxy-resource`, 88 | { 89 | restApi, 90 | parentId: restApi.rootResourceId, 91 | pathPart: '{proxy+}' 92 | } 93 | ); 94 | 95 | const proxyMethod = new aws.apigateway.Method(`${projectName}-proxy-method`, { 96 | restApi: restApi, 97 | resourceId: proxyResource.id, 98 | httpMethod: 'ANY', 99 | authorization: 'NONE' 100 | }); 101 | 102 | const proxyIntegration = new aws.apigateway.Integration( 103 | `${projectName}-proxy-integration`, 104 | { 105 | restApi, 106 | resourceId: proxyResource.id, 107 | httpMethod: proxyMethod.httpMethod, 108 | integrationHttpMethod: 'POST', 109 | type: 'AWS_PROXY', 110 | uri: lambda.arn.apply(lambdaArn) 111 | } 112 | ); 113 | 114 | // Create a deployment of the Rest API. 115 | const deployment = new aws.apigateway.Deployment( 116 | `${projectName}-restapi-deployment>`, 117 | { 118 | restApi: restApi, 119 | // Note: Set to empty to avoid creating an implicit stage, we'll create it explicitly below instead. 120 | stageName: '' 121 | }, 122 | { dependsOn: [rootApigatewayIntegration, proxyIntegration] } 123 | ); 124 | 125 | // Create a stage, which is an addressable instance of the Rest API. Set it to point at the latest deployment. 126 | const stage = new aws.apigateway.Stage(`${projectName}-restapi-stage`, { 127 | restApi: restApi, 128 | deployment: deployment, 129 | stageName: stageName 130 | }); 131 | 132 | // Give permissions from API Gateway to invoke the Lambda 133 | const invokePermission = new aws.lambda.Permission( 134 | `${projectName}-restapi-lambda-permission`, 135 | { 136 | action: 'lambda:invokeFunction', 137 | function: lambda, 138 | principal: 'apigateway.amazonaws.com', 139 | sourceArn: pulumi.interpolate`${deployment.executionArn}*/*` 140 | } 141 | ); 142 | 143 | exports.endpoint = pulumi.interpolate`${deployment.invokeUrl}${stageName}`; 144 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/nestjs/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.app.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/webapp/infrastructure/cdn.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | 3 | import { Output } from '@pulumi/pulumi'; 4 | import { Bucket } from '@pulumi/aws/s3'; 5 | import { Distribution } from '@pulumi/aws/cloudfront'; 6 | import { createAliasRecord } from './utils'; 7 | 8 | export function createCdn( 9 | config: any, 10 | contentBucket: Bucket, 11 | certificateArn?: Output 12 | ): Distribution { 13 | // logsBucket is an S3 bucket that will contain the CDN's request logs. 14 | const logsBucket = new Bucket(`${config.projectName}-requestLogs`, { 15 | acl: 'private', 16 | forceDestroy: true 17 | }); 18 | 19 | const tenMinutes = 60 * 10; 20 | 21 | // distributionArgs configures the CloudFront distribution. Relevant documentation: 22 | // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html 23 | // https://www.terraform.io/docs/providers/aws/r/cloudfront_distribution.html 24 | const distributionArgs: aws.cloudfront.DistributionArgs = { 25 | enabled: true, 26 | // Alternate aliases the CloudFront distribution can be reached at, in addition to https://xxxx.cloudfront.net. 27 | // Required if you want to access the distribution via config.customDomainName as well. 28 | aliases: config.customDomainName ? [config.customDomainName] : undefined, 29 | 30 | // We only specify one origin for this distribution, the S3 content bucket. 31 | origins: [ 32 | { 33 | originId: contentBucket.arn, 34 | domainName: contentBucket.websiteEndpoint, 35 | customOriginConfig: { 36 | // Amazon S3 doesn't support HTTPS connections when using an S3 bucket configured as a website endpoint. 37 | // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesOriginProtocolPolicy 38 | originProtocolPolicy: 'http-only', 39 | httpPort: 80, 40 | httpsPort: 443, 41 | originSslProtocols: ['TLSv1.2'] 42 | } 43 | } 44 | ], 45 | 46 | defaultRootObject: 'index.html', 47 | 48 | // A CloudFront distribution can configure different cache behaviors based on the request path. 49 | // Here we just specify a single, default cache behavior which is just read-only requests to S3. 50 | defaultCacheBehavior: { 51 | targetOriginId: contentBucket.arn, 52 | 53 | viewerProtocolPolicy: 'redirect-to-https', 54 | allowedMethods: ['GET', 'HEAD', 'OPTIONS'], 55 | cachedMethods: ['GET', 'HEAD', 'OPTIONS'], 56 | 57 | forwardedValues: { 58 | cookies: { forward: 'none' }, 59 | queryString: false 60 | }, 61 | 62 | minTtl: 0, 63 | defaultTtl: tenMinutes, 64 | maxTtl: tenMinutes 65 | }, 66 | 67 | // "All" is the most broad distribution, and also the most expensive. 68 | // "100" is the least broad, and also the least expensive. 69 | priceClass: 'PriceClass_100', 70 | 71 | // You can customize error responses. When CloudFront recieves an error from the origin (e.g. S3 or some other 72 | // web service) it can return a different error code, and return the response for a different resource. 73 | customErrorResponses: [ 74 | { errorCode: 404, responseCode: 200, responsePagePath: '/index.html' } 75 | ], 76 | 77 | restrictions: { 78 | geoRestriction: { 79 | restrictionType: 'none' 80 | } 81 | }, 82 | 83 | viewerCertificate: { 84 | acmCertificateArn: certificateArn, // Per AWS, ACM certificate must be in the us-east-1 region. 85 | cloudfrontDefaultCertificate: certificateArn ? false : true, 86 | sslSupportMethod: 'sni-only' 87 | }, 88 | 89 | loggingConfig: { 90 | bucket: logsBucket.bucketDomainName, 91 | includeCookies: false, 92 | prefix: config.customDomainName ? `${config.customDomainName}/` : '' 93 | } 94 | }; 95 | 96 | const cdn = new Distribution(`${config.projectName}-cdn`, distributionArgs); 97 | 98 | if (config.customDomainName) { 99 | const aliasRecord = createAliasRecord(config.customDomainName, cdn); 100 | } 101 | 102 | return cdn; 103 | } 104 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/webapp/infrastructure/certificate.ts.template: -------------------------------------------------------------------------------- 1 | import { Provider, config, route53 } from '@pulumi/aws'; 2 | import { Certificate, CertificateValidation } from '@pulumi/aws/acm'; 3 | import { Record } from '@pulumi/aws/route53'; 4 | import { Output } from '@pulumi/pulumi'; 5 | import { getDomainAndSubdomain } from './utils'; 6 | 7 | export function createCertificate(customDomainName: string): Output { 8 | const tenMinutes = 60 * 10; 9 | 10 | const eastRegion = new Provider('east', { 11 | region: 'us-east-1', // Per AWS, ACM certificate must be in the us-east-1 region. 12 | profile: config.profile 13 | }); 14 | 15 | const certificate = new Certificate( 16 | 'certificate', 17 | { 18 | domainName: customDomainName, 19 | validationMethod: 'DNS' 20 | }, 21 | { provider: eastRegion } 22 | ); 23 | 24 | const domainParts = getDomainAndSubdomain(customDomainName); 25 | const zoneId = route53 26 | .getZone({ name: domainParts.parentDomain }, { async: true }) 27 | .then(zone => zone.zoneId); 28 | 29 | // /** 30 | // * Create a DNS record to prove that we _own_ the domain we're requesting a certificate for. 31 | // * See https://docs.aws.amazon.com/acm/latest/userguide/gs-acm-validate-dns.html for more info. 32 | // */ 33 | const certificateValidationDomain = new Record( 34 | `${customDomainName}-validation`, 35 | { 36 | name: certificate.domainValidationOptions[0].resourceRecordName, 37 | zoneId, 38 | type: certificate.domainValidationOptions[0].resourceRecordType, 39 | records: [certificate.domainValidationOptions[0].resourceRecordValue], 40 | ttl: tenMinutes 41 | } 42 | ); 43 | 44 | /** 45 | * This is a _special_ resource that waits for ACM to complete validation via the DNS record 46 | * checking for a status of "ISSUED" on the certificate itself. No actual resources are 47 | * created (or updated or deleted). 48 | * 49 | * See https://www.terraform.io/docs/providers/aws/r/acm_certificate_validation.html for slightly more detail 50 | * and https://github.com/terraform-providers/terraform-provider-aws/blob/master/aws/resource_aws_acm_certificate_validation.go 51 | * for the actual implementation. 52 | */ 53 | const certificateValidation = new CertificateValidation( 54 | 'certificateValidation', 55 | { 56 | certificateArn: certificate.arn, 57 | validationRecordFqdns: [certificateValidationDomain.fqdn] 58 | }, 59 | { provider: eastRegion, parent: certificate } 60 | ); 61 | 62 | return certificateValidation.certificateArn; 63 | } 64 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/webapp/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as aws from '@pulumi/aws'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | import { Distribution } from '@pulumi/aws/cloudfront'; 4 | import { Output } from '@pulumi/pulumi'; 5 | import * as mime from 'mime'; 6 | 7 | import { createCdn } from './cdn'; 8 | import { createCertificate } from './certificate'; 9 | import { crawlDirectory } from './utils'; 10 | 11 | const stackConfig = new pulumi.Config(); 12 | const config = { 13 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 14 | projectName: stackConfig.get('projectName'), 15 | distPath: stackConfig.get('distPath'), 16 | useCdn: stackConfig.getBoolean('useCdn'), 17 | customDomainName: stackConfig.get('customDomainName') 18 | // ===== END ====== 19 | }; 20 | const projectName = config.projectName; 21 | 22 | // contentBucket is the S3 bucket that the website's contents will be stored in. 23 | const contentBucket = new aws.s3.Bucket(`${projectName}-contentBucket`, { 24 | acl: 'public-read', 25 | // Configure S3 to serve bucket contents as a website. This way S3 will automatically convert 26 | // requests for "foo/" to "foo/index.html". 27 | website: { 28 | indexDocument: 'index.html', 29 | errorDocument: 'index.html' 30 | }, 31 | forceDestroy: true 32 | }); 33 | 34 | // Sync the contents of the source directory with the S3 bucket, which will in-turn show up on the CDN. 35 | crawlDirectory(config.distPath, (filePath: string) => { 36 | const relativeFilePath = filePath.replace(config.distPath + '/', ''); 37 | const contentFile = new aws.s3.BucketObject( 38 | relativeFilePath, 39 | { 40 | key: relativeFilePath, 41 | 42 | acl: 'public-read', 43 | bucket: contentBucket, 44 | contentType: mime.getType(filePath) || undefined, 45 | source: new pulumi.asset.FileAsset(filePath) 46 | }, 47 | { 48 | parent: contentBucket 49 | } 50 | ); 51 | }); 52 | 53 | let cdn: Distribution; 54 | if (config.useCdn) { 55 | let certificateArn: Output; 56 | if (config.customDomainName) { 57 | certificateArn = createCertificate(config.customDomainName); 58 | } 59 | 60 | cdn = createCdn(config, contentBucket, certificateArn); 61 | } 62 | 63 | // Export properties from this stack. This prints them at the end of `pulumi up` and 64 | // makes them easier to access from the pulumi.com. 65 | export const staticEndpoint = contentBucket.websiteEndpoint; 66 | export const cdnEndpoint = cdn && cdn.domainName; 67 | export const cdnCustomDomain = 68 | config.customDomainName && 69 | pulumi.interpolate`https://${config.customDomainName}`; 70 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/aws/webapp/infrastructure/utils.ts.template: -------------------------------------------------------------------------------- 1 | import { Distribution } from '@pulumi/aws/cloudfront'; 2 | import { Record } from '@pulumi/aws/route53'; 3 | import { route53 } from '@pulumi/aws'; 4 | import { readdirSync, statSync } from 'fs'; 5 | 6 | export function getDomainAndSubdomain( 7 | domain: string 8 | ): { subdomain: string; parentDomain: string } { 9 | const parts = domain.split('.'); 10 | if (parts.length < 2) { 11 | throw new Error(`No TLD found on ${domain}`); 12 | } 13 | if (parts.length === 2) { 14 | return { subdomain: '', parentDomain: domain }; 15 | } 16 | 17 | const subdomain = parts[0]; 18 | parts.shift(); 19 | return { 20 | subdomain, 21 | // Trailing "." to canonicalize domain. 22 | parentDomain: parts.join('.') + '.' 23 | }; 24 | } 25 | 26 | // Creates a new Route53 DNS record pointing the domain to the CloudFront distribution. 27 | export function createAliasRecord( 28 | targetDomain: string, 29 | distribution: Distribution 30 | ): Record { 31 | const domainParts = getDomainAndSubdomain(targetDomain); 32 | const hostedZoneId = route53 33 | .getZone({ name: domainParts.parentDomain }, { async: true }) 34 | .then(zone => zone.zoneId); 35 | return new Record(targetDomain, { 36 | name: domainParts.subdomain, 37 | zoneId: hostedZoneId, 38 | type: 'A', 39 | aliases: [ 40 | { 41 | name: distribution.domainName, 42 | zoneId: distribution.hostedZoneId, 43 | evaluateTargetHealth: true 44 | } 45 | ] 46 | }); 47 | } 48 | 49 | export function crawlDirectory(dir: string, f: (_: string) => void) { 50 | const files = readdirSync(dir); 51 | for (const file of files) { 52 | const filePath = `${dir}/${file}`; 53 | const stat = statSync(filePath); 54 | if (stat.isDirectory()) { 55 | crawlDirectory(filePath, f); 56 | } 57 | if (stat.isFile()) { 58 | f(filePath); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/functions/host.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "http": { 5 | "routePrefix": "" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/functions/local.settings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/functions/main/function.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "route": "{*segments}" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ], 16 | "scriptFile": "../server/main.js" 17 | } 18 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as createHandler from 'azure-aws-serverless-express'; 2 | import { app } from '../../../<%= getRootDirectory() %>server'; 3 | 4 | export default createHandler(app()); 5 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/functions/proxies.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as azure from '@pulumi/azure'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | import * as mime from 'mime'; 4 | 5 | import { CDNCustomDomainResource } from './cdnCustomDomain'; 6 | import { createAzureFunction } from './server-side-rendering'; 7 | import { crawlDirectory } from './utils'; 8 | import { StorageStaticWebsite } from './static-website.resource'; 9 | 10 | const stackConfig = new pulumi.Config(); 11 | const config = { 12 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 13 | projectName: stackConfig.get('projectName'), 14 | distPath: stackConfig.get('distPath'), 15 | useCdn: stackConfig.getBoolean('useCdn'), 16 | customDomainName: stackConfig.get('customDomainName'), 17 | angularUniversalDeploymentType: stackConfig.get( 18 | 'angularUniversalDeploymentType' 19 | ) 20 | // ===== END ====== 21 | }; 22 | const projectName = config.projectName; 23 | 24 | // Create an Azure Resource Group 25 | const resourceGroup = new azure.core.ResourceGroup(`${projectName}-rg`); 26 | 27 | let azureFunction: { endpoint: pulumi.Output }; 28 | let cdnEndpointResource: azure.cdn.Endpoint; 29 | let cdnCustomDomainResource: CDNCustomDomainResource; 30 | let storageAccount: azure.storage.Account; 31 | 32 | if (config.angularUniversalDeploymentType === 'ssr') { 33 | azureFunction = createAzureFunction(projectName, resourceGroup); 34 | } else { 35 | // Create a Storage Account for our static website 36 | storageAccount = new azure.storage.Account(`account`, { 37 | resourceGroupName: resourceGroup.name, 38 | accountReplicationType: 'LRS', 39 | accountTier: 'Standard', 40 | accountKind: 'StorageV2', 41 | staticWebsite: { 42 | indexDocument: 'index.html' 43 | } 44 | }); 45 | 46 | // There's currently a bug in to enable the Static Web Site feature of a storage account via ARM 47 | // Therefore, we created a custom resource which wraps corresponding Azure CLI commands 48 | const storageStaticWebsite = new StorageStaticWebsite(`static`, { 49 | accountName: storageAccount.name 50 | }); 51 | 52 | crawlDirectory(config.distPath, (filePath: string) => { 53 | const relativeFilePath = filePath.replace(config.distPath + '/', ''); 54 | const contentFile = new azure.storage.Blob(relativeFilePath, { 55 | name: relativeFilePath, 56 | storageAccountName: storageAccount.name, 57 | storageContainerName: '$web', 58 | type: 'Block', 59 | source: new pulumi.asset.FileAsset(filePath), 60 | contentType: mime.getType(filePath) || undefined 61 | }); 62 | }); 63 | 64 | if (config.useCdn) { 65 | const cdnProfile = new azure.cdn.Profile(`pr-cdn`, { 66 | resourceGroupName: resourceGroup.name, 67 | sku: 'Standard_Microsoft' 68 | }); 69 | 70 | cdnEndpointResource = new azure.cdn.Endpoint(`cdn-ep`, { 71 | // TODO: handle long custom domains max characters 50 72 | name: 73 | (config.customDomainName && 74 | config.customDomainName.replace(/\./gi, '-')) || 75 | undefined, 76 | resourceGroupName: resourceGroup.name, 77 | profileName: cdnProfile.name, 78 | originHostHeader: storageAccount.primaryWebHost, 79 | origins: [ 80 | { 81 | name: 'blobstorage', 82 | hostName: storageAccount.primaryWebHost 83 | } 84 | ] 85 | }); 86 | 87 | if (config.customDomainName) { 88 | cdnCustomDomainResource = new CDNCustomDomainResource( 89 | 'cdnCustomDomain', 90 | { 91 | resourceGroupName: resourceGroup.name, 92 | // Ensure that there is a CNAME record for mycompany.com pointing to my-cdn-endpoint.azureedge.net. 93 | // You would do that in your domain registrar's portal. 94 | customDomainHostName: config.customDomainName, 95 | profileName: cdnProfile.name, 96 | endpointName: cdnEndpointResource.name, 97 | /** 98 | * This will enable HTTPS through Azure's one-click 99 | * automated certificate deployment. The certificate is 100 | * fully managed by Azure from provisioning to automatic renewal 101 | * at no additional cost to you. 102 | */ 103 | httpsEnabled: true 104 | }, 105 | { parent: cdnEndpointResource } 106 | ); 107 | } 108 | } 109 | } 110 | 111 | export const staticEndpoint = 112 | storageAccount && storageAccount.primaryWebEndpoint; 113 | export const cdnEndpoint = 114 | cdnEndpointResource && 115 | pulumi.interpolate`https://${cdnEndpointResource.hostName}/`; 116 | export const cdnCustomDomain = 117 | cdnCustomDomainResource && 118 | pulumi.interpolate`https://${config.customDomainName}`; 119 | export const anuglarUniversalEndpoint = azureFunction && azureFunction.endpoint; 120 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/server-side-rendering.ts.template: -------------------------------------------------------------------------------- 1 | import { ResourceGroup } from '@pulumi/azure/core'; 2 | import { ArchiveFunctionApp } from '@pulumi/azure/appservice'; 3 | import { AssetArchive, FileArchive } from '@pulumi/pulumi/asset'; 4 | 5 | export function createAzureFunction( 6 | projectName: string, 7 | resourceGroup: ResourceGroup 8 | ) { 9 | const nodeApp = new ArchiveFunctionApp(`${projectName}-functions`, { 10 | resourceGroup, 11 | archive: new AssetArchive({ 12 | [`dist/${projectName}/browser`]: new FileArchive( 13 | `../../../dist/${projectName}/browser` 14 | ), 15 | 'server/': new FileArchive(`../../../dist/${projectName}/server`), 16 | '.': new FileArchive(`./functions`) 17 | }), 18 | version: '~3', 19 | nodeVersion: '~10' 20 | }); 21 | 22 | return { 23 | endpoint: nodeApp.endpoint.apply((endpoint: string) => 24 | endpoint.replace(/api\/$/, '') 25 | ) 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/static-website.resource.ts.template: -------------------------------------------------------------------------------- 1 | import * as pulumi from '@pulumi/pulumi'; 2 | 3 | const ACCOUNT_NAME_PROP = 'accountName'; 4 | 5 | export interface StorageStaticWebsiteArgs { 6 | [ACCOUNT_NAME_PROP]: pulumi.Input; 7 | } 8 | 9 | // There's currently no way to enable the Static Web Site feature of a storage account via ARM 10 | // Therefore, we created a custom provider which wraps corresponding Azure CLI commands 11 | class StorageStaticWebsiteProvider implements pulumi.dynamic.ResourceProvider { 12 | public async check( 13 | olds: any, 14 | news: any 15 | ): Promise { 16 | const failures = []; 17 | 18 | if (news[ACCOUNT_NAME_PROP] === undefined) { 19 | failures.push({ 20 | property: ACCOUNT_NAME_PROP, 21 | reason: `required property '${ACCOUNT_NAME_PROP}' missing` 22 | }); 23 | } 24 | 25 | return { inputs: news, failures }; 26 | } 27 | 28 | public async diff( 29 | id: pulumi.ID, 30 | olds: any, 31 | news: any 32 | ): Promise { 33 | const replaces = []; 34 | 35 | if (olds[ACCOUNT_NAME_PROP] !== news[ACCOUNT_NAME_PROP]) { 36 | replaces.push(ACCOUNT_NAME_PROP); 37 | } 38 | 39 | return { replaces }; 40 | } 41 | 42 | public async create(inputs: any): Promise { 43 | const { execSync } = require('child_process'); 44 | const url = require('url'); 45 | const accountName = inputs[ACCOUNT_NAME_PROP]; 46 | 47 | // Helper function to execute a command, supress the warnings from polluting the output, and parse the result as JSON 48 | const executeToJson = (command: string) => 49 | JSON.parse( 50 | execSync(command, { stdio: ['pipe', 'pipe', 'ignore'] }).toString() 51 | ); 52 | 53 | // Install Azure CLI extension for storage (currently, only the preview version has the one we need) 54 | execSync('az extension add --name storage-preview', { stdio: 'ignore' }); 55 | 56 | // Update the service properties of the storage account to enable static website and validate the result 57 | const update = executeToJson( 58 | `az storage blob service-properties update --account-name "${accountName}" --static-website --404-document index.html` 59 | ); 60 | if (!update.staticWebsite.enabled) { 61 | throw new Error(`Static website update failed: ${update}`); 62 | } 63 | 64 | return { 65 | id: `${accountName}StaticWebsite` 66 | }; 67 | } 68 | } 69 | 70 | export class StorageStaticWebsite extends pulumi.dynamic.Resource { 71 | public readonly endpoint: pulumi.Output; 72 | public readonly hostName: pulumi.Output; 73 | public readonly webContainerName: pulumi.Output; 74 | 75 | constructor( 76 | name: string, 77 | args: StorageStaticWebsiteArgs, 78 | opts?: pulumi.CustomResourceOptions 79 | ) { 80 | super( 81 | new StorageStaticWebsiteProvider(), 82 | name, 83 | { 84 | ...args, 85 | endpoint: undefined, 86 | hostName: undefined, 87 | webContainerName: undefined 88 | }, 89 | opts 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.server.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | }, 8 | "files": [ 9 | "functions/main/index.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/angular-universal/infrastructure/utils.ts.template: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from 'fs'; 2 | 3 | export function crawlDirectory(dir: string, f: (_: string) => void) { 4 | const files = readdirSync(dir); 5 | for (const file of files) { 6 | const filePath = `${dir}/${file}`; 7 | const stat = statSync(filePath); 8 | if (stat.isDirectory()) { 9 | crawlDirectory(filePath, f); 10 | } 11 | if (stat.isFile()) { 12 | f(filePath); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/__rootDir__/main.azure.ts.template: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | const app = express(); 4 | 5 | app.get('/', (req, res) => { 6 | res.send({ message: 'Welcome to your deployed app!' }); 7 | }); 8 | 9 | // export your express application as expressApp 10 | export const expressApp = app; 11 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/.gitignore.template: -------------------------------------------------------------------------------- 1 | functions/dist 2 | buildcache 3 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/functions/host.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "http": { 5 | "routePrefix": "" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/functions/local.settings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/functions/main/function.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "route": "{*segments}" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ], 16 | "scriptFile": "../dist/main/index.js" 17 | } 18 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as createHandler from 'azure-aws-serverless-express'; 2 | import { expressApp } from '../../../<%= getRootDirectory() %>/main.azure'; 3 | 4 | export default createHandler(expressApp); 5 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/functions/proxies.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as azure from '@pulumi/azure'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | const stackConfig = new pulumi.Config(); 5 | const config = { 6 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 7 | projectName: stackConfig.get('projectName') 8 | // ===== END ====== 9 | }; 10 | const projectName = config.projectName; 11 | 12 | const resourceGroup = new azure.core.ResourceGroup(`${projectName}-rg`); 13 | 14 | const nodeApp = new azure.appservice.ArchiveFunctionApp( 15 | `${projectName}-functions`, 16 | { 17 | resourceGroup, 18 | archive: new pulumi.asset.FileArchive('./functions'), 19 | version: '~3', 20 | nodeVersion: '~10', 21 | siteConfig: { 22 | cors: { allowedOrigins: ['*'] } 23 | } 24 | } 25 | ); 26 | 27 | export const nodeEndpoint = nodeApp.endpoint.apply((endpoint: string) => 28 | endpoint.replace(/api\/$/, '') 29 | ); 30 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/express/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.app.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/__rootDir__/main.azure.ts.template: -------------------------------------------------------------------------------- 1 | import { INestApplication } from '@nestjs/common'; 2 | import { NestFactory } from '@nestjs/core'; 3 | import { <%= getRootModuleName() %> } from './<%= getRootModulePath() %>'; 4 | 5 | export async function createApp(): Promise { 6 | const app = await NestFactory.create(<%= getRootModuleName() %>); 7 | 8 | app.enableCors(); 9 | await app.init(); 10 | return app; 11 | } -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/.gitignore.template: -------------------------------------------------------------------------------- 1 | functions/dist 2 | buildcache 3 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/functions/host.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0", 3 | "extensions": { 4 | "http": { 5 | "routePrefix": "" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/functions/local.settings.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "IsEncrypted": false, 3 | "Values": { 4 | "AzureWebJobsStorage": "", 5 | "FUNCTIONS_WORKER_RUNTIME": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/functions/main/function.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "bindings": [ 3 | { 4 | "authLevel": "anonymous", 5 | "type": "httpTrigger", 6 | "direction": "in", 7 | "name": "req", 8 | "route": "{*segments}" 9 | }, 10 | { 11 | "type": "http", 12 | "direction": "out", 13 | "name": "res" 14 | } 15 | ], 16 | "scriptFile": "../dist/main/index.js" 17 | } 18 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import { Context, HttpRequest } from '@azure/functions'; 2 | import { AzureHttpAdapter } from '@nestjs/azure-func-http'; 3 | import { createApp } from '../../../<%= getRootDirectory() %>/main.azure'; 4 | 5 | export default function(context: Context, req: HttpRequest): void { 6 | AzureHttpAdapter.handle(createApp, context, req); 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/functions/proxies.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/proxies", 3 | "proxies": {} 4 | } 5 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as azure from '@pulumi/azure'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | const stackConfig = new pulumi.Config(); 5 | const config = { 6 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 7 | projectName: stackConfig.get('projectName') 8 | // ===== END ====== 9 | }; 10 | const projectName = config.projectName; 11 | 12 | const resourceGroup = new azure.core.ResourceGroup(`${projectName}-rg`); 13 | 14 | const nodeApp = new azure.appservice.ArchiveFunctionApp( 15 | `${projectName}-functions`, 16 | { 17 | resourceGroup, 18 | archive: new pulumi.asset.FileArchive('./functions'), 19 | version: '~3', 20 | nodeVersion: '~10', 21 | siteConfig: { 22 | cors: { allowedOrigins: ['*'] } 23 | } 24 | } 25 | ); 26 | 27 | export const nodeEndpoint = nodeApp.endpoint.apply((endpoint: string) => 28 | endpoint.replace(/api\/$/, '') 29 | ); 30 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/nestjs/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.app.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/webapp/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as azure from '@pulumi/azure'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | import * as mime from 'mime'; 4 | 5 | import { StorageStaticWebsite } from './static-website.resource'; 6 | import { CDNCustomDomainResource } from './cdnCustomDomain'; 7 | import { crawlDirectory } from './utils'; 8 | 9 | const stackConfig = new pulumi.Config(); 10 | const config = { 11 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 12 | projectName: stackConfig.get('projectName'), 13 | distPath: stackConfig.get('distPath'), 14 | useCdn: stackConfig.getBoolean('useCdn'), 15 | customDomainName: stackConfig.get('customDomainName') 16 | // ===== END ====== 17 | }; 18 | const projectName = config.projectName; 19 | 20 | // Create an Azure Resource Group 21 | const resourceGroup = new azure.core.ResourceGroup(`${projectName}-rg`); 22 | 23 | // Create a Storage Account for our static website 24 | const storageAccount = new azure.storage.Account(`account`, { 25 | resourceGroupName: resourceGroup.name, 26 | accountReplicationType: 'LRS', 27 | accountTier: 'Standard', 28 | accountKind: 'StorageV2', 29 | staticWebsite: { 30 | indexDocument: 'index.html' 31 | } 32 | }); 33 | 34 | // There's currently a bug in to enable the Static Web Site feature of a storage account via ARM 35 | // Therefore, we created a custom resource which wraps corresponding Azure CLI commands 36 | const storageStaticWebsite = new StorageStaticWebsite(`static`, { 37 | accountName: storageAccount.name 38 | }); 39 | 40 | crawlDirectory(config.distPath, (filePath: string) => { 41 | const relativeFilePath = filePath.replace(config.distPath + '/', ''); 42 | const contentFile = new azure.storage.Blob(relativeFilePath, { 43 | name: relativeFilePath, 44 | storageAccountName: storageAccount.name, 45 | storageContainerName: '$web', 46 | type: 'Block', 47 | source: new pulumi.asset.FileAsset(filePath), 48 | contentType: mime.getType(filePath) || undefined 49 | }); 50 | }); 51 | 52 | let cdnEndpointResource: azure.cdn.Endpoint; 53 | let cdnCustomDomainResource: CDNCustomDomainResource; 54 | if (config.useCdn) { 55 | const cdnProfile = new azure.cdn.Profile(`pr-cdn`, { 56 | resourceGroupName: resourceGroup.name, 57 | sku: 'Standard_Microsoft' 58 | }); 59 | 60 | cdnEndpointResource = new azure.cdn.Endpoint(`cdn-ep`, { 61 | // TODO: handle long custom domains max characters 50 62 | name: 63 | (config.customDomainName && 64 | config.customDomainName.replace(/\./gi, '-')) || 65 | undefined, 66 | resourceGroupName: resourceGroup.name, 67 | profileName: cdnProfile.name, 68 | originHostHeader: storageAccount.primaryWebHost, 69 | origins: [ 70 | { 71 | name: 'blobstorage', 72 | hostName: storageAccount.primaryWebHost 73 | } 74 | ] 75 | }); 76 | 77 | if (config.customDomainName) { 78 | cdnCustomDomainResource = new CDNCustomDomainResource( 79 | 'cdnCustomDomain', 80 | { 81 | resourceGroupName: resourceGroup.name, 82 | // Ensure that there is a CNAME record for mycompany.com pointing to my-cdn-endpoint.azureedge.net. 83 | // You would do that in your domain registrar's portal. 84 | customDomainHostName: config.customDomainName, 85 | profileName: cdnProfile.name, 86 | endpointName: cdnEndpointResource.name, 87 | /** 88 | * This will enable HTTPS through Azure's one-click 89 | * automated certificate deployment. The certificate is 90 | * fully managed by Azure from provisioning to automatic renewal 91 | * at no additional cost to you. 92 | */ 93 | httpsEnabled: true 94 | }, 95 | { parent: cdnEndpointResource } 96 | ); 97 | } 98 | } 99 | 100 | export const staticEndpoint = 101 | storageAccount && storageAccount.primaryWebEndpoint; 102 | export const cdnEndpoint = 103 | cdnEndpointResource && 104 | pulumi.interpolate`https://${cdnEndpointResource.hostName}/`; 105 | export const cdnCustomDomain = 106 | cdnCustomDomainResource && 107 | pulumi.interpolate`https://${config.customDomainName}`; 108 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/webapp/infrastructure/static-website.resource.ts.template: -------------------------------------------------------------------------------- 1 | import * as pulumi from '@pulumi/pulumi'; 2 | 3 | const ACCOUNT_NAME_PROP = 'accountName'; 4 | 5 | export interface StorageStaticWebsiteArgs { 6 | [ACCOUNT_NAME_PROP]: pulumi.Input; 7 | } 8 | 9 | // There's currently no way to enable the Static Web Site feature of a storage account via ARM 10 | // Therefore, we created a custom provider which wraps corresponding Azure CLI commands 11 | class StorageStaticWebsiteProvider implements pulumi.dynamic.ResourceProvider { 12 | public async check( 13 | olds: any, 14 | news: any 15 | ): Promise { 16 | const failures = []; 17 | 18 | if (news[ACCOUNT_NAME_PROP] === undefined) { 19 | failures.push({ 20 | property: ACCOUNT_NAME_PROP, 21 | reason: `required property '${ACCOUNT_NAME_PROP}' missing` 22 | }); 23 | } 24 | 25 | return { inputs: news, failures }; 26 | } 27 | 28 | public async diff( 29 | id: pulumi.ID, 30 | olds: any, 31 | news: any 32 | ): Promise { 33 | const replaces = []; 34 | 35 | if (olds[ACCOUNT_NAME_PROP] !== news[ACCOUNT_NAME_PROP]) { 36 | replaces.push(ACCOUNT_NAME_PROP); 37 | } 38 | 39 | return { replaces }; 40 | } 41 | 42 | public async create(inputs: any): Promise { 43 | const { execSync } = require('child_process'); 44 | const url = require('url'); 45 | const accountName = inputs[ACCOUNT_NAME_PROP]; 46 | 47 | // Helper function to execute a command, supress the warnings from polluting the output, and parse the result as JSON 48 | const executeToJson = (command: string) => 49 | JSON.parse( 50 | execSync(command, { stdio: ['pipe', 'pipe', 'ignore'] }).toString() 51 | ); 52 | 53 | // Install Azure CLI extension for storage (currently, only the preview version has the one we need) 54 | execSync('az extension add --name storage-preview', { stdio: 'ignore' }); 55 | 56 | // Update the service properties of the storage account to enable static website and validate the result 57 | const update = executeToJson( 58 | `az storage blob service-properties update --account-name "${accountName}" --static-website --404-document index.html` 59 | ); 60 | if (!update.staticWebsite.enabled) { 61 | throw new Error(`Static website update failed: ${update}`); 62 | } 63 | 64 | return { 65 | id: `${accountName}StaticWebsite` 66 | }; 67 | } 68 | } 69 | 70 | export class StorageStaticWebsite extends pulumi.dynamic.Resource { 71 | public readonly endpoint: pulumi.Output; 72 | public readonly hostName: pulumi.Output; 73 | public readonly webContainerName: pulumi.Output; 74 | 75 | constructor( 76 | name: string, 77 | args: StorageStaticWebsiteArgs, 78 | opts?: pulumi.CustomResourceOptions 79 | ) { 80 | super( 81 | new StorageStaticWebsiteProvider(), 82 | name, 83 | { 84 | ...args, 85 | endpoint: undefined, 86 | hostName: undefined, 87 | webContainerName: undefined 88 | }, 89 | opts 90 | ); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/azure/webapp/infrastructure/utils.ts.template: -------------------------------------------------------------------------------- 1 | import { readdirSync, statSync } from 'fs'; 2 | 3 | export function crawlDirectory(dir: string, f: (_: string) => void) { 4 | const files = readdirSync(dir); 5 | for (const file of files) { 6 | const filePath = `${dir}/${file}`; 7 | const stat = statSync(filePath); 8 | if (stat.isDirectory()) { 9 | crawlDirectory(filePath, f); 10 | } 11 | if (stat.isFile()) { 12 | f(filePath); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/angular-universal/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import { app } from '../../../<%= getRootDirectory() %>server'; 2 | 3 | export const handler = app(); 4 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/angular-universal/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as gcp from '@pulumi/gcp'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | import { readdirSync, statSync } from 'fs'; 4 | import * as mime from 'mime'; 5 | import { basename } from 'path'; 6 | import { createCloudFunction } from './server-side-rendering'; 7 | 8 | const stackConfig = new pulumi.Config(); 9 | const config = { 10 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 11 | projectName: stackConfig.get('projectName'), 12 | distPath: stackConfig.get('distPath'), 13 | useCdn: stackConfig.getBoolean('useCdn'), 14 | customDomainName: stackConfig.require('customDomainName'), 15 | angularUniversalDeploymentType: stackConfig.get( 16 | 'angularUniversalDeploymentType' 17 | ) 18 | // ===== END ====== 19 | }; 20 | const projectName = config.projectName; 21 | let contentBucket: gcp.storage.Bucket; 22 | let cloudFunction: gcp.cloudfunctions.Function; 23 | 24 | if (config.angularUniversalDeploymentType === 'ssr') { 25 | cloudFunction = createCloudFunction(projectName); 26 | } else { 27 | contentBucket = new gcp.storage.Bucket('contentBucket', { 28 | name: config.customDomainName, 29 | website: { 30 | mainPageSuffix: 'index.html', 31 | notFoundPage: 'index.html' 32 | }, 33 | forceDestroy: true 34 | }); 35 | 36 | const oacResource = new gcp.storage.DefaultObjectAccessControl( 37 | `${projectName}-storage-oac`, 38 | { 39 | bucket: contentBucket.name, 40 | entity: 'allUsers', 41 | role: 'READER' 42 | } 43 | ); 44 | 45 | // crawlDirectory recursive crawls the provided directory, applying the provided function 46 | // to every file it contains. Doesn't handle cycles from symlinks. 47 | function crawlDirectory(dir: string, f: (_: string) => void) { 48 | const files = readdirSync(dir); 49 | for (const file of files) { 50 | const filePath = `${dir}/${file}`; 51 | const stat = statSync(filePath); 52 | if (stat.isDirectory()) { 53 | crawlDirectory(filePath, f); 54 | } 55 | if (stat.isFile()) { 56 | f(filePath); 57 | } 58 | } 59 | } 60 | 61 | // Sync the contents of the source directory with the GCP bucket. 62 | crawlDirectory(config.distPath, (filePath: string) => { 63 | const relativeFilePath = filePath.replace(config.distPath + '/', ''); 64 | const file = new gcp.storage.BucketObject( 65 | relativeFilePath, 66 | { 67 | bucket: contentBucket.name, 68 | source: new pulumi.asset.FileAsset(filePath), 69 | name: basename(relativeFilePath), 70 | contentType: mime.getType(filePath) || undefined 71 | }, 72 | { dependsOn: oacResource } 73 | ); 74 | }); 75 | 76 | if (config.useCdn) { 77 | const cdnEndpointResource = new gcp.compute.BackendBucket( 78 | `${projectName}-cbb`, 79 | { 80 | bucketName: contentBucket.name, 81 | enableCdn: true 82 | } 83 | ); 84 | } 85 | } 86 | 87 | export const cdnCustomDomain = 88 | contentBucket && pulumi.interpolate`https://${config.customDomainName}`; 89 | export const nodeEndpoint = cloudFunction && cloudFunction.httpsTriggerUrl; 90 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/angular-universal/infrastructure/server-side-rendering.ts.template: -------------------------------------------------------------------------------- 1 | import * as gcp from '@pulumi/gcp'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | export function createCloudFunction(projectName: string) { 5 | const bucket = new gcp.storage.Bucket(`${projectName}_bucket`); 6 | const bucketObjectGo = new gcp.storage.BucketObject('zip-archive', { 7 | bucket: bucket.name, 8 | source: new pulumi.asset.AssetArchive({ 9 | [`dist/${projectName}/browser`]: new pulumi.asset.FileArchive( 10 | `../../../dist/${projectName}/browser` 11 | ), 12 | '.': new pulumi.asset.FileArchive(`../../../dist/${projectName}/server`) 13 | }) 14 | }); 15 | 16 | const cloudFunction = new gcp.cloudfunctions.Function(`${projectName}-func`, { 17 | sourceArchiveBucket: bucket.name, 18 | runtime: 'nodejs10', 19 | sourceArchiveObject: bucketObjectGo.name, 20 | entryPoint: 'handler', 21 | triggerHttp: true, 22 | availableMemoryMb: 128 23 | }); 24 | 25 | const permission = new gcp.cloudfunctions.FunctionIamMember( 26 | `${projectName}-func-role`, 27 | { 28 | cloudFunction: cloudFunction.name, 29 | role: 'roles/cloudfunctions.invoker', 30 | member: 'allUsers' 31 | } 32 | ); 33 | 34 | return cloudFunction; 35 | } 36 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/angular-universal/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.server.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | }, 8 | "files": [ 9 | "functions/main/index.ts" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/express/__rootDir__/main.gcp.ts.template: -------------------------------------------------------------------------------- 1 | import * as express from 'express'; 2 | 3 | const app = express(); 4 | 5 | app.get('/', (req, res) => { 6 | res.send({ message: 'Welcome to your deployed app!' }); 7 | }); 8 | 9 | // export your express application as expressApp 10 | export const expressApp = app; 11 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/express/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import { expressApp } from '../../../<%= getRootDirectory() %>/main.gcp'; 2 | 3 | export const handler = expressApp; 4 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/express/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as gcp from '@pulumi/gcp'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | const stackConfig = new pulumi.Config(); 5 | const config = { 6 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 7 | projectName: stackConfig.get('projectName') 8 | // ===== END ====== 9 | }; 10 | const projectName = config.projectName; 11 | 12 | const bucket = new gcp.storage.Bucket(`${projectName}_bucket`); 13 | const bucketObjectGo = new gcp.storage.BucketObject('zip-archive', { 14 | bucket: bucket.name, 15 | source: new pulumi.asset.AssetArchive({ 16 | '.': new pulumi.asset.FileArchive('./functions/dist/main') 17 | }) 18 | }); 19 | 20 | const cloudFunction = new gcp.cloudfunctions.Function( 21 | `${projectName}-func`, 22 | { 23 | sourceArchiveBucket: bucket.name, 24 | runtime: 'nodejs10', 25 | sourceArchiveObject: bucketObjectGo.name, 26 | entryPoint: 'handler', 27 | triggerHttp: true, 28 | availableMemoryMb: 128 29 | } 30 | ); 31 | 32 | const permission = new gcp.cloudfunctions.FunctionIamMember( 33 | `${projectName}-func-role`, 34 | { 35 | cloudFunction: cloudFunction.name, 36 | role: 'roles/cloudfunctions.invoker', 37 | member: 'allUsers' 38 | } 39 | ); 40 | 41 | export const nodeEndpoint = cloudFunction.httpsTriggerUrl; 42 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/express/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.app.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/nestjs/__rootDir__/main.gcp.ts.template: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { <%= getRootModuleName() %> } from './<%= getRootModulePath() %>'; 3 | import { ExpressAdapter } from '@nestjs/platform-express'; 4 | 5 | export const createNestServer = async expressApp => { 6 | expressApp.disable('x-powered-by'); 7 | const app = await NestFactory.create(<%= getRootModuleName() %>, new ExpressAdapter(expressApp)); 8 | 9 | app.enableCors(); 10 | return app.init(); 11 | }; 12 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/nestjs/infrastructure/functions/main/index.ts.template: -------------------------------------------------------------------------------- 1 | import { createNestServer } from '../../../<%= getRootDirectory() %>/main.gcp'; 2 | import * as express from 'express'; 3 | 4 | const expressApp = express(); 5 | createNestServer(expressApp); 6 | 7 | export const handler = expressApp; 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/nestjs/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as gcp from '@pulumi/gcp'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | 4 | const stackConfig = new pulumi.Config(); 5 | const config = { 6 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 7 | projectName: stackConfig.get('projectName') 8 | // ===== END ====== 9 | }; 10 | const projectName = config.projectName; 11 | 12 | const bucket = new gcp.storage.Bucket(`${projectName}_nestjs`); 13 | const bucketObjectGo = new gcp.storage.BucketObject('zip-archive', { 14 | bucket: bucket.name, 15 | source: new pulumi.asset.AssetArchive({ 16 | '.': new pulumi.asset.FileArchive('./functions/dist/main') 17 | }) 18 | }); 19 | 20 | const cloudFunction = new gcp.cloudfunctions.Function( 21 | `${projectName}-nest-func`, 22 | { 23 | sourceArchiveBucket: bucket.name, 24 | runtime: 'nodejs10', 25 | sourceArchiveObject: bucketObjectGo.name, 26 | entryPoint: 'handler', 27 | triggerHttp: true, 28 | availableMemoryMb: 128 29 | } 30 | ); 31 | 32 | const permission = new gcp.cloudfunctions.FunctionIamMember( 33 | `${projectName}-func-role`, 34 | { 35 | cloudFunction: cloudFunction.name, 36 | role: 'roles/cloudfunctions.invoker', 37 | member: 'allUsers' 38 | } 39 | ); 40 | 41 | export const nodeEndpoint = cloudFunction.httpsTriggerUrl; 42 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/nestjs/infrastructure/tsconfig.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.app.json", 3 | "compilerOptions": { 4 | "target": "es2015", 5 | "moduleResolution": "node", 6 | "module": "commonjs" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/files/gcp/webapp/infrastructure/index.ts.template: -------------------------------------------------------------------------------- 1 | import * as gcp from '@pulumi/gcp'; 2 | import * as pulumi from '@pulumi/pulumi'; 3 | import { readdirSync, statSync } from 'fs'; 4 | import * as mime from 'mime'; 5 | import { basename } from 'path'; 6 | import { interpolate } from '@pulumi/pulumi'; 7 | 8 | const stackConfig = new pulumi.Config(); 9 | const config = { 10 | // ===== DONT'T TOUCH THIS -> CONFIG REQUIRED BY nx-deploy-it ====== 11 | projectName: stackConfig.get('projectName'), 12 | distPath: stackConfig.get('distPath'), 13 | useCdn: stackConfig.getBoolean('useCdn'), 14 | customDomainName: stackConfig.require('customDomainName') 15 | // ===== END ====== 16 | }; 17 | const projectName = config.projectName; 18 | 19 | const contentBucket = new gcp.storage.Bucket('contentBucket', { 20 | name: config.customDomainName, 21 | website: { 22 | mainPageSuffix: 'index.html', 23 | notFoundPage: 'index.html' 24 | }, 25 | forceDestroy: true 26 | }); 27 | 28 | const oacResource = new gcp.storage.DefaultObjectAccessControl( 29 | `${projectName}-storage-oac`, 30 | { 31 | bucket: contentBucket.name, 32 | entity: 'allUsers', 33 | role: 'READER' 34 | } 35 | ); 36 | 37 | // crawlDirectory recursive crawls the provided directory, applying the provided function 38 | // to every file it contains. Doesn't handle cycles from symlinks. 39 | function crawlDirectory(dir: string, f: (_: string) => void) { 40 | const files = readdirSync(dir); 41 | for (const file of files) { 42 | const filePath = `${dir}/${file}`; 43 | const stat = statSync(filePath); 44 | if (stat.isDirectory()) { 45 | crawlDirectory(filePath, f); 46 | } 47 | if (stat.isFile()) { 48 | f(filePath); 49 | } 50 | } 51 | } 52 | 53 | // Sync the contents of the source directory with the GCP bucket. 54 | crawlDirectory(config.distPath, (filePath: string) => { 55 | const relativeFilePath = filePath.replace(config.distPath + '/', ''); 56 | const file = new gcp.storage.BucketObject( 57 | relativeFilePath, 58 | { 59 | bucket: contentBucket.name, 60 | source: new pulumi.asset.FileAsset(filePath), 61 | name: basename(relativeFilePath), 62 | contentType: mime.getType(filePath) || undefined 63 | }, 64 | { dependsOn: oacResource } 65 | ); 66 | }); 67 | 68 | if (config.useCdn) { 69 | const cdnEndpointResource = new gcp.compute.BackendBucket( 70 | `${projectName}-cbb`, 71 | { 72 | bucketName: contentBucket.name, 73 | enableCdn: true 74 | } 75 | ); 76 | } 77 | 78 | export const endpoint = interpolate`https://${config.customDomainName}`; 79 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/schema.d.ts: -------------------------------------------------------------------------------- 1 | import { NxDeployItBaseOptions } from '../../adapter/base.adapter.model'; 2 | 3 | export interface NxDeployItInitSchematicSchema extends NxDeployItBaseOptions { 4 | customDomainName?: string; 5 | 'azure:location'?: string; 6 | 'aws:region'?: string; 7 | 'aws:profile'?: string; 8 | 'gcp:projectId'?: string; 9 | } 10 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "id": "NxDeployItInit", 4 | "title": "Add nx-deploy-it cloud configuration for an application", 5 | "type": "object", 6 | "properties": { 7 | "project": { 8 | "type": "string", 9 | "description": "The name of the project.", 10 | "$default": { 11 | "$source": "projectName" 12 | } 13 | }, 14 | "provider": { 15 | "type": "string", 16 | "description": "Your cloud provider", 17 | "x-prompt": { 18 | "message": "Please choose your provider", 19 | "type": "list", 20 | "items": [ 21 | { 22 | "label": "AWS", 23 | "value": "aws" 24 | }, 25 | { 26 | "label": "Azure", 27 | "value": "azure" 28 | }, 29 | { 30 | "label": "Google Cloud Platform", 31 | "value": "gcp" 32 | } 33 | ] 34 | } 35 | }, 36 | "customDomainName": { 37 | "type": "string", 38 | "description": "Your custom domain which will be mapped to the static website / cdn" 39 | } 40 | }, 41 | "required": ["provider"] 42 | } 43 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/schematic.spec.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; 3 | import { createEmptyWorkspace } from '@nrwl/workspace/testing'; 4 | import { join } from 'path'; 5 | import { NxDeployItInitSchematicSchema } from './schema'; 6 | import { readJsonInTree } from '@nrwl/workspace'; 7 | import { stdin } from 'mock-stdin'; 8 | 9 | import { PROVIDER } from '../../utils/provider'; 10 | import { createPulumiMockProjectInTree } from '../../utils-test/pulumi.mock'; 11 | import * as childProcess from 'child_process'; 12 | import * as fs from 'fs'; 13 | import { createApplication } from '../../utils-test/app.utils'; 14 | import { 15 | answerInitQuestionsAWS, 16 | answerInitQuestionsAzure, 17 | answerInitQuestionsGCP 18 | } from '../../utils-test/enquirer.utils'; 19 | 20 | describe('init schematic', () => { 21 | let appTree: Tree; 22 | const projectName = 'mock-project'; 23 | 24 | const unlinkSync = jest.spyOn(fs, 'unlinkSync'); 25 | const spawnSync = jest.spyOn(childProcess, 'spawnSync'); 26 | 27 | const originReadFileSync = fs.readFileSync; 28 | 29 | (fs.readFileSync as any) = jest 30 | .fn(originReadFileSync) 31 | .mockImplementation((path, options) => { 32 | if (path === `apps/${projectName}/infrastructure/Pulumi.yaml`) { 33 | return ''; 34 | } 35 | 36 | return originReadFileSync(path, options); 37 | }); 38 | 39 | const testRunner = new SchematicTestRunner( 40 | '@dev-thought/nx-deploy-it', 41 | join(__dirname, '../../../collection.json') 42 | ); 43 | 44 | let io = null; 45 | beforeAll(() => (io = stdin())); 46 | afterAll(() => io.restore()); 47 | 48 | beforeEach(() => { 49 | appTree = createEmptyWorkspace(Tree.empty()); 50 | }); 51 | 52 | afterEach(() => { 53 | jest.clearAllMocks(); 54 | }); 55 | 56 | describe('pulumi project', () => { 57 | const options: NxDeployItInitSchematicSchema = { 58 | project: projectName, 59 | provider: PROVIDER.AWS 60 | }; 61 | 62 | beforeEach(async () => { 63 | appTree = await createApplication( 64 | testRunner, 65 | projectName, 66 | 'nest', 67 | appTree 68 | ); 69 | 70 | spawnSync.mockImplementation(() => { 71 | createPulumiMockProjectInTree(appTree, PROVIDER.AWS, projectName); 72 | return {} as any; 73 | }); 74 | unlinkSync.mockImplementation(); 75 | }); 76 | 77 | it('should be initialized', async () => { 78 | answerInitQuestionsAWS(io, null, null); 79 | 80 | const tree = await testRunner 81 | .runSchematicAsync('init', options, appTree) 82 | .toPromise(); 83 | 84 | expect( 85 | tree.exists(`apps/${projectName}/infrastructure/Pulumi.yaml`) 86 | ).toBeTruthy(); 87 | 88 | expect(spawnSync).toHaveBeenCalled(); 89 | expect(spawnSync.mock.calls[0][1][1]).toContain('aws-typescript'); 90 | expect(spawnSync.mock.calls[0][1][3]).toBe(projectName); 91 | expect(spawnSync.mock.calls[0][1][5]).toContain( 92 | `apps/${projectName}/infrastructure` 93 | ); 94 | expect(spawnSync.mock.calls[0][1][7]).toContain( 95 | 'Infrastructure as Code based on Pulumi - managed by @dev-thought/nx-deploy-it' 96 | ); 97 | expect(unlinkSync).toHaveBeenCalledTimes(5); 98 | }); 99 | 100 | it('should fail if the project already has an deploy integration', async () => { 101 | answerInitQuestionsAWS(io, null, null); 102 | 103 | await testRunner.runSchematicAsync('init', options, appTree).toPromise(); 104 | await testRunner.runSchematicAsync('init', options, appTree).toPromise(); 105 | 106 | expect(spawnSync).toHaveBeenCalledTimes(1); 107 | }); 108 | }); 109 | 110 | describe('aws provider', () => { 111 | const options: NxDeployItInitSchematicSchema = { 112 | project: projectName, 113 | provider: PROVIDER.AWS 114 | }; 115 | 116 | beforeEach(async () => { 117 | appTree = await createApplication( 118 | testRunner, 119 | projectName, 120 | 'nest', 121 | appTree 122 | ); 123 | 124 | spawnSync.mockImplementation(() => { 125 | createPulumiMockProjectInTree(appTree, PROVIDER.AWS, projectName); 126 | return {} as any; 127 | }); 128 | unlinkSync.mockImplementation(); 129 | }); 130 | 131 | it('should add dependencies to package.json', async () => { 132 | answerInitQuestionsAWS(io, null, null); 133 | 134 | spawnSync.mockImplementation(() => { 135 | createPulumiMockProjectInTree(appTree, PROVIDER.AWS, projectName); 136 | return {} as any; 137 | }); 138 | unlinkSync.mockImplementation(); 139 | const tree = await testRunner 140 | .runSchematicAsync('init', options, appTree) 141 | .toPromise(); 142 | 143 | const packageJSON = readJsonInTree(tree, 'package.json'); 144 | expect(packageJSON.dependencies['@pulumi/pulumi']).toBeDefined(); 145 | expect(packageJSON.dependencies['@pulumi/aws']).toBeDefined(); 146 | expect(packageJSON.dependencies['@pulumi/awsx']).toBeDefined(); 147 | }); 148 | 149 | it('should extend the project configuration options with aws profile', async () => { 150 | answerInitQuestionsAWS(io, 'eu-central-1', 'my-aws-profile'); 151 | 152 | const tree = await testRunner 153 | .runSchematicAsync('init', options, appTree) 154 | .toPromise(); 155 | 156 | const workspaceJson = readJsonInTree(tree, 'workspace.json'); 157 | expect( 158 | workspaceJson.projects[projectName].architect.deploy 159 | ).toMatchSnapshot(); 160 | expect( 161 | workspaceJson.projects[projectName].architect.destroy 162 | ).toMatchSnapshot(); 163 | }); 164 | 165 | it('should extend the project default configuration options', async () => { 166 | answerInitQuestionsAWS(io, 'eu-central-1'); 167 | 168 | const tree = await testRunner 169 | .runSchematicAsync('init', options, appTree) 170 | .toPromise(); 171 | 172 | const workspaceJson = readJsonInTree(tree, 'workspace.json'); 173 | expect( 174 | workspaceJson.projects[projectName].architect.deploy 175 | ).toMatchSnapshot('Deploy Action'); 176 | expect( 177 | workspaceJson.projects[projectName].architect.destroy 178 | ).toMatchSnapshot('Destroy Action'); 179 | }); 180 | }); 181 | 182 | describe('azure provider', () => { 183 | const options: NxDeployItInitSchematicSchema = { 184 | project: projectName, 185 | provider: PROVIDER.AZURE 186 | }; 187 | 188 | beforeEach(async () => { 189 | appTree = await createApplication( 190 | testRunner, 191 | projectName, 192 | 'nest', 193 | appTree 194 | ); 195 | 196 | spawnSync.mockImplementation(() => { 197 | createPulumiMockProjectInTree(appTree, PROVIDER.AZURE, projectName); 198 | return {} as any; 199 | }); 200 | unlinkSync.mockImplementation(); 201 | }); 202 | 203 | it('should add dependencies to package.json', async () => { 204 | answerInitQuestionsAzure(io, null); 205 | 206 | const tree = await testRunner 207 | .runSchematicAsync('init', options, appTree) 208 | .toPromise(); 209 | 210 | const packageJSON = readJsonInTree(tree, 'package.json'); 211 | expect(packageJSON.dependencies['@pulumi/pulumi']).toBeDefined(); 212 | expect(packageJSON.dependencies['@pulumi/azure']).toBeDefined(); 213 | }); 214 | 215 | it('should extend the project default configuration options', async () => { 216 | answerInitQuestionsAzure(io, 'eastasia'); 217 | 218 | const tree = await testRunner 219 | .runSchematicAsync('init', options, appTree) 220 | .toPromise(); 221 | 222 | const workspaceJson = readJsonInTree(tree, 'workspace.json'); 223 | expect( 224 | workspaceJson.projects[projectName].architect.deploy 225 | ).toMatchSnapshot('Deploy Action'); 226 | expect( 227 | workspaceJson.projects[projectName].architect.destroy 228 | ).toMatchSnapshot('Destroy Action'); 229 | }); 230 | }); 231 | 232 | describe('google cloud platform provider', () => { 233 | const options: NxDeployItInitSchematicSchema = { 234 | project: projectName, 235 | provider: PROVIDER.GOOGLE_CLOUD_PLATFORM 236 | }; 237 | 238 | beforeEach(async () => { 239 | appTree = await createApplication( 240 | testRunner, 241 | projectName, 242 | 'nest', 243 | appTree 244 | ); 245 | 246 | spawnSync.mockImplementation(() => { 247 | createPulumiMockProjectInTree( 248 | appTree, 249 | PROVIDER.GOOGLE_CLOUD_PLATFORM, 250 | projectName 251 | ); 252 | return {} as any; 253 | }); 254 | unlinkSync.mockImplementation(); 255 | }); 256 | 257 | it('should add dependencies to package.json', async () => { 258 | answerInitQuestionsGCP(io, 'my-google-project-id', 'europe-west1'); 259 | 260 | const tree = await testRunner 261 | .runSchematicAsync('init', options, appTree) 262 | .toPromise(); 263 | 264 | const packageJSON = readJsonInTree(tree, 'package.json'); 265 | expect(packageJSON.dependencies['@pulumi/pulumi']).toBeDefined(); 266 | expect(packageJSON.dependencies['@pulumi/gcp']).toBeDefined(); 267 | }); 268 | 269 | it('should extend the project default configuration options', async () => { 270 | answerInitQuestionsGCP(io, 'my-google-project-id', 'europe-west1'); 271 | 272 | const tree = await testRunner 273 | .runSchematicAsync('init', options, appTree) 274 | .toPromise(); 275 | 276 | const workspaceJson = readJsonInTree(tree, 'workspace.json'); 277 | expect( 278 | workspaceJson.projects[projectName].architect.deploy 279 | ).toMatchSnapshot('Deploy Action'); 280 | expect( 281 | workspaceJson.projects[projectName].architect.destroy 282 | ).toMatchSnapshot('Destroy Action'); 283 | }); 284 | }); 285 | }); 286 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/init/schematic.ts: -------------------------------------------------------------------------------- 1 | import { NxDeployItInitSchematicSchema } from './schema'; 2 | import { 3 | Rule, 4 | Tree, 5 | chain, 6 | noop, 7 | apply, 8 | url, 9 | mergeWith, 10 | move, 11 | SchematicContext, 12 | branchAndMerge, 13 | } from '@angular-devkit/schematics'; 14 | import { 15 | readJsonInTree, 16 | addDepsToPackageJson, 17 | getWorkspace, 18 | updateJsonInTree, 19 | updateWorkspace, 20 | } from '@nrwl/workspace'; 21 | import { spawnSync } from 'child_process'; 22 | import { resolve, join } from 'path'; 23 | import { getCloudTemplateName } from '../../utils/provider'; 24 | import { getPulumiBinaryPath, getAdapter } from '../../utils/workspace'; 25 | import { readFileSync, unlinkSync } from 'fs'; 26 | import { BaseAdapter } from '../../adapter/base.adapter'; 27 | 28 | export function updateProject(adapter: BaseAdapter): Rule { 29 | return async () => { 30 | return chain([ 31 | updateWorkspace((workspace) => { 32 | const project = workspace.projects.get(adapter.options.project); 33 | project.targets.add({ 34 | name: 'deploy', 35 | ...adapter.getDeployActionConfiguration(), 36 | }); 37 | project.targets.add({ 38 | name: 'destroy', 39 | ...adapter.getDestroyActionConfiguration(), 40 | }); 41 | }), 42 | updateJsonInTree( 43 | join(adapter.project.root, 'tsconfig.app.json'), 44 | (json) => { 45 | const exclude: string[] = json.exclude; 46 | const excludePaths = 'infrastructure/**/*.ts'; 47 | 48 | if (!exclude) { 49 | json.exclude = [excludePaths]; 50 | } else { 51 | exclude.push(excludePaths); 52 | } 53 | return json; 54 | } 55 | ), 56 | ]); 57 | }; 58 | } 59 | 60 | function addDependenciesFromPulumiProjectToPackageJson( 61 | adapter: BaseAdapter 62 | ): Rule { 63 | return (host: Tree): Rule => { 64 | const pulumiPackageJson = host.read( 65 | join(adapter.project.root, 'infrastructure', 'package.json') 66 | ); 67 | if (!pulumiPackageJson) { 68 | throw new Error('Can not find generated pulumi package.json'); 69 | } 70 | const pulumiCloudProviderDependencies = JSON.parse( 71 | pulumiPackageJson.toString() 72 | ).dependencies; 73 | const packageJson = readJsonInTree(host, 'package.json'); 74 | const dependencyList: { name: string; version: string }[] = []; 75 | 76 | for (const name in pulumiCloudProviderDependencies) { 77 | const version = pulumiCloudProviderDependencies[name]; 78 | if (version) { 79 | if (!packageJson.dependencies[name]) { 80 | dependencyList.push({ 81 | name, 82 | version, 83 | }); 84 | } 85 | } 86 | } 87 | 88 | dependencyList.push(...adapter.addRequiredDependencies()); 89 | 90 | if (!dependencyList.length) { 91 | return noop(); 92 | } 93 | 94 | return addDepsToPackageJson( 95 | dependencyList.reduce((dictionary, value) => { 96 | dictionary[value.name] = value.version; 97 | return dictionary; 98 | }, {}), 99 | {} 100 | ); 101 | }; 102 | } 103 | 104 | function generateNewPulumiProject(adapter: BaseAdapter): Rule { 105 | return (): Rule => { 106 | const template = getCloudTemplateName(adapter.options.provider); 107 | const args = [ 108 | 'new', 109 | template, 110 | '--name', 111 | adapter.options.project, 112 | '--dir', 113 | resolve(join(adapter.project.root, 'infrastructure')), 114 | '--description', 115 | 'Infrastructure as Code based on Pulumi - managed by @dev-thought/nx-deploy-it', 116 | '--generate-only', 117 | '--yes', 118 | ]; 119 | 120 | spawnSync(getPulumiBinaryPath(), args, { 121 | env: { ...process.env, PULUMI_SKIP_UPDATE_CHECK: '1' }, 122 | }); 123 | 124 | return addDependenciesFromPulumiProjectToPackageJson(adapter); 125 | }; 126 | } 127 | 128 | function mergePulumiProjectIntoTree(adapter: BaseAdapter) { 129 | return (host: Tree) => { 130 | const infraDir = join(adapter.project.root, 'infrastructure'); 131 | 132 | const PulumiFile = join(infraDir, 'Pulumi.yaml'); 133 | const pulumiContent = readFileSync(PulumiFile); 134 | unlinkSync(PulumiFile); 135 | host.create(PulumiFile, pulumiContent); 136 | 137 | return host; 138 | }; 139 | } 140 | 141 | function cleanupTempPulumiProject(adapter: BaseAdapter) { 142 | return (host: Tree) => { 143 | const infraDir = join(adapter.project.root, 'infrastructure'); 144 | unlinkSync(resolve(infraDir, '.gitignore')); 145 | unlinkSync(resolve(infraDir, 'index.ts')); 146 | unlinkSync(resolve(infraDir, 'tsconfig.json')); 147 | unlinkSync(resolve(infraDir, 'package.json')); 148 | 149 | return host; 150 | }; 151 | } 152 | 153 | function generateInfrastructureCode(adapter: BaseAdapter) { 154 | return (host: Tree, context: SchematicContext) => { 155 | const template = adapter.getApplicationTypeTemplate(); 156 | if (!template) { 157 | throw new Error(`Can't find a supported build target for the project`); 158 | } 159 | const templateSource = apply( 160 | url(`./files/${adapter.getApplicationTemplatePath()}`), 161 | [template, move(join(adapter.project.root))] 162 | ); 163 | 164 | const rule = chain([branchAndMerge(chain([mergeWith(templateSource)]))]); 165 | return rule(host, context); 166 | }; 167 | } 168 | 169 | function initializeCloudProviderApplication(adapter: BaseAdapter) { 170 | return chain([ 171 | generateNewPulumiProject(adapter), 172 | mergePulumiProjectIntoTree(adapter), 173 | cleanupTempPulumiProject(adapter), 174 | generateInfrastructureCode(adapter), 175 | updateProject(adapter), 176 | ]); 177 | } 178 | 179 | export default function (options: NxDeployItInitSchematicSchema) { 180 | return async (host: Tree, context: SchematicContext): Promise => { 181 | const workspace = await getWorkspace(host); 182 | const project = workspace.projects.get(options.project); 183 | 184 | if (!project) { 185 | context.logger.error(`Project doesn't exist`); 186 | return chain([]); 187 | } 188 | 189 | if (project.targets.has('deploy')) { 190 | context.logger.error( 191 | `Your project is already configured with a deploy job` 192 | ); 193 | return chain([]); 194 | } 195 | 196 | if (host.exists(join(project.root, 'infrastructure', 'Pulumi.yaml'))) { 197 | context.logger.error(`This project already has an infrastructure`); 198 | return chain([]); 199 | } 200 | 201 | const adapter = getAdapter(project, options, host); 202 | await adapter.extendOptionsByUserInput(); 203 | 204 | return chain([initializeCloudProviderApplication(adapter)]); 205 | }; 206 | } 207 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/scan/__snapshots__/schematic.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`scan schematic should find no applications 1`] = ` 4 | Object { 5 | "level": "info", 6 | "message": "no applications found", 7 | "name": "scan", 8 | "path": Array [ 9 | "test", 10 | ], 11 | } 12 | `; 13 | 14 | exports[`scan schematic with applications should aboard setup because of no selections 1`] = ` 15 | Object { 16 | "level": "info", 17 | "message": "We found 1 supported applications.", 18 | "name": "scan", 19 | "path": Array [ 20 | "test", 21 | ], 22 | } 23 | `; 24 | 25 | exports[`scan schematic with applications should aboard setup because of no selections 2`] = ` 26 | Object { 27 | "level": "info", 28 | "message": "No applications selected. Skipping setup", 29 | "name": "scan", 30 | "path": Array [ 31 | "test", 32 | ], 33 | } 34 | `; 35 | 36 | exports[`scan schematic with applications should setup the selected nest application 1`] = ` 37 | Object { 38 | "level": "info", 39 | "message": "We found 1 supported applications.", 40 | "name": "scan", 41 | "path": Array [ 42 | "test", 43 | ], 44 | } 45 | `; 46 | 47 | exports[`scan schematic with applications should setup the selected nest application 2`] = ` 48 | Array [ 49 | "@dev-thought/nx-deploy-it", 50 | "init", 51 | Object { 52 | "azure:location": "eastasia", 53 | "project": "mock-project", 54 | "provider": "azure", 55 | }, 56 | ] 57 | `; 58 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/scan/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema", 3 | "id": "scan", 4 | "title": "", 5 | "type": "object", 6 | "properties": {} 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/scan/schematic.spec.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; 3 | import { createEmptyWorkspace } from '@nrwl/workspace/testing'; 4 | import { join } from 'path'; 5 | 6 | import { stdin } from 'mock-stdin'; 7 | import * as childProcess from 'child_process'; 8 | import * as fs from 'fs'; 9 | import { createPulumiMockProjectInTree } from '../../utils-test/pulumi.mock'; 10 | import { PROVIDER } from '../../utils/provider'; 11 | import { createApplication } from '../../utils-test/app.utils'; 12 | import { clearTimestampFromLogEntry } from '../../utils-test/logger.utils'; 13 | import { 14 | answerScanQuestions, 15 | answerScanQuestionsWithNoApp 16 | } from '../../utils-test/enquirer.utils'; 17 | import * as schematics from '@angular-devkit/schematics'; 18 | 19 | describe('scan schematic', () => { 20 | let appTree: Tree; 21 | const projectName = 'mock-project'; 22 | let testRunner: SchematicTestRunner; 23 | 24 | const unlinkSync = jest.spyOn(fs, 'unlinkSync'); 25 | const spawnSync = jest.spyOn(childProcess, 'spawnSync'); 26 | 27 | const originReadFileSync = fs.readFileSync; 28 | 29 | (fs.readFileSync as any) = jest 30 | .fn(originReadFileSync) 31 | .mockImplementation((path, options) => { 32 | if (path === `apps/${projectName}/infrastructure/Pulumi.yaml`) { 33 | return ''; 34 | } 35 | 36 | return originReadFileSync(path, options); 37 | }); 38 | 39 | let io = null; 40 | beforeAll(() => (io = stdin())); 41 | afterAll(() => io.restore()); 42 | 43 | beforeEach(() => { 44 | appTree = createEmptyWorkspace(Tree.empty()); 45 | 46 | testRunner = new SchematicTestRunner( 47 | '@dev-thought/nx-deploy-it', 48 | join(__dirname, '../../../collection.json') 49 | ); 50 | 51 | spawnSync.mockImplementation(() => { 52 | createPulumiMockProjectInTree(appTree, PROVIDER.AZURE, projectName); 53 | return {} as any; 54 | }); 55 | unlinkSync.mockImplementation(); 56 | }); 57 | 58 | afterEach(() => { 59 | jest.clearAllMocks(); 60 | }); 61 | 62 | it('should find no applications', async () => { 63 | testRunner.logger.subscribe(log => { 64 | clearTimestampFromLogEntry(log); 65 | expect(log).toMatchSnapshot(); 66 | }); 67 | 68 | await testRunner.runSchematicAsync('scan', {}, appTree).toPromise(); 69 | }); 70 | 71 | describe('with applications', () => { 72 | beforeEach(async () => { 73 | appTree = await createApplication( 74 | testRunner, 75 | projectName, 76 | 'nest', 77 | appTree 78 | ); 79 | }); 80 | 81 | it('should aboard setup because of no selections', async () => { 82 | answerScanQuestionsWithNoApp(io); 83 | 84 | testRunner.logger.subscribe(log => { 85 | clearTimestampFromLogEntry(log); 86 | expect(log).toMatchSnapshot(); 87 | }); 88 | 89 | await testRunner.runSchematicAsync('scan', {}, appTree).toPromise(); 90 | }); 91 | 92 | it('should setup the selected nest application', async () => { 93 | const spyInstance = jest 94 | .spyOn(schematics, 'externalSchematic') 95 | .mockImplementation(() => schematics.chain([])); 96 | answerScanQuestions(io, 'eastasia'); 97 | 98 | testRunner.logger.subscribe(log => { 99 | clearTimestampFromLogEntry(log); 100 | expect(log).toMatchSnapshot(); 101 | }); 102 | 103 | await testRunner.runSchematicAsync('scan', {}, appTree).toPromise(); 104 | expect(schematics.externalSchematic).toHaveBeenCalled(); 105 | expect(spyInstance.mock.calls[0]).toMatchSnapshot(); 106 | }); 107 | }); 108 | }); 109 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/schematics/scan/schematic.ts: -------------------------------------------------------------------------------- 1 | import { 2 | chain, 3 | Rule, 4 | Tree, 5 | SchematicContext, 6 | externalSchematic, 7 | } from '@angular-devkit/schematics'; 8 | import { getWorkspace } from '@nrwl/workspace'; 9 | import { getApplications } from '../../utils/workspace'; 10 | import { prompt } from 'enquirer'; 11 | import { ApplicationType } from '../../utils/application-type'; 12 | import { QUESTIONS } from '../../utils/questions'; 13 | import { PROVIDER } from '../../utils/provider'; 14 | 15 | export default function (): Rule { 16 | return async (host: Tree, context: SchematicContext): Promise => { 17 | const workspace = await getWorkspace(host); 18 | const applications = getApplications(workspace, host); 19 | const questions: any[] = []; 20 | 21 | if (applications.length === 0) { 22 | context.logger.log('info', 'no applications found'); 23 | return chain([]); 24 | } 25 | 26 | context.logger.log( 27 | 'info', 28 | `We found ${applications.length} supported applications.` 29 | ); 30 | 31 | const choosenApplications = await prompt<{ 32 | setupApplications: { 33 | projectName: string; 34 | applicationType: ApplicationType; 35 | }[]; 36 | }>({ 37 | ...QUESTIONS.setupApplications, 38 | choices: applications.map((app) => ({ 39 | name: `${app.projectName} (${app.applicationType})`, 40 | value: app, 41 | })), 42 | result: function (result: string) { 43 | return Object.values(this.map(result)); 44 | }, 45 | } as any); 46 | 47 | if (choosenApplications.setupApplications.length === 0) { 48 | context.logger.log('info', 'No applications selected. Skipping setup'); 49 | return chain([]); 50 | } 51 | 52 | const { provider } = await prompt<{ provider: PROVIDER }>([ 53 | QUESTIONS.whichProvider, 54 | ]); 55 | 56 | switch (provider) { 57 | case PROVIDER.AWS: 58 | questions.push(QUESTIONS.awsProfile, QUESTIONS.awsRegion); 59 | break; 60 | 61 | case PROVIDER.AZURE: 62 | questions.push(QUESTIONS.azureLocation); 63 | break; 64 | 65 | case PROVIDER.GOOGLE_CLOUD_PLATFORM: 66 | questions.push(QUESTIONS.gcpProjectId); 67 | break; 68 | 69 | default: 70 | break; 71 | } 72 | 73 | const options = await prompt(questions); 74 | 75 | return chain( 76 | choosenApplications.setupApplications.map((application) => { 77 | return externalSchematic('@dev-thought/nx-deploy-it', 'init', { 78 | ...options, 79 | provider, 80 | project: application.projectName, 81 | }); 82 | }) 83 | ); 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils-test/app.utils.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@angular-devkit/schematics'; 2 | import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; 3 | 4 | export function createApplication( 5 | testRunner: SchematicTestRunner, 6 | projectName: string, 7 | applicationType: 'nest' | 'express' | 'angular' | 'react', 8 | tree: Tree 9 | ) { 10 | return testRunner 11 | .runExternalSchematicAsync( 12 | `@nrwl/${applicationType}`, 13 | 'application', 14 | { 15 | name: projectName 16 | }, 17 | tree 18 | ) 19 | .toPromise(); 20 | } 21 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils-test/builders.utils.ts: -------------------------------------------------------------------------------- 1 | import { Architect } from '@angular-devkit/architect'; 2 | import { TestingArchitectHost } from '@angular-devkit/architect/testing'; 3 | import { schema } from '@angular-devkit/core'; 4 | import { MockBuilderContext } from '@nrwl/workspace/testing'; 5 | import { join } from 'path'; 6 | 7 | export async function getTestArchitect() { 8 | const architectHost = new TestingArchitectHost('/root', '/root'); 9 | const registry = new schema.CoreSchemaRegistry(); 10 | registry.addPostTransform(schema.transforms.addUndefinedDefaults); 11 | 12 | const architect = new Architect(architectHost, registry); 13 | 14 | await architectHost.addBuilderFromPackage(join(__dirname, '../..')); 15 | 16 | return [architect, architectHost] as [Architect, TestingArchitectHost]; 17 | } 18 | 19 | export async function getMockContext() { 20 | const [architect, architectHost] = await getTestArchitect(); 21 | 22 | const context = new MockBuilderContext(architect, architectHost); 23 | await context.addBuilderFromPackage(join(__dirname, '../..')); 24 | return context; 25 | } 26 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils-test/enquirer.utils.ts: -------------------------------------------------------------------------------- 1 | import { MockSTDIN } from 'mock-stdin'; 2 | 3 | export const KEYS = { 4 | UP: '\x1B\x5B\x41', 5 | DOWN: '\x1B\x5B\x42', 6 | ENTER: '\x0D', 7 | SPACE: '\x20' 8 | }; 9 | 10 | export const delay = (ms: number) => 11 | new Promise(resolve => setTimeout(resolve, ms)); 12 | 13 | export function answerInitQuestionsAWS( 14 | io: MockSTDIN, 15 | region: string, 16 | awsProfile?: string 17 | ) { 18 | const initQuestions = async () => { 19 | if (region) { 20 | io.send(region); 21 | } 22 | io.send(KEYS.ENTER); 23 | await delay(10); 24 | 25 | if (awsProfile) { 26 | io.send('my-aws-profile'); 27 | } 28 | io.send(KEYS.ENTER); 29 | }; 30 | setTimeout(() => initQuestions().then(), 5); 31 | } 32 | 33 | export function answerInitQuestionsAzure(io: MockSTDIN, location: string) { 34 | const initQuestions = async () => { 35 | if (location) { 36 | io.send(location); 37 | } 38 | io.send(KEYS.ENTER); 39 | }; 40 | setTimeout(() => initQuestions().then(), 5); 41 | } 42 | 43 | export function answerInitQuestionsGCP( 44 | io: MockSTDIN, 45 | projectId: string, 46 | region?: string 47 | ) { 48 | const initQuestions = async () => { 49 | io.send(projectId); 50 | io.send(KEYS.ENTER); 51 | 52 | await delay(10); 53 | if (region) { 54 | io.send(region); 55 | io.send(KEYS.ENTER); 56 | } 57 | }; 58 | setTimeout(() => initQuestions().then(), 5); 59 | } 60 | 61 | export function answerScanQuestionsWithNoApp(io: MockSTDIN) { 62 | const initQuestions = async () => { 63 | io.send(KEYS.ENTER); 64 | }; 65 | setTimeout(() => initQuestions().then(), 5); 66 | } 67 | 68 | export function answerScanQuestions(io: MockSTDIN, azureLocation: string) { 69 | const initQuestions = async () => { 70 | io.send(KEYS.SPACE); 71 | io.send(KEYS.ENTER); 72 | 73 | await delay(10); 74 | 75 | io.send(KEYS.DOWN); 76 | io.send(KEYS.ENTER); 77 | 78 | await delay(10); 79 | 80 | if (azureLocation) { 81 | io.send(azureLocation); 82 | io.send(KEYS.ENTER); 83 | } 84 | }; 85 | setTimeout(() => initQuestions().then(), 5); 86 | } 87 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils-test/logger.utils.ts: -------------------------------------------------------------------------------- 1 | export function clearTimestampFromLogEntry(logEntry: any) { 2 | delete logEntry.timestamp; 3 | } 4 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils-test/pulumi.mock.ts: -------------------------------------------------------------------------------- 1 | import { PROVIDER } from '../utils/provider'; 2 | import { Tree } from '@angular-devkit/schematics'; 3 | 4 | export function createPulumiMockProjectInTree( 5 | tree: Tree, 6 | provider: PROVIDER, 7 | projectName: string 8 | ) { 9 | let dependencies: { [index: string]: string } = { 10 | '@pulumi/pulumi': '^1.2.3' 11 | }; 12 | switch (provider) { 13 | case PROVIDER.AWS: 14 | dependencies = { 15 | ...dependencies, 16 | '@pulumi/aws': '^1.2.3', 17 | '@pulumi/awsx': '^1.2.3' 18 | }; 19 | break; 20 | 21 | case PROVIDER.AZURE: 22 | dependencies = { 23 | ...dependencies, 24 | '@pulumi/azure': '^1.2.3' 25 | }; 26 | break; 27 | case PROVIDER.GOOGLE_CLOUD_PLATFORM: 28 | dependencies = { 29 | ...dependencies, 30 | '@pulumi/gcp': '^1.2.3' 31 | }; 32 | break; 33 | 34 | default: 35 | break; 36 | } 37 | 38 | tree.create( 39 | `./apps/${projectName}/infrastructure/package.json`, 40 | JSON.stringify({ 41 | dependencies 42 | }) 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils/application-type.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TargetDefinition, 3 | TargetDefinitionCollection 4 | } from '@angular-devkit/core/src/workspace'; 5 | import { resolve } from 'path'; 6 | import * as ts from 'typescript'; 7 | import { readFileSync } from 'fs-extra'; 8 | import { hasImport } from './ats.utils'; 9 | import { Tree } from '@angular-devkit/schematics'; 10 | 11 | export enum ApplicationType { 12 | ANGULAR = 'angular', 13 | REACT = 'react', 14 | NESTJS = 'nestjs', 15 | EXPRESS = 'express', 16 | ANGULAR_UNIVERSAL = 'angular-universal' 17 | } 18 | 19 | function getTarget( 20 | targets: TargetDefinitionCollection | {}, 21 | targetName: string 22 | ): TargetDefinition { 23 | if ( 24 | (targets as TargetDefinitionCollection).get && 25 | typeof (targets as TargetDefinitionCollection).get === 'function' 26 | ) { 27 | return (targets as TargetDefinitionCollection).get(targetName); 28 | } 29 | 30 | return targets[targetName]; 31 | } 32 | 33 | function isAngular(targets: TargetDefinitionCollection): boolean { 34 | const build = getTarget(targets, 'build'); 35 | if (!build) { 36 | return false; 37 | } 38 | return build.builder === '@angular-devkit/build-angular:browser'; 39 | } 40 | 41 | function isAngularCustomWebpack(targets: TargetDefinitionCollection): boolean { 42 | const build = getTarget(targets, 'build'); 43 | if (!build) { 44 | return false; 45 | } 46 | return build.builder === '@angular-builders/custom-webpack:browser'; 47 | } 48 | 49 | function isAngularUniversal(targets: TargetDefinitionCollection): boolean { 50 | const build = getTarget(targets, 'build'); 51 | if (!build) { 52 | return false; 53 | } 54 | const serveSsr = getTarget(targets, 'serve-ssr'); 55 | const prerender = getTarget(targets, 'prerender'); 56 | const server = getTarget(targets, 'server'); 57 | return ( 58 | build.builder === '@angular-devkit/build-angular:browser' && 59 | !!serveSsr && 60 | !!prerender && 61 | !!server 62 | ); 63 | } 64 | 65 | function isNestJS(targets: TargetDefinitionCollection, host: Tree): boolean { 66 | const build = getTarget(targets, 'build'); 67 | if (!build) { 68 | return false; 69 | } 70 | if (build.builder !== '@nrwl/node:build') { 71 | return false; 72 | } 73 | const mainPath = build.options.main.toString(); 74 | let mainSource: string; 75 | if (host) { 76 | mainSource = host.read(mainPath).toString('utf-8'); 77 | } else { 78 | mainSource = readFileSync(resolve(mainPath)).toString('utf-8'); 79 | } 80 | const main = ts.createSourceFile( 81 | mainPath, 82 | mainSource, 83 | ts.ScriptTarget.Latest 84 | ); 85 | return hasImport(main.statements, '@nestjs'); 86 | } 87 | 88 | function isExpressJS(targets: TargetDefinitionCollection): boolean { 89 | const build = getTarget(targets, 'build'); 90 | if (!build) { 91 | return false; 92 | } 93 | if (build.builder !== '@nrwl/node:build') { 94 | return false; 95 | } 96 | const mainPath = resolve(build.options.main.toString()); 97 | const mainSource = readFileSync(mainPath).toString('utf-8'); 98 | const main = ts.createSourceFile( 99 | mainPath, 100 | mainSource, 101 | ts.ScriptTarget.Latest 102 | ); 103 | return ( 104 | !hasImport(main.statements, '@nestjs') && 105 | hasImport(main.statements, 'express') 106 | ); 107 | } 108 | 109 | function isReact(targets: TargetDefinitionCollection): boolean { 110 | const build = getTarget(targets, 'build'); 111 | if (!build) { 112 | return false; 113 | } 114 | return ( 115 | build.builder === '@nrwl/web:build' && 116 | build.options.webpackConfig.toString().startsWith('@nrwl/react') && 117 | build.options.main.toString().endsWith('.tsx') 118 | ); 119 | } 120 | 121 | export function getApplicationType( 122 | targets: TargetDefinitionCollection, 123 | host?: Tree 124 | ): ApplicationType { 125 | if (!targets) { 126 | return null; 127 | } 128 | 129 | if (isNestJS(targets, host)) { 130 | return ApplicationType.NESTJS; 131 | } 132 | if (isExpressJS(targets)) { 133 | return ApplicationType.EXPRESS; 134 | } 135 | 136 | if (isAngularUniversal(targets)) { 137 | return ApplicationType.ANGULAR_UNIVERSAL; 138 | } 139 | if (isAngular(targets)) { 140 | return ApplicationType.ANGULAR; 141 | } 142 | if (isAngularCustomWebpack(targets)) { 143 | return ApplicationType.ANGULAR; 144 | } 145 | if (isReact(targets)) { 146 | return ApplicationType.REACT; 147 | } 148 | 149 | return null; 150 | } 151 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils/ats.utils.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export function hasImport( 4 | statements: ts.NodeArray, 5 | importModule: string 6 | ): boolean { 7 | return !!statements 8 | .filter(node => { 9 | return node.kind === ts.SyntaxKind.ImportDeclaration; 10 | }) 11 | .map((node: ts.ImportDeclaration) => (node.moduleSpecifier as any).text) 12 | .find((importName: string) => importName.indexOf(importModule) > -1); 13 | } 14 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils/provider.ts: -------------------------------------------------------------------------------- 1 | export enum PROVIDER { 2 | AWS = 'aws', 3 | AZURE = 'azure', 4 | GOOGLE_CLOUD_PLATFORM = 'gcp' 5 | } 6 | 7 | export function getCloudTemplateName(cloudProvider: string) { 8 | return `${cloudProvider}-typescript`; 9 | } 10 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils/questions.ts: -------------------------------------------------------------------------------- 1 | import { PROVIDER } from './provider'; 2 | import { ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE } from '../adapter/angular-universal/deployment-type.enum'; 3 | 4 | export const QUESTIONS = { 5 | // get list with: aws ec2 describe-regions --profile cli-dev-thought | jq ".Regions[] | .RegionName" 6 | awsRegion: { 7 | type: 'autocomplete', 8 | name: 'aws:region', 9 | message: 'The AWS region to deploy into:', 10 | limit: 10, 11 | choices: [ 12 | 'eu-north-1', 13 | 'ap-south-1', 14 | 'eu-west-3', 15 | 'eu-west-2', 16 | 'eu-west-1', 17 | 'ap-northeast-2', 18 | 'ap-northeast-1', 19 | 'sa-east-1', 20 | 'ca-central-1', 21 | 'ap-southeast-1', 22 | 'ap-southeast-2', 23 | 'eu-central-1', 24 | 'us-east-1', 25 | 'us-east-2', 26 | 'us-west-1', 27 | 'us-west-2', 28 | 29 | 'ap-east-1', 30 | 'ap-northeast-3', 31 | 'me-south-1' 32 | ] 33 | }, 34 | 35 | // https://cloud.google.com/functions/docs/reference/rest/v1/projects.locations/list 36 | gcpRegionCloudFunctions: { 37 | type: 'autocomplete', 38 | name: 'gcp:region', 39 | message: 'The GCP region to deploy into:', 40 | limit: 10, 41 | choices: [ 42 | 'europe-west2', 43 | 'europe-west1', 44 | 'europe-west3', 45 | 'us-east4', 46 | 'us-central1', 47 | 'us-east1', 48 | 'asia-east2', 49 | 'asia-northeast1' 50 | ] 51 | }, 52 | 53 | gcpProjectId: { 54 | type: 'input', 55 | name: 'gcp:project', 56 | message: 'Your google project ID where all the resources will be deployed' 57 | }, 58 | 59 | // get list with: az account list-locations | jq ".[] | .name" 60 | azureLocation: { 61 | type: 'autocomplete', 62 | name: 'azure:location', 63 | message: 'The Azure location to deploy into:', 64 | limit: 10, 65 | choices: [ 66 | 'eastasia', 67 | 'southeastasia', 68 | 'centralus', 69 | 'eastus', 70 | 'eastus2', 71 | 'westus', 72 | 'northcentralus', 73 | 'southcentralus', 74 | 'northeurope', 75 | 'westeurope', 76 | 'japanwest', 77 | 'japaneast', 78 | 'brazilsouth', 79 | 'australiaeast', 80 | 'australiasoutheast', 81 | 'southindia', 82 | 'centralindia', 83 | 'westindia', 84 | 'canadacentral', 85 | 'canadaeast', 86 | 'uksouth', 87 | 'ukwest', 88 | 'westcentralus', 89 | 'westus2', 90 | 'koreacentral', 91 | 'koreasouth', 92 | 'francecentral', 93 | 'francesouth', 94 | 'australiacentral', 95 | 'australiacentral2', 96 | 'uaecentral', 97 | 'uaenorth', 98 | 'southafricanorth', 99 | 'southafricawest', 100 | 'switzerlandnorth', 101 | 'switzerlandwest', 102 | 'germanynorth', 103 | 'germanywestcentral', 104 | 'norwaywest', 105 | 'norwayeast' 106 | ] 107 | }, 108 | 109 | customDomainName: { 110 | type: 'input', 111 | name: 'customDomainName', 112 | message: 113 | 'GCP requires a customDomainName which needs to be set up by you. Find more in the documentation.', 114 | initial: 'www.example.com' 115 | }, 116 | 117 | awsProfile: { 118 | type: 'input', 119 | name: 'aws:profile', 120 | message: 121 | 'Do you want to use a specific aws profile? Just skip if you want to use the default one.' 122 | }, 123 | 124 | setupApplications: { 125 | type: 'MultiSelect', 126 | name: 'setupApplications', 127 | message: 128 | "Please select the applications you want to setup. If you don't select one, you will skip this process and you can do it later again." 129 | }, 130 | 131 | whichProvider: { 132 | type: 'select', 133 | name: 'provider', 134 | choices: [ 135 | { name: 'AWS', value: PROVIDER.AWS }, 136 | { name: 'Azure', value: PROVIDER.AZURE }, 137 | { name: 'Google Cloud Platform', value: PROVIDER.GOOGLE_CLOUD_PLATFORM } 138 | ], 139 | result: function(r: string) { 140 | return Object.values(this.map(r))[0]; 141 | } 142 | } as any, 143 | 144 | angularUniversal: { 145 | type: 'select', 146 | name: 'angularUniversalDeploymentType', 147 | message: 'Please select the deployment type of angular universal.', 148 | choices: [ 149 | { 150 | name: 'Prerendering', 151 | value: ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE.PRERENDERING 152 | }, 153 | { 154 | name: 'Server Side Rendering', 155 | value: ANGULAR_UNIVERSAL_DEPLOYMENT_TYPE.SERVER_SIDE_RENDERING 156 | } 157 | ], 158 | result: function(r: string) { 159 | return Object.values(this.map(r))[0]; 160 | } 161 | } 162 | }; 163 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils/versions.ts: -------------------------------------------------------------------------------- 1 | export const pulumiVersion = '^1.1.4'; 2 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/src/utils/workspace.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { 3 | ProjectDefinition, 4 | WorkspaceDefinition, 5 | } from '@angular-devkit/core/src/workspace'; 6 | import { BaseAdapter } from '../adapter/base.adapter'; 7 | import { getApplicationType, ApplicationType } from './application-type'; 8 | import { NxDeployItInitSchematicSchema } from '../schematics/init/schema'; 9 | import { WebappAdapter } from '../adapter/webapp/webapp.adapter'; 10 | import { NestJSAdapter } from '../adapter/nestjs/nestjs.adapter'; 11 | import { ExpressAdapter } from '../adapter/express/express.adapter'; 12 | import { Tree } from '@angular-devkit/schematics'; 13 | import { AngularUniversalAdapter } from '../adapter/angular-universal/angular-universal.adapter'; 14 | import { BuilderContext } from '@angular-devkit/architect'; 15 | import { readWorkspaceConfig } from '@nrwl/workspace'; 16 | 17 | export function getRealWorkspacePath() { 18 | // TODO!: find a better way 19 | return process.cwd(); 20 | } 21 | 22 | export function getPulumiBinaryPath() { 23 | return resolve(getRealWorkspacePath(), 'node_modules/.bin/pulumi'); 24 | } 25 | 26 | export function getAdapterByApplicationType( 27 | applicationType: ApplicationType, 28 | project: ProjectDefinition, 29 | options: NxDeployItInitSchematicSchema 30 | ): BaseAdapter { 31 | switch (applicationType) { 32 | case ApplicationType.ANGULAR: 33 | case ApplicationType.REACT: 34 | return new WebappAdapter(project, options, applicationType); 35 | case ApplicationType.NESTJS: 36 | return new NestJSAdapter(project, options, applicationType); 37 | case ApplicationType.EXPRESS: 38 | return new ExpressAdapter(project, options, applicationType); 39 | case ApplicationType.ANGULAR_UNIVERSAL: 40 | return new AngularUniversalAdapter(project, options, applicationType); 41 | default: 42 | } 43 | 44 | throw new Error( 45 | `Can't recognize application type. Supported list can be found here: https://github.com/Dev-Thought/nx-plugins/libs/nx-deploy-it` 46 | ); 47 | } 48 | 49 | export function getAdapter( 50 | project: ProjectDefinition, 51 | options: NxDeployItInitSchematicSchema, 52 | host?: Tree 53 | ): BaseAdapter { 54 | return getAdapterByApplicationType( 55 | getApplicationType(project.targets, host), 56 | project, 57 | options 58 | ); 59 | } 60 | 61 | export function getApplications( 62 | workspace: WorkspaceDefinition, 63 | host: Tree 64 | ): { projectName: string; applicationType: ApplicationType }[] { 65 | const applications: { 66 | projectName: string; 67 | applicationType: ApplicationType; 68 | }[] = []; 69 | workspace.projects.forEach((project, projectName) => { 70 | const applicationType = getApplicationType(project.targets, host); 71 | if (applicationType) { 72 | applications.push({ 73 | projectName, 74 | applicationType, 75 | }); 76 | } 77 | }); 78 | 79 | return applications; 80 | } 81 | 82 | export function getProjectConfig(context: BuilderContext) { 83 | const workspaceConfig = readWorkspaceConfig({ format: 'angularCli' }); 84 | 85 | return workspaceConfig.projects[context.target.project]; 86 | } 87 | 88 | export function getDistributionPath(context: BuilderContext) { 89 | const project = getProjectConfig(context); 90 | 91 | return resolve( 92 | context.workspaceRoot, 93 | project.architect.build.options.outputPath 94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "types": ["node", "jest"] 5 | }, 6 | "include": ["**/*.ts"] 7 | } 8 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "rootDir": ".", 8 | "types": ["node"] 9 | }, 10 | "exclude": ["**/*.spec.ts", "src/schematics/*/files/**/*"], 11 | "include": ["**/*", "src/schematics/*/files/**/*.template"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/nx-deploy-it/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 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "dev-thought", 3 | "implicitDependencies": { 4 | "workspace.json": "*", 5 | "package.json": { 6 | "dependencies": "*", 7 | "devDependencies": "*" 8 | }, 9 | "tsconfig.json": "*", 10 | "tslint.json": "*", 11 | "nx.json": "*" 12 | }, 13 | "projects": { 14 | "nx-deploy-it": { 15 | "tags": [] 16 | }, 17 | "nx-deploy-it-e2e": { 18 | "tags": [], 19 | "implicitDependencies": ["nx-deploy-it"] 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-plugins", 3 | "version": "0.0.0", 4 | "repository": "https://github.com/dev-thought/nx-plugins", 5 | "bugs": { 6 | "url": "https://github.com/dev-thought/nx-plugins/issues", 7 | "email": "mitko@dev-thought.cool" 8 | }, 9 | "author": "Mitko Tschimev ", 10 | "license": "MIT", 11 | "scripts": { 12 | "nx": "nx", 13 | "start": "nx serve", 14 | "build": "nx build", 15 | "test": "nx test", 16 | "lint": "nx workspace-lint && nx lint", 17 | "e2e": "nx e2e", 18 | "affected:apps": "nx affected:apps", 19 | "affected:libs": "nx affected:libs", 20 | "affected:build": "nx affected:build", 21 | "affected:e2e": "nx affected:e2e", 22 | "affected:test": "nx affected:test", 23 | "affected:lint": "nx affected:lint", 24 | "affected:dep-graph": "nx affected:dep-graph", 25 | "affected": "nx affected", 26 | "format": "nx format:write", 27 | "format:write": "nx format:write", 28 | "format:check": "nx format:check", 29 | "update": "nx migrate latest", 30 | "workspace-schematic": "nx workspace-schematic", 31 | "dep-graph": "nx dep-graph", 32 | "help": "nx help" 33 | }, 34 | "private": true, 35 | "dependencies": { 36 | "@vercel/ncc": "^0.25.1", 37 | "enquirer": "^2.3.6", 38 | "fs-extra": "^8.1.0" 39 | }, 40 | "devDependencies": { 41 | "@nrwl/angular": "11.2.12", 42 | "@nrwl/devkit": "^11.2.12", 43 | "@nrwl/eslint-plugin-nx": "11.2.12", 44 | "@nrwl/jest": "11.2.12", 45 | "@nrwl/nest": "11.2.12", 46 | "@nrwl/nx-plugin": "11.2.12", 47 | "@nrwl/workspace": "11.2.12", 48 | "@types/jest": "26.0.8", 49 | "@types/node": "12.12.38", 50 | "@typescript-eslint/eslint-plugin": "4.3.0", 51 | "@typescript-eslint/parser": "4.3.0", 52 | "dotenv": "6.2.0", 53 | "eslint": "7.10.0", 54 | "eslint-config-prettier": "6.0.0", 55 | "jest": "26.2.2", 56 | "mock-stdin": "^1.0.0", 57 | "prettier": "2.1.2", 58 | "ts-jest": "26.4.0", 59 | "ts-node": "9.1.1", 60 | "tslint": "6.1.3", 61 | "typescript": "4.0.5", 62 | "yarn": "^1.22.10" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tools/schematics/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Dev-Thought/nx-plugins/4f34f6cba6fd7d7330b68407f528bff83ebfd06e/tools/schematics/.gitkeep -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.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.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": "es2015", 12 | "module": "esnext", 13 | "typeRoots": ["node_modules/@types"], 14 | "lib": ["es2017", "dom"], 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@dev-thought/nx-deploy-it": ["libs/nx-deploy-it/src/index.ts"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "projects": { 4 | "nx-deploy-it": { 5 | "root": "libs/nx-deploy-it", 6 | "sourceRoot": "libs/nx-deploy-it/src", 7 | "projectType": "library", 8 | "schematics": {}, 9 | "architect": { 10 | "lint": { 11 | "builder": "@nrwl/linter:lint", 12 | "options": { 13 | "linter": "eslint", 14 | "config": "libs/nx-deploy-it/.eslintrc", 15 | "tsConfig": [ 16 | "libs/nx-deploy-it/tsconfig.lib.json", 17 | "libs/nx-deploy-it/tsconfig.spec.json" 18 | ], 19 | "exclude": ["**/node_modules/**", "!libs/nx-deploy-it/**"] 20 | } 21 | }, 22 | "test": { 23 | "builder": "@nrwl/jest:jest", 24 | "options": { 25 | "jestConfig": "libs/nx-deploy-it/jest.config.js", 26 | "tsConfig": "libs/nx-deploy-it/tsconfig.spec.json", 27 | "passWithNoTests": true 28 | } 29 | }, 30 | "build": { 31 | "builder": "@nrwl/node:package", 32 | "options": { 33 | "outputPath": "dist/libs/nx-deploy-it", 34 | "tsConfig": "libs/nx-deploy-it/tsconfig.lib.json", 35 | "packageJson": "libs/nx-deploy-it/package.json", 36 | "main": "libs/nx-deploy-it/src/index.ts", 37 | "assets": [ 38 | "libs/nx-deploy-it/*.md", 39 | { 40 | "input": "./libs/nx-deploy-it/src", 41 | "glob": "**/*.!(ts)", 42 | "output": "./src" 43 | }, 44 | { 45 | "input": "./libs/nx-deploy-it", 46 | "glob": "collection.json", 47 | "output": "." 48 | }, 49 | { 50 | "input": "./libs/nx-deploy-it", 51 | "glob": "builders.json", 52 | "output": "." 53 | } 54 | ] 55 | } 56 | } 57 | } 58 | }, 59 | "nx-deploy-it-e2e": { 60 | "projectType": "application", 61 | "root": "apps/nx-deploy-it-e2e", 62 | "sourceRoot": "apps/nx-deploy-it-e2e/src", 63 | "architect": { 64 | "e2e": { 65 | "builder": "@nrwl/nx-plugin:e2e", 66 | "options": { 67 | "target": "nx-deploy-it:build", 68 | "npmPackageName": "@dev-thought/nx-deploy-it", 69 | "pluginOutputPath": "dist/libs/nx-deploy-it", 70 | "jestConfig": "apps/nx-deploy-it-e2e/jest.config.js", 71 | "tsSpecConfig": "apps/nx-deploy-it-e2e/tsconfig.spec.json" 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "cli": { 78 | "defaultCollection": "@nrwl/workspace" 79 | }, 80 | "schematics": { 81 | "@nrwl/workspace": { 82 | "library": { 83 | "linter": "eslint" 84 | } 85 | }, 86 | "@nrwl/cypress": { 87 | "cypress-project": { 88 | "linter": "eslint" 89 | } 90 | }, 91 | "@nrwl/react": { 92 | "application": { 93 | "linter": "eslint" 94 | }, 95 | "library": { 96 | "linter": "eslint" 97 | } 98 | }, 99 | "@nrwl/next": { 100 | "application": { 101 | "linter": "eslint" 102 | } 103 | }, 104 | "@nrwl/web": { 105 | "application": { 106 | "linter": "eslint" 107 | } 108 | }, 109 | "@nrwl/node": { 110 | "application": { 111 | "linter": "eslint" 112 | }, 113 | "library": { 114 | "linter": "eslint" 115 | } 116 | }, 117 | "@nrwl/nx-plugin": { 118 | "plugin": { 119 | "linter": "eslint" 120 | } 121 | }, 122 | "@nrwl/nest": { 123 | "application": { 124 | "linter": "eslint" 125 | } 126 | }, 127 | "@nrwl/express": { 128 | "application": { 129 | "linter": "eslint" 130 | }, 131 | "library": { 132 | "linter": "eslint" 133 | } 134 | } 135 | } 136 | } 137 | --------------------------------------------------------------------------------