├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js └── tsconfig.json ├── .vscode └── extensions.json ├── README.md ├── apps ├── .gitkeep └── nx-workshop-e2e │ ├── jest.config.js │ ├── project.json │ ├── tests │ └── nx-workshop.spec.ts │ ├── tsconfig.json │ └── tsconfig.spec.json ├── docs ├── assets │ ├── game-demo.gif │ ├── lab10_directory-structure.png │ ├── lab1_directory-structure.png │ ├── lab2_cmds.png │ ├── lab2_file_structure.png │ ├── lab2_result.png │ ├── lab3_build_cmds.png │ ├── lab3_directory-structure.png │ ├── lab3_screenshot.png │ ├── lab4_directory-structure.png │ ├── lab4_screenshot.png │ ├── lab5_directory-structure.png │ ├── lab5_screenshot.png │ ├── lab6_directory-structure.png │ ├── lab6_screenshot.png │ ├── lab7_directory-structure.png │ ├── lab8_screenshot.png │ ├── lab9_directory-structure.png │ ├── nxtitle.png │ └── storybook.gif ├── lab1 │ ├── LAB.md │ └── SOLUTION.md ├── lab10 - bonus │ ├── LAB.md │ └── SOLUTION.md ├── lab11 - bonus │ └── LAB.md ├── lab12 │ ├── LAB.md │ └── SOLUTION.md ├── lab13 │ ├── LAB.md │ └── SOLUTION.md ├── lab14 │ ├── LAB.md │ └── SOLUTION.md ├── lab15 │ ├── LAB.md │ ├── SOLUTION.md │ ├── github_actions.png │ ├── no_affected.png │ └── store_affected.png ├── lab16 │ ├── LAB.md │ ├── distrib_caching_confirmation.png │ ├── nx_cloud_enabled.png │ └── run_details.png ├── lab17 │ ├── LAB.md │ ├── cache_hit_miss.png │ ├── nx-cloud-projects.png │ └── nx_cloud_bot.png ├── lab18 │ ├── LAB.md │ └── SOLUTION.md ├── lab19-alt │ ├── LAB.md │ ├── SOLUTION.md │ └── solution-structure.png ├── lab19 │ ├── LAB.md │ └── SOLUTION.md ├── lab2 │ ├── LAB.md │ └── SOLUTION.md ├── lab20-alt │ ├── LAB.md │ └── lab20_result.png ├── lab20 │ └── LAB.md ├── lab21-alt │ ├── LAB.md │ ├── SOLUTION.md │ └── github_secrets.png ├── lab21 │ ├── LAB.md │ ├── SOLUTION.md │ └── github_secrets.png ├── lab22 │ ├── LAB.md │ └── SOLUTION.md ├── lab3.1 │ └── LAB.md ├── lab3 │ ├── LAB.md │ └── SOLUTION.md ├── lab4 │ ├── LAB.md │ └── SOLUTION.md ├── lab5 │ ├── LAB.md │ └── SOLUTION.md ├── lab6 │ ├── LAB.md │ └── SOLUTION.md ├── lab7 │ ├── LAB.md │ └── SOLUTION.md ├── lab8 │ ├── LAB.md │ └── SOLUTION.md └── lab9 │ ├── LAB.md │ └── SOLUTION.md ├── examples ├── lab2 │ └── apps │ │ └── store │ │ └── src │ │ ├── app │ │ ├── app.component.css │ │ ├── app.component.html │ │ └── app.component.ts │ │ ├── assets │ │ ├── beans.png │ │ ├── cat.png │ │ └── chess.png │ │ └── fake-api │ │ └── index.ts ├── lab4 │ └── libs │ │ └── store │ │ └── ui-shared │ │ └── src │ │ └── lib │ │ └── header │ │ ├── header.component.html │ │ └── header.component.ts ├── lab5 │ └── libs │ │ └── store │ │ └── util-formatters │ │ └── src │ │ └── lib │ │ └── store-util-formatters.ts ├── lab6 │ └── libs │ │ └── store │ │ └── feature-game-detail │ │ └── src │ │ └── lib │ │ └── game-detail │ │ ├── game-detail.component.css │ │ ├── game-detail.component.html │ │ └── game-detail.component.ts ├── lab7 │ └── apps │ │ └── api │ │ └── src │ │ └── app │ │ ├── app.controller.ts │ │ └── app.service.ts ├── lab8 │ ├── apps │ │ └── store │ │ │ └── src │ │ │ └── app │ │ │ └── app.component.ts │ └── libs │ │ └── store │ │ └── feature-game-detail │ │ └── src │ │ └── lib │ │ ├── game-detail │ │ ├── game-detail.component.css │ │ ├── game-detail.component.html │ │ └── game-detail.component.ts │ │ └── store-feature-game-detail.module.ts └── lab9 │ └── libs │ └── api │ └── util-interface │ └── src │ └── lib │ └── api-util-interface.ts ├── jest.config.ts ├── jest.preset.js ├── libs ├── .gitkeep └── nx-workshop │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── executors.json │ ├── generators.json │ ├── jest.config.ts │ ├── migrations.json │ ├── package.json │ ├── project.json │ ├── src │ ├── executors │ │ └── build │ │ │ ├── executor.spec.ts │ │ │ ├── executor.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ ├── generators │ │ ├── complete-labs │ │ │ ├── generator.spec.ts │ │ │ ├── generator.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ │ └── nx-workshop │ │ │ ├── files │ │ │ └── src │ │ │ │ └── index.ts__template__ │ │ │ ├── generator.spec.ts │ │ │ ├── generator.ts │ │ │ ├── schema.d.ts │ │ │ └── schema.json │ ├── index.ts │ └── migrations │ │ ├── complete-lab-1 │ │ └── complete-lab-1.ts │ │ ├── complete-lab-10 │ │ └── complete-lab-10.ts │ │ ├── complete-lab-11 │ │ └── complete-lab-11.ts │ │ ├── complete-lab-12 │ │ └── complete-lab-12.ts │ │ ├── complete-lab-13 │ │ ├── complete-lab-13a.ts │ │ ├── complete-lab-13b.ts │ │ └── complete-lab-13c.ts │ │ ├── complete-lab-14 │ │ └── complete-lab-14.ts │ │ ├── complete-lab-15 │ │ └── complete-lab-15.ts │ │ ├── complete-lab-16 │ │ └── complete-lab-16.ts │ │ ├── complete-lab-17 │ │ └── complete-lab-17.ts │ │ ├── complete-lab-18 │ │ └── complete-lab-18.ts │ │ ├── complete-lab-19-alt │ │ └── complete-lab-19-alt.ts │ │ ├── complete-lab-19 │ │ └── complete-lab-19.ts │ │ ├── complete-lab-2 │ │ └── complete-lab-2.ts │ │ ├── complete-lab-20-alt │ │ └── complete-lab-20-alt.ts │ │ ├── complete-lab-20 │ │ └── complete-lab-20.ts │ │ ├── complete-lab-21-alt │ │ └── complete-lab-21-alt.ts │ │ ├── complete-lab-21 │ │ └── complete-lab-21.ts │ │ ├── complete-lab-22 │ │ └── complete-lab-22.ts │ │ ├── complete-lab-3 │ │ └── complete-lab-3.ts │ │ ├── complete-lab-4 │ │ └── complete-lab-4.ts │ │ ├── complete-lab-5 │ │ └── complete-lab-5.ts │ │ ├── complete-lab-6 │ │ └── complete-lab-6.ts │ │ ├── complete-lab-7 │ │ └── complete-lab-7.ts │ │ ├── complete-lab-8 │ │ └── complete-lab-8.ts │ │ ├── complete-lab-9 │ │ └── complete-lab-9.ts │ │ └── utils.ts │ ├── tsconfig.json │ ├── tsconfig.lib.json │ └── tsconfig.spec.json ├── migrations.json ├── nx.json ├── package.json ├── tools ├── EMAIL_TEMPLATE.md ├── cherry-pick-all.sh ├── rebase-all-master.sh └── tsconfig.tools.json ├── tsconfig.base.json └── yarn.lock /.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 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": ["**/*"], 4 | "plugins": ["@nx"], 5 | "overrides": [ 6 | { 7 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 8 | "rules": { 9 | "@nx/enforce-module-boundaries": [ 10 | "error", 11 | { 12 | "enforceBuildableLibDependency": true, 13 | "allow": [], 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "*", 17 | "onlyDependOnLibsWithTags": ["*"] 18 | } 19 | ] 20 | } 21 | ] 22 | } 23 | }, 24 | { 25 | "files": ["*.ts", "*.tsx"], 26 | "extends": ["plugin:@nx/typescript"], 27 | "rules": {} 28 | }, 29 | { 30 | "files": ["*.js", "*.jsx"], 31 | "extends": ["plugin:@nx/javascript"], 32 | "rules": {} 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /.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 | .env 29 | .npmrc 30 | .local.env 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | .angular 45 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // uncomment the property below if you want to apply some webpack config globally 3 | // webpackFinal: async (config, { configType }) => { 4 | // // Make whatever fine-grained changes you need that should apply to all storybook configs 5 | // // Return the altered config 6 | // return config; 7 | // }, 8 | }; 9 | -------------------------------------------------------------------------------- /.storybook/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "exclude": [ 4 | "../**/*.spec.js", 5 | "../**/*.test.js", 6 | "../**/*.spec.ts", 7 | "../**/*.test.ts", 8 | "../**/*.spec.tsx", 9 | "../**/*.test.tsx", 10 | "../**/*.spec.jsx", 11 | "../**/*.test.jsx" 12 | ], 13 | "include": ["../**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "esbenp.prettier-vscode", 4 | "dbaeumer.vscode-eslint", 5 | "firsttris.vscode-jest-runner" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /apps/nx-workshop-e2e/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | displayName: 'nx-workshop-e2e', 3 | preset: '../../jest.preset.js', 4 | globals: {}, 5 | transform: { 6 | '^.+\\.[tj]s$': [ 7 | 'ts-jest', 8 | { 9 | tsconfig: '/tsconfig.spec.json', 10 | }, 11 | ], 12 | }, 13 | moduleFileExtensions: ['ts', 'js', 'html'], 14 | coverageDirectory: '../../coverage/apps/nx-workshop-e2e', 15 | }; 16 | -------------------------------------------------------------------------------- /apps/nx-workshop-e2e/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-workshop-e2e", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "projectType": "application", 5 | "sourceRoot": "apps/nx-workshop-e2e/src", 6 | "targets": { 7 | "build-migrations": { 8 | "executor": "nx:run-commands", 9 | "options": { 10 | "parallel": false, 11 | "commands": [ 12 | "npx nx build nx-workshop", 13 | "cp -R dist/libs/nx-workshop/* tmp/nx-e2e/proj/node_modules/@nrwl/nx-workshop" 14 | ] 15 | } 16 | }, 17 | "e2e": { 18 | "executor": "@nx/jest:jest", 19 | "options": { 20 | "jestConfig": "apps/nx-workshop-e2e/jest.config.js", 21 | "runInBand": true 22 | }, 23 | "dependsOn": ["nx-workshop:build"] 24 | } 25 | }, 26 | "tags": [], 27 | "implicitDependencies": ["nx-workshop"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/nx-workshop-e2e/tests/nx-workshop.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | checkFilesExist, 3 | ensureNxProject, 4 | runNxCommand, 5 | } from '@nx/plugin/testing'; 6 | describe('nx-workshop e2e', () => { 7 | it('should run the migrations', () => { 8 | ensureNxProject('@nrwl/nx-workshop', 'dist/libs/nx-workshop'); 9 | expect(() => checkFilesExist(`libs`)).not.toThrow(); 10 | expect(() => checkFilesExist(`node_modules/.bin/nx`)).not.toThrow(); 11 | }, 120000); 12 | function completeLab(labNumber) { 13 | console.log(`Completing lab ${labNumber}`); 14 | process.env.NX_DAEMON = 'false'; 15 | runNxCommand( 16 | `generate @nrwl/nx-workshop:complete-labs --lab=${labNumber}` 17 | ); 18 | runNxCommand('migrate --run-migrations'); 19 | runNxCommand('run-many --target=e2e --parallel=false'); 20 | } 21 | for (let i=1;i<21;i++) { 22 | describe(`lab ${i}`, () => { 23 | it(`should complete`, () => { 24 | completeLab(i); 25 | }); 26 | }); 27 | } 28 | // describe('lab 1', () => { 29 | // it(`should complete`, () => { 30 | // completeLab(1); 31 | // }); 32 | // }); 33 | // describe('lab 2', () => { 34 | // it(`should complete`, () => { 35 | // completeLab(2); 36 | // }); 37 | // }); 38 | // describe('lab 3', () => { 39 | // it(`should complete`, () => { 40 | // completeLab(3); 41 | // }); 42 | // }); 43 | // describe('lab 4', () => { 44 | // it(`should complete`, () => { 45 | // completeLab(4); 46 | // }); 47 | // }); 48 | // describe('lab 5', () => { 49 | // it(`should complete`, () => { 50 | // completeLab(5); 51 | // }); 52 | // }); 53 | // describe('lab 6', () => { 54 | // it(`should complete`, () => { 55 | // completeLab(6); 56 | // }); 57 | // }); 58 | // describe('lab 7', () => { 59 | // it(`should complete`, () => { 60 | // completeLab(7); 61 | // }); 62 | // }); 63 | // describe('lab 8', () => { 64 | // it(`should complete`, () => { 65 | // completeLab(8); 66 | // }); 67 | // }); 68 | // describe('lab 9', () => { 69 | // it(`should complete`, () => { 70 | // completeLab(9); 71 | // }); 72 | // }); 73 | // describe('lab 10', () => { 74 | // it(`should complete`, () => { 75 | // completeLab(10); 76 | // }); 77 | // }); 78 | }); 79 | -------------------------------------------------------------------------------- /apps/nx-workshop-e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.spec.json" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /apps/nx-workshop-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": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /docs/assets/game-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/game-demo.gif -------------------------------------------------------------------------------- /docs/assets/lab10_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab10_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab1_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab1_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab2_cmds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab2_cmds.png -------------------------------------------------------------------------------- /docs/assets/lab2_file_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab2_file_structure.png -------------------------------------------------------------------------------- /docs/assets/lab2_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab2_result.png -------------------------------------------------------------------------------- /docs/assets/lab3_build_cmds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab3_build_cmds.png -------------------------------------------------------------------------------- /docs/assets/lab3_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab3_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab3_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab3_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab4_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab4_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab4_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab4_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab5_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab5_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab5_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab5_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab6_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab6_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab6_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab6_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab7_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab7_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab8_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab8_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab9_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/lab9_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/nxtitle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/nxtitle.png -------------------------------------------------------------------------------- /docs/assets/storybook.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/assets/storybook.gif -------------------------------------------------------------------------------- /docs/lab1/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 1 - generate an empty workspace 2 | 3 | ###### ⏰ Estimated time: 5-10 minutes 4 |
5 | 6 | ## 📚 Learning outcomes: 7 | 8 | - **Understand how to bootstrap a new Nx workspace** 9 |


10 | 11 | ## 📲 After this workshop, your app should look similar to this: 12 | 13 |
14 | File structure 15 | lab7 file structure 16 |
17 |
18 | 19 | ## 🏋️‍♀️ Steps : 20 | 21 | 1. **Generate an empty Nx workspace** for a fictional company called "The Board Game Hoard" 22 |

23 | 2. **The workspace name should be `bg-hoard`.** Make sure you select `integrated` style, the `apps` preset and `No to NxCloud` when asked 24 |

25 | 26 | --- 27 | 28 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 29 | 30 | --- 31 | 32 | [➡️ Next lab ➡️](../lab2/LAB.md) 33 | -------------------------------------------------------------------------------- /docs/lab1/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### To create a new Nx workspace: 2 | 3 | ```shell 4 | npx create-nx-workspace bg-hoard --preset=apps --no-nx-cloud 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab10 - bonus/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 10 - Generate Storybook stories for the shared ui component 2 | 3 | ###### ⏰ Estimated time: 10-15 minutes 4 | 5 |
6 | 7 | Let's explore some more Nx plugins by generating and running a storybook configuration for our shared store header. 8 |

9 | 10 | ## 📚 Learning outcomes: 11 | 12 | - **Explore other Nx plugins to create a storybook configuration** 13 |


14 | 15 | ## 📲 After this workshop, you should have: 16 | 17 |
18 | App Screenshot 19 | No change in how the app looks! 20 |
21 | 22 |
23 | File structure 24 | lab10 file structure 25 |
26 |
27 | 28 | ## 🏋️‍♀️ Steps: 29 | 30 | 1. **Install `@nx/storybook`** 31 |

32 | 2. Use the `@nx/angular:storybook-configuration` generator to **generate a storybook configuration** for the `store-ui-shared` project 33 | 34 | ⚠️ Answer **YES** to all questions 35 |

36 | 37 | 3. Inside `libs/store/ui-shared/src/lib/header/header.component.stories.ts`: 38 | 39 | - **Import the `MatToolbarModule`** 40 | 41 |
42 | 🐳 Hint 43 | 44 | ```ts 45 | //IMPORT TOOLBAR MODULE 46 | import { MatToolbarModule } from '@angular/material/toolbar'; 47 | 48 | //...... 49 | export default { 50 | title: 'HeaderComponent', 51 | component: HeaderComponent, 52 | decorators: [ 53 | moduleMetadata({ 54 | imports: [MatToolbarModule], // <-- import the module 55 | }), 56 | //... 57 | ], 58 | } as Meta; 59 | ``` 60 | 61 |
62 |
63 | 64 | 4. Inside `libs/store/ui-shared/project.json`: 65 | 66 | - **Add the Material stylesheet to the `build-storybook` target** 67 | 68 |
69 | 🐳 Hint 70 | 71 | ```json 72 | "build-storybook": { 73 | "executor": "@storybook/angular:build-storybook", 74 | "outputs": ["{options.outputDir}"], 75 | "options": { 76 | "outputDir": "dist/storybook/store-ui-shared", 77 | "configDir": "libs/store/ui-shared/.storybook", 78 | "browserTarget": "store:build", 79 | "compodoc": false, 80 | "styles": [ 81 | "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css" 82 | ] 83 | } 84 | } 85 | ``` 86 | 87 |
88 |
89 | 90 | 5. **Serve storybook!** 91 | 92 |
93 | 🐳 Hint 94 | 95 | `nx storybook store-ui-shared` 96 | 97 |
98 |
99 | 100 | 6. Start typing in different titles and **see how they appear** in the header 101 | 102 | the header component running in storybook 103 |

104 | 105 | 7. **Inspect what changed** from the last time you committed, then **commit your changes** 106 |

107 | 108 | --- 109 | 110 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 111 | 112 | --- 113 | 114 | [➡️ Next lab ➡️](../lab11%20-%20bonus/LAB.md) 115 | -------------------------------------------------------------------------------- /docs/lab10 - bonus/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Lab 11 - generate Storybook stories for the shared component 2 | 3 | ```shell 4 | nx generate @nx/angular:storybook-configuration store-ui-shared 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab11 - bonus/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 11 - e2e test the shared component 2 | 3 | ###### ⏰ Estimated time: 5 minutes 4 |
5 | 6 | The storybook generator we invoked earlier also generated some e2e tests. Let's try them out! 7 |

8 | 9 | ## 📚 Learning outcomes: 10 | 11 | - **Take advantage of the e2e tests Nx generated earlier to test your app** 12 |


13 | 14 | ## 🏋️‍♀️ Steps: 15 | 16 | 1. Our previous command generated a new `apps/store-ui-shared-e2e` folder. Let's **run them**: `nx e2e store-ui-shared-e2e` 17 | - The tests should pass! 18 |

19 | 2. Open `apps/store-ui-shared-e2e/src/e2e/header/header.component.cy.ts` and **give the title a value**: 20 | 21 | ```ts 22 | cy.visit( 23 | '/iframe.html?id=headercomponent--primary&args=title:BoardGameHoard;' 24 | ) 25 | ``` 26 |

27 | 28 | 3. Now **add a test** to check if it contains that value 29 | 30 | ```ts 31 | it('should show the title', () => { 32 | cy.get('bg-hoard-header').contains('BoardGameHoard'); 33 | }); 34 | ``` 35 |

36 | 37 | 4. **Re-run the tests** 38 |

39 | 5. **Inspect what changed** from the last time you committed, then **commit your changes** 40 |

41 | 42 | --- 43 | 44 | [➡️ Next lab ➡️](../lab12/LAB.md) 45 | -------------------------------------------------------------------------------- /docs/lab12/LAB.md: -------------------------------------------------------------------------------- 1 | # 💡 Lab 12 - Module boundaries 2 | 3 | ###### ⏰ Estimated time: 10-15 minutes 4 | 5 |

6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Understand how to assign scopes and type tags to your libraries** 10 | - **How to specify boundaries around your tags and avoid circular dependencies in your repo** 11 | - **How to use linting to trigger warnings or errors when you are not respecting these boundaries** 12 |


13 | 14 | ## 🏋️‍♀️ Steps : 15 | 16 | 1. Open the `project.json` files for each project and **finish tagging the apps** accordingly: 17 | 18 | ``` 19 | // apps/store/project.json 20 | { 21 | "projectType": "application", 22 | "root": "apps/store", 23 | "sourceRoot": "apps/store/src", 24 | "prefix": "bg-hoard", 25 | "targets": { ... }, 26 | "tags": ["scope:store", "type:app"] 27 | } 28 | ``` 29 | 30 |

31 | 32 | 2. Open the root `.eslintrc.json`, find the `"@nx/enforce-module-boundaries"` rule and **set the `depConstraints`**: 33 | 34 | ``` 35 | "depConstraints": [ 36 | { 37 | "sourceTag": "scope:store", 38 | "onlyDependOnLibsWithTags": ["scope:store", "scope:shared"] 39 | }, 40 | .... <-- finish adding constraints for the tags we defined in the previous step 41 | ] 42 | ``` 43 | 44 |

45 | 46 | 3. **Run `nx run-many --target=lint --all --parallel`** 47 | 48 | 💡 `nx run-many` allows you run a specific target against a specific set of projects 49 | via the `--projects=[..]` option. However, you can also pass it the `--all` option 50 | to run that target against all projects in your workspace. 51 | 52 | 💡 `--parallel` launches all the `lint` processes in parallel 53 |

54 | 55 | 4. We talked about how importing a **Feature** lib should not be allowed from a 56 | **UI** lib. Let's **test our lint rules** by doing just that: - In `libs/store/ui-shared/src/lib/store-ui-shared.module.ts` - Try to `import { StoreFeatureGameDetailModule } from '@bg-hoard/store/feature-game-detail';` 57 |

58 | 59 | 5. **Run linting** against all the projects again. 60 |

61 | 6. You should see the expected error. Great! You can now **delete the import** above. 62 |

63 | 7. We also talked about the importance of setting boundaries between your workspace scopes. 64 | Let's try and **import a `store` lib** from an `api` scope. - In `apps/api/src/app/app.service.ts` - Try to `import { formatRating } from '@bg-hoard/store/util-formatters';` 65 |

66 | 67 | 8. **Run linting** on all projects - you should see another expected error. 68 |

69 | 9. You can now **delete the import** above. 70 |

71 | 10. **Run linting** again and check if all the errors went away. 72 |

73 | 11. **Commit everything** before moving on to the next lab 74 | 75 | --- 76 | 77 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 78 | 79 | --- 80 | 81 | [➡️ Next lab ➡️](../lab13/LAB.md) 82 | -------------------------------------------------------------------------------- /docs/lab12/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### The complete tags in your project.json files: 2 | 3 | - store: `"tags": ["scope:store", "type:app"]` 4 | - store-e2e: `"tags": ["scope:store", "type:e2e"]` 5 | - store-ui-shared: `"tags": ["scope:store", "type:ui"]` 6 | - store-util-formatters: `"tags": ["scope:store", "type:util"]` 7 | - store-feature-game-detail: `"tags": ["scope:store", "type:feature"]` 8 | - api: `"tags": ["scope:api", "type:app"]` 9 | - util-interface: `"tags": ["scope:shared", "type:util"]` 10 | - store-ui-shared-e2e: `"tags": ["scope:store", "type:e2e"]` 11 | 12 | ##### nx-enforce-module-boundaries rules: 13 | 14 | ```json 15 | "depConstraints": [ 16 | { 17 | "sourceTag": "scope:store", 18 | "onlyDependOnLibsWithTags": ["scope:store", "scope:shared"] 19 | }, 20 | { 21 | "sourceTag": "scope:api", 22 | "onlyDependOnLibsWithTags": ["scope:api", "scope:shared"] 23 | }, 24 | { 25 | "sourceTag": "type:feature", 26 | "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"] 27 | }, 28 | { 29 | "sourceTag": "type:ui", 30 | "onlyDependOnLibsWithTags": ["type:ui", "type:util"] 31 | }, 32 | { 33 | "sourceTag": "type:util", 34 | "onlyDependOnLibsWithTags": ["type:util"] 35 | } 36 | ] 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/lab13/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a `internal-plugin` plugin: 2 | 3 | ```shell script 4 | nx generate @nx/plugin:plugin internal-plugin --minimal 5 | ``` 6 | 7 | #### Generate a `util-lib` generator: 8 | 9 | ```shell 10 | nx generate @nx/plugin:generator util-lib --project=internal-plugin 11 | ``` 12 | 13 | ##### Running the generator in dry mode 14 | 15 | ```shell 16 | nx generate @bg-hoard/internal-plugin:util-lib test --dry-run 17 | ``` 18 | 19 | ##### Prefixing the name 20 | 21 | ```ts 22 | import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'; 23 | import { libraryGenerator } from '@nx/js'; 24 | import { UtilLibGeneratorSchema } from './schema'; 25 | 26 | export default async function (tree: Tree, schema: UtilLibGeneratorSchema) { 27 | await libraryGenerator(tree, { 28 | name: `util-${schema.name}`, 29 | }); 30 | await formatFiles(tree); 31 | return () => { 32 | installPackagesTask(tree); 33 | }; 34 | } 35 | ``` 36 | 37 | ##### Adding an enum to a generator that prompts when empty 38 | 39 | ```json 40 | { 41 | "directory": { 42 | "type": "string", 43 | "description": "The scope of your lib.", 44 | "x-prompt": { 45 | "message": "Which directory do you want the lib to be in?", 46 | "type": "list", 47 | "items": [ 48 | { 49 | "value": "store", 50 | "label": "store" 51 | }, 52 | { 53 | "value": "api", 54 | "label": "api" 55 | }, 56 | { 57 | "value": "shared", 58 | "label": "shared" 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | ``` 65 | 66 | ##### Passing in tags 67 | 68 | ```ts 69 | import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'; 70 | import { libraryGenerator } from '@nx/js'; 71 | import { UtilLibGeneratorSchema } from './schema'; 72 | 73 | export default async function (tree: Tree, schema: UtilLibGeneratorSchema) { 74 | await libraryGenerator(tree, { 75 | name: `util-${schema.name}`, 76 | directory: schema.directory, 77 | tags: `type:util, scope:${schema.directory}`, 78 | }); 79 | await formatFiles(tree); 80 | return () => { 81 | installPackagesTask(tree); 82 | }; 83 | } 84 | ``` 85 | 86 | ##### Typed Schema 87 | 88 | ```typescript 89 | export interface UtilLibGeneratorSchema { 90 | name: string; 91 | directory: 'store' | 'api' | 'shared'; 92 | } 93 | ``` 94 | 95 | ##### BONUS: Testing 96 | 97 | ```typescript 98 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 99 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 100 | 101 | import generator from './generator'; 102 | import { UtilLibGeneratorSchema } from './schema'; 103 | 104 | describe('util-lib generator', () => { 105 | let appTree: Tree; 106 | const options: UtilLibGeneratorSchema = { name: 'foo', directory: 'store' }; 107 | 108 | beforeEach(() => { 109 | appTree = createTreeWithEmptyWorkspace(); 110 | }); 111 | 112 | it('should add util to the name and add appropriate tags', async () => { 113 | await generator(appTree, options); 114 | const config = readProjectConfiguration(appTree, 'store-util-foo'); 115 | expect(config).toBeDefined(); 116 | expect(config.tags).toEqual(['type:util', 'scope:store']); 117 | }); 118 | }); 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/lab14/LAB.md: -------------------------------------------------------------------------------- 1 | # 🧵 Lab 14 - Workspace Plugins and Generators - Modifying files 2 | 3 | ###### ⏰ Estimated time: 25-35 minutes 4 | 5 |
6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Explore some more advanced, real-world usages of generators** 10 | - **Understand how to modify existing source code with generators** 11 |


12 | 13 | ## 🏋️‍♀️ Steps : 14 | 15 | 1. Generate another generator called `update-scope-schema` 16 |

17 | 18 | 2. As a start let's make it change the `defaultProject` from `store` to `api` in our `nx.json` file: 19 | 20 |
21 | 🐳 Hint 22 | 23 | - Refer to the [docs](https://nx.dev/devkit/index#updatejson) 24 | - Use this utility: 25 | - `import { updateJson } from '@nx/devkit';` 26 | - As always, the answer is in the [the solution](SOLUTION.md). Try a few different approaches on your own first. 27 |
28 | 29 | ⚠️ When you run the above, it might complain that you haven't supplied a `name`. Since 30 | we don't need this property in the generate, you can remove it from the schema. 31 |

32 | 33 | 3. Now that we had some practice with the `updateJson` util - Let's build something even more useful: 34 | 35 | - When large teams work in the same workspace, they will occasionally be adding new projects and hence, **new scope tags** 36 | - We want to make sure that scope tags specified in our `util-lib` generator are up to date and take into account all these new scopes that teams have been adding 37 | - We want to check if there is a new scope tag in any of our `project.json` files and update our generator schema 38 | - We can use the [`getProjects`](https://nx.dev/devkit/index#getprojects) util to read all the projects at once. 39 | 40 | ⚠️ You can use the function provided in the Hint to extract the `scopes` 41 | 42 |
43 | 🐳 Hint 44 | 45 | ```typescript 46 | function getScopes(projectMap: Map) { 47 | const projects: any[] = Array.from(projectMap.values()); 48 | const allScopes: string[] = projects 49 | .map((project) => 50 | project.tags 51 | // take only those that point to scope 52 | .filter((tag: string) => tag.startsWith('scope:')) 53 | ) 54 | // flatten the array 55 | .reduce((acc, tags) => [...acc, ...tags], []) 56 | // remove prefix `scope:` 57 | .map((scope: string) => scope.slice(6)); 58 | // remove duplicates 59 | return Array.from(new Set(allScopes)); 60 | } 61 | ``` 62 | 63 |
64 | 65 |
66 | 67 | 4. It's good practice to have your generator run your modified files through Prettier after modifying them. You might already have this, but just in case you removed it: 68 | 69 | - Use `import { formatFiles } from '@nx/devkit';` 70 | - `await` this at the end of your generator 71 |

72 | 73 | 5. The `util-lib` generator also has a `schema.d.ts` with a Typescript interface that should be updated. For modifying files that are not JSON we can use `host.read(path)` and `host.write(path, content)` methods. 74 | 75 | ⚠️ You can use the function provided in the Hint to replace the `scopes` 76 | 77 |
78 | 🐳 Hint 79 | 80 | ```typescript 81 | function updateSchemaInterface(tree: Tree, scopes: string[]) { 82 | const joinScopes = scopes.map((s) => `'${s}'`).join(' | '); 83 | const interfaceDefinitionFilePath = 84 | 'libs/internal-plugin/src/generators/util-lib/schema.d.ts'; 85 | const newContent = `export interface UtilLibGeneratorSchema { 86 | name: string; 87 | directory: ${joinScopes}; 88 | }`; 89 | tree.write(interfaceDefinitionFilePath, newContent); 90 | } 91 | ``` 92 | 93 |
94 |
95 | 96 | 6. So we can test our changes, create a new app and define a scope for it. 97 | 98 |
99 | 🐳 Hint 100 | 101 | ```shell 102 | nx g app videos --tags=scope:videos 103 | ``` 104 | 105 |
106 |
107 | 108 | 7. Run your generator and notice the resulting changes. Commit them so you start fresh on your next lab. 109 |

110 | 111 | 8. **BONUS** - As a bonus, if project doesn't have `scope` tag defined, we will assume it's the first segment of the name (e.g `admin-ui-lib` -> `scope:admin`) and we will go ahead and set one for it. Now run the generator again and see what changed. 112 |

113 | 114 | 9. **BONUS BONUS** - use a tool like [Husky](https://typicode.github.io/husky/#/) to run your 115 | generator automatically before each commit. This will ensure developers never forget to add 116 | their scope files. 117 |

118 | 119 | 10. **BONUS BONUS BONUS** - create a test to automate verification of this generator in `libs/internal-plugin/src/generators/update-scope-schema/generator.spec.ts`. \*\*This will be particularly difficult, as you'll need to create a project with the actual source code of your `util-lib` generator as part of the setup for this test. (Check [the solution](SOLUTION.md) if you get stuck!) 120 | 121 | --- 122 | 123 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 124 | 125 | --- 126 | 127 | [➡️ Next lab ➡️](../lab15/LAB.md) 128 | -------------------------------------------------------------------------------- /docs/lab15/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Final CI config 2 | 3 | ```yml 4 | name: Run CI checks 5 | 6 | on: [pull_request] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | name: Building affected apps 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 0 16 | - uses: bahmutov/npm-install@v1 17 | - run: npx nx affected --target=build --base=origin/main --parallel 18 | - run: npx nx affected --target=test --base=origin/main --parallel 19 | - run: npx nx affected --target=lint --base=origin/main --parallel 20 | - run: npx nx affected --target=e2e --base=origin/main --parallel 21 | ``` 22 | 23 | ##### Marking all projects as affected 24 | 25 | ```json 26 | "implicitDependencies": { 27 | ".github/workflows/ci.yml": "*", 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/lab15/github_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab15/github_actions.png -------------------------------------------------------------------------------- /docs/lab15/no_affected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab15/no_affected.png -------------------------------------------------------------------------------- /docs/lab15/store_affected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab15/store_affected.png -------------------------------------------------------------------------------- /docs/lab16/LAB.md: -------------------------------------------------------------------------------- 1 | # 🔌 Lab 16 - Distributed caching 2 | 3 | ###### ⏰ Estimated time: 10 minutes 4 |
5 | 6 | ## 📚 Learning outcomes: 7 | 8 | - **Understand the difference between local and distributed caching** 9 | - **Learn how to add NxCloud and enable distributed on an existing Nx workspace** 10 |


11 | 12 | ## 🏋️‍♀️ Steps : 13 | 14 | 1. Earlier in the workshop, we discussed about Nx's [local caching](https://nx.dev/concepts/how-caching-works) 15 | capabilities. Let's enable distributed caching. 16 | 17 | ``` 18 | npx nx g @nrwl/nx-cloud:init 19 | ``` 20 | 21 | ![Nx Cloud Confirmation](./nx_cloud_enabled.png) 22 |

23 | 24 | 2. Inspect the changes added in `nx.json` - especially the access token. We'll explain that more in a bit! 25 |

26 | 3. **Very important:** Make sure, at this stage, you commit and push your changes: 27 | 28 | ``` 29 | # make sure you're on main 30 | git checkout main 31 | git add . && git commit -m "add nx cloud" 32 | git push origin main 33 | ``` 34 |
35 | 36 | 4. Run a build: `nx run-many --target=build --all` 37 | 38 | 🕑 Watch the process in the terminal - it might take a few seconds... 39 |

40 | 41 | 5. You'll see a link at the end, let's see what's there: 42 | 43 | ![Run Details Link](./run_details.png) 44 | 45 | We'll talk more about these links later! 46 |

47 | 48 | 6. Try to build all projects again: `nx run-many --target=build --all` 49 | 50 | ⚡ It should finish **much quicker** this time - because it just pulled from the local cache! 51 |

52 | 53 | 7. Let's try something different now - in a different folder on your machine, let's try and do a **fresh** copy of your repository: 54 | 55 | ``` 56 | # go into a new folder 57 | cd .. 58 | # clone your repo again 59 | git clone git@github.com:/.git test-distributed-caching 60 | cd test-distributed-caching 61 | # install dependencies 62 | npm ci 63 | ``` 64 |
65 | 66 | 8. In your new instance, let's try and build again: `nx run-many --target=build --all` 67 | 68 | ⚡ It should be almost instant... 69 |

70 | 71 | 9. **But how?** You have no local cache: we just did a fresh pull of the repository. 72 | 73 | Check your terminal output - you should see this message: 74 | 75 | ![NxCloud cache pull](./distrib_caching_confirmation.png) 76 | 77 | That means that instead of rebuilding locally again, we just pulled from the distributed cache. 78 |

79 | 80 | 10. Let's try a different command - in the same folder you are in, try to run: 81 | 82 | ``` 83 | nx run-many --target=lint --all 84 | ``` 85 | 86 | 🕑 It should start the linting work, and take a few seconds... 87 |

88 | 89 | 10. Now let's go back to our main workshop repository and run: 90 | 91 | ``` 92 | nx run-many --target=lint --all 93 | ``` 94 | 95 | ⚡ It should pull again from the NxCloud cache...This is even works across laptops! CI will use it as well! 96 |

97 | 98 | --- 99 | 100 | [➡️ Next lab ➡️](../lab17/LAB.md) 101 | -------------------------------------------------------------------------------- /docs/lab16/distrib_caching_confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab16/distrib_caching_confirmation.png -------------------------------------------------------------------------------- /docs/lab16/nx_cloud_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab16/nx_cloud_enabled.png -------------------------------------------------------------------------------- /docs/lab16/run_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab16/run_details.png -------------------------------------------------------------------------------- /docs/lab17/LAB.md: -------------------------------------------------------------------------------- 1 | # 🔍 Lab 17 - NxCloud GitHub bot 2 | 3 | ###### ⏰ Estimated time: 10 minutes 4 |
5 | 6 | ## 📚 Learning outcomes: 7 | 8 | - **Explore the NxCloud Run-Detail pages** 9 | - **Configure the NxCloud bot to get easy to read reports on the Nx checks performed during CI** 10 |


11 | 12 | ## 🏋️‍♀️ Steps : 13 | 14 | 1. **Enable the NxCloud GitHub bot** on your GitHub repository: [https://github.com/apps/nx-cloud](https://github.com/apps/nx-cloud) 15 |

16 | 2. Switch to a new branch: `git checkout -b nxcloud-bot` 17 |

18 | 19 | 3. Make a change (add a `console.log`) in the store: `apps/store/src/app/app.component.ts` (so that it will trigger our affected commands in CI): 20 | 21 | ``` 22 | export class AppComponent { 23 | constructor(private http: HttpClient) { 24 | console.log("component constructed") <-- add console log 25 | } 26 | ``` 27 | 28 |

29 | 30 | 4. Commit everything and push your branch 31 |

32 | 5. Make a PR on GitHub 33 |

34 | 6. Once the checks finish you should see something similar to this: 35 | 36 | ![NxCloud Bot](./nx_cloud_bot.png) 37 |
38 | 39 | If your install is failing with `@storybook/angular` add following to the root `package.json`: 40 | 41 | ``` 42 | "overrides": { 43 | "@storybook/angular": { 44 | "zone.js": "0.12.0" 45 | } 46 | } 47 | ``` 48 | 49 | 7. Click on one of the "failed" commands (if any). On the "Run Details" page, click on one of the projects and inspect the terminal output: 50 | 51 | ![Nx Cloud project](./nx-cloud-projects.png) 52 | 53 | 🔥 Rather than reading through CI logs, you can use this view to filter to the failed projects and inspect the failure reason scoped to that project. 54 | 55 |

56 | 57 | 8. Have a look at the "Cache Hit" and "Cache Miss" filters. What do you think they do? 58 | 59 | ![Cache hit/miss](./cache_hit_miss.png) 60 |
61 | 62 | 9. Finally, you should see a "Claim workspace" button at the top - it's a good idea to do that at this stage. We'll explain more about that in a bit! 63 |

64 | 65 | 10. Merge your PR into `main` and pull latest locally: 66 | 67 | ``` 68 | git checkout main 69 | git pull 70 | ``` 71 |
72 | 73 | 11. **BONUS**: [Have a look at some of the docs](https://nx.dev/nx-cloud/set-up/set-up-dte#ci/cd-examples) for setting up NxCloud on CI to see how the set-up might apply to your CI provider. 74 | 12. **BONUS**: Read [this blog post](https://nx.dev/concepts/dte) on "Distributed Task Execution". We'll briefly talk about this after the lab. 75 | 76 | --- 77 | 78 | [➡️ Next lab ➡️](../lab18/LAB.md) 79 | -------------------------------------------------------------------------------- /docs/lab17/cache_hit_miss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab17/cache_hit_miss.png -------------------------------------------------------------------------------- /docs/lab17/nx-cloud-projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab17/nx-cloud-projects.png -------------------------------------------------------------------------------- /docs/lab17/nx_cloud_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab17/nx_cloud_bot.png -------------------------------------------------------------------------------- /docs/lab18/LAB.md: -------------------------------------------------------------------------------- 1 | # 📎 Lab 18 - Run-Commands and deploying the frontend 2 | 3 | ###### ⏰ Estimated time: 15-20 minutes 4 | 5 |
6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Understand how to create custom targets via the "run-commands" workspace executor** 10 | - **Explore real-world usages of "run-commands" by creating a frontend "deploy" executor** 11 | - **Learn how to expose custom ENV variables to Nx** 12 |


13 | 14 | ## 🏋️‍♀️ Steps : 15 | 16 | 1. Make sure you are on the `main` branch 17 |

18 | 2. We'll use a CLI tool called [Surge](https://surge.sh/) to statically deploy the front-end: 19 | 20 | ```bash 21 | npm i -S surge 22 | ``` 23 | 24 |
25 | 26 | 3. Get the surge token (you'll need to create an account with an email and password): 27 | 28 | ``` 29 | npx surge token 30 | ``` 31 | 32 | ☝️ Copy the token you get 33 |

34 | 35 | 4. Let's use the Surge CLI to deploy our project: 36 | 37 | ```bash 38 | # make sure the project is built first - and we have something in dist 39 | nx build store 40 | # use surge to deploy whatever assets are in dist/apps/store 41 | npx surge dist/apps/store https://.surge.sh --token 42 | ``` 43 | 44 | ⚠️ Make sure you chose a **unique value** for your domain above, otherwise 45 | it will fail as you won't have permission to deploy to an existing one. 46 | 47 | ⚠️ You should see surge deploying to your URL - if you click you'll see just the header though, because it doesn't have a server yet to get the games from (the Network Dev Tools tab will have failing requests). 48 |

49 | 50 | 5. Let's now abstract away the above command into an Nx target. Generate a new **"deploy"** target using the `@nx/workspace:run-commands` generator: 51 | 52 | - under the `store` project 53 | - the "command" will be the same as the one you typed in the previous step 54 | 55 |
56 | 🐳 Hint 57 | 58 | Consult the run-commands generator docs [here](https://nx.dev/packages/workspace/generators/run-commands) 59 |
60 |
61 | 62 | 6. Use Git to inspect the changes in `project.json` and try to deploy the store using Nx! 63 |

64 | 7. We're now storing the surge token in `project.json`. We don't want to check-in this file and risk exposing this secret token. Also, we might want to deploy to different domains depending on the environment. Let's move these out: 65 | 66 | 📁 Create a new file `apps/store/.local.env` 67 | 68 | 🔒 And let's add your secrets to it 69 | 70 | ``` 71 | SURGE_TOKEN= 72 | SURGE_DOMAIN=https://.surge.sh 73 | ``` 74 | 75 | ✅ Finally, update your "deploy" command, so that it loads the values from the ENV, using the `${ENV_VAR}` syntax. 76 | 77 |
78 | 🐳 Hint 79 | 80 | ```bash 81 | surge dist/apps/store ${SURGE_DOMAIN} --token ${SURGE_TOKEN} 82 | ``` 83 |
84 |
85 | 86 | 8. Now invoke the deploy target again, and check if it all still works. 87 | 88 | ⚠️ Note for Windows users: the command might fail, as we're trying to access env variables the Linux-way. 89 | To make it pass, you can generate a separate `windows-deploy` executor (make sure you keep the existing `deploy` target intact - it will be used by GitHub Actions): 90 | 91 | ```bash 92 | nx generate run-commands windows-deploy --project=store --command="surge dist/apps/store %SURGE_DOMAIN% --token %SURGE_TOKEN%" 93 | nx windows-deploy store 94 | ``` 95 | 96 |
97 | 98 | 9. **BONUS** - Notice how you can keep running the deploy command and it will always fully run it, it will not pull from the cache. We'll talk about this after the lab. 99 | 100 | 10. We currently have to remember to always run the build before deploying an app. [Can you fix this](https://nx.dev/reference/project-configuration#dependson), so that it always makes sure the app is built before we deploy? 101 | 102 | --- 103 | 104 | ❓ We did not load those environment variables into the deploy process anywhere. 105 | We just added a `.local.env` file. How does that work? 106 | 107 | Nx [automatically picks up](https://nx.dev/recipe/define-environment-variables#setting-environment-variables) any `.env` or `.local.env` files in your workspace, 108 | and loads them into processes invoked for that specific app. 109 | 110 | --- 111 | 112 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 113 | 114 | --- 115 | 116 | [➡️ For the next lab, head over to our chapter list to chose your path ➡️](https://github.com/nrwl/nx-workshop#day-2) 117 | -------------------------------------------------------------------------------- /docs/lab18/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a new run-commands config: 2 | 3 | ```shell 4 | nx generate run-commands deploy --project=store --command="surge dist/apps/store https://.surge.sh --token " 5 | ``` 6 | 7 | ##### Deploy the store via Nx 8 | 9 | ```shell 10 | nx deploy store 11 | ``` 12 | 13 | ##### Building the store for production 14 | 15 | ```bash 16 | nx build store --configuration production 17 | ``` 18 | 19 | ##### The full deploy executor configuration 20 | 21 | ```json 22 | "deploy": { 23 | "executor": "@nx/workspace:run-commands", 24 | "outputs": [], 25 | "dependsOn": ["build"], 26 | "options": { 27 | "command": "surge dist/apps/store ${SURGE_DOMAIN_STORE} --token ${SURGE_TOKEN}" 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/lab19-alt/LAB.md: -------------------------------------------------------------------------------- 1 | # 🧲 Lab 19 Alternative - Creating and deploying a 2nd frontend 2 | 3 | ###### ⏰ Estimated time: 15-20 minutes 4 | 5 |
6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Recap what you've learned about generating apps and creating custom executors with "run-commands"** 10 |


11 | 12 | ## 🏋️‍♀️ Steps : 13 | 14 | In this lab, we'll practice generating a 2nd frontend, using React. This is in preparation for the next few labs, where we'll be deploying the two frontends independently in our GitHub Actions based Continous Deployment setup. 15 | 16 | 1. We want to build a new Admin UI for our store. But we'll use React as our framework of choice. 17 | **Generate a new React app called "admin-ui"** 18 | You can use any configuration options you want. 19 | 20 | ⚠️ There will be fewer hints in this lab, but you can always use the [solution](SOLUTION.md) as a last resort. 21 |

22 | 23 | 2. We won't make any changes to it. Let's serve it to see if it looks okay locally. 24 |

25 | 26 | 3. **Now let's build it for production**. Since we added a lot of files, also commit our changes. 27 |

28 | 29 | 4. Following the same steps as [Lab 18](../lab18/LAB.md), add a `"deploy"` target to it. 30 | 31 | ⚠️ **BONUS POINTS:** Create a custom workspace generator that adds a `"deploy"` target for a frontend project, so that we don't have to manually re-do the steps in [Lab 18](../lab18/LAB.md) each time. 32 | 33 | ⚠️ Hint: You can have a `.local.env` at the root of your workspace as well, for any variables that need to be shared. 34 | You can move your `SURGE_TOKEN` variable to the root, so it can be shared among your projects. [READ MORE](https://nx.dev/recipe/define-environment-variables#setting-environment-variables) 35 |

36 | 37 | 5. Try to deploy both apps and check if they still work. 38 | 39 | ⚠️ **BONUS:** Use a single NX command to tell it to deploy all projects in parallel. 40 |

41 | 42 | 6. **BONUS** - Add proper scopes for your new app in your `project.json` files and run your `update-scope-schema` workspace generator you created a few labs ago. 43 |
44 | 45 | 7. Commit everything before moving on to the next lab 46 |

47 | 48 | --- 49 | 50 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 51 | 52 | --- 53 | 54 | [➡️ Next lab ➡️](../lab20-alt/LAB.md) 55 | -------------------------------------------------------------------------------- /docs/lab19-alt/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a React app 2 | 3 | ```shell 4 | npm i -S @nrwl/react 5 | nx g @nrwl/react:app admin-ui 6 | nx serve admin-ui 7 | ``` 8 | 9 | ##### Adding a deploy config 10 | 11 | ```shell 12 | nx generate run-commands deploy --project=admin-ui --command="surge dist/apps/admin-ui \${SURGE_DOMAIN_ADMIN_UI} --token \${SURGE_TOKEN}" 13 | ``` 14 | 15 | ##### Bonus 16 | 17 | ```shell 18 | nx g workspace-generator add-deploy-target 19 | ``` 20 | 21 | ![Folder structure](./solution-structure.png) 22 | 23 | `./files/.local.env`: 24 | 25 | ``` 26 | SURGE_DOMAIN_<%= undercaps(project) %>=https://<%= subdomain %>.surge.sh 27 | ``` 28 | 29 | `index.ts`: 30 | 31 | ```typescript 32 | import { 33 | Tree, 34 | formatFiles, 35 | installPackagesTask, 36 | generateFiles, 37 | } from '@nx/devkit'; 38 | import { runCommandsGenerator } from '@nx/workspace/generators'; 39 | import { join } from 'path'; 40 | 41 | interface Schema { 42 | project: string; 43 | subdomain: string; 44 | } 45 | 46 | export default async function (host: Tree, schema: Schema) { 47 | await runCommandsGenerator(host, { 48 | name: 'deploy', 49 | project: schema.project, 50 | command: `surge dist/apps/${ 51 | schema.project 52 | } \${SURGE_DOMAIN_${underscoreWithCaps( 53 | schema.project 54 | )}} --token \${SURGE_TOKEN}`, 55 | }); 56 | await generateFiles( 57 | host, 58 | join(__dirname, './files'), 59 | `apps/${schema.project}`, 60 | { 61 | tmpl: '', 62 | project: schema.project, 63 | subdomain: schema.subdomain, 64 | undercaps: underscoreWithCaps, 65 | } 66 | ); 67 | await formatFiles(host); 68 | return () => { 69 | installPackagesTask(host); 70 | }; 71 | } 72 | 73 | export function underscoreWithCaps(value: string): string { 74 | return value.replace(/-/g, '_').toLocaleUpperCase(); 75 | } 76 | ``` 77 | 78 | `schema.json`: 79 | 80 | ```json 81 | { 82 | "cli": "nx", 83 | "id": "add-deploy-target", 84 | "type": "object", 85 | "properties": { 86 | "project": { 87 | "type": "string", 88 | "description": "Project name to generate the deploy target for", 89 | "$default": { 90 | "$source": "argv", 91 | "index": 0 92 | } 93 | }, 94 | "subdomain": { 95 | "type": "string", 96 | "description": "Surge subdomain where you want it deployed.", 97 | "x-prompt": "What is the Surge subdomain you want it deployed under? (https://.surge.sh)" 98 | } 99 | }, 100 | "required": ["project", "subdomain"] 101 | } 102 | ``` 103 | 104 | ##### Deploy all projects in parallel 105 | 106 | ```shell 107 | nx run-many --all --target=deploy --parallel 108 | ``` 109 | -------------------------------------------------------------------------------- /docs/lab19-alt/solution-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab19-alt/solution-structure.png -------------------------------------------------------------------------------- /docs/lab19/SOLUTION.md: -------------------------------------------------------------------------------- 1 | #### Generating an executor 2 | 3 | ```bash 4 | npx nx g @nx/plugin:executor --name=fly-deploy --project=internal-plugin 5 | ``` 6 | 7 | ##### BONUS: Executor Test 8 | 9 | ```typescript 10 | import { FlyDeployExecutorSchema } from './schema'; 11 | import executor from './executor'; 12 | jest.mock('child_process', () => ({ 13 | execSync: jest.fn(), 14 | })); 15 | import { execSync } from 'child_process'; 16 | 17 | describe('FlyDeploy Executor', () => { 18 | beforeEach(() => { 19 | (execSync as any) = jest.fn(); 20 | }); 21 | 22 | it('runs the correct fly cli commands', async () => { 23 | const options: FlyDeployExecutorSchema = { 24 | distLocation: 'dist/apps/foo', 25 | flyAppName: 'foo', 26 | }; 27 | const output = await executor(options); 28 | expect(output.success).toBe(true); 29 | expect(execSync).toHaveBeenCalledWith(`fly apps list`, { 30 | cwd: 'dist/apps/foo', 31 | }); 32 | expect(execSync).toHaveBeenCalledWith( 33 | `fly launch --now --name=foo --region=lax`, 34 | { 35 | cwd: 'dist/apps/foo', 36 | } 37 | ); 38 | }); 39 | }); 40 | ``` 41 | -------------------------------------------------------------------------------- /docs/lab2/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 2 - Generate an Angular app 2 | 3 | ###### ⏰ Estimated time: 15-20 minutes 4 | 5 |
6 | 7 | In this lab we'll generate our first Angular application within the new monorepo. 8 |

9 | 10 | ## 📚 Learning outcomes: 11 | 12 | - **Get familiar with generating new apps within your workspace using the Nx CLI** 13 |


14 | 15 | ## 📲 After this workshop, your app should look similar to this: 16 | 17 |
18 | App Screenshot 19 | screenshot of lab2 result 20 |
21 | 22 |
23 | File structure 24 | lab2 file structure 25 |
26 |
27 | 28 | ## 🏋️‍♀️ Steps: 29 | 30 | 1. Make sure you can run Nx commands: 31 | - try out `nx --version` and see if it outputs a version number 32 | - install the CLI globally: `npm i -g nx` 33 | - if you don't want to install it globally, use `npx nx` instead of `nx` in all the commands below 34 | 35 | > Please make sure you are using the latest version of Nx (14.5+) 36 | 37 |
38 | 39 | 2. **Run `nx list`** to see which plugins you have installed and which are available 40 |

41 | 42 | 3. **Add the Angular plugin: `@nx/angular`** 43 |

44 | 45 | 4. Let's also **add Angular Material** so we can use some of their components: `@angular/material @angular/cdk` 46 |

47 | 5. **Use the [`@nx/angular` plugin](https://nx.dev/packages/angular/generators/application) to generate an Angular app** called `store` in your new workspace 48 | 49 | ⚠️**Important:** Make sure you **enable routing** when asked! 50 | 51 |
52 | 🐳 Hint 53 | Nx generate cmd structure 54 |
55 |
56 | 57 | 6. **Create a new `index.ts` file in the folder `apps/store/src/fake-api`** in your app, it returns an array of some games (you can copy the code from [here](../../examples/lab2/apps/store/src/fake-api/index.ts)) 58 | 59 | ⏳**Reminder:** When you are given example files to copy, the folder they're in hints to the _folder_ and _filename_ you can place them in when you do the copying. 60 |

61 | 62 | 7. **Add some basic styling to your new component** and display the games from the Fake API (to not spend too much time on this, you can copy it from here [.html](../../examples/lab2/apps/store/src/app/app.component.html) / [.css](../../examples/lab2/apps/store/src/app/app.component.css) / [.ts](../../examples/lab2/apps/store/src/app/app.component.ts) - and replace the full contents of the files) 63 | 64 | - You can get the example game images from [here](../../examples/lab2/apps/store/src/assets) 65 |
⚠️ Make sure you put them in the correct folder 66 |

67 | 68 | 8. **Add the Material Card Module to `app.module.ts`**: 69 | 70 | ```ts 71 | import { MatCardModule } from '@angular/material/card'; 72 | ``` 73 | 74 | Don't forget to add it to the imports section as well. 75 |

76 | 77 | 9. **Serve the app**: `nx serve store` 78 |

79 | 80 | 10. **See your app** live at [http://localhost:4200/](http://localhost:4200/) 81 | 82 | ⚠️ In a company context you might need to define a hostname `nx serve store --host=my.host.name ` 83 |

84 | 85 | 12. **Inspect what changed** from the last time you committed, then **commit your changes** 86 |

87 | 88 | --- 89 | 90 | Now we're starting to see some content! But there are some styles missing: the Angular Material theme! We'll look at how to add it in the next workshop! 91 | 92 | The ratings also don't look that good - we'll fix those in **Lab 5**. 93 | 94 | --- 95 | 96 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 97 | 98 | --- 99 | 100 | [➡️ Next lab ➡️](../lab3/LAB.md) 101 | -------------------------------------------------------------------------------- /docs/lab2/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### To generate a new Angular application: 2 | 3 | `nx generate @nx/angular:application store` (or `nx g app store`) 4 | -------------------------------------------------------------------------------- /docs/lab20-alt/LAB.md: -------------------------------------------------------------------------------- 1 | # 🧲 Lab 20 Alternative - Mock Store 2 | 3 | ###### ⏰ Estimated time: 5 minutes 4 |
5 | 6 | ## 🏋️‍♀️ Steps : 7 | 8 | For now, our `store` project has no API when it is deployed. Hence, it is only displaying the header. 9 | 10 | 1. If you removed your `fake-api/index.ts` from the `store`, let's [re-add it](https://github.com/nrwl/nx-workshop/blob/master/examples/lab2/apps/store/src/fake-api/index.ts) 11 |

12 | 13 | 2. Import it in your `apps/store/src/app/app.component.ts` 14 | 15 |
16 | 🐳 Hint 17 | 18 | ```typescript 19 | import { getAllGames } from '../fake-api/index'; 20 | //.... 21 | games = getAllGames(); 22 | ``` 23 | 24 | ```html 25 | 26 |
27 |
28 | 33 |
34 | 35 | 3. Build and deploy your `store` project. Your deployed version should now be showing some games. 36 | 37 | ⚠️ Clicking on games and displaying game details will still not work. We can fix that later. 38 | 39 | screenshot of lab20 result 40 |
41 | 42 | --- 43 | 44 | [➡️ Next lab ➡️](../lab21-alt/LAB.md) 45 | -------------------------------------------------------------------------------- /docs/lab20-alt/lab20_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab20-alt/lab20_result.png -------------------------------------------------------------------------------- /docs/lab20/LAB.md: -------------------------------------------------------------------------------- 1 | # 🎸 Lab 20 - Connecting the frontend and backend 2 | 3 | ###### ⏰ Estimated time: 5 minutes 4 | 5 |
6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Configure the Angular app for production** 10 |


11 | 12 | ## 🏋️‍♀️ Steps: 13 | 14 | When we serve the Store and API locally, they work great, because of the configured 15 | proxy discussed in previous labs. The Store will think the API lives at the same address. 16 | 17 | When deployed separately however, they do not yet know about each other. Let's configure 18 | a production URL for the API. 19 | 20 | 1. In `apps/store/src/environments/environment.prod.ts` change it to: 21 | 22 | ```ts 23 | export const environment = { 24 | production: true, 25 | apiUrl: 'https://.fly.dev', 26 | }; 27 | ``` 28 | 29 |
30 | 31 | 2. In `apps/store/src/environments/environment.ts`: 32 | 33 | ```ts 34 | export const environment = { 35 | production: false, 36 | apiUrl: '', 37 | }; 38 | ``` 39 | 40 |
41 | 42 | 3. In `apps/store/project.json`: 43 | 44 | ```jsonc 45 | { 46 | //... 47 | "targets": { 48 | //... 49 | "build": { 50 | //... 51 | "configurations": { 52 | "production": { 53 | //... 54 | // Add this property: 55 | "fileReplacements": [ 56 | { 57 | "replace": "apps/store/src/environments/environment.ts", 58 | "with": "apps/store/src/environments/environment.prod.ts" 59 | } 60 | ] 61 | } 62 | } 63 | } 64 | } 65 | } 66 | ``` 67 | 68 |
69 | 70 | 4. In `apps/store/src/app/app.module.ts`: 71 | - `import { environment } from '../environments/environment';` 72 | - Add a new provider: 73 | `ts providers: [{ provide: 'baseUrl', useValue: environment.apiUrl }], ` 74 |

75 | 5. In `apps/store/src/app/app.component.ts`, inject your new token: 76 | 77 | ```ts 78 | constructor(private http: HttpClient, @Inject('baseUrl') private baseUrl: string) {} 79 | ``` 80 | 81 | Then use it: 82 | 83 | ```ts 84 | games = this.http.get(`${this.baseUrl}/api/games`); 85 | ``` 86 | 87 |
88 | 89 | 6. In `libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts` 90 | 91 | - Inject it in the constructor: `@Inject('baseUrl') private baseUrl: string` 92 | - Use it: 93 | ```typescript 94 | this.http.get(`${this.baseUrl}/api/games/${id}`); 95 | ``` 96 |
97 | 98 | 7. Build the Store for production and trigger a deployment 99 |

100 | 101 | 8. Go to your Surge deployment URL - you should now see the full app with all the games. 102 |

103 | 104 | --- 105 | 106 | [➡️ Next lab ➡️](../lab21/LAB.md) 107 | -------------------------------------------------------------------------------- /docs/lab21-alt/LAB.md: -------------------------------------------------------------------------------- 1 | # 🎈 Lab 21 - Setting up CD for automatic deployment 2 | 3 | ###### ⏰ Estimated time: 10-20 minutes 4 |
5 | 6 | ## 📚 Learning outcomes: 7 | 8 | - **Understand how to configure a simple Continuous Deployment system using Nx and GitHub actions** 9 | - **Learn how to expose custom secrets on GitHub to your CD processes** 10 |


11 | 12 | ## 🏋️‍♀️ Steps : 13 | 14 | In this lab we'll be setting up GitHub actions to build and deploy our projects whenever changes go into the `main` branch. 15 | 16 | 1. Add a `.github/workflows/deploy.yml` file 17 |

18 | 2. Using your `ci.yml` config as an example, see if you can configure automated deployments from the `main` branch: 19 | 20 | Anytime we push or merge something to the `main` branch it should: 21 | - build the `store` and `admin-ui` for production 22 | - deploy the `store` and `admin-ui` 23 | 24 | We'll start you off: 25 | 26 | ```yml 27 | name: Deploy Website 28 | 29 | on: 30 | push: 31 | branches: 32 | - main <-- workflow will run everytime we push or merge something to main 33 | jobs: 34 | build: 35 | runs-on: ubuntu-latest 36 | name: Deploying apps 37 | steps: 38 | .... <-- ADD THE STEPS HERE 39 | ``` 40 |
41 | 42 | 3. Our "deploy" targets are using some secret ENV variables though. We'll need to make these available on GitHub: 43 | 1. Go to your GitHub workshop repo 44 | 2. Click on **"Settings"** at the top 45 | 3. Then **"Secrets"** on the left menu bar 46 | 4. Add values for all the variables we've been keeping in `.local.env` files 47 | 48 | ![GitHub secrets](./github_secrets.png) 49 |

50 | 51 | 4. Then back in our `deploy.yml` file, let's expose these secrets to the processes (use `ci.yml` as an example of where to put these): 52 | 53 | ```yml 54 | env: 55 | SURGE_DOMAIN_STORE: ${{ secrets.SURGE_DOMAIN_STORE }} 56 | SURGE_DOMAIN_ADMIN_UI: ${{ secrets.SURGE_DOMAIN_ADMIN_UI }} 57 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 58 | ``` 59 |
60 | 61 | 5. Since we'll be re-deploying, we want to test if we're looking at a new version of our code: 62 | - Make a change to your AdminUI (maybe change the text in the header) 63 | - Make a change to your Store (maybe change the title in the header) 64 |

65 | 6. Commit everything locally on `main` and then push (it's important we push to the `main` branch as that's where our workflow runs) 66 |

67 | 7. You should see your new workflow start up under the "Actions" tab on your GitHub repo 68 |

69 | 8. Once it's done, navigate to your frontend Surge deployment URLs and test if you notice the new changes 70 |

71 | 72 | --- 73 | 74 | 🎓 If you get stuck, check out [the solution](SOLUTION.md) 75 | 76 | --- 77 | 78 | [➡️ Next lab ➡️](../lab22/LAB.md) 79 | -------------------------------------------------------------------------------- /docs/lab21-alt/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### GitHub CD setup 2 | 3 | ``` 4 | name: Deploy Website 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | env: 12 | SURGE_DOMAIN_STORE: ${{ secrets.SURGE_DOMAIN_STORE }} 13 | SURGE_DOMAIN_ADMIN_UI: ${{ secrets.SURGE_DOMAIN_ADMIN_UI }} 14 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | name: Deploying apps 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - uses: bahmutov/npm-install@v1 25 | - run: npx nx build store --configuration=production 26 | - run: npx nx build admin-ui --configuration=production 27 | - run: npx nx deploy store 28 | - run: npx nx deploy admin-ui 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/lab21-alt/github_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab21-alt/github_secrets.png -------------------------------------------------------------------------------- /docs/lab21/LAB.md: -------------------------------------------------------------------------------- 1 | # 🎈 Lab 21 - Setting up CD for automatic deployment 2 | 3 | ###### ⏰ Estimated time: 10-20 minutes 4 |
5 | 6 | ## 📚 Learning outcomes: 7 | 8 | - **Understand how to configure a simple Continous Deployment system using Nx and GitHub actions** 9 | - **Learn how to expose custom secrets on GitHub to your CD processes** 10 | 11 | ## 🏋️‍♀️ Steps : 12 | 13 | 1. Add a `.github/workflows/deploy.yml` file 14 |

15 | 2. Using your `ci.yml` config as an example, see if you can configure automated deployments from the `main` branch: 16 | 17 | Anytime we push or merge something to the `main` branch it: 18 | - builds the `store` and `api` for production 19 | - deploys the `store` and `api` 20 | 21 | We'll start you off: 22 | 23 | ```yml 24 | name: Deploy Website 25 | 26 | on: 27 | push: 28 | branches: 29 | - main <-- workflow will run everytime we push or merge something to `main` 30 | jobs: 31 | build: 32 | runs-on: ubuntu-latest 33 | name: Deploying apps 34 | steps: 35 | .... <-- ADD THE STEPS HERE 36 | ``` 37 |
38 | 39 | 3. Our "deploy" targets are using some secret ENV variables though. We'll need to make these available on GitHub: 40 | 1. Go to your GitHub workshop repo 41 | 2. Click on **"Settings"** at the top 42 | 3. Then **"Secrets"** on the left menu bar 43 | 4. Add values for all the variables we've been keeping in `.local.env` files 44 | 45 | ![GitHub secrets](./github_secrets.png) 46 |

47 | 48 | 4. Then back in our `deploy.yml` file, let's expose these secrets to the processes (use `ci.yml` as an example of where to put these): 49 | 50 | ```yml 51 | env: 52 | SURGE_DOMAIN: ${{ secrets.SURGE_DOMAIN }} 53 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 54 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 55 | ``` 56 |
57 | 58 | 5. Since we'll be re-deploying, we want to test if we're looking at a new version of our code: 59 | - Make a change to your API (maybe change the name of one of the games) 60 | - Make a change to your Store (maybe change the title in the header) 61 |

62 | 6. Commit everything locally on `main` and then push (it's important we push to the `main` branch as that's where our workflow runs) 63 |

64 | 7. You should see your new workflow start up under the "Actions" tab on your GitHub repo 65 |

66 | 8. Once it's done, navigate to your frontend Surge deployment URL and test if you notice the new changes (the ones you made to the Store and also to the API) 67 |

68 | 69 | --- 70 | 71 | 🎓 If you get stuck, check out [the solution](SOLUTION.md) 72 | 73 | --- 74 | 75 | [➡️ Next lab ➡️](../lab22/LAB.md) 76 | -------------------------------------------------------------------------------- /docs/lab21/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### GitHub CD setup 2 | 3 | ``` 4 | name: Deploy Website 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | env: 12 | SURGE_DOMAIN: ${{ secrets.SURGE_DOMAIN }} 13 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 14 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | name: Deploying apps 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - uses: bahmutov/npm-install@v1 25 | - run: npx nx build store --configuration=production 26 | - run: npx nx build api --configuration=production 27 | - run: npx nx deploy store 28 | - run: npx nx deploy api 29 | ``` 30 | -------------------------------------------------------------------------------- /docs/lab21/github_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/docs/lab21/github_secrets.png -------------------------------------------------------------------------------- /docs/lab22/LAB.md: -------------------------------------------------------------------------------- 1 | # 💈 Lab 22 - Deploying only what changed 2 | 3 | ###### ⏰ Estimated time: 20-25 minutes 4 | 5 |
6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Explore an advanced example of the `nx affected` by deploying only the affected apps on the `main` branch** 10 | - **Understand how to configure the `base` commit for the `nx affected` in a CD scenario** 11 |


12 | 13 | ## 🏋️‍♀️ Steps : 14 | 15 | In the previous labs, we set up automatic deployments. But every time we push to the `main`, we're always building and running the deployment scripts for ALL the apps in our workspace. As our repo grows, this is not scalable. We only want to build and deploy the apps that have changed, and need re-deploying. 16 | 17 | 1. Update your `deploy.yml` file so that it builds only the affected apps, and deploys only the affected apps 18 | 19 | ⚠️ You can compare against the previous commit for now: `--base=HEAD~1` 20 |

21 | 22 | 2. If you haven't already, ensure you run your "affected" commands in parallel 23 |

24 | 25 | 3. Commit everything 26 |

27 | 4. Now make a change just to the `store`. Maybe update the title again 28 | - Commit and push 29 | - Inspect your workflow on the GitHub actions tab - it should only be building and deploying 30 | whatever changed **in the last commit**: only the Store. 31 |

32 | 33 | --- 34 | 35 | ⛔ The problem now is that it's always comparing against the last commit: 36 | 37 | - Let's say I make some changes to the API (or AdminUI) over a few commits - and I don't push them. 38 | - Then I make one small change to the Store, commit it, and push it to the `main`. 39 | - Even though **I've pushed lots of commits with changes to both the Store and the API** (or AdminUI), because our CD Workflow is only 40 | looking at the last commit, **it will only deploy the Store.** 👎 41 | 42 |
43 | There is also the problem of potential failures 🧨 44 | 45 | Now our setup is simple: it just builds. 46 | But let's say we wanted to run the E2E tests again before deploying - just to be extra safe! 47 | In that case, if I change the API (or AdminUI) and push, the E2E tests might fail. So API (or AdminUI) will not get deployed. 48 | I then fix the E2E tests, but because the API (or AdminUI) does not depend on its E2E tests, the `nx affected` will not mark it for deployment. 49 | So even though we changed the API (or AdminUI), it did not get deployed. 50 |
51 | 52 | 💡 Solution: **last successful commit!** 53 | 54 | - If we constantly compare against **the last commit where all the affected apps got succesfully deployed** - we 55 | will never miss a deployment 56 | - In our case, "successfully deployed" means when our `deploy.yml` workflow completes without errors. That's a succesful commit! 57 | - Getting the last successful commit is different on each platform: 58 | - [Netlify has the `CACHED_COMMIT_REF`](https://docs.netlify.com/configure-builds/environment-variables/#git-metadata) 59 | - On `CircleCI`, we can use the [Nx Orb](https://circleci.com/developer/orbs/orb/nrwl/nx)'s `set-shas` command 60 | - For `GitHub Actions`, we can use the [nrwl/nx-set-shas](https://github.com/marketplace/actions/nx-set-shas) action 61 | 62 | --- 63 | 64 | 5. Right after the `npm-install` step, let's trigger the action to get the last successful commit: 65 | 66 | ```yml 67 | - uses: bahmutov/npm-install@v1 68 | - uses: nrwl/nx-set-shas@v2 69 | with: 70 | main-branch-name: 'main' # remember to set this correctly 71 | ``` 72 | 73 |
74 | 75 | 6. You can now remove the `base` parameter from the `affected` as it will be now automatically read from the environment variables. 76 |

77 | 78 | 7. Commit everything and push. Let it build. It should compare against the immediately previous commit (because your workflow ran against it, and it passed) 79 |

80 | 81 | 8. Try to go through one of the problematic scenarios described above. It should now work, and it should build both the API (or AdminUI) and the Store (instead of just the Store) 82 | 83 | > Let's say I make some changes to the API (or AdminUI) over a few commits - and I don't push them. Then I make one small change to the Store, commit it, and push to main. 84 | > Even though I've pushed lots of commits with changes to both the Store and the API (or AdminUI), because our CD Workflow is only looking at the last commit, it will only deploy the Store. 85 | >
86 | 87 | --- 88 | 89 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 90 | -------------------------------------------------------------------------------- /docs/lab22/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### The full deploy script 2 | 3 | ``` 4 | name: Deploy Website 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | 11 | env: 12 | SURGE_DOMAIN_STORE: ${{ secrets.SURGE_DOMAIN_STORE }} 13 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 14 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | name: Deploying apps 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - uses: bahmutov/npm-install@v1 25 | - uses: nrwl/nx-set-shas@v2 26 | with: 27 | main-branch-name: 'main' # remember to set this correctly 28 | - run: npx nx affected --target=build --base=${{ env.NX_BASE }} --parallel --configuration=production 29 | - run: npx nx affected --target=deploy --base=${{ env.NX_BASE }} --parallel 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/lab3.1/LAB.md: -------------------------------------------------------------------------------- 1 | # 🚂 Lab 3.1 - Migrations 2 | 3 | ###### ⏰ Estimated time: 5-15 minutes 4 | 5 |
6 | 7 | We'll learn about migration generators and use them to jump to a specific lab in the workshop. 8 | 9 |

10 | 11 | ## 📚 Learning outcomes: 12 | 13 | - **Understand the `nx migrate` command** 14 | - **Install the `@nrwl/nx-workshop` package** 15 | - **Migrate to a specific version** 16 | - **Modify the migrations.json file** 17 |


18 | 19 | ## 📲 After this workshop, you should have: 20 | 21 |
22 | App Screenshot 23 | screenshot of lab3 result 24 |
25 | 26 |
27 | File structure 28 | lab3 file structure 29 |
30 | 31 | ## 🏋️‍♀️ Steps: 32 | 33 | 1. Install an old version of the `@nrwl/nx-workshop` npm package: `nx-workshop@0.0.1` 34 | 2. Make sure you've committed all your changes to this point: `git commit -am "lab 3"` 35 | 3. Migrate to the latest version of `@nrwl/nx-workshop` 36 | 37 |
38 | 🐳 Hint 39 | 40 | `nx migrate @nrwl/nx-workshop@latest` 41 | 42 |
43 |
44 | 45 | 4. Look at the `migrations.json` file. It contains the generators to complete every lab in the workshop. We don't want to run everything, so let's delete every migration entry except for labs 1 through 3. 46 | 5. The `migrations.json` file should now only contain generators for the first 3 labs. Let's run those migrations: `nx migrate --run-migrations`. 47 | 6. There's also a generator that comes with `@nrwl/nx-workshop` to help you set up the `migrations.json` file to complete a specific lab or to complete a range of labs in option 1 or option 2. Experiment with the `complete-labs` generator so that later on you can catch up if you get stuck on a lab. `nx g @nrwl/nx-workshop:complete-labs --help` 48 | 49 | [➡️ Next lab ➡️](../lab4/LAB.md) 50 | -------------------------------------------------------------------------------- /docs/lab3/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 3 - Executors 2 | 3 | ###### ⏰ Estimated time: 5-15 minutes 4 | 5 |
6 | 7 | We'll build the app we just created, and look at what executors are and how to customize them. 8 | 9 |

10 | 11 | ## 📚 Learning outcomes: 12 | 13 | - **Understand what a `target` and `executor` are** 14 | - **Learn how to invoke executors** 15 | - **Configure executors by passing them different options** 16 | - **Understand how an executor can invoke another executor** 17 |


18 | 19 | ## 📲 After this workshop, you should have: 20 | 21 |
22 | App Screenshot 23 | screenshot of lab3 result 24 |
25 | 26 |
27 | File structure 28 | lab3 file structure 29 |
30 | 31 | ## 🏋️‍♀️ Steps: 32 | 33 | 1. **Build the app** 34 | 35 |
36 | 🐳 Hint 37 | Nx executor command structure 38 |
39 |
40 | 41 | 2. You should now have a `dist` folder - let's **open it up**! 42 | - This is your whole app's output! If we wanted we could push this now to a server and it would all work. 43 |

44 | 3. **Open up `apps/store/project.json`** and look at the object under `targets/build` 45 | - this is the **target**, and it has a **executor** option, that points to `@angular-devkit/build-angular:browser` 46 | - Remember how we copied some images into our `/assets` folder earlier? Look through the executor options and try to find how it knows to include them in the final build! 47 |

48 | 4. Notice the `defaultConfiguration` executor option is pointing to `production`. This means it applies all the prod optimisations to our outputs, as per the `production` configuration in `project.json`. **Send a flag to the executor** so that it builds for development instead. 49 | 50 |
51 | 🐳 Hint 52 | 53 | `--configuration=development` 54 | 55 |
56 |
57 | 58 | 5. **Open up the `dist` folder** again - notice how all the file names have no hashes, and the contents themselves are human readable. 59 |

60 | 6. **Modify `project.json`** and instruct the executor to import the Angular Material styles: `./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css` 61 | 62 |
63 | 🐳 Hint 64 | 65 | Add it to: `"styles": ["apps/store/src/styles.css"]` 66 |
67 | 68 | 🎓Notice how we can configure executors by either modifying their options in `project.json` (this step) or through the command line (step 4)! 69 |

70 | 71 | 7. The **serve** target (located a bit lower in `project.json`) also contains a executor, that _uses_ the output from the **build** target we just changed 72 | (see `serve --> options --> browserTarget` --> it points to the `build` target of the `store` project) 73 | - so we can just re-start `nx serve store` see the new styles you added! 74 |

75 | 8. **Inspect what changed** from the last time you committed, then **commit your changes** 76 |

77 | 78 | --- 79 | 80 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 81 | 82 | --- 83 | 84 | [➡️ Next lab ➡️](../lab3.1/LAB.md) 85 | -------------------------------------------------------------------------------- /docs/lab3/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### To build the app (defaults to "production"): 2 | 3 | `nx build store` 4 | 5 | ##### To build the app for development: 6 | 7 | `nx build store --configuration=development` 8 | -------------------------------------------------------------------------------- /docs/lab4/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 4 - Generate a component lib 2 | 3 | ###### ⏰ Estimated time: 10 minutes 4 | 5 | Let's add a header to our app! Because headers can be shared with other components, we will create a common lib that others can import as well. 6 |
7 | 8 | ## 📚 Learning outcomes: 9 | 10 | - **Get familiar with generating project specific component libraries inside a folder** 11 |


12 | 13 | ## 📲 After this workshop, you should have: 14 | 15 |
16 | App Screenshot 17 | screenshot of lab4 result 18 |
19 | 20 |
21 | File structure 22 | lab4 file structure 23 |
24 |
25 | 26 | ## 🏋️‍♀️ Steps: 27 | 28 | 1. **Stop the `nx serve`** 29 |

30 | 31 | 2. **Generate a new Angular library** called `ui-shared` in the `libs/store` folder 32 | 33 |
34 | 🐳 Hint 35 | 36 | - it's a generator! you've used it before in the second lab, but instead of an `app`, we now want to generate a `lib` 37 | - use the `--help` command to figure out how to generate it in a **directory** 38 | 39 |
40 |
41 | 42 | 3. **Generate a new Angular component**, called `header`, inside the lib you just created 43 | 44 | ⚠️ Play around with the generator options so that the generated component is automatically **exported** from the lib's module 45 | 46 |
🐳 Hint 47 | 48 | use `--help` to figure out how to specify under which **project** you want to generate the new component and how to automatically have it **exported** 49 | 50 |
51 |
52 | 53 | 4. **Import `MatToolbarModule`** in the new shared module you just created 54 | 55 | 56 |
57 | 58 | 🐳 Hint 59 | 60 | ```ts 61 | import { MatToolbarModule } from '@angular/material/toolbar'; 62 | 63 | @NgModule({ 64 | imports: [CommonModule, MatToolbarModule], 65 | //... 66 | ``` 67 | 68 |
69 |
70 | 71 | 5. **Replace the `header` component's [template](../../examples/lab4/libs/store/ui-shared/src/lib/header/header.component.html) / [class](../../examples/lab4/libs/store/ui-shared/src/lib/header/header.component.ts)** 72 |

73 | 6. **Import the `StoreUiSharedModule`** you just created in the `apps/store/src/app/app.module.ts` 74 | 75 |
76 | 🐳 Hint 77 | 78 | ```typescript 79 | import { StoreUiSharedModule } from '@bg-hoard/store/ui-shared'; 80 | ``` 81 | 82 |
83 | 84 | ⚠️ You might need to restart the TS compiler in your editor (`CTRL+SHIFT+P` in VSCode and search for `Restart Typescript`) 85 |

86 | 87 | 7. Let's use the new shared header component we created 88 | 89 | - Add your new component to `apps/store/src/app/app.component.html` 90 | 91 |
92 | 🐳 Hint 93 | 94 | ```html 95 | 96 | 97 |
98 | ``` 99 | 100 |
101 |
102 | 103 | 8. **Serve the project** and test the changes 104 |

105 | 9. **Run the command to inspect the project graph** - What do you see? (Remember to "Select all" in the top left corner) 106 |
107 | 🐳 Hint 108 | 109 | ```bash 110 | nx graph 111 | ``` 112 | 113 |
114 |
115 | 10. **Inspect what changed** from the last time you committed, then **commit your changes** 116 |

117 | 118 | --- 119 | 120 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 121 | 122 | --- 123 | 124 | [➡️ Next lab ➡️](../lab5/LAB.md) 125 | -------------------------------------------------------------------------------- /docs/lab4/SOLUTION.md: -------------------------------------------------------------------------------- 1 | #### Generate a new lib: 2 | 3 | ```bash 4 | nx generate @nx/angular:lib ui-shared --directory=store 5 | ``` 6 | 7 | #### Generate a new component in a project: 8 | 9 | ```bash 10 | nx generate @nx/angular:component header --export --project=store-ui-shared 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/lab5/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 5 - Generate a utility lib 2 | 3 | ###### ⏰ Estimated time: 5-10 minutes 4 | 5 |
6 | 7 | Let's fix the ratings! They don't look that good and they could benefit from some formatting. 8 | 9 | We will create a shared utility lib where we'll add our formatters and see how to import them in our components afterwards. 10 |

11 | 12 | ## 📚 Learning outcomes: 13 | 14 | - **Get familiar with generating project specific, framework agnostic utility libs** 15 | 16 | ## 📲 After this workshop, you should have: 17 | 18 |
19 | App Screenshot 20 | screenshot of lab5 result 21 |
22 | 23 |
24 | File structure 25 | lab5 file structure 26 |
27 |
28 | 29 | ## 🏋️‍♀️ Steps: 30 | 31 | 1. **Stop the `nx serve`** 32 |

33 | 2. **Use the `@nx/js` package to generate another lib** in the `libs/store` folder - let's call it `util-formatters`. 34 |

35 | 3. **Add the [code for the utility function](../../examples/lab5/libs/store/util-formatters/src/lib/store-util-formatters.ts)** to the new library you just created `libs/store/util-formatters/src/lib/store-util-formatters.ts` 36 |

37 | 4. **Use it in your frontend project** to format the rating for each game 38 | 39 |
40 | 🐳 Hint 41 | 42 | `app.component.ts`: 43 | 44 | ```ts 45 | import { formatRating } from '@bg-hoard/store/util-formatters'; 46 | 47 | export class AppComponent { 48 | //... 49 | formatRating = formatRating; 50 | } 51 | ``` 52 | 53 | `app.component.html`: 54 | 55 | ```html 56 | {{ formatRating(game.rating) }} 57 | ``` 58 | 59 |
60 |
61 | 62 | 5. Serve the store app - notice how the ratings are formatted. 63 |

64 | 6. Launch the project graph - notice how the app depends on two libs now. 65 |

66 | 7. Inspect what changed from the last time you committed, then commit your changes 67 |

68 | 69 | --- 70 | 71 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 72 | 73 | --- 74 | 75 | [➡️ Next lab ➡️](../lab6/LAB.md) 76 | -------------------------------------------------------------------------------- /docs/lab5/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a framework agnostic lib 2 | 3 | `nx generate @nx/js:lib util-formatters --directory=store` 4 | -------------------------------------------------------------------------------- /docs/lab6/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 6 - Generate a route lib 2 | 3 | ###### ⏰ Estimated time: 15-25 minutes 4 | 5 |
6 | 7 | We'll look at more advanced usages of the `@nx/angular` generators and generate a new route lib for our store application. We'll see how Nx takes care of most of the work, and we just have to do the wiring up! 8 |

9 | 10 | ## 📚 Learning outcomes: 11 | 12 | - **Get familiar with more advanced usages of Nx generators to create an Angular route lib** 13 |


14 | 15 | ## 📲 After this workshop, you should have: 16 | 17 |
18 | App Screenshot 19 | screenshot of lab6 result 20 |
21 | 22 |
23 | File structure 24 | lab6 file structure 25 |
26 |
27 | 28 | ## 🏋️‍♀️ Steps: 29 | 30 | 1. **Stop `nx serve`** 31 |

32 | 2. **Use the `@nx/angular:lib` generator to generate a new routing library** called `feature-game-detail` that: 33 | 34 | - lives under `libs/store` 35 | - has lazy loading 36 | - has routing enabled 37 | - its parent routing module is `apps/store/src/app/app.module.ts` 38 | 39 | ⚠️ **Use `--help`** with the above generator to figure out which options you need to use to enable **all** the above (See the solution if still unsure) 40 | ⚠️ You may need to remove the file at `apps/store/src/app/app.routing.ts` and update the `app.module.ts` file with an empty array. 41 |

42 | 43 | 3. **Generate a new Angular component** called `game-detail` under the above lib you created 44 |

45 | 4. **Change the routing path** in `apps/store/src/app/app.module.ts` to pick up the game ID from the URL 46 | 47 |
48 | 🐳 Hint 49 | 50 | ```ts 51 | { 52 | path: 'game/:id', // <---- HERE 53 | loadChildren: () => 54 | import('@bg-hoard/store/feature-game-detail').then(/* ... */) 55 | } 56 | ``` 57 | 58 |
59 |
60 | 61 | 5. Adjust your routes in `libs/store/feature-game-detail/src/lib/lib.routes.ts` and make sure it's pointing to the `game-detail` component you generated above 62 | 63 |
64 | 🐳 Hint 65 | 66 | ```ts 67 | import { Route } from '@angular/router'; 68 | import { GameDetailComponent } from './game-detail/game-detail.component'; 69 | 70 | export const storeFeatureGameDetailRoutes: Route[] = [ 71 | { path: '', pathMatch: 'full', component: GameDetailComponent }, 72 | ]; 73 | ``` 74 | 75 |
76 | 77 |
78 | 79 | 6. **Import `MatCardModule`** in `store-feature-game-detail.module.ts` and add it to the module's `imports: [...]`: 80 | 81 |
82 | 🐳 Hint 83 | 84 | ```ts 85 | import { MatCardModule } from '@angular/material/card'; 86 | ``` 87 | 88 |
89 |
90 | 91 | 7. **Populate your new component** with the provided files: `game-detail.component.`[ts](../../examples/lab6/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts) / [css](../../examples/lab6/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.css) / [html](../../examples/lab6/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.html) 92 |

93 | 8. We now need to **display your new routed component**. Let's add a `` below our list of cards: 94 | 95 |
96 | 🐳 Hint 97 | 98 | `apps/store/src/app/app.component.html`: 99 | 100 | ```html 101 |
102 |
103 | ... 104 |
105 | <--- ADD IT HERE 106 |
107 | ``` 108 | 109 |
110 |
111 | 112 | 9. **Make clicking on each card route** to the `feature-game-detail` module with the game's ID: 113 | 114 |
115 | 🐳 Hint 116 | 117 | ```html 118 |
119 |
120 | 125 | <--- HERE ... 126 | 127 |
128 | 129 |
130 | ``` 131 | 132 |
133 |
134 | 135 | 10. **Serve your app** again, click on some games, and compare with the screenshot above 136 |

137 | 11. **Launch the project graph** and see what's been added 138 |

139 | 12. **Inspect what changed** from the last time you committed, then **commit your changes** 140 | 141 | --- 142 | 143 | The result is still pretty simple though. Our route just displays the ID of the selected game in a card. It would be great if we had some API to get the full game from that ID! 144 | 145 | --- 146 | 147 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 148 | 149 | --- 150 | 151 | [➡️ Next lab ➡️](../lab7/LAB.md) 152 | -------------------------------------------------------------------------------- /docs/lab6/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a lazy routing lib in a specific directory that is pre-configured with a certain parent module 2 | 3 | ```bash 4 | nx generate @nx/angular:lib feature-game-detail --directory=store --lazy --routing --parent="apps/store/src/app/app.routes.ts" 5 | ``` 6 | 7 | ##### Generate an Angular component in a specific project 8 | 9 | ```bash 10 | nx generate @nx/angular:component game-detail --project=store-feature-game-detail 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/lab7/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 7 - Add a NestJS API 2 | 3 | ###### ⏰ Estimated time: 10-15 minutes 4 | 5 |
6 | 7 | Up until now we've had a single app in our repository, and a few other libs that it uses. 8 | 9 | But remember how we created that `fake-api` way back in the second lab, that only our `store` app can access? 10 | 11 | Our new routed component suddenly needs access to the games as well, so in this lab we'll be adding a completely new app, this time on the backend, as an API. And we'll use the `@nx/nest` package to easily generate everything we need. 12 | 13 | You do not need to be familiar with Nest (and you can use the `@nrwl/express:app` plugin instead if you wish). All the NestJS specific code for serving the games is provided in the solution. 14 |

