├── .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 | [](https://www.npmjs.com/package/@dev-thought/nx-deploy-it)
4 | [](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 | 
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 |
--------------------------------------------------------------------------------