15 | 16 | ## 📚 Learning outcomes: 17 | 18 | - **Explore other plugins in the Nx ecosystem** 19 |


20 | 21 | ## 📲 After this workshop, you should have: 22 | 23 |
24 | App Screenshot 25 | No change in how the app looks! 26 |
27 | 28 |
29 | File structure 30 | lab7 file structure 31 |
32 |
33 | 34 | ## 🏋️‍♀️ Steps: 35 | 36 | 1. **Stop any running `nx serve` instance** 37 |

38 | 2. **Install `@nx/nest`** 39 |

40 | 3. **Generate a new NestJS app**, called `api` 41 | 42 | ⚠️ Make sure you instruct the generator to configure a proxy from the frontend `store` to the new `api` service (use `--help` to see the available options) 43 |

44 | 45 | 4. **Update `proxy.conf.json`** in `apps/store` to point to port `3000` 46 | 5. **Copy the code** from the `fake api` to the new Nest `apps/api/src/app/`[app.service.ts](../../examples/lab7/apps/api/src/app/app.service.ts) and expose the `getGames()` and `getGame()` methods 47 |

48 | 6. **Update the Nest [app.controller.ts](../../examples/lab7/apps/api/src/app/app.controller.ts)** to use the new methods from the service 49 |

50 | 7. Let's now **inspect the project graph**! 51 |

52 | 8. **Inspect what changed** from the last time you committed, then **commit your changes**\ 53 |

54 | 55 | --- 56 | 57 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 58 | 59 | --- 60 | 61 | [➡️ Next lab ➡️](../lab8/LAB.md) 62 | -------------------------------------------------------------------------------- /docs/lab7/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a new NestJS API app, and configure the proxy to the `store` project 2 | 3 | `nx generate @nx/nest:application api --frontendProject=store` 4 | -------------------------------------------------------------------------------- /docs/lab8/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 8 - Displaying a full game in the routed game-detail component 2 | 3 | ###### ⏰ Estimated time: 15-20 minutes 4 | 5 |
6 | 7 | Now we have a proper API that we can use to make HTTP requests. We'll look at how the Nrwl NestJS generators created a helpful proxy configuration for us. 8 |

9 | 10 | ## 📚 Learning outcomes: 11 | 12 | - **Learn how to connect frontend and backend apps in an Nx workspace** 13 |


14 | 15 | ## 📲 After this workshop, you should have: 16 | 17 |
18 | App screenshot 19 | screenshot of lab8 result 20 |
21 |
22 | 23 | ## 🏋️‍♀️ Steps: 24 | 25 | 1. **Import the `HttpClientModule`** in `apps/store/src/app/app.module.ts` and add it to the module's imports array: 26 | 27 |
28 | 🐳 Hint 29 | 30 | ```ts 31 | import { HttpClientModule } from '@angular/common/http'; 32 | ``` 33 | 34 |
35 |
36 | 37 | 2. Within the same folder, **inject the `HttpClient`** in the [app.component.ts](../../examples/lab8/apps/store/src/app/app.component.ts)'s constructor and **call your new API** as an _HTTP request_ 38 | 39 | ⚠️ _Notice how we assume it will be available at `/api` (more on that below)_ 40 |

41 | 42 | 3. Because our list of `games` is now an Observable, we need to **add an `async` pipe** in the template that gets the games: 43 | 44 |
45 | 🐳 Hint 46 | 47 | ```html 48 | ... 54 | ``` 55 | 56 |
57 |
58 | 59 | 4. **Run `nx serve api`** 60 | 61 | ⚠️ Notice the _PORT_ number 62 |

63 | 64 | 5. In a different tab, **run `nx serve store`** 65 | 66 | ⚠️ Again, notice the _PORT_ number 67 |

68 | 69 | 6. Everything should still look/function the same! 70 | 71 | 🎓 You can inspect your Network tab in the dev tools and notice an XHR request made to `http://localhost:4200/api/games` 72 |

73 | 74 | --- 75 | 76 | 🎓 Even though the frontend and server are being exposed at different ports, we can call `/api` from the frontend store because `Nx` created a proxy configuration for us (see `apps/store/proxy.conf.json`) so any calls to `/api` are being routed to the correct address/port where the API is running. 77 | This helps you avoid CORS issues while developing locally. 78 | 79 | --- 80 | 81 | Now let's load the full game in our routed component! 82 | 83 | 8. Inside the `libs/store/feature-game-detail/src/lib` folder, **replace the following files**: 84 | 85 | - `/game-detail/game-detail.component.` [ts](../../examples/lab8/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts) / [html](../../examples/lab8/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.html) 86 | - [/store-feature-game-detail.module.ts](../../examples/lab8/libs/store/feature-game-detail/src/lib/store-feature-game-detail.module.ts) 87 | 88 | ⚠️ Notice how we're using the shared `formatRating()` function in our routed component as well! 89 |

90 | 91 | 9. You should now see a fully-fleshed out detail component when you link on the card list at the top! (you might need to restart your `nx serve store` so the new button styles can be copied over) 92 |

93 | 10. **Inspect what changed** from the last time you committed, then **commit your changes** 94 |

95 | 96 | --- 97 | 98 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 99 | 100 | --- 101 | 102 | [➡️ Next lab ➡️](../lab9/LAB.md) 103 | -------------------------------------------------------------------------------- /docs/lab8/SOLUTION.md: -------------------------------------------------------------------------------- 1 | All the code necessary to finish this workshop is inside the hints in the lab. 2 | -------------------------------------------------------------------------------- /docs/lab9/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 9 - Generate a type lib that the API and frontend can share 2 | 3 | ###### ⏰ Estimated time: 15 minutes 4 | 5 |
6 | 7 | Now our project graph looks a bit disconnected. The frontend and the API still do not have anything in common. The power of Nx libraries is that they can be shared among any number of projects. 8 | 9 | We'll look at creating libs to store Typescript interfaces and then we'll use the Nx **Move** generator to move that library around our project, with minimal effort. 10 |

11 | 12 | ## 📚 Learning outcomes: 13 | 14 | - **Explore other real-world examples of creating shared libs for a specific project** 15 | - **Learn to use the `move` generator** 16 |


17 | 18 | ## 📲 After this workshop, you should have: 19 | 20 |
21 | App Screenshot 22 | No change in how the app looks! 23 |
24 | 25 |
26 | File structure 27 | lab9 file structure 28 |
29 |
30 | 31 | ## 🏋️‍♀️ Steps: 32 | 33 | 1. **Stop serving** both the API and the frontend 34 |

35 | 2. **Generate a new `@nx/js` lib** called `util-interface` inside the `libs/api` folder. 36 | 37 | ⚠️ It's **important** that we create it in the `/api` folder for now 38 |

39 | 40 | 3. **Create your `Game` interface**: see `libs/api/util-interface/src/lib/`[api-util-interface.ts](../../examples/lab9/libs/api/util-interface/src/lib/api-util-interface.ts) 41 |

42 | 4. **Import it** in the API service: `apps/api/src/app/app.service.ts` 43 | 44 | ⚠️ You might need to restart the Typescript compiler in your editor 45 | 46 |
47 | 🐳 Hint 48 | 49 | ```typescript 50 | import { Game } from '@bg-hoard/api/util-interface'; 51 | const games: Game[] = [...]; 52 | ``` 53 | 54 |
55 |
56 | 57 | 5. **Build the API** and make sure there are no errors 58 | 59 |
60 | 🐳 Hint 61 | 62 | ```shell 63 | nx build api 64 | ``` 65 | 66 |
67 |
68 | 69 | 6. **Inspect the project graph** 70 |

71 | 7. Make sure to **commit everything** before proceeding! 72 |

73 | 74 | --- 75 | 76 | Our frontend store makes calls to the API via the `HttpClient` service: 77 | 78 | ```typescript 79 | this.http.get(`/api/games/${id}`); 80 | ``` 81 | 82 | But it's currently typed to `any` - so our component has no idea about the shape of the objects it'll get back! 83 | 84 | Let's fix that - we already have a `Game` interface in a lib. But it's nested in the `api` folder - we need to move it out to the root `libs/` folder so any project can use it! 85 | 86 | --- 87 | 88 | 8. Use the `@nx/workspace:move` generator to **move the interface lib** created above into the root `/libs` folder 89 | 90 | ⚠️ Make sure you use the `--dry-run` flag until you're confident your command is correct 91 | 92 |
93 | 🐳 Hint 1 94 | Nx generate cmd structure 95 |
96 | 97 |
98 | 🐳 Hint 2 99 | 100 | Use the `--help` command to figure out how to target a specific **project** 101 | Alternatively, check out the [docs](https://nx.dev/packages/workspace/generators/move) 102 | 103 |
104 | 105 |
106 | 107 | 🐳 Hint 3 108 | 109 | Your library name is `api-util-interface` - to move it to root, its new name needs to be `util-interface` 110 | 111 |
112 |
113 | 114 | 9. We can now **import it in the frontend components** and use it when making the `http` request: 115 | 116 |
117 | 🐳 Hint 118 | 119 | Frontend store shell app: `apps/store/src/app/app.component.ts` 120 | 121 | ```typescript 122 | import { Game } from '@bg-hoard/util-interface'; 123 | 124 | this.http.get('/api/games'); 125 | ``` 126 | 127 | *** 128 | 129 | Routed game detail component: `libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts` 130 | 131 | ```typescript 132 | this.http.get(`/api/games/${id}`); 133 | ``` 134 | 135 |
136 | 137 | ⚠️ Open `apps/api/src/app/app.service.ts`. Notice how we didn't have to update the imports in the API. The `move` generator took care of that for us! 138 |

139 | 140 | 10. **Trigger a build** of both the store and the API projects and make sure it passes 141 |

142 | 11. **Inspect the project graph** 143 |

144 | 12. **Inspect what changed** from the last time you committed, then **commit your changes** 145 |

146 | 147 | --- 148 | 149 | 🎓If you get stuck, check out [the solution](SOLUTION.md) 150 | 151 | --- 152 | 153 | [➡️ Next lab ➡️](../lab10%20-%20bonus/LAB.md) 154 | -------------------------------------------------------------------------------- /docs/lab9/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a new type lib for the API 2 | 3 | ```shell 4 | nx generate @nx/js:lib util-interface --directory=api 5 | ``` 6 | 7 | ##### Use the `move` generator to move a nested lib to root 8 | 9 | ```shell 10 | nx generate @nx/workspace:move --projectName=api-util-interface util-interface 11 | ``` 12 | -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | * { 2 | font-family: 'Roboto', sans-serif; 3 | } 4 | 5 | .games-layout { 6 | display: flex; 7 | justify-content: space-between; 8 | margin-bottom: 20px; 9 | } 10 | 11 | .container { 12 | max-width: 800px; 13 | margin: 50px auto; 14 | } 15 | 16 | .game-card { 17 | max-width: 205px; 18 | display: flex; 19 | flex-direction: column; 20 | justify-content: space-between; 21 | } 22 | 23 | .game-card:hover { 24 | box-shadow: 0px 0px 12px 0px rgba(0, 0, 0, 0.3); 25 | transform: translate3d(0, -2px, 0); 26 | cursor: pointer; 27 | } 28 | .game-card:focus { 29 | outline: none; 30 | } 31 | 32 | .center-content { 33 | display: flex; 34 | justify-content: center; 35 | } 36 | 37 | .game-details { 38 | display: flex; 39 | flex-direction: column; 40 | margin: 0; 41 | } 42 | -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | {{ game.name }} 6 | 7 | Photo of board game {{ game.name }} 12 | 13 |

14 | {{ game.description }} 15 |

16 | 17 | Rating: {{ game.rating }} 18 | 19 |
20 |
21 |
22 |
23 | -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { getAllGames } from '../fake-api'; 3 | 4 | @Component({ 5 | selector: 'bg-hoard-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent { 10 | title = 'Board Game Hoard'; 11 | games = getAllGames(); 12 | } 13 | -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/assets/beans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/examples/lab2/apps/store/src/assets/beans.png -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/assets/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/examples/lab2/apps/store/src/assets/cat.png -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/assets/chess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/examples/lab2/apps/store/src/assets/chess.png -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/fake-api/index.ts: -------------------------------------------------------------------------------- 1 | const games = [ 2 | { 3 | id: 'settlers-in-the-can', 4 | name: 'Settlers in the Can', 5 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 6 | description: 7 | 'Help your bug family claim the best real estate in a spilled can of beans.', 8 | price: 35, 9 | rating: Math.random() 10 | }, 11 | { 12 | id: 'chess-pie', 13 | name: 'Chess Pie', 14 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 15 | description: 'A circular game of Chess that you can eat as you play.', 16 | price: 15, 17 | rating: Math.random() 18 | }, 19 | { 20 | id: 'purrfection', 21 | name: 'Purrfection', 22 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 23 | description: 'A cat grooming contest goes horribly wrong.', 24 | price: 45, 25 | rating: Math.random() 26 | } 27 | ]; 28 | 29 | export const getAllGames = () => games; 30 | export const getGame = (id: string) => games.find(game => game.id === id); 31 | -------------------------------------------------------------------------------- /examples/lab4/libs/store/ui-shared/src/lib/header/header.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ title }} 3 | 4 | -------------------------------------------------------------------------------- /examples/lab4/libs/store/ui-shared/src/lib/header/header.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'bg-hoard-header', 5 | templateUrl: './header.component.html', 6 | styleUrls: ['./header.component.css'] 7 | }) 8 | export class HeaderComponent { 9 | @Input() title = ''; 10 | } 11 | -------------------------------------------------------------------------------- /examples/lab5/libs/store/util-formatters/src/lib/store-util-formatters.ts: -------------------------------------------------------------------------------- 1 | export function formatRating(rating: number) { 2 | return `${Math.round(rating * 100) / 10} / 10`; 3 | } 4 | -------------------------------------------------------------------------------- /examples/lab6/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.css: -------------------------------------------------------------------------------- 1 | .game-image { 2 | width: 300px; 3 | border-radius: 20px; 4 | margin-right: 20px; 5 | } 6 | 7 | .content { 8 | display: flex; 9 | } 10 | 11 | .details { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: space-between; 15 | } 16 | 17 | .sell-info { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .buy-button { 23 | margin-right: 20px; 24 | } 25 | -------------------------------------------------------------------------------- /examples/lab6/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ gameId$ | async }} 3 | 4 | -------------------------------------------------------------------------------- /examples/lab6/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute, ParamMap } from '@angular/router'; 3 | import { map } from 'rxjs/operators'; 4 | 5 | @Component({ 6 | selector: 'bg-hoard-game-detail', 7 | templateUrl: './game-detail.component.html', 8 | styleUrls: ['./game-detail.component.css'] 9 | }) 10 | export class GameDetailComponent { 11 | constructor(private route: ActivatedRoute) {} 12 | 13 | gameId$ = this.route.paramMap.pipe( 14 | map((params: ParamMap) => params.get('id')) 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /examples/lab7/apps/api/src/app/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, Param } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller('games') 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getAllGames() { 10 | return this.appService.getAllGames(); 11 | } 12 | 13 | @Get('/:id') 14 | getGame(@Param('id') id: string) { 15 | return this.appService.getGame(id); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/lab7/apps/api/src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | const games = [ 4 | { 5 | id: 'settlers-in-the-can', 6 | name: 'Settlers in the Can', 7 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 8 | description: 9 | 'Help your bug family claim the best real estate in a spilled can of beans.', 10 | price: 35, 11 | rating: Math.random() 12 | }, 13 | { 14 | id: 'chess-pie', 15 | name: 'Chess Pie', 16 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 17 | description: 'A circular game of Chess that you can eat as you play.', 18 | price: 15, 19 | rating: Math.random() 20 | }, 21 | { 22 | id: 'purrfection', 23 | name: 'Purrfection', 24 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 25 | description: 'A cat grooming contest goes horribly wrong.', 26 | price: 45, 27 | rating: Math.random() 28 | } 29 | ]; 30 | 31 | @Injectable() 32 | export class AppService { 33 | getAllGames = () => games; 34 | getGame = (id: string) => games.find(game => game.id === id); 35 | } 36 | -------------------------------------------------------------------------------- /examples/lab8/apps/store/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { formatRating } from '@bg-hoard/store/util-formatters'; 3 | import { HttpClient } from '@angular/common/http'; 4 | 5 | @Component({ 6 | selector: 'bg-hoard-root', 7 | templateUrl: './app.component.html', 8 | styleUrls: ['./app.component.css'] 9 | }) 10 | export class AppComponent { 11 | constructor(private http: HttpClient) {} 12 | 13 | title = 'Board Game Hoard'; 14 | formatRating = formatRating; 15 | games = this.http.get('/api/games'); 16 | } 17 | -------------------------------------------------------------------------------- /examples/lab8/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.css: -------------------------------------------------------------------------------- 1 | .game-image { 2 | width: 300px; 3 | border-radius: 20px; 4 | margin-right: 20px; 5 | } 6 | 7 | .content { 8 | display: flex; 9 | } 10 | 11 | .details { 12 | display: flex; 13 | flex-direction: column; 14 | justify-content: space-between; 15 | } 16 | 17 | .sell-info { 18 | display: flex; 19 | flex-direction: column; 20 | } 21 | 22 | .buy-button { 23 | margin-right: 20px; 24 | } 25 | -------------------------------------------------------------------------------- /examples/lab8/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ game.name }} 5 | 6 | 7 | 8 | Picture of board game {{ game.name }} 13 |
14 |

{{ game.description }}

15 |
16 | Price: 18 | {{ game.price | currency: 'USD' }} 20 | 21 | Rating: 22 | {{ formatRating(game.rating) }} 23 | 24 |
25 |
26 | 27 | 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /examples/lab8/libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ActivatedRoute, ParamMap } from '@angular/router'; 3 | import { map, switchMap } from 'rxjs/operators'; 4 | import { HttpClient } from '@angular/common/http'; 5 | import { formatRating } from '@bg-hoard/store/util-formatters'; 6 | 7 | @Component({ 8 | selector: 'bg-hoard-game-detail', 9 | templateUrl: './game-detail.component.html', 10 | styleUrls: ['./game-detail.component.css'] 11 | }) 12 | export class GameDetailComponent { 13 | constructor(private route: ActivatedRoute, private http: HttpClient) {} 14 | 15 | game$ = this.route.paramMap.pipe( 16 | map((params: ParamMap) => params.get('id')), 17 | switchMap(id => this.http.get(`/api/games/${id}`)) 18 | ); 19 | formatRating = formatRating; 20 | } 21 | -------------------------------------------------------------------------------- /examples/lab8/libs/store/feature-game-detail/src/lib/store-feature-game-detail.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { CommonModule } from '@angular/common'; 3 | import { RouterModule } from '@angular/router'; 4 | import { GameDetailComponent } from './game-detail/game-detail.component'; 5 | import { MatCardModule } from '@angular/material/card'; 6 | import { MatButtonModule } from '@angular/material/button'; 7 | 8 | @NgModule({ 9 | imports: [ 10 | CommonModule, 11 | MatCardModule, 12 | MatButtonModule, 13 | RouterModule.forChild([ 14 | { path: '', pathMatch: 'full', component: GameDetailComponent } 15 | ]) 16 | ], 17 | declarations: [GameDetailComponent] 18 | }) 19 | export class StoreFeatureGameDetailModule {} 20 | -------------------------------------------------------------------------------- /examples/lab9/libs/api/util-interface/src/lib/api-util-interface.ts: -------------------------------------------------------------------------------- 1 | export interface Game { 2 | id: string; 3 | name: string; 4 | image: string; 5 | description: string; 6 | price: number; 7 | rating: number; 8 | } 9 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | const { getJestProjects } = require('@nx/jest'); 2 | 3 | export default { 4 | projects: getJestProjects(), 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | /* TODO: Update to latest Jest snapshotFormat 6 | * By default Nx has kept the older style of Jest Snapshot formats 7 | * to prevent breaking of any existing tests with snapshots. 8 | * It's recommend you update to the latest format. 9 | * You can do this by removing snapshotFormat property 10 | * and running tests with --update-snapshot flag. 11 | * Example: "nx affected --targets=test --update-snapshot" 12 | * More info: https://jestjs.io/docs/upgrading-to-jest29#snapshot-format 13 | */ 14 | snapshotFormat: { escapeString: true, printBasicPrototype: true }, 15 | }; 16 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/libs/.gitkeep -------------------------------------------------------------------------------- /libs/nx-workshop/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /libs/nx-workshop/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["../../.eslintrc.json"], 3 | "ignorePatterns": ["!**/*"], 4 | "overrides": [ 5 | { 6 | "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], 7 | "rules": {} 8 | }, 9 | { 10 | "files": ["*.ts", "*.tsx"], 11 | "rules": {} 12 | }, 13 | { 14 | "files": ["*.js", "*.jsx"], 15 | "rules": {} 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /libs/nx-workshop/README.md: -------------------------------------------------------------------------------- 1 | # Nx Workshop Utility 2 | 3 | > This library was generated with [Nx](https://nx.dev). 4 | 5 | This library contains migrations, generators and executors used during the [Nx Workshop](https://github.com/nrwl/nx-workshop). 6 | 7 | ## How to use lab migrations generator 8 | 9 | 1. Install `@nrwl/nx-workshop` package as dev dependency (e.g. `npm i -D @nrwl/nx-workshop`) 10 | 2. Run the generator with one of the following options: 11 | - Provide `lab` you want to complete: `nx g @nrwl/nx-workshop:complete-labs --lab=5` 12 | - Use `from` range to finish until end: `nx g @nrwl/nx-workshop:complete-labs --from=2` 13 | - Use `to` range to catch up with given lab: `nx g @nrwl/nx-workshop:complete-labs --to=5` 14 | - Use `from/to` range to catch up with several labs in between: `nx g @nrwl/nx-workshop:complete-labs --from=2 --from=7` 15 | - Use `option` to specify if you want track 1 or track 2: `nx g @nrwl/nx-workshop:complete-labs --from=19 --option=option2` 16 | 3. Run the generated migrations: `npx nx migrate --run-migrations` 17 | -------------------------------------------------------------------------------- /libs/nx-workshop/executors.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "executors": { 4 | "build": { 5 | "implementation": "./src/executors/build/executor", 6 | "schema": "./src/executors/build/schema.json", 7 | "description": "build executor" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /libs/nx-workshop/generators.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "name": "nx-workshop", 4 | "version": "0.0.1", 5 | "generators": { 6 | "nx-workshop": { 7 | "factory": "./src/generators/nx-workshop/generator", 8 | "schema": "./src/generators/nx-workshop/schema.json", 9 | "description": "nx-workshop generator" 10 | }, 11 | "complete-labs": { 12 | "factory": "./src/generators/complete-labs/generator", 13 | "schema": "./src/generators/complete-labs/schema.json", 14 | "description": "Completes the chosen labs" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /libs/nx-workshop/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'nx-workshop', 4 | 5 | globals: {}, 6 | testEnvironment: 'node', 7 | transform: { 8 | '^.+\\.[tj]sx?$': [ 9 | 'ts-jest', 10 | { 11 | tsconfig: '/tsconfig.spec.json', 12 | }, 13 | ], 14 | }, 15 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], 16 | coverageDirectory: '../../coverage/libs/nx-workshop', 17 | preset: '../../jest.preset.js', 18 | }; 19 | -------------------------------------------------------------------------------- /libs/nx-workshop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nrwl/nx-workshop", 3 | "version": "0.4.11", 4 | "main": "src/index.js", 5 | "publishConfig": { 6 | "access": "public", 7 | "registry": "https://registry.npmjs.org/" 8 | }, 9 | "private": false, 10 | "generators": "./generators.json", 11 | "executors": "./executors.json", 12 | "nx-migrations": { 13 | "migrations": "./migrations.json" 14 | }, 15 | "dependencies": { 16 | "@nrwl/nx-cloud": "latest", 17 | "cors": "*", 18 | "node-fetch": "^2.x", 19 | "surge": "*", 20 | "typescript": "5.0.4", 21 | "@nx/devkit": "16.3.0", 22 | "@nx/workspace": "16.3.0", 23 | "@nx/storybook": "16.3.0", 24 | "@nx/plugin": "16.3.0", 25 | "@nx/nest": "16.3.0", 26 | "@nx/angular": "16.3.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /libs/nx-workshop/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-workshop", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/nx-workshop/src", 5 | "projectType": "library", 6 | "targets": { 7 | "lint": { 8 | "executor": "@nx/linter:eslint", 9 | "outputs": ["{options.outputFile}"], 10 | "options": { 11 | "lintFilePatterns": ["libs/nx-workshop/**/*.ts"] 12 | } 13 | }, 14 | "test": { 15 | "executor": "@nx/jest:jest", 16 | "outputs": ["{workspaceRoot}/coverage/libs/nx-workshop"], 17 | "options": { 18 | "jestConfig": "libs/nx-workshop/jest.config.ts", 19 | "passWithNoTests": true 20 | } 21 | }, 22 | "build": { 23 | "executor": "@nx/js:tsc", 24 | "outputs": ["{options.outputPath}"], 25 | "options": { 26 | "outputPath": "dist/libs/nx-workshop", 27 | "tsConfig": "libs/nx-workshop/tsconfig.lib.json", 28 | "packageJson": "libs/nx-workshop/package.json", 29 | "main": "libs/nx-workshop/src/index.ts", 30 | "assets": [ 31 | "libs/nx-workshop/*.md", 32 | { 33 | "input": "./libs/nx-workshop/src", 34 | "glob": "**/!(*.ts)", 35 | "output": "./src" 36 | }, 37 | { 38 | "input": "./libs/nx-workshop/src", 39 | "glob": "**/*.d.ts", 40 | "output": "./src" 41 | }, 42 | { 43 | "input": "./libs/nx-workshop", 44 | "glob": "generators.json", 45 | "output": "." 46 | }, 47 | { 48 | "input": "./libs/nx-workshop", 49 | "glob": "executors.json", 50 | "output": "." 51 | }, 52 | { 53 | "input": "./libs/nx-workshop", 54 | "glob": "migrations.json", 55 | "output": "." 56 | } 57 | ] 58 | } 59 | }, 60 | "publish": { 61 | "executor": "nx:run-commands", 62 | "options": { 63 | "parallel": false, 64 | "commands": [ 65 | "npx nx build nx-workshop", 66 | "npm publish dist/libs/nx-workshop" 67 | ] 68 | } 69 | } 70 | }, 71 | "tags": [] 72 | } 73 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/executors/build/executor.spec.ts: -------------------------------------------------------------------------------- 1 | import { BuildExecutorSchema } from './schema'; 2 | import executor from './executor'; 3 | 4 | const options: BuildExecutorSchema = {}; 5 | 6 | describe('Build Executor', () => { 7 | it('can run', async () => { 8 | const output = await executor(options); 9 | expect(output.success).toBe(true); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/executors/build/executor.ts: -------------------------------------------------------------------------------- 1 | import { BuildExecutorSchema } from './schema'; 2 | 3 | export default async function runExecutor(options: BuildExecutorSchema) { 4 | console.log('Executor ran for Build', options); 5 | return { 6 | success: true, 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/executors/build/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface BuildExecutorSchema {} // eslint-disable-line 2 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/executors/build/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "title": "Build executor", 4 | "description": "", 5 | "type": "object", 6 | "properties": {}, 7 | "required": [] 8 | } 9 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/complete-labs/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 2 | import { Tree, readProjectConfiguration, readJson } from '@nx/devkit'; 3 | 4 | import generator from './generator'; 5 | import migrations from '../../../migrations.json'; 6 | 7 | jest.mock('@nx/devkit', () => ({ 8 | ...jest.requireActual('@nx/devkit'), 9 | readJsonFile: () => migrations, 10 | })); 11 | 12 | describe('Complete Labs generator', () => { 13 | let appTree: Tree; 14 | 15 | beforeEach(() => { 16 | appTree = createTreeWithEmptyWorkspace(); 17 | }); 18 | 19 | it('should create migration for single lab', async () => { 20 | await generator(appTree, { lab: 3 }); 21 | const { migrations } = readJson(appTree, 'migrations.json'); 22 | expect(migrations.length).toBe(1); 23 | expect(migrations[0].version).toEqual('0.1.3'); 24 | }); 25 | 26 | it('should create migration up to given lab', async () => { 27 | await generator(appTree, { to: 10 }); 28 | const { migrations } = readJson(appTree, 'migrations.json'); 29 | expect(migrations.length).toBe(10); 30 | expect(migrations[0].version).toEqual('0.1.1'); 31 | expect(migrations[9].version).toEqual('0.1.10'); 32 | }); 33 | 34 | it('should create migration from given lab', async () => { 35 | await generator(appTree, { from: 8 }); 36 | const { migrations } = readJson(appTree, 'migrations.json'); 37 | expect(migrations.length).toBe(17); 38 | expect(migrations[0].version).toEqual('0.1.8'); 39 | expect(migrations[16].version).toEqual('0.1.22'); 40 | // const config = readProjectConfiguration(appTree, 'test'); 41 | // expect(config).toBeDefined(); 42 | }); 43 | 44 | it('should respect options - option 1', async () => { 45 | await generator(appTree, { from: 8, option: 'option1' }); 46 | const { migrations } = readJson(appTree, 'migrations.json'); 47 | expect(migrations.length).toBe(17); 48 | expect(migrations[16].version).toEqual('0.1.22'); 49 | }); 50 | 51 | it('should respect options - option 2', async () => { 52 | await generator(appTree, { from: 8, option: 'option2' }); 53 | const { migrations } = readJson(appTree, 'migrations.json'); 54 | expect(migrations.length).toBe(17); 55 | expect(migrations[16].version).toEqual('0.1.22'); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/complete-labs/generator.ts: -------------------------------------------------------------------------------- 1 | import { formatFiles, readJsonFile, Tree } from '@nx/devkit'; 2 | import { CompleteLabsGeneratorSchema } from './schema'; 3 | 4 | export default async function ( 5 | tree: Tree, 6 | options: CompleteLabsGeneratorSchema 7 | ) { 8 | const { lab, from, to, option } = options; 9 | const migrationDefinitions = readJsonFile( 10 | 'node_modules/@nrwl/nx-workshop/migrations.json' 11 | ).generators; 12 | let migrations = Object.keys(migrationDefinitions).map((name) => { 13 | const { version, description, implementation, cli } = 14 | migrationDefinitions[name]; 15 | return { 16 | version, 17 | description, 18 | factory: implementation, 19 | cli, 20 | package: '@nrwl/nx-workshop', 21 | name, 22 | }; 23 | }); 24 | let including = false; 25 | migrations = migrations 26 | .filter((migration) => { 27 | const versionParts = migration.version.split('.'); 28 | const lastVersionPart = versionParts[versionParts.length - 1]; 29 | const optionSuffix = option === 'option1' ? '-alt' : ''; 30 | const firstLab = from || lab || 1; 31 | const firstLabString = 32 | firstLab < 19 ? firstLab + '' : firstLab + optionSuffix; 33 | if (lastVersionPart === firstLabString) { 34 | including = true; 35 | } 36 | const lastLab = to || lab; 37 | const lastLabString = 38 | lastLab < 19 ? lastLab + '' : lastLab + optionSuffix; 39 | if (lastVersionPart === lastLabString) { 40 | including = false; 41 | return true; 42 | } 43 | return including; 44 | }) 45 | .filter((migration) => { 46 | const versionParts = migration.version.split('.'); 47 | const lastVersionPart = +versionParts[versionParts.length - 1]; 48 | if (option === 'option2') { 49 | return !Number.isNaN(lastVersionPart); 50 | } else { 51 | return ( 52 | lastVersionPart < 19 || 53 | lastVersionPart > 21 || 54 | Number.isNaN(lastVersionPart) 55 | ); 56 | } 57 | }); 58 | tree.write('migrations.json', JSON.stringify({ migrations }, undefined, 2)); 59 | await formatFiles(tree); 60 | console.log('Migration file generated, to complete the labs run:'); 61 | console.log('nx migrate --run-migrations=migrations.json'); 62 | } 63 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/complete-labs/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface CompleteLabsGeneratorSchema { 2 | lab?: number; 3 | option?: 'option1' | 'option2'; 4 | from?: number; 5 | to?: number; 6 | } 7 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/complete-labs/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "CompleteLabs", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "lab": { 8 | "type": "number", 9 | "description": "Complete a single lab", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | } 14 | }, 15 | "option": { 16 | "type": "string", 17 | "enum": [ 18 | "option1", 19 | "option2" 20 | ], 21 | "description": "Complete labs in option 1 or option 2 of the second day" 22 | }, 23 | "from": { 24 | "type": "number", 25 | "description": "Complete a range of labs starting at this lab (inclusive)" 26 | }, 27 | "to": { 28 | "type": "number", 29 | "description": "Complete a range of labs ending at this lab (inclusive)" 30 | } 31 | }, 32 | "required": [] 33 | } 34 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/nx-workshop/files/src/index.ts__template__: -------------------------------------------------------------------------------- 1 | const variable = "<%= projectName %>"; -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/nx-workshop/generator.spec.ts: -------------------------------------------------------------------------------- 1 | import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 2 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 3 | 4 | import generator from './generator'; 5 | import { NxWorkshopGeneratorSchema } from './schema'; 6 | 7 | describe('nx-workshop generator', () => { 8 | let appTree: Tree; 9 | const options: NxWorkshopGeneratorSchema = { name: 'test' }; 10 | 11 | beforeEach(() => { 12 | appTree = createTreeWithEmptyWorkspace(); 13 | }); 14 | 15 | it('should run successfully', async () => { 16 | await generator(appTree, options); 17 | const config = readProjectConfiguration(appTree, 'test'); 18 | expect(config).toBeDefined(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/nx-workshop/generator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addProjectConfiguration, 3 | formatFiles, 4 | generateFiles, 5 | getWorkspaceLayout, 6 | names, 7 | offsetFromRoot, 8 | Tree, 9 | } from '@nx/devkit'; 10 | import * as path from 'path'; 11 | import { NxWorkshopGeneratorSchema } from './schema'; 12 | 13 | interface NormalizedSchema extends NxWorkshopGeneratorSchema { 14 | projectName: string; 15 | projectRoot: string; 16 | projectDirectory: string; 17 | parsedTags: string[]; 18 | } 19 | 20 | function normalizeOptions( 21 | tree: Tree, 22 | options: NxWorkshopGeneratorSchema 23 | ): NormalizedSchema { 24 | const name = names(options.name).fileName; 25 | const projectDirectory = options.directory 26 | ? `${names(options.directory).fileName}/${name}` 27 | : name; 28 | const projectName = projectDirectory.replace(new RegExp('/', 'g'), '-'); 29 | const projectRoot = `${getWorkspaceLayout(tree).libsDir}/${projectDirectory}`; 30 | const parsedTags = options.tags 31 | ? options.tags.split(',').map((s) => s.trim()) 32 | : []; 33 | 34 | return { 35 | ...options, 36 | projectName, 37 | projectRoot, 38 | projectDirectory, 39 | parsedTags, 40 | }; 41 | } 42 | 43 | function addFiles(tree: Tree, options: NormalizedSchema) { 44 | const templateOptions = { 45 | ...options, 46 | ...names(options.name), 47 | offsetFromRoot: offsetFromRoot(options.projectRoot), 48 | template: '', 49 | }; 50 | generateFiles( 51 | tree, 52 | path.join(__dirname, 'files'), 53 | options.projectRoot, 54 | templateOptions 55 | ); 56 | } 57 | 58 | export default async function (tree: Tree, options: NxWorkshopGeneratorSchema) { 59 | const normalizedOptions = normalizeOptions(tree, options); 60 | addProjectConfiguration(tree, normalizedOptions.projectName, { 61 | root: normalizedOptions.projectRoot, 62 | projectType: 'library', 63 | sourceRoot: `${normalizedOptions.projectRoot}/src`, 64 | targets: { 65 | build: { 66 | executor: '@nrwl/nx-workshop:build', 67 | }, 68 | }, 69 | tags: normalizedOptions.parsedTags, 70 | }); 71 | addFiles(tree, normalizedOptions); 72 | await formatFiles(tree); 73 | } 74 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/nx-workshop/schema.d.ts: -------------------------------------------------------------------------------- 1 | export interface NxWorkshopGeneratorSchema { 2 | name: string; 3 | tags?: string; 4 | directory?: string; 5 | } 6 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/generators/nx-workshop/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "$id": "NxWorkshop", 4 | "title": "", 5 | "type": "object", 6 | "properties": { 7 | "name": { 8 | "type": "string", 9 | "description": "", 10 | "$default": { 11 | "$source": "argv", 12 | "index": 0 13 | }, 14 | "x-prompt": "What name would you like to use?" 15 | }, 16 | "tags": { 17 | "type": "string", 18 | "description": "Add tags to the project (used for linting)", 19 | "alias": "t" 20 | }, 21 | "directory": { 22 | "type": "string", 23 | "description": "A directory where the project is placed", 24 | "alias": "d" 25 | } 26 | }, 27 | "required": [ 28 | "name" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-workshop/82fd7fbc9d76531574554f5f39348206a1ebdcfb/libs/nx-workshop/src/index.ts -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-1/complete-lab-1.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | formatFiles, 4 | Tree, 5 | updateJson, 6 | getProjects, 7 | readJson, 8 | installPackagesTask, 9 | readJsonFile, 10 | } from '@nx/devkit'; 11 | import { removeGenerator } from '@nx/workspace'; 12 | import { execSync } from 'child_process'; 13 | 14 | export default async function update(tree: Tree) { 15 | // npx create-nx-workspace bg-hoard --preset=empty --no-nx-cloud 16 | const projects = getProjects(tree); 17 | const projectsToRemove = [ 18 | 'store-e2e', 19 | 'store', 20 | 'api', 21 | 'api-e2e', 22 | 'api-util-interface', 23 | 'util-interface', 24 | 'store-feature-game-detail', 25 | 'ui-shared', 26 | 'store-ui-shared', 27 | 'store-ui-shared-e2e', 28 | 'store-util-formatters', 29 | 'api-util-notifications', 30 | 'video', 31 | 'video-e2e', 32 | 'internal-plugin', 33 | 'internal-plugin-e2e', 34 | 'admin-ui', 35 | ].filter((removeProject) => projects.has(removeProject)); 36 | projectsToRemove.forEach( 37 | async (projectName) => 38 | await removeGenerator(tree, { 39 | projectName, 40 | skipFormat: true, 41 | forceRemove: true, 42 | }) 43 | ); 44 | // hack to fix remove generator 45 | if (tree.exists('tsconfig.base.json')) { 46 | updateJson(tree, 'tsconfig.base.json', (json) => { 47 | json.compilerOptions.paths = {}; 48 | return json; 49 | }); 50 | } 51 | 52 | // Lab 10 53 | tree.delete('.storybook'); 54 | // Lab 13 55 | tree.delete('tools/generators/util-lib'); 56 | // Lab 14 57 | tree.delete('tools/generators/update-scope-schema'); 58 | // Lab 15 59 | tree.delete('.github/workflows/ci.yml'); 60 | // Lab 19 61 | if (tree.exists('.nx-workshop.json')) { 62 | const { flyName } = readJson(tree, '.nx-workshop.json'); 63 | const flyApps = await execSync(`fly apps list`).toString(); 64 | if (flyApps.includes(flyName)) { 65 | execSync(`fly apps destroy ${flyName} --yes`); 66 | } 67 | tree.delete('.nx-workshop.json'); 68 | } 69 | // Lab 19-alt 70 | tree.delete('tools/generators/add-deploy-target'); 71 | // Lab 21 72 | tree.delete('.github/workflows/deploy.yml'); 73 | // Set npmScope to bg-hoard 74 | updateJson(tree, 'nx.json', (json) => { 75 | json.npmScope = 'bg-hoard'; 76 | return json; 77 | }); 78 | updateJson(tree, 'package.json', (json) => { 79 | json.name = '@bg-hoard/source'; 80 | return json; 81 | }); 82 | await formatFiles(tree); 83 | return () => { 84 | installPackagesTask(tree); 85 | console.log(readJson(tree, 'package.json').name); 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-10/complete-lab-10.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { addDependenciesToPackageJson, formatFiles, Tree } from '@nx/devkit'; 3 | import { storybookConfigurationGenerator } from '@nx/angular/generators'; 4 | import { dependencies } from '../../../package.json'; 5 | import { Linter } from '@nx/linter'; 6 | import { insertImport } from '@nx/workspace/src/generators/utils/insert-import'; 7 | 8 | export default async function update(host: Tree) { 9 | // add @nx/storybook 10 | await addDependenciesToPackageJson( 11 | host, 12 | {}, 13 | { 14 | '@nx/storybook': dependencies['@nx/storybook'], 15 | } 16 | ); 17 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 18 | // nx generate @nx/angular:storybook-configuration store-ui-shared 19 | await storybookConfigurationGenerator(host, { 20 | name: 'store-ui-shared', 21 | configureCypress: true, 22 | generateStories: true, 23 | generateCypressSpecs: true, 24 | linter: Linter.EsLint, 25 | }); 26 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 27 | 28 | const headerComponentStoriesPath = `libs/store/ui-shared/src/lib/header/header.component.stories.ts`; 29 | insertImport( 30 | host, 31 | headerComponentStoriesPath, 32 | 'MatToolbarModule', 33 | '@angular/material/toolbar' 34 | ); 35 | host.write( 36 | headerComponentStoriesPath, 37 | host 38 | .read(headerComponentStoriesPath) 39 | .toString() 40 | .replace('imports: []', 'imports: [MatToolbarModule]') 41 | ); 42 | host.write( 43 | 'apps/store-ui-shared-e2e/src/e2e/header/header.component.cy.ts', 44 | ` 45 | describe('store-ui-shared', () => { 46 | beforeEach(() => 47 | cy.visit('/iframe.html?id=headercomponent--primary&args=title:BoardGameHoard') 48 | ); 49 | it('should render the component', () => { 50 | cy.get('bg-hoard-header').contains('BoardGameHoard'); 51 | }); 52 | }); 53 | ` 54 | ); 55 | await formatFiles(host); 56 | } 57 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-11/complete-lab-11.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | 4 | export default async function update(host: Tree) { 5 | host.write( 6 | 'apps/store-ui-shared-e2e/src/e2e/header/header.component.cy.ts', 7 | ` 8 | describe('store-ui-shared', () => { 9 | beforeEach(() => 10 | cy.visit('/iframe.html?id=headercomponent--primary&args=title:BoardGameHoard') 11 | ); 12 | it('should render the component', () => { 13 | cy.get('bg-hoard-header').contains('BoardGameHoard'); 14 | }); 15 | }); 16 | ` 17 | ); 18 | await formatFiles(host); 19 | } 20 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-12/complete-lab-12.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | formatFiles, 4 | readProjectConfiguration, 5 | Tree, 6 | updateJson, 7 | updateProjectConfiguration, 8 | } from '@nx/devkit'; 9 | 10 | export default async function update(host: Tree) { 11 | const projectUpdates = { 12 | store: { 13 | tags: ['scope:store', 'type:app'], 14 | }, 15 | 'store-e2e': { 16 | tags: ['scope:store', 'type:e2e'], 17 | implicitDependencies: ['store'], 18 | }, 19 | 'store-ui-shared': { 20 | tags: ['scope:store', 'type:ui'], 21 | }, 22 | 'store-util-formatters': { 23 | tags: ['scope:store', 'type:util'], 24 | }, 25 | 'store-feature-game-detail': { 26 | tags: ['scope:store', 'type:feature'], 27 | }, 28 | api: { 29 | tags: ['scope:api', 'type:app'], 30 | }, 31 | 'util-interface': { 32 | tags: ['scope:shared', 'type:util'], 33 | }, 34 | 'store-ui-shared-e2e': { 35 | tags: ['scope:store', 'type:e2e'], 36 | implicitDependencies: ['store-ui-shared'], 37 | }, 38 | }; 39 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 40 | Object.keys(projectUpdates).forEach((projectName) => { 41 | const config = readProjectConfiguration(host, projectName); 42 | config.tags = projectUpdates[projectName].tags; 43 | config.implicitDependencies = 44 | projectUpdates[projectName].implicitDependencies || []; 45 | updateProjectConfiguration(host, projectName, config); 46 | }); 47 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 48 | 49 | updateJson(host, '.eslintrc.json', (json) => { 50 | json.overrides[0].rules[ 51 | '@nx/enforce-module-boundaries' 52 | ][1].depConstraints = [ 53 | { 54 | sourceTag: 'scope:store', 55 | onlyDependOnLibsWithTags: ['scope:store', 'scope:shared'], 56 | }, 57 | { 58 | sourceTag: 'scope:api', 59 | onlyDependOnLibsWithTags: ['scope:api', 'scope:shared'], 60 | }, 61 | { 62 | sourceTag: 'type:feature', 63 | onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:util'], 64 | }, 65 | { 66 | sourceTag: 'type:ui', 67 | onlyDependOnLibsWithTags: ['type:ui', 'type:util'], 68 | }, 69 | { 70 | sourceTag: 'type:util', 71 | onlyDependOnLibsWithTags: ['type:util'], 72 | }, 73 | ]; 74 | return json; 75 | }); 76 | host.write( 77 | 'apps/api-e2e/src/api/lint.spec.ts', 78 | ` 79 | import { execSync } from 'child_process'; 80 | import { writeFileSync } from 'node:fs'; 81 | 82 | describe('Dependencies', () => { 83 | it('should fail linting when tag rules are applied', async () => { 84 | writeFileSync( 85 | 'libs/util-interface/src/index.ts', 86 | \`import {} from '@bg-hoard/store/ui-shared'; 87 | 88 | export * from './lib/api-util-interface'; 89 | \` 90 | ); 91 | expect(() => execSync('nx lint util-interface')).toThrow(); 92 | }); 93 | afterAll(() => { 94 | writeFileSync( 95 | 'libs/util-interface/src/index.ts', 96 | \`export * from './lib/api-util-interface'; 97 | \` 98 | ); 99 | }); 100 | }); 101 | ` 102 | ); 103 | await formatFiles(host); 104 | } 105 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-13/complete-lab-13a.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree, updateJson } from '@nx/devkit'; 3 | import { pluginGenerator, generatorGenerator } from '@nx/plugin/generators'; 4 | import { Linter } from '@nx/linter'; 5 | 6 | export default async function update(host: Tree) { 7 | // nx generate @nx/workspace:workspace-generator util-lib 8 | await pluginGenerator(host, { 9 | name: 'internal-plugin', 10 | skipTsConfig: false, 11 | unitTestRunner: 'jest', 12 | linter: Linter.EsLint, 13 | compiler: 'tsc', 14 | skipFormat: false, 15 | skipLintChecks: false, 16 | }); 17 | 18 | await generatorGenerator(host, { 19 | name: 'util-lib', 20 | project: 'internal-plugin', 21 | unitTestRunner: 'jest', 22 | }); 23 | 24 | host.write( 25 | 'libs/internal-plugin/src/generators/util-lib/generator.ts', 26 | `import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'; 27 | import { libraryGenerator } from '@nx/js'; 28 | import { UtilLibGeneratorSchema } from './schema'; 29 | 30 | export default async function (tree: Tree, schema: UtilLibGeneratorSchema) { 31 | await libraryGenerator(tree, { 32 | name: \`util-\${schema.name}\`, 33 | directory: schema.directory, 34 | tags: \`type:util, scope:\${schema.directory}\`, 35 | }); 36 | await formatFiles(tree); 37 | return () => { 38 | installPackagesTask(tree); 39 | }; 40 | } 41 | ` 42 | ); 43 | host.write( 44 | 'libs/internal-plugin/src/generators/util-lib/schema.d.ts', 45 | `export interface UtilLibGeneratorSchema { 46 | name: string; 47 | directory: 'store' | 'api' | 'shared'; 48 | } 49 | ` 50 | ); 51 | updateJson( 52 | host, 53 | 'libs/internal-plugin/src/generators/util-lib/schema.json', 54 | (json) => { 55 | delete json.properties.tags; 56 | return { 57 | ...json, 58 | properties: { 59 | ...json.properties, 60 | directory: { 61 | type: 'string', 62 | description: 'The scope of your lib.', 63 | 'x-prompt': { 64 | message: 'Which directory do you want the lib to be in?', 65 | type: 'list', 66 | items: [ 67 | { 68 | value: 'store', 69 | label: 'store', 70 | }, 71 | { 72 | value: 'api', 73 | label: 'api', 74 | }, 75 | { 76 | value: 'shared', 77 | label: 'shared', 78 | }, 79 | ], 80 | }, 81 | }, 82 | }, 83 | }; 84 | } 85 | ); 86 | host.write( 87 | 'libs/internal-plugin/src/generators/util-lib/generator.spec.ts', 88 | `import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 89 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 90 | 91 | import generator from './generator'; 92 | import { UtilLibGeneratorSchema } from './schema'; 93 | 94 | describe('util-lib generator', () => { 95 | let appTree: Tree; 96 | const options: UtilLibGeneratorSchema = { name: 'foo', directory: 'store' }; 97 | 98 | beforeEach(() => { 99 | appTree = createTreeWithEmptyWorkspace(); 100 | }); 101 | 102 | it('should add util to the name and add appropriate tags', async () => { 103 | await generator(appTree, options); 104 | const config = readProjectConfiguration(appTree, 'store-util-foo'); 105 | expect(config).toBeDefined(); 106 | expect(config.tags).toEqual(['type:util', 'scope:store']); 107 | }); 108 | }); 109 | ` 110 | ); 111 | await formatFiles(host); 112 | } 113 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-13/complete-lab-13b.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | import { execSync } from 'child_process'; 4 | 5 | export default function update(host: Tree) { 6 | execSync( 7 | 'npx nx generate @bg-hoard/internal-plugin:util-lib --name=notifications --directory=api' 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-13/complete-lab-13c.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | 4 | export default async function update(host: Tree) { 5 | host.write( 6 | 'libs/api/util-notifications/src/lib/api-util-notifications.ts', 7 | ` 8 | export function sendNotification(clientId: string) { 9 | console.log("sending notification to client: ", clientId); 10 | } 11 | ` 12 | ); 13 | await formatFiles(host); 14 | } 15 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-14/complete-lab-14.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | import { generatorGenerator } from '@nx/plugin/generators'; 4 | 5 | export default async function update(host: Tree) { 6 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 7 | await generatorGenerator(host, { 8 | name: 'update-scope-schema', 9 | project: 'internal-plugin', 10 | unitTestRunner: 'jest', 11 | }); 12 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 13 | 14 | host.write( 15 | 'libs/internal-plugin/src/generators/update-scope-schema/generator.ts', 16 | `import { 17 | Tree, 18 | updateJson, 19 | formatFiles, 20 | ProjectConfiguration, 21 | getProjects, 22 | updateProjectConfiguration, 23 | } from '@nx/devkit'; 24 | 25 | export default async function (tree: Tree) { 26 | addScopeIfMissing(tree); 27 | const scopes = getScopes(getProjects(tree)); 28 | updateSchemaJson(tree, scopes); 29 | updateSchemaInterface(tree, scopes); 30 | await formatFiles(tree); 31 | } 32 | 33 | function addScopeIfMissing(tree: Tree) { 34 | const projectMap = getProjects(tree); 35 | Array.from(projectMap.keys()).forEach((projectName) => { 36 | const project = projectMap.get(projectName); 37 | if (!project.tags.some((tag) => tag.startsWith('scope:'))) { 38 | const scope = projectName.split('-')[0]; 39 | project.tags.push(\`scope:\${scope}\`); 40 | updateProjectConfiguration(tree, projectName, project); 41 | } 42 | }); 43 | } 44 | 45 | function getScopes(projectMap: Map) { 46 | const projects: any[] = Array.from(projectMap.values()); 47 | const allScopes: string[] = projects 48 | .map((project) => 49 | project.tags.filter((tag: string) => tag.startsWith('scope:')) 50 | ) 51 | .reduce((acc, tags) => [...acc, ...tags], []) 52 | .map((scope: string) => scope.slice(6)); 53 | return Array.from(new Set(allScopes)); 54 | } 55 | 56 | function updateSchemaJson(tree: Tree, scopes: string[]) { 57 | updateJson( 58 | tree, 59 | 'libs/internal-plugin/src/generators/util-lib/schema.json', 60 | (schemaJson) => { 61 | schemaJson.properties.directory['x-prompt'].items = scopes.map( 62 | (scope) => ({ 63 | value: scope, 64 | label: scope, 65 | }) 66 | ); 67 | return schemaJson; 68 | } 69 | ); 70 | } 71 | 72 | function updateSchemaInterface(tree: Tree, scopes: string[]) { 73 | const joinScopes = scopes.map((s) => \`'\${s}'\`).join(' | '); 74 | const interfaceDefinitionFilePath = 75 | 'libs/internal-plugin/src/generators/util-lib/schema.d.ts'; 76 | const newContent = \`export interface UtilLibGeneratorSchema { 77 | name: string; 78 | directory: \${joinScopes}; 79 | }\`; 80 | tree.write(interfaceDefinitionFilePath, newContent); 81 | } 82 | ` 83 | ); 84 | 85 | host.write( 86 | 'libs/internal-plugin/src/generators/update-scope-schema/schema.json', 87 | ` 88 | { 89 | "$schema": "http://json-schema.org/schema", 90 | "cli": "nx", 91 | "$id": "UpdateScopeSchema", 92 | "properties": {} 93 | } 94 | 95 | ` 96 | ); 97 | 98 | host.delete( 99 | 'libs/internal-plugin/src/generators/update-scope-schema/schema.d.ts' 100 | ); 101 | 102 | await formatFiles(host); 103 | } 104 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-15/complete-lab-15.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree, updateJson } from '@nx/devkit'; 3 | 4 | export default async function update(host: Tree) { 5 | host.write( 6 | '.github/workflows/ci.yml', 7 | ` 8 | name: Run CI checks 9 | 10 | on: [pull_request] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | name: Building affected apps 16 | steps: 17 | - uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | - uses: bahmutov/npm-install@v1 21 | - run: npx nx affected --target=build --base=origin/main --parallel 22 | - run: npx nx affected --target=test --base=origin/main --parallel 23 | - run: npx nx affected --target=lint --base=origin/main --parallel 24 | - run: npx nx affected --target=e2e --base=origin/main --parallel 25 | ` 26 | ); 27 | updateJson(host, 'nx.json', (json) => { 28 | json['implicitDependencies'] = json['implicitDependencies'] || {}; 29 | json['implicitDependencies']['.github/workflows/ci.yml'] = '*'; 30 | return json; 31 | }); 32 | await formatFiles(host); 33 | } 34 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-16/complete-lab-16.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | import { execSync } from 'child_process'; 4 | 5 | export default function update(host: Tree) { 6 | execSync(`npx nx g @nrwl/nx-cloud:init`, { 7 | stdio: [0, 1, 2], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-17/complete-lab-17.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree, formatFiles } from '@nx/devkit'; 3 | 4 | export default async function update(host: Tree) { 5 | host.write( 6 | '.github/workflows/ci.yml', 7 | ` 8 | name: Run CI checks 9 | 10 | on: [pull_request] 11 | 12 | env: 13 | NX_BRANCH: \${{ github.event.number }} 14 | NX_RUN_GROUP: \${{ github.run_id }} 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | name: Building affected apps 20 | steps: 21 | - uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | - uses: bahmutov/npm-install@v1 25 | - run: npx nx affected --target=build --base=origin/main --parallel 26 | - run: npx nx affected --target=test --base=origin/main --parallel 27 | - run: npx nx affected --target=lint --base=origin/main --parallel 28 | - run: npx nx affected --target=e2e --base=origin/main --parallel 29 | ` 30 | ); 31 | await formatFiles(host); 32 | } 33 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-18/complete-lab-18.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | addDependenciesToPackageJson, 4 | readProjectConfiguration, 5 | Tree, 6 | updateJson, 7 | updateProjectConfiguration, 8 | } from '@nx/devkit'; 9 | import { uniq } from '@nx/plugin/testing'; 10 | import { formatFiles, runCommandsGenerator } from '@nx/workspace'; 11 | import { execSync } from 'child_process'; 12 | import { readJsonFile } from '@nx/devkit'; 13 | import { stripConsoleColors } from '../utils'; 14 | 15 | export default async function update(host: Tree) { 16 | await addDependenciesToPackageJson( 17 | host, 18 | {}, 19 | { 20 | surge: '0.23.1', 21 | } 22 | ); 23 | 24 | let surgeToken, surgeName; 25 | if (host.exists('.nx-workshop.json')) { 26 | const workshopConstants = readJsonFile('.nx-workshop.json'); 27 | surgeToken = workshopConstants.surgeToken; 28 | surgeName = workshopConstants.surgeName; 29 | } 30 | if (!surgeToken || !surgeName) { 31 | surgeToken = stripConsoleColors( 32 | execSync('npx surge token').toString().trim() 33 | ); 34 | surgeName = uniq(`prophetic-narwhal-`); 35 | if (host.exists('.nx-workshop.json')) { 36 | updateJson(host, '.nx-workshop.json', (json) => { 37 | json.surgeToken = surgeToken; 38 | json.surgeName = surgeName; 39 | return json; 40 | }); 41 | } else { 42 | host.write( 43 | '.nx-workshop.json', 44 | JSON.stringify({ surgeToken, surgeName }) 45 | ); 46 | } 47 | } 48 | 49 | host.write( 50 | 'apps/store/.local.env', 51 | `SURGE_TOKEN=${surgeToken}\nSURGE_DOMAIN=https://${surgeName}.surge.sh` 52 | ); 53 | 54 | // nx generate run-commands deploy --project=store --command="surge dist/apps/store https://.surge.sh --token " 55 | runCommandsGenerator(host, { 56 | name: 'deploy', 57 | project: 'store', 58 | command: `surge dist/apps/store \${SURGE_DOMAIN} --token \${SURGE_TOKEN}`, 59 | }); 60 | const config = readProjectConfiguration(host, 'store'); 61 | config.targets['deploy'].dependsOn = ['build']; 62 | updateProjectConfiguration(host, 'store', config); 63 | 64 | await formatFiles(); 65 | } 66 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-19-alt/complete-lab-19-alt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | addDependenciesToPackageJson, 4 | formatFiles, 5 | installPackagesTask, 6 | Tree, 7 | } from '@nx/devkit'; 8 | import { applicationGenerator } from '@nx/nest'; 9 | import { runCommandsGenerator } from '@nx/workspace'; 10 | import workspaceGenerator from '@nx/workspace/src/generators/workspace-generator/workspace-generator'; 11 | import { dependencies } from '../../../package.json'; 12 | 13 | export default async function update(host: Tree) { 14 | // yarn add @nx/nest # or "npm i -S @nx/nest" 15 | addDependenciesToPackageJson( 16 | host, 17 | {}, 18 | { 19 | '@nx/nest': dependencies['@nx/nest'], 20 | } 21 | ); 22 | // nx g @nx/nest:app admin-ui 23 | await applicationGenerator(host, { 24 | name: 'admin-ui', 25 | }); 26 | // nx generate run-commands deploy --project=admin-ui --command="surge dist/apps/admin-ui/exported \${SURGE_DOMAIN_ADMIN_UI} --token \${SURGE_TOKEN}" 27 | runCommandsGenerator(host, { 28 | name: 'deploy', 29 | project: 'admin-ui', 30 | command: 31 | 'surge dist/apps/admin-ui/exported ${SURGE_DOMAIN_ADMIN_UI} --token ${SURGE_TOKEN}', 32 | }); 33 | 34 | // nx g workspace-generator add-deploy-target 35 | workspaceGenerator(host, { 36 | name: 'add-deploy-target', 37 | skipFormat: true, 38 | }); 39 | 40 | host.write( 41 | `tools/generators/add-deploy-target/files/.local.env`, 42 | ` 43 | SURGE_DOMAIN_<%= undercaps(project) %>=https://<%= subdomain %>.surge.sh 44 | ` 45 | ); 46 | 47 | host.write( 48 | `tools/generators/add-deploy-target/index.ts`, 49 | ` 50 | import { 51 | Tree, 52 | formatFiles, 53 | installPackagesTask, 54 | generateFiles, 55 | } from '@nx/devkit'; 56 | import { runCommandsGenerator } from '@nx/js'; 57 | import { join } from 'path'; 58 | 59 | interface Schema { 60 | project: string; 61 | subdomain: string; 62 | } 63 | 64 | export default async function (host: Tree, schema: Schema) { 65 | await runCommandsGenerator(host, { 66 | name: 'deploy', 67 | project: schema.project, 68 | command: \`surge dist/apps/\${ 69 | schema.project 70 | } \\\${SURGE_DOMAIN_\${underscoreWithCaps( 71 | schema.project 72 | )}} --token \\\${SURGE_TOKEN}\`, 73 | }); 74 | await generateFiles( 75 | host, 76 | join(__dirname, './files'), 77 | \`apps/\${schema.project}\`, 78 | { 79 | tmpl: '', 80 | project: schema.project, 81 | subdomain: schema.subdomain, 82 | undercaps: underscoreWithCaps, 83 | } 84 | ); 85 | await formatFiles(host); 86 | return () => { 87 | installPackagesTask(host); 88 | }; 89 | } 90 | 91 | export function underscoreWithCaps(value: string): string { 92 | return value.replace(/-/g, '_').toLocaleUpperCase(); 93 | } 94 | ` 95 | ); 96 | 97 | host.write( 98 | `tools/generators/add-deploy-target/schema.json`, 99 | ` 100 | { 101 | "cli": "nx", 102 | "id": "add-deploy-target", 103 | "type": "object", 104 | "properties": { 105 | "project": { 106 | "type": "string", 107 | "description": "Project name to generate the deploy target for", 108 | "$default": { 109 | "$source": "argv", 110 | "index": 0 111 | } 112 | }, 113 | "subdomain": { 114 | "type": "string", 115 | "description": "Surge subdomain where you want it deployed.", 116 | "x-prompt": "What is the Surge subdomain you want it deployed under? (https://.surge.sh)" 117 | } 118 | }, 119 | "required": ["project", "subdomain"] 120 | } 121 | ` 122 | ); 123 | await formatFiles(host); 124 | return async () => { 125 | installPackagesTask(host); 126 | }; 127 | } 128 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-19/complete-lab-19.ts: -------------------------------------------------------------------------------- 1 | import { 2 | addDependenciesToPackageJson, 3 | formatFiles, 4 | readJsonFile, 5 | readProjectConfiguration, 6 | Tree, 7 | updateJson, 8 | updateProjectConfiguration, 9 | } from '@nx/devkit'; 10 | import { uniq } from '@nx/plugin/testing'; 11 | import { execSync } from 'child_process'; 12 | import { replaceInFile } from '../utils'; 13 | import executorGenerator from '@nx/plugin/src/generators/executor/executor'; 14 | 15 | export default async function update(host: Tree) { 16 | let flyToken, flyName; 17 | if (host.exists('.nx-workshop.json')) { 18 | const workshopConstants = readJsonFile('.nx-workshop.json'); 19 | flyToken = workshopConstants.flyToken; 20 | flyName = workshopConstants.flyName; 21 | } 22 | if (!flyToken || !flyName) { 23 | flyToken = execSync('fly auth token') 24 | .toString() 25 | .split('\n') 26 | .map((line) => line.trim())[0]; 27 | flyName = uniq(`prophetic-narwhal-`); 28 | if (host.exists('.nx-workshop.json')) { 29 | updateJson(host, '.nx-workshop.json', (json) => { 30 | json.flyToken = flyToken; 31 | json.flyName = flyName; 32 | return json; 33 | }); 34 | } else { 35 | host.write('.nx-workshop.json', JSON.stringify({ flyName, flyToken })); 36 | } 37 | } 38 | 39 | host.write( 40 | 'apps/api/.local.env', 41 | `FLY_API_TOKEN=${flyToken} 42 | ` 43 | ); 44 | host.write( 45 | 'apps/api/src/fly.toml', 46 | ` 47 | app = "${flyName}" 48 | kill_signal = "SIGINT" 49 | kill_timeout = 5 50 | processes = [] 51 | 52 | [build] 53 | builder = "paketobuildpacks/builder:base" 54 | buildpacks = ["gcr.io/paketo-buildpacks/nodejs"] 55 | 56 | [env] 57 | PORT = "8080" 58 | 59 | [experimental] 60 | cmd = ["PORT=8080 node main.js"] 61 | 62 | [[services]] 63 | http_checks = [] 64 | internal_port = 8080 65 | processes = ["app"] 66 | protocol = "tcp" 67 | script_checks = [] 68 | [services.concurrency] 69 | hard_limit = 25 70 | soft_limit = 20 71 | type = "connections" 72 | 73 | [[services.ports]] 74 | force_https = true 75 | handlers = ["http"] 76 | port = 80 77 | 78 | [[services.ports]] 79 | handlers = ["tls", "http"] 80 | port = 443 81 | 82 | [[services.tcp_checks]] 83 | grace_period = "1s" 84 | interval = "15s" 85 | restart_limit = 0 86 | timeout = "2s" 87 | ` 88 | ); 89 | 90 | await executorGenerator(host, { 91 | name: `fly-deploy`, 92 | includeHasher: false, 93 | project: 'internal-plugin', 94 | unitTestRunner: 'jest', 95 | }); 96 | 97 | host.write( 98 | 'libs/internal-plugin/src/executors/fly-deploy/schema.d.ts', 99 | `export interface FlyDeployExecutorSchema { 100 | distLocation: string; 101 | flyAppName: string; 102 | } 103 | ` 104 | ); 105 | 106 | host.write( 107 | 'libs/internal-plugin/src/executors/fly-deploy/schema.json', 108 | `{ 109 | "$schema": "http://json-schema.org/schema", 110 | "cli": "nx", 111 | "title": "FlyDeploy executor", 112 | "description": "", 113 | "type": "object", 114 | "properties": { 115 | "distLocation": { 116 | "type": "string" 117 | }, 118 | "flyAppName": { 119 | "type": "string" 120 | } 121 | }, 122 | "required": ["distLocation", "flyAppName"] 123 | } 124 | ` 125 | ); 126 | 127 | host.write( 128 | 'libs/internal-plugin/src/executors/fly-deploy/executor.ts', 129 | `import { FlyDeployExecutorSchema } from './schema'; 130 | import { execSync } from 'child_process'; 131 | 132 | export default async function runExecutor(options: FlyDeployExecutorSchema) { 133 | const cwd = options.distLocation; 134 | 135 | const results = execSync(\`fly apps list\`); 136 | if (results.toString().includes(options.flyAppName)) { 137 | execSync(\`fly deploy\`, { cwd }); 138 | } else { 139 | execSync(\`fly launch --now --name=\${options.flyAppName} --region=lax\`, { 140 | cwd, 141 | }); 142 | } 143 | return { 144 | success: true, 145 | }; 146 | } 147 | ` 148 | ); 149 | 150 | const apiConfig = readProjectConfiguration(host, 'api'); 151 | apiConfig.targets.build.configurations.production.externalDependencies = [ 152 | '@nestjs/microservices', 153 | '@nestjs/microservices/microservices-module', 154 | '@nestjs/websockets/socket-module', 155 | 'class-transformer', 156 | 'class-validator', 157 | 'cache-manager', 158 | 'cache-manager/package.json', 159 | ]; 160 | apiConfig.targets.build.configurations.production.assets = [ 161 | 'apps/api/src/assets', 162 | 'apps/api/src/fly.toml', 163 | ]; 164 | apiConfig.targets.deploy = { 165 | executor: '@bg-hoard/internal-plugin:fly-deploy', 166 | outputs: [], 167 | options: { 168 | distLocation: 'dist/apps/api', 169 | flyAppName: flyName, 170 | }, 171 | dependsOn: [{ target: 'build', projects: 'self', params: 'forward' }], 172 | }; 173 | updateProjectConfiguration(host, 'api', apiConfig); 174 | 175 | addDependenciesToPackageJson(host, { cors: '*' }, {}); 176 | replaceInFile( 177 | host, 178 | 'apps/api/src/main.ts', 179 | `app.setGlobalPrefix(globalPrefix); 180 | const port = process.env.PORT || 3000; 181 | `, 182 | `app.setGlobalPrefix(globalPrefix); 183 | app.enableCors(); 184 | const port = process.env.PORT || 3000; 185 | ` 186 | ); 187 | await formatFiles(host); 188 | } 189 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-20-alt/complete-lab-20-alt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | import { replaceInFile } from '../utils'; 4 | 5 | export default async function update(host: Tree) { 6 | host.write( 7 | 'apps/store/src/fake-api/index.ts', 8 | ` 9 | const games = [ 10 | { 11 | id: 'settlers-in-the-can', 12 | name: 'Settlers in the Can', 13 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 14 | description: 15 | 'Help your bug family claim the best real estate in a spilled can of beans.', 16 | price: 35, 17 | rating: Math.random() 18 | }, 19 | { 20 | id: 'chess-pie', 21 | name: 'Chess Pie', 22 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 23 | description: 'A circular game of Chess that you can eat as you play.', 24 | price: 15, 25 | rating: Math.random() 26 | }, 27 | { 28 | id: 'purrfection', 29 | name: 'Purrfection', 30 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 31 | description: 'A cat grooming contest goes horribly wrong.', 32 | price: 45, 33 | rating: Math.random() 34 | } 35 | ]; 36 | 37 | export const getAllGames = () => games; 38 | export const getGame = (id: string) => games.find(game => game.id === id); 39 | ` 40 | ); 41 | 42 | replaceInFile( 43 | host, 44 | `apps/store/src/app/app.component.ts`, 45 | `games = this.http.get('/api/games');`, 46 | 'games = getAllGames();' 47 | ); 48 | replaceInFile( 49 | host, 50 | `apps/store/src/app/app.component.html`, 51 | `games | async`, 52 | 'games' 53 | ); 54 | 55 | await formatFiles(host); 56 | } 57 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-20/complete-lab-20.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | formatFiles, 4 | readJsonFile, 5 | readProjectConfiguration, 6 | Tree, 7 | updateProjectConfiguration, 8 | } from '@nx/devkit'; 9 | import { insertImport } from '@nx/workspace/src/generators/utils/insert-import'; 10 | import { replaceInFile } from '../utils'; 11 | 12 | export default async function update(host: Tree) { 13 | const { flyName } = readJsonFile('.nx-workshop.json'); 14 | host.write( 15 | 'apps/store/src/environments/environment.prod.ts', 16 | `export const environment = { 17 | production: true, 18 | apiUrl: 'https://${flyName}.fly.dev' 19 | }; 20 | ` 21 | ); 22 | host.write( 23 | 'apps/store/src/environments/environment.ts', 24 | `export const environment = { 25 | production: false, 26 | apiUrl: '' 27 | }; 28 | ` 29 | ); 30 | const config = readProjectConfiguration(host, 'store'); 31 | config.targets['build'].configurations.production.fileReplacements = [ 32 | { 33 | replace: 'apps/store/src/environments/environment.ts', 34 | with: 'apps/store/src/environments/environment.prod.ts', 35 | }, 36 | ]; 37 | updateProjectConfiguration(host, 'store', config); 38 | 39 | const modulePath = 'apps/store/src/app/app.module.ts'; 40 | replaceInFile( 41 | host, 42 | modulePath, 43 | `providers: []`, 44 | `providers: [{ 45 | provide: 'baseUrl', 46 | useValue: environment.apiUrl 47 | }]` 48 | ); 49 | insertImport(host, modulePath, 'environment', '../environments/environment'); 50 | 51 | const appComponentPath = `apps/store/src/app/app.component.ts`; 52 | insertImport(host, appComponentPath, 'Inject', '@angular/core'); 53 | replaceInFile( 54 | host, 55 | appComponentPath, 56 | `constructor(private http: HttpClient)`, 57 | `constructor(private http: HttpClient, @Inject('baseUrl') private baseUrl: string)` 58 | ); 59 | replaceInFile( 60 | host, 61 | appComponentPath, 62 | "games = this.http.get('/api/games');", 63 | 'games = this.http.get(`${this.baseUrl}/api/games`);' 64 | ); 65 | 66 | const gameDetailComponentPath = `libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts`; 67 | insertImport(host, gameDetailComponentPath, 'Inject', '@angular/core'); 68 | replaceInFile( 69 | host, 70 | gameDetailComponentPath, 71 | `constructor(private route: ActivatedRoute, private http: HttpClient)`, 72 | `constructor(private route: ActivatedRoute, private http: HttpClient, @Inject('baseUrl') private baseUrl: string)` 73 | ); 74 | replaceInFile( 75 | host, 76 | gameDetailComponentPath, 77 | '`/api/games/${id}`', 78 | '`${this.baseUrl}/api/games/${id}`' 79 | ); 80 | await formatFiles(host); 81 | } 82 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-21-alt/complete-lab-21-alt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree, formatFiles } from '@nx/devkit'; 3 | 4 | export default async function update(host: Tree) { 5 | host.write( 6 | `.github/workflows/deploy.yml`, 7 | ` 8 | name: Deploy Website 9 | 10 | on: 11 | push: 12 | branches: 13 | - main 14 | 15 | env: 16 | SURGE_DOMAIN_STORE: \${{ secrets.SURGE_DOMAIN_STORE }} 17 | SURGE_DOMAIN_ADMIN_UI: \${{ secrets.SURGE_DOMAIN_ADMIN_UI }} 18 | SURGE_TOKEN: \${{ secrets.SURGE_TOKEN }} 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | name: Deploying apps 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | - uses: bahmutov/npm-install@v1 29 | - run: npx nx build store --configuration=production 30 | - run: npx nx build admin-ui --configuration=production 31 | - run: npx nx deploy store 32 | - run: npx nx deploy admin-ui 33 | ` 34 | ); 35 | await formatFiles(host); 36 | } 37 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-21/complete-lab-21.ts: -------------------------------------------------------------------------------- 1 | import { formatFiles, Tree } from '@nx/devkit'; 2 | 3 | export default async function update(host: Tree) { 4 | host.write( 5 | '.github/workflows/deploy.yml', 6 | ` 7 | name: Deploy Website 8 | 9 | on: 10 | push: 11 | branches: 12 | - main 13 | 14 | env: 15 | SURGE_DOMAIN: \${{ secrets.SURGE_DOMAIN }} 16 | SURGE_TOKEN: \${{ secrets.SURGE_TOKEN }} 17 | FLY_API_TOKEN: \${{ secrets.FLY_API_TOKEN }} 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | name: Deploying apps 23 | steps: 24 | - uses: actions/checkout@v3 25 | with: 26 | fetch-depth: 0 27 | - uses: bahmutov/npm-install@v1 28 | - run: npx nx build store --configuration=production 29 | - run: npx nx build api --configuration=production 30 | - run: npx nx deploy store 31 | - run: npx nx deploy api 32 | ` 33 | ); 34 | await formatFiles(host); 35 | } 36 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-22/complete-lab-22.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | 4 | export default async function update(host: Tree) { 5 | host.write( 6 | '.github/workflows/deploy.yml', 7 | ` 8 | name: Deploy Website 9 | 10 | on: 11 | push: 12 | branches: 13 | - main 14 | 15 | env: 16 | SURGE_DOMAIN: \${{ secrets.SURGE_DOMAIN }} 17 | SURGE_TOKEN: \${{ secrets.SURGE_TOKEN }} 18 | FLY_API_TOKEN: \${{ secrets.FLY_API_TOKEN }} 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | name: Deploying apps 24 | steps: 25 | - uses: actions/checkout@v3 26 | with: 27 | fetch-depth: 0 28 | - uses: bahmutov/npm-install@v1 29 | - uses: nrwl/nx-set-shas@v2 30 | - run: npx nx affected --target=build --base=\${{ env.NX_BASE }} --parallel --configuration=production 31 | - run: npx nx affected --target=deploy --base=\${{ env.NX_BASE }} --parallel 32 | ` 33 | ); 34 | await formatFiles(host); 35 | } 36 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-3/complete-lab-3.ts: -------------------------------------------------------------------------------- 1 | import { 2 | formatFiles, 3 | readProjectConfiguration, 4 | Tree, 5 | updateProjectConfiguration, 6 | } from '@nx/devkit'; 7 | 8 | export default async function update(host: Tree) { 9 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 10 | const config = readProjectConfiguration(host, 'store'); 11 | config.targets['build'].options.styles = Array.from( 12 | new Set([ 13 | ...config.targets['build'].options.styles, 14 | `./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css`, 15 | ]) 16 | ); 17 | updateProjectConfiguration(host, 'store', config); 18 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 19 | await formatFiles(host); 20 | } 21 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-4/complete-lab-4.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | import { libraryGenerator } from '@nx/angular/generators'; 4 | import { wrapAngularDevkitSchematic } from '@nx/devkit/ngcli-adapter'; 5 | import { insertNgModuleImport } from '@nx/angular/src/generators/utils'; 6 | import { insertImport } from '@nx/workspace/src/generators/utils/insert-import'; 7 | 8 | export default async function update(tree: Tree) { 9 | // nx generate @nx/angular:lib ui-shared --directory=store 10 | await libraryGenerator(tree, { 11 | name: 'ui-shared', 12 | directory: 'store', 13 | }); 14 | // nx generate @nx/angular:component header --export --project=store-ui-shared 15 | const componentGenerator = wrapAngularDevkitSchematic( 16 | '@schematics/angular', 17 | 'component' 18 | ); 19 | await componentGenerator(tree, { 20 | name: 'header', 21 | project: 'store-ui-shared', 22 | export: true, 23 | }); 24 | 25 | // Add MatToolbarModule to AppModule imports 26 | const modulePath = 'libs/store/ui-shared/src/lib/store-ui-shared.module.ts'; 27 | insertNgModuleImport(tree, modulePath, 'MatToolbarModule'); 28 | insertImport( 29 | tree, 30 | modulePath, 31 | 'MatToolbarModule', 32 | '@angular/material/toolbar' 33 | ); 34 | 35 | tree.write( 36 | 'libs/store/ui-shared/src/lib/header/header.component.html', 37 | ` 38 | {{ title }} 39 | ` 40 | ); 41 | tree.write( 42 | 'libs/store/ui-shared/src/lib/header/header.component.ts', 43 | `import { Component, Input } from '@angular/core'; 44 | 45 | @Component({ 46 | selector: 'bg-hoard-header', 47 | templateUrl: './header.component.html', 48 | styleUrls: ['./header.component.css'] 49 | }) 50 | export class HeaderComponent { 51 | @Input() title = ''; 52 | }` 53 | ); 54 | 55 | // Add StoreUiSharedModule to AppModule imports 56 | const appModulePath = 'apps/store/src/app/app.module.ts'; 57 | insertNgModuleImport(tree, appModulePath, 'StoreUiSharedModule'); 58 | insertImport( 59 | tree, 60 | appModulePath, 61 | 'StoreUiSharedModule', 62 | '@bg-hoard/store/ui-shared' 63 | ); 64 | 65 | tree.write( 66 | 'apps/store/src/app/app.component.html', 67 | ` 68 |
69 |
70 | 71 | 72 | {{ game.name }} 73 | 74 | Photo of board game {{ game.name }} 79 | 80 |

81 | {{ game.description }} 82 |

83 | 84 | Rating: {{ game.rating }} 85 | 86 |
87 |
88 |
89 |
90 | ` 91 | ); 92 | tree.write( 93 | 'apps/store-e2e/src/e2e/app.cy.ts', 94 | `describe('store', () => { 95 | beforeEach(() => cy.visit('/')); 96 | 97 | it('should have 3 games', () => { 98 | cy.contains('Settlers in the Can'); 99 | cy.contains('Chess Pie'); 100 | cy.contains('Purrfection'); 101 | }); 102 | it('should have a header', () => { 103 | cy.contains('Board Game Hoard'); 104 | }); 105 | }); 106 | ` 107 | ); 108 | 109 | await formatFiles(tree); 110 | } 111 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-5/complete-lab-5.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | import { libraryGenerator } from '@nx/js'; 4 | import { replaceInFile } from '../utils'; 5 | 6 | export default async function update(host: Tree) { 7 | // nx generate @nx/js:lib util-formatters --directory=store 8 | await libraryGenerator(host, { 9 | name: 'util-formatters', 10 | directory: 'store', 11 | }); 12 | 13 | host.write( 14 | 'libs/store/util-formatters/src/lib/store-util-formatters.ts', 15 | `export function formatRating(rating = 0) { 16 | return \`\${Math.round(rating * 100) / 10} / 10\`; 17 | } 18 | ` 19 | ); 20 | 21 | host.write( 22 | 'apps/store/src/app/app.component.ts', 23 | `import { Component } from '@angular/core'; 24 | import { getAllGames } from '../fake-api'; 25 | import { formatRating } from '@bg-hoard/store/util-formatters'; 26 | 27 | @Component({ 28 | selector: 'bg-hoard-root', 29 | templateUrl: './app.component.html', 30 | styleUrls: ['./app.component.css'] 31 | }) 32 | export class AppComponent { 33 | formatRating = formatRating; 34 | title = 'Board Game Hoard'; 35 | games = getAllGames(); 36 | }` 37 | ); 38 | 39 | replaceInFile( 40 | host, 41 | 'apps/store/src/app/app.component.html', 42 | '{{ game.rating }}', 43 | '{{ formatRating(game.rating) }}' 44 | ); 45 | host.write( 46 | 'apps/store-e2e/cypress.config.ts', 47 | `import { defineConfig } from 'cypress'; 48 | import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; 49 | import { execSync } from 'child_process'; 50 | 51 | export default defineConfig({ 52 | e2e: { 53 | ...nxE2EPreset(__dirname), 54 | setupNodeEvents(on, config) { 55 | on('task', { 56 | showProjects() { 57 | return execSync('nx show projects').toString(); 58 | }, 59 | }); 60 | }, 61 | }, 62 | });` 63 | ); 64 | host.write( 65 | 'apps/store-e2e/src/e2e/app.cy.ts', 66 | `describe('store', () => { 67 | beforeEach(() => cy.visit('/')); 68 | 69 | it('should have 3 games', () => { 70 | cy.contains('Settlers in the Can'); 71 | cy.contains('Chess Pie'); 72 | cy.contains('Purrfection'); 73 | }); 74 | it('should have a header', () => { 75 | cy.contains('Board Game Hoard'); 76 | }); 77 | it('should have a store-util-formatters library', () => { 78 | cy.task('showProjects').should('contain', 'store-util-formatters'); 79 | }); 80 | }); 81 | ` 82 | ); 83 | 84 | await formatFiles(host); 85 | } 86 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-6/complete-lab-6.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { libraryGenerator } from '@nx/angular/generators'; 3 | import { insertNgModuleImport } from '@nx/angular/src/generators/utils'; 4 | import { formatFiles, Tree } from '@nx/devkit'; 5 | import { wrapAngularDevkitSchematic } from '@nx/devkit/ngcli-adapter'; 6 | import { insertImport } from '@nx/workspace/src/generators/utils/insert-import'; 7 | import { replaceInFile } from '../utils'; 8 | 9 | export default async function update(host: Tree) { 10 | const appRoutingPath = 'apps/store/src/app/app.routes.ts'; 11 | // nx generate @nx/angular:lib feature-game-detail --directory=store --lazy --routing --parent="apps/store/src/app/app.routes.ts" 12 | await libraryGenerator(host, { 13 | name: 'feature-game-detail', 14 | directory: 'store', 15 | lazy: true, 16 | routing: true, 17 | parent: appRoutingPath, 18 | }); 19 | 20 | replaceInFile( 21 | host, 22 | appRoutingPath, 23 | `path: 'store-feature-game-detail'`, 24 | `path: 'game/:id'` 25 | ); 26 | 27 | const gameDetailRoutesPath = 28 | 'libs/store/feature-game-detail/src/lib/lib.routes.ts'; 29 | host.write( 30 | gameDetailRoutesPath, 31 | `import { Route } from '@angular/router'; 32 | import { GameDetailComponent } from './game-detail/game-detail.component'; 33 | 34 | export const storeFeatureGameDetailRoutes: Route[] = [ 35 | { path: '', pathMatch: 'full', component: GameDetailComponent } 36 | ];` 37 | ); 38 | 39 | // Add MatCardModule to StoreFeatureGameDetailModule imports 40 | const gameDetailModulePath = 41 | 'libs/store/feature-game-detail/src/lib/store-feature-game-detail.module.ts'; 42 | insertNgModuleImport(host, gameDetailModulePath, 'MatCardModule'); 43 | insertImport( 44 | host, 45 | gameDetailModulePath, 46 | 'MatCardModule', 47 | '@angular/material/card' 48 | ); 49 | 50 | // nx generate @nx/angular:component game-detail --project=store-feature-game-detail 51 | const componentGenerator = wrapAngularDevkitSchematic( 52 | '@schematics/angular', 53 | 'component' 54 | ); 55 | await componentGenerator(host, { 56 | name: 'game-detail', 57 | project: 'store-feature-game-detail', 58 | }); 59 | host.write( 60 | 'libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.css', 61 | `.game-image { 62 | width: 300px; 63 | border-radius: 20px; 64 | margin-right: 20px; 65 | } 66 | 67 | .content { 68 | display: flex; 69 | } 70 | 71 | .details { 72 | display: flex; 73 | flex-direction: column; 74 | justify-content: space-between; 75 | } 76 | 77 | .sell-info { 78 | display: flex; 79 | flex-direction: column; 80 | } 81 | 82 | .buy-button { 83 | margin-right: 20px; 84 | } 85 | ` 86 | ); 87 | host.write( 88 | 'libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.html', 89 | ` 90 | {{ gameId$ | async }} 91 | 92 | ` 93 | ); 94 | host.write( 95 | 'libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts', 96 | `import { Component } from '@angular/core'; 97 | import { ActivatedRoute, ParamMap } from '@angular/router'; 98 | import { map } from 'rxjs/operators'; 99 | 100 | @Component({ 101 | selector: 'bg-hoard-game-detail', 102 | templateUrl: './game-detail.component.html', 103 | styleUrls: ['./game-detail.component.css'] 104 | }) 105 | export class GameDetailComponent { 106 | constructor(private route: ActivatedRoute) {} 107 | 108 | gameId$ = this.route.paramMap.pipe( 109 | map((params: ParamMap) => params.get('id')) 110 | ); 111 | } 112 | ` 113 | ); 114 | 115 | const appComponentHtmlPath = 'apps/store/src/app/app.component.html'; 116 | replaceInFile( 117 | host, 118 | appComponentHtmlPath, 119 | `
120 |
`, 121 | ` 122 | 123 | ` 124 | ); 125 | replaceInFile( 126 | host, 127 | appComponentHtmlPath, 128 | '', 129 | '' 130 | ); 131 | host.write( 132 | 'apps/store-e2e/src/e2e/app.cy.ts', 133 | `describe('store', () => { 134 | beforeEach(() => cy.visit('/')); 135 | 136 | it('should have 3 games', () => { 137 | cy.contains('Settlers in the Can'); 138 | cy.contains('Chess Pie'); 139 | cy.contains('Purrfection'); 140 | }); 141 | it('should have a header', () => { 142 | cy.contains('Board Game Hoard'); 143 | }); 144 | it('should have a store-util-formatters library', () => { 145 | cy.task('showProjects').should('contain', 'store-util-formatters'); 146 | }); 147 | it('should navigate to game details', () => { 148 | cy.contains('Settlers in the Can').click(); 149 | cy.location('pathname').should('contain', 'settlers-in-the-can'); 150 | }); 151 | }); 152 | 153 | ` 154 | ); 155 | await formatFiles(host); 156 | } 157 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-7/complete-lab-7.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | addDependenciesToPackageJson, 4 | formatFiles, 5 | Tree, 6 | updateJson, 7 | } from '@nx/devkit'; 8 | import { applicationGenerator } from '@nx/nest'; 9 | import { dependencies } from '../../../package.json'; 10 | 11 | export default async function update(host: Tree) { 12 | await addDependenciesToPackageJson( 13 | host, 14 | {}, 15 | { 16 | '@nx/nest': dependencies['@nx/nest'], 17 | } 18 | ); 19 | 20 | // nx generate @nx/nest:application api --frontendProject=store 21 | await applicationGenerator(host, { 22 | name: 'api', 23 | frontendProject: 'store', 24 | }); 25 | host.write( 26 | 'apps/api/src/app/app.service.ts', 27 | `import { Injectable } from '@nestjs/common'; 28 | 29 | const games = [ 30 | { 31 | id: 'settlers-in-the-can', 32 | name: 'Settlers in the Can', 33 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 34 | description: 35 | 'Help your bug family claim the best real estate in a spilled can of beans.', 36 | price: 35, 37 | rating: Math.random() 38 | }, 39 | { 40 | id: 'chess-pie', 41 | name: 'Chess Pie', 42 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 43 | description: 'A circular game of Chess that you can eat as you play.', 44 | price: 15, 45 | rating: Math.random() 46 | }, 47 | { 48 | id: 'purrfection', 49 | name: 'Purrfection', 50 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 51 | description: 'A cat grooming contest goes horribly wrong.', 52 | price: 45, 53 | rating: Math.random() 54 | } 55 | ]; 56 | 57 | @Injectable() 58 | export class AppService { 59 | getAllGames = () => games; 60 | getGame = (id: string) => games.find(game => game.id === id); 61 | }` 62 | ); 63 | host.write( 64 | 'apps/api/src/app/app.controller.ts', 65 | `import { Controller, Get, Param } from '@nestjs/common'; 66 | import { AppService } from './app.service'; 67 | 68 | @Controller('games') 69 | export class AppController { 70 | constructor(private readonly appService: AppService) {} 71 | 72 | @Get() 73 | getAllGames() { 74 | return this.appService.getAllGames(); 75 | } 76 | 77 | @Get('/:id') 78 | getGame(@Param('id') id: string) { 79 | return this.appService.getGame(id); 80 | } 81 | } 82 | ` 83 | ); 84 | updateJson(host, 'apps/store/proxy.conf.json', (json) => { 85 | json['/api'].target = 'http://localhost:3000'; 86 | return json; 87 | }); 88 | host.write( 89 | 'apps/api-e2e/src/api/api.spec.ts', 90 | `import axios from 'axios'; 91 | import { exec } from 'child_process'; 92 | 93 | describe('GET /api/games', () => { 94 | it('should return a list of games', async () => { 95 | exec('nx serve api'); 96 | const res = await axios.get(\`/api/games\`); 97 | 98 | expect(res.status).toBe(200); 99 | expect(res.data).toMatchObject([ 100 | { 101 | description: 102 | 'Help your bug family claim the best real estate in a spilled can of beans.', 103 | id: 'settlers-in-the-can', 104 | image: '/assets/beans.png', 105 | name: 'Settlers in the Can', 106 | price: 35, 107 | }, 108 | { 109 | description: 'A circular game of Chess that you can eat as you play.', 110 | id: 'chess-pie', 111 | image: '/assets/chess.png', 112 | name: 'Chess Pie', 113 | price: 15, 114 | }, 115 | { 116 | description: 'A cat grooming contest goes horribly wrong.', 117 | id: 'purrfection', 118 | image: '/assets/cat.png', 119 | name: 'Purrfection', 120 | price: 45, 121 | }, 122 | ]); 123 | }); 124 | }); 125 | ` 126 | ); 127 | 128 | await formatFiles(host); 129 | } 130 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-8/complete-lab-8.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { insertNgModuleImport } from '@nx/angular/src/generators/utils'; 3 | import { formatFiles, Tree } from '@nx/devkit'; 4 | import { insertImport } from '@nx/workspace/src/generators/utils/insert-import'; 5 | import { replaceInFile } from '../utils'; 6 | 7 | export default async function update(host: Tree) { 8 | // Add HttpClientModule to StoreFeatureGameDetailModule imports 9 | const appModulePath = 'apps/store/src/app/app.module.ts'; 10 | insertNgModuleImport(host, appModulePath, 'HttpClientModule'); 11 | insertImport(host, appModulePath, 'HttpClientModule', '@angular/common/http'); 12 | 13 | const appComponentPath = 'apps/store/src/app/app.component.ts'; 14 | insertImport(host, appComponentPath, 'HttpClient', '@angular/common/http'); 15 | replaceInFile( 16 | host, 17 | appComponentPath, 18 | `} 19 | `, 20 | ` constructor(private http: HttpClient) {} 21 | } 22 | ` 23 | ); 24 | replaceInFile( 25 | host, 26 | appComponentPath, 27 | 'games = getAllGames();', 28 | "games = this.http.get('/api/games');" 29 | ); 30 | 31 | replaceInFile( 32 | host, 33 | 'apps/store/src/app/app.component.html', 34 | `let game of games`, 35 | `let game of games | async` 36 | ); 37 | 38 | host.write( 39 | 'libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts', 40 | `import { Component } from '@angular/core'; 41 | import { ActivatedRoute, ParamMap } from '@angular/router'; 42 | import { map, switchMap } from 'rxjs/operators'; 43 | import { HttpClient } from '@angular/common/http'; 44 | import { formatRating } from '@bg-hoard/store/util-formatters'; 45 | 46 | @Component({ 47 | selector: 'bg-hoard-game-detail', 48 | templateUrl: './game-detail.component.html', 49 | styleUrls: ['./game-detail.component.css'] 50 | }) 51 | export class GameDetailComponent { 52 | constructor(private route: ActivatedRoute, private http: HttpClient) {} 53 | 54 | game$ = this.route.paramMap.pipe( 55 | map((params: ParamMap) => params.get('id')), 56 | switchMap(id => this.http.get(\`/api/games/\${id}\`)) 57 | ); 58 | formatRating = formatRating; 59 | } 60 | ` 61 | ); 62 | host.write( 63 | 'libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.html', 64 | ` 65 | 66 | 67 | {{ game.name }} 68 | 69 | 70 | 71 | Picture of board game {{ game.name }} 76 |
77 |

{{ game.description }}

78 |
79 | Price: 81 | {{ game.price | currency: 'USD' }} 83 | 84 | Rating: 85 | {{ formatRating(game.rating) }} 86 | 87 |
88 |
89 | 90 | 91 |
92 |
93 |
94 |
95 | ` 96 | ); 97 | // Add MatButtonModule to StoreFeatureGameDetailModule imports 98 | const storeFeatureGameDetailModulePath = 99 | 'libs/store/feature-game-detail/src/lib/store-feature-game-detail.module.ts'; 100 | insertNgModuleImport( 101 | host, 102 | storeFeatureGameDetailModulePath, 103 | 'MatButtonModule' 104 | ); 105 | insertImport( 106 | host, 107 | storeFeatureGameDetailModulePath, 108 | 'MatButtonModule', 109 | '@angular/material/button' 110 | ); 111 | 112 | await formatFiles(host); 113 | } 114 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/complete-lab-9/complete-lab-9.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { formatFiles, Tree } from '@nx/devkit'; 3 | import { moveGenerator } from '@nx/workspace'; 4 | import { libraryGenerator } from '@nx/js'; 5 | import { insertImport } from '@nx/workspace/src/generators/utils/insert-import'; 6 | import { replaceInFile } from '../utils'; 7 | 8 | export default async function update(host: Tree) { 9 | // nx generate @nx/js:lib util-interface --directory=api 10 | await libraryGenerator(host, { 11 | name: 'util-interface', 12 | directory: 'api', 13 | }); 14 | host.write( 15 | 'libs/api/util-interface/src/lib/api-util-interface.ts', 16 | `export interface Game { 17 | id: string; 18 | name: string; 19 | image: string; 20 | description: string; 21 | price: number; 22 | rating: number; 23 | } 24 | ` 25 | ); 26 | 27 | // nx generate @nx/workspace:move --projectName=api-util-interface util-interface 28 | await moveGenerator(host, { 29 | projectName: 'api-util-interface', 30 | destination: 'util-interface', 31 | updateImportPath: true, 32 | }); 33 | const appSevicePath = 'apps/api/src/app/app.service.ts'; 34 | insertImport(host, appSevicePath, 'Game', '@bg-hoard/util-interface'); 35 | replaceInFile( 36 | host, 37 | appSevicePath, 38 | `const games = [`, 39 | `const games: Game[] = [` 40 | ); 41 | const appComponentPath = 'apps/store/src/app/app.component.ts'; 42 | insertImport(host, appComponentPath, 'Game', '@bg-hoard/util-interface'); 43 | replaceInFile( 44 | host, 45 | appComponentPath, 46 | 'this.http.get', 47 | 'this.http.get' 48 | ); 49 | 50 | const gameDetailComponentPath = 51 | 'libs/store/feature-game-detail/src/lib/game-detail/game-detail.component.ts'; 52 | insertImport( 53 | host, 54 | gameDetailComponentPath, 55 | 'Game', 56 | '@bg-hoard/util-interface' 57 | ); 58 | replaceInFile( 59 | host, 60 | gameDetailComponentPath, 61 | 'this.http.get(`/api/games/${id}`);', 62 | 'this.http.get(`/api/games/${id}`);' 63 | ); 64 | host.write( 65 | 'apps/api-e2e/src/api/graph.spec.ts', 66 | `import { execSync } from 'child_process'; 67 | import { readFileSync } from 'node:fs'; 68 | 69 | describe('Dependencies', () => { 70 | it('should have three dependencies on util-interface', async () => { 71 | execSync('nx graph --file=graph.json'); 72 | const graph = JSON.parse(readFileSync('graph.json').toString()); 73 | expect(graph.graph.dependencies['store'].some(dep => dep.target === 'util-interface')).toBe(true); 74 | expect(graph.graph.dependencies['store-feature-game-detail'].some(dep => dep.target === 'util-interface')).toBe(true); 75 | expect(graph.graph.dependencies['api'].some(dep => dep.target === 'util-interface')).toBe(true); 76 | }); 77 | }); 78 | ` 79 | ); 80 | await formatFiles(host); 81 | } 82 | -------------------------------------------------------------------------------- /libs/nx-workshop/src/migrations/utils.ts: -------------------------------------------------------------------------------- 1 | import { Tree } from '@nx/devkit'; 2 | import * as ts from 'typescript'; 3 | import { insertImport as astInsertImport } from '@nx/js'; 4 | 5 | export function replaceInFile( 6 | host: Tree, 7 | path: string, 8 | find: string, 9 | replace: string 10 | ) { 11 | const newContent = host.read(path).toString().replace(find, replace); 12 | host.write(path, newContent); 13 | } 14 | export function insertImport( 15 | host: Tree, 16 | path: string, 17 | symbolName: string, 18 | importPath: string 19 | ) { 20 | const moduleSource = host.read(path, 'utf-8'); 21 | 22 | const sourceFile = ts.createSourceFile( 23 | path, 24 | moduleSource, 25 | ts.ScriptTarget.Latest, 26 | true 27 | ); 28 | 29 | return astInsertImport(host, sourceFile, path, symbolName, importPath); 30 | } 31 | 32 | export function stripConsoleColors(log: string): string { 33 | return log.replace( 34 | // eslint-disable-next-line no-control-regex 35 | /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, 36 | '' 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /libs/nx-workshop/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.base.json", 3 | "files": [], 4 | "include": [], 5 | "references": [ 6 | { 7 | "path": "./tsconfig.lib.json" 8 | }, 9 | { 10 | "path": "./tsconfig.spec.json" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /libs/nx-workshop/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "declaration": true, 7 | "resolveJsonModule": true, 8 | "types": ["node"] 9 | }, 10 | "exclude": ["**/*.spec.ts", "**/*.test.ts", "jest.config.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/nx-workshop/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "resolveJsonModule": true, 7 | "esModuleInterop": true, 8 | "types": ["jest", "node"] 9 | }, 10 | "include": [ 11 | "**/*.test.ts", 12 | "**/*.spec.ts", 13 | "**/*.test.tsx", 14 | "**/*.spec.tsx", 15 | "**/*.test.js", 16 | "**/*.spec.js", 17 | "**/*.test.jsx", 18 | "**/*.spec.jsx", 19 | "**/*.d.ts", 20 | "jest.config.ts" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "migrations": [ 3 | { 4 | "cli": "nx", 5 | "version": "16.2.0-beta.0", 6 | "description": "Remove outputPath from run commands", 7 | "implementation": "./src/migrations/update-16-2-0/remove-run-commands-output-path", 8 | "package": "nx", 9 | "name": "16.2.0-remove-output-path-from-run-commands" 10 | }, 11 | { 12 | "cli": "nx", 13 | "version": "16.2.0-beta.0", 14 | "description": "Replace @nx/plugin:e2e with @nx/jest", 15 | "implementation": "./src/migrations/update-16-2-0/replace-e2e-executor", 16 | "package": "@nx/plugin", 17 | "name": "update-16-2-0-replace-e2e-executor" 18 | }, 19 | { 20 | "cli": "nx", 21 | "version": "16.1.0-beta.1", 22 | "requires": { 23 | "@angular/core": ">=15.0.0" 24 | }, 25 | "description": "Remove exported `@angular/platform-server` `renderModule` method. The `renderModule` method is now exported by the Angular CLI.", 26 | "factory": "./src/migrations/update-16-1-0/remove-render-module-platform-server-exports", 27 | "package": "@nx/angular", 28 | "name": "remove-render-module-platform-server-exports" 29 | }, 30 | { 31 | "cli": "nx", 32 | "version": "16.1.0-beta.1", 33 | "requires": { 34 | "@angular/core": ">=16.0.0-rc.4" 35 | }, 36 | "description": "Remove 'ngcc' invocation if exists from the 'postinstall' script in package.json.", 37 | "factory": "./src/migrations/update-16-1-0/remove-ngcc-invocation", 38 | "package": "@nx/angular", 39 | "name": "remove-ngcc-invocation" 40 | }, 41 | { 42 | "cli": "nx", 43 | "version": "16.1.0-beta.1", 44 | "requires": { 45 | "@angular/core": ">=16.0.0-rc.4" 46 | }, 47 | "description": "Extract the app config for standalone apps", 48 | "factory": "./src/migrations/update-16-1-0/extract-standalone-config-from-bootstrap", 49 | "package": "@nx/angular", 50 | "name": "extract-app-config-for-standalone" 51 | }, 52 | { 53 | "cli": "nx", 54 | "version": "16.1.0-beta.1", 55 | "requires": { 56 | "@angular/core": ">=16.0.0-rc.4" 57 | }, 58 | "description": "Update server executors' configuration to disable 'buildOptimizer' for non optimized builds.", 59 | "factory": "./src/migrations/update-16-1-0/update-server-executor-config", 60 | "package": "@nx/angular", 61 | "name": "update-server-executor-config" 62 | }, 63 | { 64 | "cli": "nx", 65 | "version": "16.1.0-beta.1", 66 | "requires": { 67 | "@angular/core": ">=16.0.0" 68 | }, 69 | "description": "Update the @angular/cli package version to ~16.0.0.", 70 | "factory": "./src/migrations/update-16-1-0/update-angular-cli", 71 | "package": "@nx/angular", 72 | "name": "update-angular-cli-version-16-0-0" 73 | }, 74 | { 75 | "cli": "nx", 76 | "version": "16.1.0-beta.0", 77 | "description": "Ignore @nx/react/plugins/storybook in Storybook eslint rules.", 78 | "factory": "./src/migrations/update-16-1-0/eslint-ignore-react-plugin", 79 | "package": "@nx/storybook", 80 | "name": "update-16-1-0" 81 | } 82 | ] 83 | } 84 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmScope": "bg-hoard", 3 | "affected": { 4 | "defaultBase": "master" 5 | }, 6 | "tasksRunnerOptions": { 7 | "default": { 8 | "runner": "nx-cloud", 9 | "options": { 10 | "cacheableOperations": [ 11 | "build", 12 | "lint", 13 | "test", 14 | "e2e" 15 | ], 16 | "parallel": 3, 17 | "accessToken": "Y2ZhY2EyNWEtMjdjNy00NWZlLTk5NWUtZDYzMjU0YmY3Y2NhfHJlYWQtd3JpdGU=" 18 | } 19 | } 20 | }, 21 | "$schema": "./node_modules/nx/schemas/nx-schema.json", 22 | "targetDefaults": { 23 | "build": { 24 | "dependsOn": [ 25 | "^build" 26 | ], 27 | "inputs": [ 28 | "production", 29 | "^production" 30 | ] 31 | }, 32 | "lint": { 33 | "inputs": [ 34 | "default", 35 | "{workspaceRoot}/.eslintrc.json" 36 | ] 37 | } 38 | }, 39 | "namedInputs": { 40 | "default": [ 41 | "{projectRoot}/**/*", 42 | "sharedGlobals" 43 | ], 44 | "sharedGlobals": [ 45 | "{workspaceRoot}/workspace.json", 46 | "{workspaceRoot}/tsconfig.base.json", 47 | "{workspaceRoot}/tslint.json", 48 | "{workspaceRoot}/nx.json" 49 | ], 50 | "production": [ 51 | "default" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bg-hoard", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "nx": "nx", 7 | "start": "nx serve", 8 | "build": "nx build", 9 | "test": "nx test", 10 | "lint": "nx workspace-lint && nx lint", 11 | "e2e": "nx e2e", 12 | "affected:apps": "nx affected:apps", 13 | "affected:libs": "nx affected:libs", 14 | "affected:build": "nx affected:build", 15 | "affected:e2e": "nx affected:e2e", 16 | "affected:test": "nx affected:test", 17 | "affected:lint": "nx affected:lint", 18 | "affected:dep-graph": "nx affected:dep-graph", 19 | "affected": "nx affected", 20 | "format": "nx format:write", 21 | "format:write": "nx format:write", 22 | "format:check": "nx format:check", 23 | "update": "nx migrate latest", 24 | "workspace-generator": "nx workspace-generator", 25 | "dep-graph": "nx dep-graph", 26 | "help": "nx help", 27 | "cherry-pick-all": "./tools/cherry-pick-all.sh" 28 | }, 29 | "private": true, 30 | "dependencies": { 31 | "@angular/core": "16.0.3", 32 | "@storybook/builder-webpack5": "7.0.17", 33 | "@storybook/core-server": "7.0.17", 34 | "@storybook/manager-webpack5": "6.5.16", 35 | "tslib": "^2.0.0" 36 | }, 37 | "devDependencies": { 38 | "@angular-devkit/core": "16.0.3", 39 | "@angular-devkit/schematics": "16.0.3", 40 | "@nrwl/nx-workshop": "^0.4.11", 41 | "@phenomnomnominal/tsquery": "^4.1.1", 42 | "@schematics/angular": "16.0.3", 43 | "@swc-node/register": "^1.4.2", 44 | "@swc/core": "^1.2.173", 45 | "@types/jest": "29.4.4", 46 | "@types/node": "18.7.1", 47 | "@typescript-eslint/eslint-plugin": "5.59.7", 48 | "@typescript-eslint/parser": "5.59.7", 49 | "dotenv": "10.0.0", 50 | "eslint": "8.15.0", 51 | "eslint-config-prettier": "8.1.0", 52 | "jest": "29.4.3", 53 | "jest-environment-jsdom": "~29.4.1", 54 | "node-fetch": "^2.6.1", 55 | "nx": "16.3.0", 56 | "nx-cloud": "16.0.5", 57 | "prettier": "2.7.1", 58 | "ts-jest": "29.1.0", 59 | "ts-node": "10.9.1", 60 | "tslib": "^2.0.0", 61 | "typescript": "5.0.4", 62 | "@nx/devkit": "16.3.0", 63 | "@nx/workspace": "16.3.0", 64 | "@nx/linter": "16.3.0", 65 | "@nx/storybook": "16.3.0", 66 | "@nx/js": "16.3.0", 67 | "@nx/eslint-plugin": "16.3.0", 68 | "@nx/plugin": "16.3.0", 69 | "@nx/jest": "16.3.0", 70 | "@nx/nest": "16.3.0", 71 | "@nx/node": "16.3.0", 72 | "@nx/webpack": "16.3.0", 73 | "@nx/angular": "16.3.0" 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tools/EMAIL_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Hi to everyone at {{ company/ conference }}! 2 | 3 | {{ co-speaker name }} and I are very excited to be able to have some time with you in the upcoming two days! 4 | 5 | Here is a link to the repo we'll be using for the workshop: https://github.com/nrwl/nx-workshop 6 | 7 | This repo includes the lab instructions as well as code snippets we'll be using in the workshop. The repo also includes branches that can be used to `jump ahead` to the start of another lab when necessary. Please take the opportunity to check this repo and ensure that the prerequisites are satisfied (excerpt below for your convenience! 8 | 9 | If you look at very bottom of the README.md you will notice there are two tracks branching on the end of the second day: 10 | - React frontends and more custom generators practice 11 | - Fly.io API deployments 12 | 13 | Please let us know which track you would like us to take. You can also decide this tomorrow after day one. 14 | 15 | Looking forward to meeting you all! 16 | 17 | {{ your name }} 18 | 19 | === 20 | 21 | ## Pre-requisites 22 | Nx has support for a lot of platforms, but in this workshop we'll be using Angular. While all the code for any Angular specific work will be provided, it will help if you have some experience with the Angular ecosystem. 23 | 24 | Make sure you have the following installed: 25 | - Node.js version 12.14.1 and up (16 recommended, but other versions might be okay as well) 26 | - `node --version` 27 | - A Github account 28 | - http://github.com 29 | 30 | Optional (these might be necessary depending on the track you select): 31 | - [Yarn](https://classic.yarnpkg.com/en/docs/install/) (you can also use `npx` or `pnpx` or global `nx CLI`) 32 | - `yarn --version` 33 | - A [Fly.io](https://fly.io/) account with the [CLI installed](https://fly.io/terminal) 34 | - `fly help` 35 | - [Docker](https://www.docker.com/get-started) 36 | - `docker --version` 37 | 38 | === 39 | -------------------------------------------------------------------------------- /tools/cherry-pick-all.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | hash=$1 4 | 5 | git checkout starting-lab2 6 | git cherry-pick "$hash" 7 | git checkout starting-lab3 8 | git cherry-pick "$hash" 9 | git checkout starting-lab3 10 | git cherry-pick "$hash" 11 | git checkout starting-lab4 12 | git cherry-pick "$hash" 13 | git checkout starting-lab5 14 | git cherry-pick "$hash" 15 | git checkout starting-lab6 16 | git cherry-pick "$hash" 17 | git checkout starting-lab7 18 | git cherry-pick "$hash" 19 | git checkout starting-lab8 20 | git cherry-pick "$hash" 21 | git checkout starting-lab9 22 | git cherry-pick "$hash" 23 | git checkout starting-lab10 24 | git cherry-pick "$hash" 25 | git checkout starting-lab11 26 | git cherry-pick "$hash" 27 | git checkout starting-lab12 28 | git cherry-pick "$hash" 29 | git checkout starting-lab13 30 | git cherry-pick "$hash" 31 | git checkout starting-lab14 32 | git cherry-pick "$hash" 33 | git checkout starting-lab15 34 | git cherry-pick "$hash" 35 | git checkout starting-lab16 36 | git cherry-pick "$hash" 37 | git checkout starting-lab17 38 | git cherry-pick "$hash" 39 | git checkout starting-lab18 40 | git cherry-pick "$hash" 41 | git checkout starting-lab19 42 | git cherry-pick "$hash" 43 | git checkout starting-lab20-frontend 44 | git cherry-pick "$hash" 45 | git checkout starting-lab21-frontend 46 | git cherry-pick "$hash" 47 | git checkout starting-lab20-api 48 | git cherry-pick "$hash" 49 | git checkout starting-lab21-api 50 | git cherry-pick "$hash" 51 | git checkout starting-lab22 52 | git cherry-pick "$hash" 53 | git checkout final-solution 54 | git cherry-pick "$hash" 55 | git checkout master 56 | -------------------------------------------------------------------------------- /tools/rebase-all-master.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git checkout starting-lab2 4 | git rebase master --ignore-date 5 | git checkout starting-lab3 6 | git rebase master --ignore-date 7 | git checkout starting-lab4 8 | git rebase master --ignore-date 9 | git checkout starting-lab5 10 | git rebase master --ignore-date 11 | git checkout starting-lab6 12 | git rebase master --ignore-date 13 | git checkout starting-lab7 14 | git rebase master --ignore-date 15 | git checkout starting-lab8 16 | git rebase master --ignore-date 17 | git checkout starting-lab9 18 | git rebase master --ignore-date 19 | git checkout starting-lab10 20 | git rebase master --ignore-date 21 | git checkout starting-lab11 22 | git rebase master --ignore-date 23 | git checkout starting-lab12 24 | git rebase master --ignore-date 25 | git checkout starting-lab13 26 | git rebase master --ignore-date 27 | git checkout starting-lab14 28 | git rebase master --ignore-date 29 | git checkout starting-lab15 30 | git rebase master --ignore-date 31 | git checkout starting-lab16 32 | git rebase master --ignore-date 33 | git checkout starting-lab17 34 | git rebase master --ignore-date 35 | git checkout starting-lab18 36 | git rebase master --ignore-date 37 | git checkout starting-lab19 38 | git rebase master --ignore-date 39 | git checkout starting-lab20-option1 40 | git rebase master --ignore-date 41 | git checkout starting-lab21-option1 42 | git rebase master --ignore-date 43 | git checkout starting-lab20-option2 44 | git rebase master --ignore-date 45 | git checkout starting-lab21-option2 46 | git rebase master --ignore-date 47 | git checkout starting-lab22 48 | git rebase master --ignore-date 49 | git checkout final-solution 50 | git rebase master --ignore-date 51 | git checkout master 52 | -------------------------------------------------------------------------------- /tools/tsconfig.tools.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "outDir": "../dist/out-tsc/tools", 5 | "rootDir": ".", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": ["node"] 9 | }, 10 | "include": ["**/*.ts"] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "es2015", 12 | "module": "esnext", 13 | "typeRoots": ["node_modules/@types"], 14 | "lib": ["es2017", "dom"], 15 | "skipLibCheck": true, 16 | "skipDefaultLibCheck": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@nrwl/nx-workshop": ["libs/nx-workshop/src/index.ts"] 20 | } 21 | }, 22 | "exclude": ["node_modules", "tmp"] 23 | } 24 | --------------------------------------------------------------------------------