├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .github ├── CONTRIBUTING.md ├── assets │ └── nx.png └── workflows │ └── ci.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .storybook ├── main.js └── tsconfig.json ├── .verdaccio ├── .htpasswd └── config.yml ├── .vscode └── extensions.json ├── .yarn └── releases │ └── yarn-4.0.1.cjs ├── .yarnrc.yml ├── README.md ├── apps ├── .gitkeep └── nx-workshop-e2e │ ├── .eslintrc.json │ ├── jest.config.ts │ ├── 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 │ └── storybook.gif ├── lab1 │ ├── LAB.md │ └── SOLUTION.md ├── lab10 │ ├── LAB.md │ └── SOLUTION.md ├── lab11 │ └── 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.module.scss │ │ └── app.tsx │ │ ├── assets │ │ ├── beans.png │ │ ├── cat.png │ │ └── chess.png │ │ └── fake-api │ │ └── index.ts ├── lab4 │ └── libs │ │ └── store │ │ └── ui-shared │ │ └── src │ │ └── lib │ │ └── header │ │ └── header.tsx ├── lab5 │ └── libs │ │ └── store │ │ └── util-formatters │ │ └── src │ │ └── lib │ │ └── store-util-formatters.ts ├── lab6 │ └── libs │ │ └── store │ │ └── feature-game-detail │ │ └── src │ │ └── lib │ │ └── store-feature-game-detail │ │ ├── store-feature-game-detail.module.scss │ │ └── store-feature-game-detail.tsx ├── lab7 │ └── apps │ │ └── api │ │ └── src │ │ ├── app │ │ └── games.repository.ts │ │ └── main.ts ├── lab8 │ ├── apps │ │ └── store │ │ │ └── src │ │ │ └── app │ │ │ └── app.tsx │ └── libs │ │ └── store │ │ └── feature-game-detail │ │ └── src │ │ └── lib │ │ └── store-feature-game-detail │ │ ├── store-feature-game-detail.module.scss │ │ └── store-feature-game-detail.tsx └── lab9 │ └── libs │ └── api │ └── util-interface │ └── src │ └── lib │ └── api-util-interface.ts ├── jest.config.ts ├── jest.preset.js ├── libs ├── .gitkeep └── nx-react-workshop │ ├── .babelrc │ ├── .eslintrc.json │ ├── README.md │ ├── generators.json │ ├── jest.config.ts │ ├── migrations.json │ ├── package.json │ ├── project.json │ ├── src │ ├── generators │ │ └── complete-labs │ │ │ ├── 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 ├── project.json ├── tools ├── cherry-pick-all.sh ├── migrate-all.sh ├── rebase-all-master.sh ├── scripts │ ├── start-local-registry.ts │ └── stop-local-registry.ts └── 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 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | `TODO` 4 | 5 | ## Local Package Publishing 6 | 7 | When iterating on the `@nrwl/nx-react-workshop-e2e` package, it's often helpful to test out a published 8 | version of the package locally. This can be done by starting the local registry, `verdaccio`, and using the `nx release` command. 9 | 10 | To start the local registry, run the `local-registry` target for the root workspace project: 11 | 12 | ```bash 13 | nx local-registry 14 | ``` 15 | 16 | In another terminal, you can then trigger deployment to that registry by running the `release` target for the `nx-react-workshop-e2e` project: 17 | 18 | ```bash 19 | nx release version prerelease && nx release publish 20 | 21 | # alternatively 22 | nx release-dev nx-react-workshop 23 | ``` 24 | 25 | Once your dev version has been published, you can then update the `nx-react-workshop-e2e` package in your consuming project to the version you just published. 26 | 27 | Since verdaccio configures itself in the `~/.npmrc` file, you can simply install using your standard package manager commands: 28 | 29 | ```bash 30 | npm add --save-dev @nrwl/nx-react-workshop@latest 31 | # or alternatively with yarn 32 | yarn add -D @nrwl/nx-react-workshop@latest 33 | ``` 34 | 35 | ## Updating workshop content 36 | 37 | The workshop content is stored in the `docs` directory. The content is written in markdown and is organized by lab. Each lab has its own directory with a `LAB.md` and `SOLUTION.md`. 38 | 39 | When updating: 40 | 41 | 1. Use the migration generator to jump to the lab before the being updating 42 | 43 | ```sh 44 | nx generate @nrwl/nx-react-workshop:complete-labs --from=1 --to= 45 | nx migrate --run-migrations=migrations.json --verbose 46 | ``` 47 | 48 | 2. Manually run through the lab and verify: 49 | 50 | - The instructions in LAB.md and language reflect the current state of the nx 51 | - The contents of SOLUTION.md match 1:1 with the steps in the lab 52 | - Any screenshots or prompt examples accurately reflect the output from the lab 53 | 54 | 3. Verify the completion migration reflects the steps outlined in the solution 55 | 56 | ```sh 57 | # After manually following the lab in step #2 58 | 59 | # git commit 60 | git add . && git commit -m 'manually run through completion steps' 61 | 62 | # reset repo state and migrate to end of lab: 63 | nx generate @nrwl/nx-react-workshop:complete-labs --from=1 --to= 64 | nx migrate --run-migrations=migrations.json --verbose 65 | 66 | # Verify there are no differences 67 | git status # should show no changes 68 | ``` 69 | -------------------------------------------------------------------------------- /.github/assets/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/.github/assets/nx.png -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | permissions: 10 | actions: read 11 | contents: read 12 | 13 | jobs: 14 | main: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | 21 | # This enables task distribution via Nx Cloud 22 | # Run this command as early as possible, before dependencies are installed 23 | # Learn more at https://nx.dev/ci/reference/nx-cloud-cli#npx-nxcloud-startcirun 24 | - run: yarn dlx nx-cloud start-ci-run --distribute-on="3 linux-medium-js" --stop-agents-after="e2e" 25 | 26 | # Cache node_modules 27 | - uses: actions/setup-node@v4 28 | with: 29 | node-version: 20 30 | cache: 'yarn' 31 | 32 | - run: yarn install --immutable 33 | - uses: nrwl/nx-set-shas@v4 34 | 35 | # Prepend any command with "nx-cloud record --" to record its logs to Nx Cloud 36 | # - run: yarn nx-cloud record -- echo Hello World 37 | # Nx Affected runs only tasks affected by the changes in this PR/commit. Learn more: https://nx.dev/ci/features/affected 38 | - run: yarn nx affected -t lint test build e2e 39 | -------------------------------------------------------------------------------- /.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 | .local.env 29 | .npmrc 30 | /.sass-cache 31 | /connect.lock 32 | /coverage 33 | /libpeerconnection.log 34 | npm-debug.log 35 | yarn-error.log 36 | .yarn/install-state.gz 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | 44 | # Local dev files 45 | .env 46 | .bashrc 47 | .nx 48 | .nx/cache 49 | .nx/workspace-data 50 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Add files here to ignore them from prettier formatting 2 | 3 | /dist 4 | /coverage 5 | 6 | /.nx/cache 7 | examples 8 | 9 | /.nx/workspace-data -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true 3 | } -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | docs: { 3 | autodocs: true 4 | }, 5 | 6 | addons: ["@storybook/addon-webpack5-compiler-swc", "@chromatic-com/storybook"] 7 | }; 8 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.verdaccio/.htpasswd: -------------------------------------------------------------------------------- 1 | test:$6FrCaT/v0dwE:autocreated 2020-03-25T19:10:50.254Z 2 | -------------------------------------------------------------------------------- /.verdaccio/config.yml: -------------------------------------------------------------------------------- 1 | # path to a directory with all packages 2 | storage: ../dist/local-registry/storage 3 | 4 | auth: 5 | htpasswd: 6 | file: ./htpasswd 7 | 8 | # a list of other known repositories we can talk to 9 | uplinks: 10 | npmjs: 11 | url: https://registry.npmjs.org/ 12 | maxage: 60m 13 | max_fails: 20 14 | fail_timeout: 2m 15 | yarn: 16 | url: https://registry.yarnpkg.com 17 | maxage: 60m 18 | max_fails: 20 19 | fail_timeout: 2m 20 | 21 | packages: 22 | '@*/*': 23 | # scoped packages 24 | access: $all 25 | publish: $all 26 | unpublish: $all 27 | proxy: npmjs 28 | 29 | '**': 30 | # allow all users (including non-authenticated users) to read and 31 | # publish all packages 32 | access: $all 33 | 34 | # allow all users (including non-authenticated users) to publish/publish packages 35 | publish: $all 36 | unpublish: $all 37 | 38 | # if package is not available locally, proxy requests to 'yarn' registry 39 | proxy: npmjs 40 | 41 | # log settings 42 | logs: 43 | type: stdout 44 | format: pretty 45 | level: warn 46 | 47 | publish: 48 | allow_offline: true # set offline to true to allow publish offline 49 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "nrwl.angular-console", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "firsttris.vscode-jest-runner" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | yarnPath: .yarn/releases/yarn-4.0.1.cjs 4 | -------------------------------------------------------------------------------- /apps/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/apps/.gitkeep -------------------------------------------------------------------------------- /apps/nx-workshop-e2e/.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 | -------------------------------------------------------------------------------- /apps/nx-workshop-e2e/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'nx-workshop-e2e', 4 | preset: '../../jest.preset.js', 5 | moduleFileExtensions: ['ts', 'js', 'html'], 6 | coverageDirectory: '../../coverage/apps/nx-workshop-e2e', 7 | globalSetup: '../../tools/scripts/start-local-registry.ts', 8 | globalTeardown: '../../tools/scripts/stop-local-registry.ts', 9 | }; 10 | -------------------------------------------------------------------------------- /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 | "tags": [], 7 | "implicitDependencies": ["nx-react-workshop"], 8 | "targets": { 9 | "e2e": { 10 | "executor": "@nx/jest:jest", 11 | "outputs": ["{workspaceRoot}/coverage/{projectRoot}"], 12 | "options": { 13 | "jestConfig": "apps/nx-workshop-e2e/jest.config.ts", 14 | "runInBand": true 15 | }, 16 | "dependsOn": ["^build"] 17 | }, 18 | 19 | "lint": { 20 | "executor": "@nx/eslint:lint" 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /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": ["jest.config.ts", "**/*.spec.ts", "**/*.d.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /docs/assets/game-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/game-demo.gif -------------------------------------------------------------------------------- /docs/assets/lab10_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab10_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab1_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab1_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab2_cmds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab2_cmds.png -------------------------------------------------------------------------------- /docs/assets/lab2_file_structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab2_file_structure.png -------------------------------------------------------------------------------- /docs/assets/lab2_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab2_result.png -------------------------------------------------------------------------------- /docs/assets/lab3_build_cmds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab3_build_cmds.png -------------------------------------------------------------------------------- /docs/assets/lab3_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab3_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab3_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab3_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab4_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab4_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab4_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab4_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab5_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab5_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab5_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab5_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab6_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab6_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab6_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab6_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab7_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab7_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/lab8_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab8_screenshot.png -------------------------------------------------------------------------------- /docs/assets/lab9_directory-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/lab9_directory-structure.png -------------------------------------------------------------------------------- /docs/assets/storybook.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/assets/storybook.gif -------------------------------------------------------------------------------- /docs/lab1/LAB.md: -------------------------------------------------------------------------------- 1 | # 💻 Lab 1 - generate an empty workspace 2 | 3 | ###### ⏰  Estimated time: 5-10 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Understand how to bootstrap a new Nx workspace** 8 | 9 | #### 📲 After this workshop, your file structure should look similar to this: 10 | 11 |
12 | File structure 13 | lab7 file structure 14 |
15 | 16 | ## 🏋️‍♀️ Steps: 17 | 18 | 1. Generate an empty Nx workspace for a fictional company called "The Board Game Hoard" 19 | 20 | 2. The workspace name should be `bg-hoard` 21 | 22 | 3. When prompted, select: 23 | - `None` as Stack (we will create apps manually) 24 | - An `Integrated` workspace layout 25 | - And `skip` configuring a `CI Provider` and `Remote Caching` for now (we'll set those up in a later lab) 26 | 27 | --- 28 | 29 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 30 | 31 | --- 32 | 33 | [➡️  Next lab ➡️](../lab2/LAB.md) 34 | -------------------------------------------------------------------------------- /docs/lab1/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### To create a new Nx workspace: 2 | 3 | ```shell 4 | npx create-nx-workspace bg-hoard --preset=apps --nx-cloud=skip 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab10/LAB.md: -------------------------------------------------------------------------------- 1 | ### 💻 Lab 10 - Generate Storybook stories for the shared ui component 2 | 3 | ###### ⏰  Estimated time: 10-15 minutes 4 | 5 | Let's explore some more Nx plugins by generating and running a storybook configuration for our shared store header. 6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Explore other Nx plugins to create a storybook configuration** 10 | 11 | #### 📲 After this workshop, you should have: 12 | 13 |
14 | App Screenshot 15 | No change in how the app looks! 16 |
17 | 18 | ## 🏋️‍♀️ Steps: 19 | 20 | 1. `yarn add @nx/storybook` or `npm i -S @nx/storybook` 21 |
22 | 23 | 2. Use the `@nx/react:storybook-configuration` generator to generate a storybook configuration for the `store-ui-shared` project 24 | 25 | ⚠️  Answer **YES** to all questions 26 |
27 | 28 | 3. Serve storybook! 29 | 30 |
31 | 🐳   Hint 32 | 33 | `nx storybook store-ui-shared` 34 | 35 |

36 | 37 | 4. Start typing in different titles and see how they appear in the header 38 | 39 | the header component running in storybook 40 | 41 |
42 | 43 | 5. Inspect what changed from the last time you committed, then commit your changes 44 |
45 | 46 | --- 47 | 48 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 49 | 50 | --- 51 | 52 | [➡️  Next lab ➡️](../lab11/LAB.md) 53 | -------------------------------------------------------------------------------- /docs/lab10/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Lab 11 - generate Storybook stories for the shared component 2 | 3 | ```shell 4 | nx generate @nx/react:storybook-configuration store-ui-shared 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab11/LAB.md: -------------------------------------------------------------------------------- 1 | ### 💻 Lab 11 - e2e test the shared component 2 | 3 | ###### ⏰  Estimated time: 5 minutes 4 | 5 | The storybook generator we invoked earlier also generated some tests. Let's try them out! 6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Take advantage of the e2e tests Nx generated earlier to test your app** 10 |
11 | 12 | ## 🏋️‍♀️ Steps: 13 | 14 | 1. Our previous command generated tests with stories as well. Let's run them: `nx storybook store-ui-shared` and in another terminal `nx test-storybook store-ui-shared` 15 | 16 | - The tests should fail 17 |
18 | 19 | 2. Open `libs/store/ui-shared/src/lib/header/header.stories.tsx` and **give the title a value**. 20 | 21 |
22 | 🐳 Hint 23 | ```ts 24 | 25 | args: { 26 | title: 'Welcome to Board Game Hoard'; 27 | } 28 | 29 | ``` 30 |
31 | 32 |
33 | 34 | 3. Now **fix the test** to check if it contains that value 35 | 36 |
37 | 🐳 Hint 38 | ```ts 39 | 40 | expect(canvas.getByText(/Welcome to Board Game Hoard/gi)).toBeTruthy(); 41 | 42 | ``` 43 |
44 | 45 |
46 | 47 | 4. **Re-run the tests** 48 |
49 | 50 | 5. **Inspect what changed** from the last time you committed, then **commit your changes** 51 |
52 | 53 | --- 54 | 55 | [➡️  Next lab ➡️](../lab12/LAB.md) 56 | -------------------------------------------------------------------------------- /docs/lab12/LAB.md: -------------------------------------------------------------------------------- 1 | # 💡 Lab 12 - Module boundaries 2 | 3 | ###### ⏰  Estimated time: 10-15 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Understand how to assign scopes and type tags to your libraries** 8 | - **How to specify boundaries around your tags and avoid circular dependencies in your repo** 9 | - **How to use linting to trigger warnings or errors when you are not respecting these boundaries** 10 |
11 | 12 | ## 🏋️‍♀️ Steps : 13 | 14 | 1. Open the `project.json` files for each project and **finish tagging the apps** accordingly: 15 | 16 | ``` 17 | // apps/store/project.json 18 | { 19 | "projectType": "application", 20 | "root": "apps/store", 21 | "sourceRoot": "apps/store/src", 22 | "prefix": "bg-hoard", 23 | "targets": { ... }, 24 | "tags": ["scope:store", "type:app"] 25 | } 26 | ``` 27 | 28 |
29 | 30 | 2. Open the root `.eslintrc.json`, find the `"@nx/enforce-module-boundaries"` rule and **set the `depConstraints`**: 31 | 32 | ``` 33 | "depConstraints": [ 34 | { 35 | "sourceTag": "scope:store", 36 | "onlyDependOnLibsWithTags": ["scope:store", "scope:shared"] 37 | }, 38 | .... <-- finish adding constraints for the tags we defined in the previous step 39 | ] 40 | ``` 41 | 42 |
43 | 44 | 3. **Run `nx run-many --target=lint --all --parallel`** 45 | 46 | 💡 `nx run-many` allows you run a specific target against a specific set of projects 47 | via the `--projects=[..]` option. However, you can also pass it the `--all` option 48 | to run that target against all projects in your workspace. 49 | 50 | 💡 `--parallel` launches all the `lint` processes in parallel 51 |
52 | 53 | 4. We talked about how importing a **Feature** lib should not be allowed from a 54 | **UI** lib. Let's **test our lint rules** by doing just that: - In `libs/store/ui-shared/src/lib/header/header.tsx` - Try to `import { StoreFeatureGameDetail } from '@bg-hoard/store-feature-game-detail';` 55 |
56 | 57 | 5. **Run linting** against all the projects again. 58 |
59 | 60 | 6. You should see the expected error. Great! You can now **delete the import** above. 61 |
62 | 63 | 7. We also talked about the importance of setting boundaries between your workspace scopes. Let's try and **import a `store` lib** from an `api` scope. - In `apps/api/src/main.ts` - Try to `import { formatRating } from '@bg-hoard/store-util-formatters';` 64 |
65 | 66 | 8. **Run linting** on all projects - you should see another expected error. 67 |
68 | 69 | 9. You can now **delete the import** above. 70 |
71 | 72 | 10. **Run linting** again and check if all the errors went away. 73 | 74 | 💡  Pass the suggested `--only-failed` option, so it doesn't relint everything. 75 | 76 | 11. **Commit everything** before moving on to the next lab 77 |
78 | 79 | --- 80 | 81 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 82 | 83 | --- 84 | 85 | [➡️  Next lab ➡️](../lab13/LAB.md) 86 | -------------------------------------------------------------------------------- /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 | 11 | ##### nx-enforce-module-boundaries rules: 12 | 13 | ```json 14 | "depConstraints": [ 15 | { 16 | "sourceTag": "scope:store", 17 | "onlyDependOnLibsWithTags": ["scope:store", "scope:shared"] 18 | }, 19 | { 20 | "sourceTag": "scope:api", 21 | "onlyDependOnLibsWithTags": ["scope:api", "scope:shared"] 22 | }, 23 | { 24 | "sourceTag": "type:feature", 25 | "onlyDependOnLibsWithTags": ["type:feature", "type:ui", "type:util"] 26 | }, 27 | { 28 | "sourceTag": "type:ui", 29 | "onlyDependOnLibsWithTags": ["type:ui", "type:util"] 30 | }, 31 | { 32 | "sourceTag": "type:util", 33 | "onlyDependOnLibsWithTags": ["type:util"] 34 | } 35 | ] 36 | ``` 37 | -------------------------------------------------------------------------------- /docs/lab13/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a `internal-plugin` plugin: 2 | 3 | ```shell script 4 | nx generate @nx/plugin:plugin internal-plugin --directory=libs/internal-plugin --minimal 5 | ``` 6 | 7 | #### Generate a `util-lib` generator: 8 | 9 | ```shell 10 | nx generate @nx/plugin:generator util-lib --directory libs/internal-plugin/src/generators/util-lib 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/generators'; 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/generators'; 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 - Generators - Modifying files 2 | 3 | ###### ⏰  Estimated time: 25-35 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Explore some more advanced, real-world usages of generators** 8 | - **Understand how to modify existing source code with generators** 9 |
10 | 11 | ## 🏋️‍♀️ Steps : 12 | 13 | 1. Generate another generator called `update-scope-schema` 14 |
15 | 16 | 2. As a start let's make it change the `defaultProject` from `store` to `api` in our `nx.json` file: 17 | 18 |
19 | 🐳   Hint 20 | 21 | - Refer to the [docs](https://nx.dev/nx-api/devkit/documents/updateJson) 22 | - Use this utility: 23 | - `import { updateJson } from '@nx/devkit';` 24 | - As always, the answer is in the [the solution](SOLUTION.md). Try a few different approaches on your own first. 25 |
26 | 27 | ⚠️  When you run the above, it might complain that you haven't supplied a `name`. Since 28 | we don't need this property in the generate, you can remove it from the schema. 29 |
30 | 31 | 3. Now that we had some practice with the `updateJson` util - Let's build something even more useful: 32 | 33 | - When large teams work in the same workspace, they will occasionally be adding new projects and hence, **new scope tags** 34 | - 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 35 | - We want to check if there is a new scope tag in any of our `project.json` files and update our generator schema 36 | - We can use the [`getProjects`](https://nx.dev/nx-api/devkit/documents/getProjects) util to read all the projects at once. 37 | - **BONUS:** Modify your generator so it fetches list of scopes from all the `project.json` files and updates the schema in util-lib with any new ones 38 | 39 | ⚠️ You can use the function provided in the Hint to extract the `scopes` 40 | 41 |
42 | 🐳 Hint 43 | 44 | ```typescript 45 | function getScopes(projectMap: Map) { 46 | const projects: any[] = Object.values(projectMap); 47 | const allScopes: string[] = projects 48 | .map((project) => 49 | project.tags 50 | // take only those that point to scope 51 | .filter((tag: string) => tag.startsWith('scope:')) 52 | ) 53 | // flatten the array 54 | .reduce((acc, tags) => [...acc, ...tags], []) 55 | // remove prefix `scope:` 56 | .map((scope: string) => scope.slice(6)); 57 | // remove duplicates 58 | return Array.from(new Set(allScopes)); 59 | } 60 | ``` 61 | 62 |
63 | 64 |
65 | 66 | 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: 67 | 68 | - Use `import { formatFiles } from '@nx/devkit';` 69 | - `await` this at the end of your generator 70 |

71 | 72 | 5. `index.ts` also has a `Schema` interface that should be updated. For modifying files that are not JSON we will use `host.read(path)` and `host.write(path, content)` methods. 73 | 74 | ⚠️ You can use the function provided in the Hint to replace the `scopes` 75 | 76 |
77 | 🐳 Hint 78 | 79 | ```typescript 80 | function replaceScopes(content: string, scopes: string[]): string { 81 | const joinScopes = scopes.map((s) => `'${s}'`).join(' | '); 82 | const PATTERN = /interface Schema \{\n.*\n.*\n\}/gm; 83 | return content.replace( 84 | PATTERN, 85 | `interface Schema { 86 | name: string; 87 | directory: ${joinScopes}; 88 | }` 89 | ); 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 generate app video-games --tags=scope:video-games 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. 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@v4 14 | - uses: bahmutov/npm-install@v1 15 | - run: npm run nx affected -- --target=build --base=origin/master --parallel 16 | test: 17 | runs-on: ubuntu-latest 18 | name: Testing affected apps 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: bahmutov/npm-install@v1 22 | - run: npm run nx affected -- --target=test --base=origin/master --parallel 23 | lint: 24 | runs-on: ubuntu-latest 25 | name: Linting affected apps 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: bahmutov/npm-install@v1 29 | - run: npm run nx affected -- --target=lint --base=origin/master --parallel 30 | e2e: 31 | runs-on: ubuntu-latest 32 | name: E2E testing affected apps 33 | steps: 34 | - uses: actions/checkout@v4 35 | - uses: bahmutov/npm-install@v1 36 | - run: npm run nx affected -- --target=e2e --base=origin/master --parallel 37 | ``` 38 | 39 | ##### Marking all projects as affected 40 | 41 | ```jsonc 42 | { 43 | "namedInputs": { 44 | // ... 45 | "sharedGlobals": [, /*..*/ "{workspaceRoot}/.github/workflows/ci.yml"] 46 | } 47 | } 48 | ``` 49 | -------------------------------------------------------------------------------- /docs/lab15/github_actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab15/github_actions.png -------------------------------------------------------------------------------- /docs/lab15/no_affected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab15/no_affected.png -------------------------------------------------------------------------------- /docs/lab15/store_affected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab15/store_affected.png -------------------------------------------------------------------------------- /docs/lab16/LAB.md: -------------------------------------------------------------------------------- 1 | # 🔌 Lab 16 - Distributed caching 2 | 3 | ###### ⏰  Estimated time: 10 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Understand the difference between local and distributed caching** 8 | - **Learn how to add NxCloud and enable distributed on an existing Nx workspace** 9 |
10 | 11 | ## 🏋️‍♀️ Steps : 12 | 13 | 1. Earlier in the workshop, we discussed about Nx's [local caching](https://nx.dev/concepts/mental-model#computation-hashing-and-caching) 14 | capabilities. Let's enable distributed caching. 15 | 16 | ``` 17 | nx connect 18 | ``` 19 | 20 | ![Nx Cloud Confirmation](./nx_cloud_enabled.png) 21 |
22 | 23 | 2. Inspect the changes added in `nx.json` - especially the access token. We'll explain that more in a bit! 24 |
25 | 26 | 3. **Very important:** Make sure, at this stage, you commit and push your changes: 27 | 28 | ``` 29 | # make sure you're on master 30 | git checkout master 31 | git add . && git commit -m "add nx cloud" 32 | git push origin master 33 | ``` 34 | 35 |
36 | 37 | 4. Run a build: `nx run-many --target=build --all` 38 | 39 | 🕑 Watch the process in the terminal - it might take a few seconds... 40 |
41 | 42 | 5. You'll see a link at the end, let's see what's there: 43 | 44 | ![Run Details Link](./run_details.png) 45 | 46 | We'll talk more about these links later! 47 |
48 | 49 | 6. Try to build all projects again: `nx run-many --target=build --all` 50 | 51 | ⚡ It should finish **much quicker** this time - because it just pulled from the local cache! 52 |
53 | 54 | 7. Let's try something different now - in a different folder on your machine, let's try and do a **fresh** of your repository: 55 | 56 | ``` 57 | # go into a new folder 58 | cd .. 59 | # clone your repo again 60 | git clone git@github.com:/.git test-distributed-caching 61 | cd test-distributed-caching 62 | # install dependencies 63 | yarn 64 | ``` 65 | 66 |
67 | 68 | 8. In your new instance, let's try and build again: `nx run-many --target=build --all` 69 | 70 | ⚡ It should be almost instant... 71 |
72 | 73 | 9. **But how?** You have no local cache: we just did a fresh pull of the repository. 74 | 75 | Check your terminal output - you should see this message: 76 | 77 | ![NxCloud cache pull](./distrib_caching_confirmation.png) 78 | 79 | That means that instead of rebuilding locally again, we just pulled from the distributed cache. 80 |
81 | 82 | 10. Let's try a different command - in the same folder you are in, try to run: 83 | 84 | ``` 85 | nx run-many --target=lint --all 86 | ``` 87 | 88 | 🕑 It should start the linting work, and take a few seconds... 89 | 90 | 91 |
92 | 93 | 11. Now let's go back to our main workshop repository and run: 94 | 95 | ``` 96 | nx run-many --target=lint --all 97 | ``` 98 | 99 | ⚡ It should pull again from the NxCloud cache...This is even works across laptops! CI will use it as well! 100 | 101 |
102 | 103 | --- 104 | 105 | [➡️  Next lab ➡️](../lab17/LAB.md) 106 | -------------------------------------------------------------------------------- /docs/lab16/distrib_caching_confirmation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab16/distrib_caching_confirmation.png -------------------------------------------------------------------------------- /docs/lab16/nx_cloud_enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab16/nx_cloud_enabled.png -------------------------------------------------------------------------------- /docs/lab16/run_details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab16/run_details.png -------------------------------------------------------------------------------- /docs/lab17/LAB.md: -------------------------------------------------------------------------------- 1 | # 🔍 Lab 17 - NxCloud GitHub bot 2 | 3 | ###### ⏰  Estimated time: 10 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Explore the NxCloud Run-Detail pages** 8 | - **Configure the NxCloud bot to get easy to read reports on the Nx checks performed during CI** 9 |
10 | 11 | ## 🏋️‍♀️ Steps : 12 | 13 | 1. **Enable the NxCloud GitHub bot** on your GitHub repository: [https://github.com/apps/nx-cloud](https://github.com/apps/nx-cloud) 14 |
15 | 16 | 2. Switch to a new branch: `git checkout -b nxcloud-bot` 17 |
18 | 19 | 4. Make a change (add a `console.log("...")` somewhere in `apps/store/src/app/app.tsx`) in the store app (so that it will trigger our affected commands in CI). 20 | 21 |
22 | 23 | 5. Commit everything and push your branch 24 |
25 | 26 | 6. Make a PR on GitHub 27 |
28 | 29 | 7. Once the checks finish you should see something similar to this: 30 | 31 | ![NxCloud Bot](./nx_cloud_bot.png) 32 |
33 | 34 | 8. 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: 35 | 36 | ![Nx Cloud project](./nx-cloud-projects.png) 37 | 38 | 🔥 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. 39 |
40 | 41 | 9. Have a look at the "Cache Hit" and "Cache Miss" filters. What do you think they do? 42 | 43 | ![Cache hit/miss](./cache_hit_miss.png) 44 |
45 | 46 | 10. 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! 47 |
48 | 49 | 11. Merge your PR into master and pull latest locally: 50 | 51 | ``` 52 | git checkout master 53 | git pull 54 | ``` 55 |
56 | 57 | 11. **BONUS**: [Have a look at some of the docs](https://nx.app/docs/configuring-ci) for setting up NxCloud on CI to see how the set-up might apply to your CI provider. 58 | 12. **BONUS**: Read this blog post on "Distributed Task Execution". We'll briefly talk about this after the lab. 59 | 60 | --- 61 | 62 | [➡️  Next lab ➡️](../lab18/LAB.md) 63 | -------------------------------------------------------------------------------- /docs/lab17/cache_hit_miss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab17/cache_hit_miss.png -------------------------------------------------------------------------------- /docs/lab17/nx-cloud-projects.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab17/nx-cloud-projects.png -------------------------------------------------------------------------------- /docs/lab17/nx_cloud_bot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/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 | ## 📚 Learning outcomes: 6 | 7 | - **Understand how to create custom targets via the "run-commands" workspace executor** 8 | - **Explore real-world usages of "run-commands" by creating a frontend "deploy" executor** 9 | - **Learn how to expose custom ENV variables to Nx** 10 |
11 | 12 | ## 🏋️‍♀️ Steps : 13 | 14 | 1. Make sure you are on the `main` branch 15 |
16 | 17 | 2. We'll use a CLI tool called [Surge](https://surge.sh/) to statically deploy the frontend: 18 | 19 | ```bash 20 | npm i -S surge 21 | ``` 22 | 23 |
24 | 25 | 3. Get the surge token (you'll need to create an account with an email and password): 26 | 27 | ``` 28 | npx surge token 29 | ``` 30 | 31 | ☝️ Copy the token you get 32 | 33 |
34 | 35 | 4. Let's use the Surge CLI to deploy our project: 36 | 37 | ```bash 38 | 39 | # make sure the project is built first - and we have something in dist 40 | 41 | nx build store 42 | 43 | # use surge to deploy whatever assets are in dist/apps/store 44 | 45 | surge dist/apps/store https://.surge.sh --token 46 | 47 | ``` 48 | 49 | ⚠️  Make sure you chose a **unique value** for your domain above, otherwise 50 | it will fail as you won't have permission to deploy to an existing one. 51 | 52 | ⚠️  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. 53 |
54 | 55 | 56 | 57 | 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: 58 | 59 | - under the `store` project 60 | - the "command" will be the same as the one you typed in the previous step 61 | 62 |
63 | 🐳   Hint 64 | 65 | Consult the run-commands generator docs [here](https://nx.dev/nx-api/workspace/generators/run-commands) 66 |

67 | 68 | 6. Use Git to inspect the changes in `project.json` and try to deploy the store using Nx! 69 |
70 | 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: 71 | 72 | 📁 Create a new file `apps/store/.local.env` 73 | 74 | 🔒 And let's add your secrets to it 75 | 76 | ``` 77 | SURGE_TOKEN= 78 | SURGE_DOMAIN_STORE=https://.surge.sh 79 | ``` 80 | 81 | ✅ Finally, update your "deploy" command, so that it loads the values from the ENV, using the `${ENV_VAR}` syntax. 82 | 83 |
84 | 🐳   Hint 85 | 86 | ```bash 87 | surge dist/apps/store ${SURGE_DOMAIN_STORE} --token ${SURGE_TOKEN} 88 | ``` 89 |

90 | 91 | 8. Now invoke the deploy target again, and check if it all still works. 92 | ⚠️  Note for Windows users: the command might fail, as we're trying to access env variables the Linux-way. 93 | 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): 94 | 95 | ```bash 96 | nx generate run-commands windows-deploy --project=store --command="surge dist/apps/store %SURGE_DOMAIN_STORE% --token %SURGE_TOKEN%" 97 | nx windows-deploy store 98 | ``` 99 | 100 |
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/latest/react/guides/environment-variables#loading-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-react-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 | "command": "surge dist/apps/store ${SURGE_DOMAIN_STORE} --token ${SURGE_TOKEN}" 24 | } 25 | ``` 26 | -------------------------------------------------------------------------------- /docs/lab19-alt/LAB.md: -------------------------------------------------------------------------------- 1 | # 🧲 Lab 19 Alternative - Creating and deploying a 2nd frontend 2 | 3 | ###### ⏰  Estimated time: 15-20 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Recap what you've learned about generating apps and creating custom executors with "run-commands"** 8 |
9 | 10 | ## 🏋️‍♀️ Steps : 11 | 12 | In this lab, we'll practice generating a 2nd frontend, using NextJS. This is in preparation for the next few labs, where we'll 13 | be deploying the two frontends independently in our GitHub Actions based Continuous Deployment setup. 14 | 15 | 1. We want to build a new Admin UI for out store. But we'll use NextJS as our framework of choice. 16 | **Generate a new NextJS app called "admin-ui"** 17 | You can use any configuration options you want. 18 | 19 | ⚠️  There will be fewer hints in this lab, but you can always use the [solution](SOLUTION.md) as a last resort. 20 |
21 | 22 | 2. We won't make any changes to it. Let's serve it to see if it looks okay locally. 23 |
24 | 25 | 3. For simplicity, we want to run it on Surge, **so let's export it as static assets for now**. Since we added a lot of files, also commit your changes. 26 | 27 | ⚠️  Look at the available "targets" for your new Next app in `project.json`. Make sure you deploy the "exported" sub-folder. 28 |
29 | 30 | 1. Following the same steps as [Lab 18](../lab18/LAB.md), add a `"deploy"` target to it. 31 | 32 | ⚠️️  **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. 33 | 34 | ⚠️  Hint: You can have a `.local.env` at the root of your workspace as well, for any variables that need to be shared. 35 | You can move your `SURGE_TOKEN` variable to the root, so it can be shared among your projects. [READ MORE](https://nx.dev/latest/react/guides/environment-variables#loading-environment-variables) 36 |
37 | 38 | 5. Try to deploy both apps and check if they still work. 39 | 40 | ⚠️️️  **BONUS:** Use a single NX command to tell it to deploy all projects in parallel. 41 | 42 |
43 | 44 | 6. Commit everything before moving on to the next lab 45 |
46 | 47 | 9. **BONUS** - Add proper scopes for your new apps in `project.json` and run your `update-scope-schema` workspace generator you created a few labs ago. 48 |
49 | 50 | --- 51 | 52 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 53 | 54 | --- 55 | 56 | [➡️  Next lab ➡️](../lab20-alt/LAB.md) 57 | -------------------------------------------------------------------------------- /docs/lab19-alt/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a React app 2 | 3 | ```shell 4 | yarn add @nx/next # or "npm i -S @nx/next" 5 | nx g @nx/next:app admin-ui 6 | nx serve admin-ui 7 | ``` 8 | 9 | ##### Exporting as static assets 10 | 11 | ```shell 12 | nx export admin-ui 13 | ``` 14 | 15 | ##### Adding a deploy config 16 | 17 | ```shell 18 | nx generate run-commands deploy --project=admin-ui --command="surge dist/apps/admin-ui/exported \${SURGE_DOMAIN_ADMIN_UI} --token \${SURGE_TOKEN}" 19 | ``` 20 | 21 | ##### Bonus 22 | 23 | ```shell 24 | nx generate @nx/plugin:generator add-deploy-target --directory libs/internal-plugin/src/generators/add-deploy-target 25 | ``` 26 | 27 | ![Folder structure](./solution-structure.png) 28 | 29 | `./files/.local.env`: 30 | 31 | ``` 32 | SURGE_DOMAIN_<%= undercaps(project) %>=https://<%= subdomain %>.surge.sh 33 | ``` 34 | 35 | `index.ts`: 36 | 37 | ```typescript 38 | import { 39 | Tree, 40 | formatFiles, 41 | installPackagesTask, 42 | generateFiles, 43 | } from '@nx/devkit'; 44 | import { runCommandsGenerator } from '@nx/workspace/generators'; 45 | import { join } from 'path'; 46 | 47 | interface Schema { 48 | project: string; 49 | subdomain: string; 50 | } 51 | 52 | export default async function (host: Tree, schema: Schema) { 53 | await runCommandsGenerator(host, { 54 | name: 'deploy', 55 | project: schema.project, 56 | command: `surge dist/apps/${ 57 | schema.project 58 | } \${SURGE_DOMAIN_${underscoreWithCaps( 59 | schema.project 60 | )}} --token \${SURGE_TOKEN}`, 61 | }); 62 | await generateFiles( 63 | host, 64 | join(__dirname, './files'), 65 | `apps/${schema.project}`, 66 | { 67 | tmpl: '', 68 | project: schema.project, 69 | subdomain: schema.subdomain, 70 | undercaps: underscoreWithCaps, 71 | } 72 | ); 73 | await formatFiles(host); 74 | return () => { 75 | installPackagesTask(host); 76 | }; 77 | } 78 | 79 | export function underscoreWithCaps(value: string): string { 80 | return value.replace(/-/g, '_').toLocaleUpperCase(); 81 | } 82 | ``` 83 | 84 | `schema.json`: 85 | 86 | ```json 87 | { 88 | "cli": "nx", 89 | "id": "add-deploy-target", 90 | "type": "object", 91 | "properties": { 92 | "project": { 93 | "type": "string", 94 | "description": "Project name to generate the deploy target for", 95 | "$default": { 96 | "$source": "argv", 97 | "index": 0 98 | } 99 | }, 100 | "subdomain": { 101 | "type": "string", 102 | "description": "Surge subdomain where you want it deployed.", 103 | "x-prompt": "What is the Surge subdomain you want it deployed under? (https://.surge.sh)" 104 | } 105 | }, 106 | "required": ["project", "subdomain"] 107 | } 108 | ``` 109 | 110 | ##### Deploy all projects in parallel 111 | 112 | ```shell 113 | nx run-many --all --target=deploy --parallel 114 | ``` 115 | -------------------------------------------------------------------------------- /docs/lab19-alt/solution-structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/docs/lab19-alt/solution-structure.png -------------------------------------------------------------------------------- /docs/lab19/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Creating an API builder 2 | 3 | ```shell 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 a React app 2 | 3 | ###### ⏰  Estimated time: 15-20 minutes 4 | 5 | In this lab we'll generate our first React application within the new monorepo. 6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Get familiar with generating new apps within your workspace using the Nx CLI** 10 | 11 | #### 📲 After this workshop, your app should look similar to this: 12 | 13 |
14 | App Screenshot 15 | screenshot of lab2 result 16 |
17 | 18 | ## 🏋️‍♀️ Steps: 19 | 20 | 1. Make sure you can run Nx commands: 21 | 22 | - try out `nx --version` and see if it outputs a version number 23 | - install the CLI globally: `npm i -g nx` 24 | - if you don't want to install it globally, use `npx/yarn/pnpm nx` (depending on the installed package manager) instead of `nx` in all the commands in the upcoming labs 25 | 26 | > Please make sure you are using the latest version of Nx (17+) 27 | >
28 | 29 | 2. Run `nx list` to see which plugins you have installed 30 |
31 | 32 | 3. Add the React plugin: `npm i -S @nx/react` (or `yarn add @nx/react` or `pnpm add @nx/react`) 33 |
34 | 35 | 4. Let's also add Material UI so we can use some of their components: `npm i -S @mui/material @emotion/react @emotion/styled` (or `yarn add ...` or `pnpm add ...`) 36 |
37 | 38 | 5. Use the [`@nx/react` plugin](https://nx.dev/nx-api/react/generators/application) to generate an React app called `store` in your new workspace 39 | 40 | ⚠️**Important:** Make sure you **add React Router**, select **SCSS** as a style, select **cypress** as E2E test runner, and use the **Webpack** bundler when asked! 41 | 42 |
43 | 🐳   Hint 44 | Nx generate cmd structure 45 |

46 | 47 | 6. Create a `fake-api.ts` file in your new app's `src` folder that returns an array of some games (you can just copy the code from [here](../../examples/lab2/apps/store/src/fake-api/index.ts)) 48 | 49 | ⏳**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 50 |
51 | 52 | 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 [.tsx](../../examples/lab2/apps/store/src/app/app.tsx) / [.scss](../../examples/lab2/apps/store/src/app/app.module.scss) - and replace the full contents of the files) 53 |
54 | 55 | 8. You can get the example game images from [here](../../examples/lab2/apps/store/src/assets) 56 | 57 | ⚠️  Make sure you put them in the correct folder 58 |
59 | 60 | 9. Serve the app: `nx serve store` 61 |
62 | 63 | 10. See your app live at [http://localhost:4200/](http://localhost:4200/) 64 |
65 | 66 | 11. Inspect what changed from the last time you committed, then commit your changes 67 |
68 | 69 | --- 70 | 71 | screenshot of lab2 result 72 | 73 | Your app should look similar to the screenshot above! 74 | 75 | Now we're starting to see some content! But the ratings also don't look that good - we'll fix those in **Lab 5**. 76 | 77 | --- 78 | 79 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 80 | 81 | --- 82 | 83 | [➡️  Next lab ➡️](../lab3/LAB.md) 84 | -------------------------------------------------------------------------------- /docs/lab2/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### To generate a new React application: 2 | 3 | ```shell 4 | nx generate @nx/react:application store --directory=apps/store # or `nx g app store --directory=apps/store` 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab20-alt/LAB.md: -------------------------------------------------------------------------------- 1 | # 🧲 Lab 20 Alternative - Mock Store 2 | 3 | ###### ⏰  Estimated time: 5 minutes 4 | 5 | ## 🏋️‍♀️ Steps : 6 | 7 | For now, our `store` project has no API when it is deployed. Hence, it is only displaying the header. 8 | 9 | 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) 10 |
11 | 12 | 2. Import it in your `apps/store/src/app/app.tsx` 13 | 14 |
15 | 🐳   Hint 16 | 17 | ```typescript 18 | import { getAllGames } from '../fake-api/index'; 19 | 20 | export const App = () => { 21 | 22 | return ( 23 | <> 24 |
25 |
26 |
27 | {getAllGames().map((x) => ( 28 | 29 | 30 | ``` 31 | 32 |

33 | 34 | 3. Build and deploy your `store` project. Your deployed version should now be showing some games. 35 | 36 | ⚠️  Clicking on games and displaying game details will still not work. We can fix that later. 37 | 38 | screenshot of lab20 result 39 |
40 | 41 | --- 42 | 43 | [➡️  Next lab ➡️](../lab21-alt/LAB.md) 44 | -------------------------------------------------------------------------------- /docs/lab20-alt/lab20_result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/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 | ## 📚 Learning outcomes: 6 | 7 | - **Configure the React app for production** 8 |
9 | 10 | ## 🏋️‍♀️ Steps: 11 | 12 | When we serve the Store and API locally, they work great, because of the configured 13 | proxy discussed in previous labs. The Store will think the API lives at the same address. 14 | 15 | When deployed separately however, they do not yet know about each other. Let's configure 16 | a production URL for the API. 17 | 18 | 1. In `apps/store/src/app/app.tsx`, inject an API URL if it's available as an env variable: 19 | 20 | ```ts 21 | fetch((process.env.NX_API_URL ?? '') + '/api/games'); 22 | ``` 23 | 24 | ⚠️  Nx automatically replaces any env var prefixed with `NX_` in your code. We are allowing devs to override the API URL above via an env variable. 25 |
26 | 27 | 2. Do the same in `libs/store/feature-game-detail/src/lib/game-detail/game-detail.tsx`: 28 | 29 | ```typescript 30 | fetch((process.env.NX_API_URL ?? '') + `/api/games/${gameId}`); 31 | ``` 32 | 33 |
34 | 35 | 3. Point your local apps to your Fly.io API. 36 | 37 | 1. Make sure your API is not running locally 38 | 2. Serve your app with `NX_API_URL=https://.fly.dev nx serve store` 39 | 3. You should see the games being loaded from Fly.io 40 |
41 | 42 | 4. Build the Store for production (make sure to make the `NX_API_URL` env var available when building) and trigger a deployment 43 |
44 | 45 | 5. Go to your Surge deployment URL - you should now see the full app with all the games. 46 |
47 | 48 | --- 49 | 50 | [➡️  Next lab ➡️](../lab21/LAB.md) 51 | -------------------------------------------------------------------------------- /docs/lab21-alt/LAB.md: -------------------------------------------------------------------------------- 1 | # 🎈 Lab 21 - Setting up CD for automatic deployment 2 | 3 | ###### ⏰  Estimated time: 10-20 minutes 4 | 5 | ## 📚 Learning outcomes: 6 | 7 | - **Understand how to configure a simple Continuous Deployment system using Nx and GitHub actions** 8 | - **Learn how to expose custom secrets on GitHub to your CD processes** 9 |
10 | 11 | ## 🏋️‍♀️ Steps : 12 | 13 | In this lab we'll be setting up GitHub actions to build and deploy our projects whenever changes go into the `main` branch. 14 | 15 | 1. Add a `.github/workflows/deploy.yml` file 16 |
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 | 22 | - build the `store` and `admin-ui` for production 23 | - deploy the `store` and `admin-ui` 24 | 25 | We'll start you off: 26 | 27 | ```yml 28 | name: Deploy Website 29 | 30 | on: 31 | push: 32 | branches: 33 | - main <-- workflow will run everytime we push or merge something to main 34 | jobs: 35 | build: 36 | runs-on: ubuntu-latest 37 | name: Deploying apps 38 | steps: .... <-- ADD THE STEPS HERE 39 | ``` 40 | 41 |
42 | 43 | 3. Our "deploy" targets are using some secret ENV variables though. We'll need to make these available on GitHub: 1. Go to your GitHub workshop repo 2. Click on **"Settings"** at the top 3. Then **"Secrets"** on the left menu bar 4. Add values for all the variables we've been keeping in `.local.env` files 44 | 45 | 46 | GitHub secrets 47 | 48 |
49 | 50 | 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): 51 | 52 | ```yml 53 | env: 54 | SURGE_DOMAIN_STORE: ${{ secrets.SURGE_DOMAIN_STORE }} 55 | SURGE_DOMAIN_ADMIN_UI: ${{ secrets.SURGE_DOMAIN_ADMIN_UI }} 56 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 57 | ``` 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: - Make a change to your AdminUI (maybe change the text in the header) - Make a change to your Store (maybe change the title in the header) 62 |
63 | 64 | 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) 65 |
66 | 67 | 7. You should see your new workflow start up under the "Actions" tab on your GitHub repo 68 |
69 | 70 | 8. Once it's done, navigate to your frontend Surge deployment URLs and test if you notice the new changes 71 |
72 | 73 | --- 74 | 75 | 🎓 If you get stuck, check out [the solution](SOLUTION.md) 76 | 77 | --- 78 | 79 | [➡️  Next lab ➡️](../lab22/LAB.md) 80 | -------------------------------------------------------------------------------- /docs/lab21-alt/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### GitHub CD setup 2 | 3 | ```yaml 4 | name: Deploy Website 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 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@v4 22 | - uses: bahmutov/npm-install@v1 23 | - run: npm run nx build store 24 | - run: npm run nx build admin-ui -- --configuration=production 25 | - run: npm run nx deploy store 26 | - run: npm run nx deploy admin-ui 27 | ``` 28 | -------------------------------------------------------------------------------- /docs/lab21-alt/github_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/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 | ## 📚 Learning outcomes: 6 | 7 | - **Understand how to configure a simple Continous Deployment system using Nx and GitHub actions** 8 | - **Learn how to expose custom secrets on GitHub to your CD processes** 9 | 10 | ## 🏋️‍♀️ Steps : 11 | 12 | 1. Add a `.github/workflows/deploy.yml` file 13 |
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 | 19 | - builds the `store` and `api` for production 20 | - deploys the `store` and `api` 21 | 22 | We'll start you off: 23 | 24 | ```yml 25 | name: Deploy Website 26 | 27 | on: 28 | push: 29 | branches: 30 | - main <-- workflow will run everytime we push or merge something to main 31 | jobs: 32 | build: 33 | runs-on: ubuntu-latest 34 | name: Deploying apps 35 | steps: .... <-- ADD THE STEPS HERE 36 | ``` 37 | 38 |
39 | 40 | 3. Our "deploy" targets are using some secret ENV variables though. We'll need to make these available on GitHub: 41 | 42 | 1. Go to your GitHub workshop repo 43 | 2. Click on **"Settings"** at the top 44 | 3. Then **"Secrets"** on the left menu bar 45 | 4. Add values for all the variables we've been keeping in `.local.env` files 46 | 47 | GitHub secrets 48 |
49 | 50 | 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): 51 | 52 | ```yml 53 | env: 54 | SURGE_DOMAIN_STORE: ${{ secrets.SURGE_DOMAIN_STORE }} 55 | SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }} 56 | FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} 57 | NX_API_URL: https://.fly.dev 58 | ``` 59 | 60 |
61 | 62 | 5. Since we'll be re-deploying, we want to test if we're looking at a new version of our code: 63 | 64 | - Make a change to your API (maybe change the name of one of the games) 65 | - Make a change to your Store (maybe change the title in the header) 66 |
67 | 68 | 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) 69 |
70 | 71 | 7. You should see your new workflow start up under the "Actions" tab on your GitHub repo 72 |
73 | 74 | 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) 75 |
76 | 77 | --- 78 | 79 | 🎓 If you get stuck, check out [the solution](SOLUTION.md) 80 | 81 | --- 82 | 83 | [➡️  Next lab ➡️](../lab22/LAB.md) 84 | -------------------------------------------------------------------------------- /docs/lab21/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### GitHub CD setup 2 | 3 | ```yaml 4 | name: Deploy Website 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 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 | NX_API_URL: https://.fly.dev 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | name: Deploying apps 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: bahmutov/npm-install@v1 24 | - run: npm run nx build store 25 | - run: npm run nx build api -- --configuration=production 26 | - run: npm run nx deploy store 27 | - run: npm run nx deploy api 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/lab21/github_secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/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 | ## 📚 Learning outcomes: 6 | 7 | - **Explore an advanced example of `nx affected` by deploying only the affected apps on the master branch** 8 | - **Understand how to configure the `base` commit for `nx affected` in a CD scenario** 9 |
10 | 11 | ## 🏋️‍♀️ Steps : 12 | 13 | In the previous labs we set up automatic deployments. But everytime we push to master, 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 actually changed, and need re-deploying. 14 | 15 | 1. Update your `deploy.yml` file so that it builds only the affected apps, and it deploys only the affected apps 16 | 17 | ⚠️  You can compare against the previous commit for now: `--base=HEAD~1` 18 | 19 |
20 | 21 | 2. If you haven't already, ensure you run your "affected" commands in parallel 22 |
23 | 24 | 3. Commit everything 25 |
26 | 27 | 4. Now make a change just to the `store`. Maybe update the title again - Commit and push - Inspect your workflow on the GitHub actions tab - it should only be building and deploying 28 | whatever changed **in the last commit**: only the Store. 29 |
30 | 31 | --- 32 | 33 | ⛔ The problem now is that it's always comparing against the last commit: 34 | 35 | - Let's say I make some changes to the API (or AdminUI) over a few commits - and I don't push them. 36 | - Then I make one small change to the Store, commit it, and push to master. 37 | - 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 38 | looking at the last commit, **it will only deploy the Store.** 👎 39 | 40 |
41 | There is also the problem of potential failures 🧨 42 | 43 | Now our setup is simple: it just builds. 44 | But let's say we wanted to run the E2E tests again before deploying - just to be extra safe! 45 | 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. 46 | I then fix the E2E tests, but because the API (or AdminUI) does not depend on its E2E tests, `nx affected` will not mark it for deployment. 47 | So even though we changed the API (or AdminUI), it did not get deployed. 48 |
49 | 50 | 💡 Solution: **last successful commit!** 51 | 52 | - If we constantly compare against **the last commit where all the affected apps got succesfully deployed** - we 53 | will never miss a deployment 54 | - In our case, "successfully deployed" means when our `deploy.yml` workflow completes without errors. That's a succesful commit! 55 | - Getting the last successful commit is different on each platform: 56 | - [Netlify has the `CACHED_COMMIT_REF`](https://docs.netlify.com/configure-builds/environment-variables/#git-metadata) 57 | - On CircleCI, we can use the `<< pipeline.git.base_revision >>` 58 | - For GitHub actions, we can use the `nrwl/nx-set-shas` action 59 | 60 | --- 61 | 62 | 5. Right after the `npm-install` step, let's trigger the action to get the last successful commit: 63 | 64 | ```yml 65 | - uses: bahmutov/npm-install@v1 66 | - uses: nrwl/nx-set-shas@v4 67 | with: 68 | main-branch-name: 'main' # remember to set this correctly 69 | ``` 70 | 71 |
72 | 73 | 6. You can now use the output from the above action in your affected commands: 74 | 75 | ```bash 76 | --base=${{ env.NX_BASE }} 77 | ``` 78 | 79 | 80 | 7. By default, the `actions/checkout` action only fetches the last commit (for efficiency). But since we now might want to compare against a larger range of commits, we need to tell it to fetch more: 81 | 82 | ```yaml 83 | - uses: actions/checkout@v4 84 | with: 85 | fetch-depth: 0 86 | ``` 87 | 88 | 8. Commit everything and push. Let it build. It should compare against the immediately previous commit (because your workflow ran against it, and it passed) 89 |
90 | 91 | 9. 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) 92 | 93 | > 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 master. 94 | > 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. 95 | >
96 | 97 | --- 98 | 99 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 100 | -------------------------------------------------------------------------------- /docs/lab22/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### The full deploy script 2 | 3 | ```yaml 4 | name: Deploy Website 5 | 6 | on: 7 | push: 8 | branches: 9 | - master 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 | NX_API_URL: https://.fly.dev 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | name: Deploying apps 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | - uses: bahmutov/npm-install@v1 26 | - uses: nrwl/nx-set-shas@v4 27 | with: 28 | main-branch-name: 'main' # remember to set this correctly 29 | - run: npm run nx affected -- --target=build --base=${{ env.NX_BASE }} --parallel --configuration=production 30 | - run: npm run nx affected -- --target=deploy --base=${{ env.NX_BASE }} --parallel 31 | ``` 32 | -------------------------------------------------------------------------------- /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-react-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-react-workshop` npm package: `yarn add -D @nrwl/nx-react-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-react-workshop` 36 | 37 |
38 | 🐳 Hint 39 | 40 | `nx migrate @nrwl/nx-react-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-react-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-react-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 | We'll build the app we just created, and look at what executors are and how to customize them. 6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Understand what a `target` and `executor` are** 10 | - **Invoking executors** 11 | - **Configure executors by passing them different options** 12 | - **Understand how an executor can invoke another executor** 13 | 14 | #### 📲 After this workshop, you should have: 15 | 16 |
17 | App Screenshot 18 | screenshot of lab3 result 19 |
20 | 21 | ## 🏋️‍♀️ Steps: 22 | 23 | 1. Build the app 24 | 25 |
26 | 🐳   Hint 27 | Nx executor command structure 28 |

29 | 30 | 2. You should now have a `dist` folder - let's open it up! 31 | 32 | - This is your whole app's output! If we wanted we could push this now to a server and it would all work. 33 | - Notice how we generated a `3rdpartylicenses.txt` file and how all files have hashes in suffix 34 | - Open one of the files, for example `main.{hash}.js` and look at it's contents. Notice how it's minified. 35 |
36 | 37 | 3. **Open up `apps/store/project.json`** and look at the object under `targets/build` 38 | 39 | - this is the **target**, and it has an **executor** option, that points to `@nx/webpack:webpack` 40 | - 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! 41 |
42 | 43 | 4. Send a flag to the executor so that it builds for development 44 | 45 |
46 | 🐳   Hint 47 | 48 | `--configuration=development` 49 | 50 |

51 | 52 | 5. Open up the `dist` folder again - notice how the `3rdpartylicenses.txt` file is gone, as per the "development" configuration in `project.json`. Also notice how filenames no longer have hashed suffixes. Open one of the files, for example `main.{hash}.js`. Notice how its content is now fully readable and there are sourcemaps attached to each of the compiled files. 53 |
54 | 55 | 6. The **serve** target (located a bit lower in `project.json`) also contains a executor, that _uses_ the output from the **build** target 56 |
57 | 58 | --- 59 | 60 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 61 | 62 | --- 63 | 64 | [➡️  Next lab ➡️](../lab3.1/LAB.md) 65 | -------------------------------------------------------------------------------- /docs/lab3/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### To build the app for 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 | ## 📚 Learning outcomes: 8 | 9 | - **Get familiar with generating project specific component libraries inside a folder** 10 | 11 | #### 📲 After this workshop, you should have: 12 | 13 |
14 | App Screenshot 15 | screenshot of lab4 result 16 |
17 | 18 | ## 🏋️‍♀️ Steps: 19 | 20 | 1. Stop the `nx serve` 21 |
22 | 23 | 2. Generate a new empty React library called `store-ui-shared` in the `libs/store/ui-shared` folder. When asked, choose `jest` as test runner, the `rollup` as a bundler and `as-provided` naming convention. 24 | 25 |
26 | 🐳   Hint 27 | 28 | - 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` 29 | - use the `--help` command to figure out how to generate it in a **directory** and that it doesn’t create default component 30 | 31 |

32 | 33 | 3. Generate a new React component, called `header`, inside the `src/lib` folder of the library you just created 34 | 35 | ⚠️  Play around with the generator options so that the generated component is automatically **exported** from the lib's module 36 | 37 |
🐳   Hint 38 | 39 | 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** and skip the component generation 40 | 41 |

42 | 43 | 4. Replace the `header` component's [code](../../examples/lab4/libs/store/ui-shared/src/lib/header/header.tsx) 44 |
45 | 46 | 5. Let's use the new shared header component we created 47 | 48 | - Add your new component to `apps/store/src/app/app.tsx` 49 | 50 |
🐳   Hint 51 | 52 | ```typescript 53 | import { Header } from '@bg-hoard/store-ui-shared'; 54 | ``` 55 | 56 | ```html 57 |
58 | 59 |
60 | ``` 61 | 62 | Wrap the App component in a fragment (`<>` and ``) 63 | 64 |
65 | 66 | ⚠️  You might need to restart the TS compiler in your editor (`CTRL+SHIFT+P` in VSCode and search for `Restart Typescript`) 67 |
68 | 69 | 6. Serve the project and test the changes 70 |
71 | 72 | 7. Run the command to inspect the dependency graph - What do you see? (Remember to "Show all projects" in left sidebar) 73 |
74 | 🐳   Hint 75 | 76 | ```bash 77 | nx graph 78 | ``` 79 | 80 |

81 | 82 | 8. Inspect what changed from the last time you committed, then commit your changes 83 |
84 | 85 | --- 86 | 87 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 88 | 89 | --- 90 | 91 | [➡️  Next lab ➡️](../lab5/LAB.md) 92 | -------------------------------------------------------------------------------- /docs/lab4/SOLUTION.md: -------------------------------------------------------------------------------- 1 | #### Generate a new lib: 2 | 3 | ```bash 4 | nx generate @nx/react:lib store-ui-shared --directory=libs/store/ui-shared --no-component 5 | ``` 6 | 7 | #### Generate a new component in a project: 8 | 9 | ```bash 10 | nx generate @nx/react:component header --export --directory=libs/store/ui-shared/src/lib/header 11 | ``` 12 | -------------------------------------------------------------------------------- /docs/lab5/LAB.md: -------------------------------------------------------------------------------- 1 | ### 💻 Lab 5 - Generate a utility lib 2 | 3 | ###### ⏰  Estimated time: 5-10 minutes 4 | 5 | Let's fix the ratings! They don't look that good and they could benefit from some formatting. 6 | 7 | We will create a shared utility lib where we'll add our formatters and see how to import them in our components afterwards. 8 | 9 | ## 📚 Learning outcomes: 10 | 11 | - **Get familiar with generating project specific, framework agnostic utility libs** 12 | 13 | #### 📲 After this workshop, you should have: 14 | 15 |
16 | App Screenshot 17 | screenshot of lab5 result 18 |
19 | 20 | ## 🏋️‍♀️ Steps: 21 | 22 | 1. Stop the `nx serve` 23 |
24 | 25 | 2. Use the `@nx/js` package to generate another lib in the `libs/store` folder - let's call it `util-formatters`. 26 |
27 | 28 | 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` 29 |
30 | 31 | 4. Use it in your frontend project to format the rating for each game 32 | 33 |
34 | 🐳   Hint 35 | 36 | `app.tsx`: 37 | 38 | ```ts 39 | import { formatRating } from '@bg-hoard/store-util-formatters'; 40 | ``` 41 | 42 | ```html 43 | Rating: {formatRating(x.rating)} 44 | ``` 45 | 46 |

47 | 48 | 5. Serve the store app - notice how the ratings are formatted. 49 |
50 | 51 | 6. Launch the dependency graph - notice how the app depends on two libs now. 52 |
53 | 54 | 7. Inspect what changed from the last time you committed, then commit your changes 55 |
56 | 57 | --- 58 | 59 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 60 | 61 | --- 62 | 63 | [➡️  Next lab ➡️](../lab6/LAB.md) 64 | -------------------------------------------------------------------------------- /docs/lab5/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a framework agnostic lib 2 | 3 | ```bash 4 | nx generate @nx/js:lib store-util-formatters --directory=libs/store/util-formatters 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab6/LAB.md: -------------------------------------------------------------------------------- 1 | ### 💻 Lab 6 - Generate a route lib 2 | 3 | ###### ⏰  Estimated time: 15-25 minutes 4 | 5 | We'll look at more advanced usages of the `@nx/react` 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! 6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Get familiar with more advanced usages of Nx generators to create a React route lib** 10 | 11 | #### 📲 After this workshop, you should have: 12 | 13 |
14 | App Screenshot 15 | screenshot of lab6 result 16 |
17 | 18 | ## 🏋️‍♀️ Steps: 19 | 20 | 1. Stop `nx serve` 21 |
22 | 23 | 2. Use the `@nx/react:lib` generator to generate a new routing library called `feature-game-detail` that: 24 | 25 | - lives under `libs/store` 26 | - its parent routing app is `store` 27 | 28 | ⚠️  **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) 29 |
30 | 31 | 3. Change the routing path in `apps/store/src/app/app.tsx` to pick up the game ID from the URL 32 | 33 |
34 | 🐳   Hint 35 | 36 | ```ts 37 | // replace routes block with 38 | 39 | } />; 40 | 41 | ``` 42 | 43 |

44 | 45 | 4. Populate your new component with the provided files: `store-feature-game-detail.`[tsx](../../examples/lab6/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.tsx) / [scss](../../examples/lab6/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.module.scss) 46 |
47 | 48 | 5. Make clicking on each card in the `apps/store/src/app/app.tsx` route to the `game/:id` with the game's ID: 49 | 50 |
51 | 🐳   Hint 52 | 53 | ```tss 54 | // add a Link around the card element 55 | 56 | 57 | 58 | ``` 59 | 60 |

61 | 62 | 6. Serve your app again, click on some games, and compare with this screenshot: 63 | 64 | screenshot of lab6 result
65 | 66 | 7. Launch the dependency graph and see what's been added 67 |
68 | 69 | 8. Inspect what changed from the last time you committed, then commit your changes 70 |
71 | 72 | --- 73 | 74 | 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! 75 | 76 | --- 77 | 78 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 79 | 80 | --- 81 | 82 | [➡️  Next lab ➡️](../lab7/LAB.md) 83 | -------------------------------------------------------------------------------- /docs/lab6/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a route lib in a specific directory that is pre-configured with a certain parent app 2 | 3 | ```bash 4 | nx generate @nx/react:library store-feature-game-detail --directory=libs/store/feature-game-detail --appProject=store 5 | ``` 6 | -------------------------------------------------------------------------------- /docs/lab7/LAB.md: -------------------------------------------------------------------------------- 1 | ### 💻 Lab 7 - Add an Express API 2 | 3 | ###### ⏰  Estimated time: 10-15 minutes 4 | 5 | Up until now we've had a single app in our repository, and a few other libs that it uses. 6 | 7 | But remember how we created that `fake-api` way back in the second lab, that only our `store` app can access? 8 | 9 | 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/express` plugin to easily generate everything we need. 10 | 11 | All the Express specific code for serving the games is provided in the solution. 12 | 13 | ## 📚 Learning outcomes: 14 | 15 | - **Explore other plugins in the Nx ecosystem** 16 | 17 | #### 📲 After this workshop, you should have: 18 | 19 |
20 | App Screenshot 21 | No change in how the app looks! 22 |
23 | 24 | ## 🏋️‍♀️ Steps: 25 | 26 | 1. Stop any running `nx serve` instance 27 |
28 | 29 | 2. `yarn add @nx/express` or `npm i -S @nx/express` 30 |
31 | 32 | 3. Generate a new Express app, called `api` 33 | 34 | ⚠️  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)
35 | 36 | 4. Copy the code from the `fake api` to a new file called `apps/api/src/app/`[games.repository.ts](../../examples/lab7/apps/api/src/app/games.repository.ts) 37 |
38 | 39 | 5. Update the Express [main.ts](../../examples/lab7/apps/api/src/main.ts) to use the repository data 40 |
41 | 42 | 6. Let's now inspect the dependency graph! 43 |
44 | 45 | 7. Inspect what changed from the last time you committed, then commit your changes 46 |
47 | 48 | --- 49 | 50 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 51 | 52 | --- 53 | 54 | [➡️  Next lab ➡️](../lab8/LAB.md) 55 | -------------------------------------------------------------------------------- /docs/lab7/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a new Express API app, and configure the proxy to the `store` project 2 | 3 | `nx generate @nx/express:application api --directory=apps/store/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 | Now that we have a proper API, we can remove the `fake-api` created earlier and make proper HTTP requests. We'll also look at how the Nrwl Express generators created a helpful proxy configuration for us. 6 | 7 | ## 📚 Learning outcomes: 8 | 9 | - **Learn how to connect frontend and backend apps in an Nx workspace** 10 | 11 | #### 📲 After this workshop, you should have: 12 | 13 |
14 | App screenshot 15 | screenshot of lab8 result 16 |
17 | 18 | ## 🏋️‍♀️ Steps: 19 | 20 | 1. We can now delete the `fake-api` from the `store` app 21 |
22 | 23 | 2. Use `fetch` in a `useEffect` hook in the [app.tsx](../../examples/lab8/apps/store/src/app/app.tsx) component and call your new API as an _HTTP request_. We also added a local state to track changes. 24 | 25 | ⚠️  _Notice how we assume it will be available at `/api` (more on that below)_ 26 |
27 | 28 | 3. Run `nx serve api` 29 | 30 | ⚠️  Notice the _PORT_ number 31 |
32 | 33 | 4. In a different tab, run `nx serve store` 34 | 35 | ⚠️  Again, notice the _PORT_ number 36 |
37 | 38 | 5. Everything should still look/function the same! 39 | 40 | 🎓  You can inspect your Network tab in the dev tools and notice an XHR request made to `http://localhost:4200/api/games` 41 |
42 | 43 | --- 44 | 45 | 🎓   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. 46 | This helps you avoid CORS issues while developing locally. 47 | 48 | --- 49 | 50 | **Now let's load the full game in our routed component!** 51 | 52 | 6. Inside the `libs/store/feature-game-detail/src/lib/game-detail` folder, replace the following files: 53 | 54 | - [tsx](../../examples/lab8/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.tsx) / [module.scss](../../examples/lab8/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.module.scss) 55 | 56 | ⚠️  Notice how we're using the shared `formatRating()` function in our routed component as well! 57 |
58 | 59 | 7. Your component should look similar to the provided screenshot! (you might need to restart your `nx serve store` so the new button styles can be copied over) 60 |
61 | 62 | 8. Inspect what changed from the last time you committed, then commit your changes 63 |
64 | 65 | --- 66 | 67 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 68 | 69 | --- 70 | 71 | [➡️  Next lab ➡️](../lab9/LAB.md) 72 | -------------------------------------------------------------------------------- /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 | Now our dependency 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. 6 | 7 | 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. 8 | 9 | ## 📚 Learning outcomes: 10 | 11 | - **Explore other real-world examples of creating shared libs for a specific project** 12 | - **Learn to use the `move` generator** 13 | 14 | #### 📲 After this workshop, you should have: 15 | 16 |
17 | App Screenshot 18 | No change in how the app looks! 19 |
20 | 21 | ## 🏋️‍♀️ Steps: 22 | 23 | 1. Stop serving both the API and the frontend 24 |
25 | 26 | 2. Generate a new `@nx/js` lib called `util-interface` inside the `libs/api` folder. 27 | 28 | ⚠️  It's **important** that we create it in the `/api` folder for now 29 |
30 | 31 | 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) 32 |
33 | 34 | 4. Import it in the API service: `apps/api/src/app/games.repository.ts` 35 | 36 | ⚠️  You might need to restart the Typescript compiler in your editor 37 | 38 |
39 | 🐳   Hint 40 | 41 | ```typescript 42 | import { Game } from '@bg-hoard/api-util-interface'; 43 | const games: Game[] = [...]; 44 | ``` 45 | 46 |

47 | 48 | 5. Build the API and make sure there are no errors 49 | 50 |
51 | 🐳   Hint 52 | 53 | ```shell 54 | nx build api 55 | ``` 56 | 57 |

58 | 59 | 6. Inspect the dependency graph 60 |
61 | 62 | 7. Make sure to commit everything before proceeding! 63 |
64 | 65 | --- 66 | 67 | Our frontend store keeps a list of `Game`s in state: 68 | 69 | ```typescript 70 | const [state, setState] = useState<{ 71 | data: any[]; 72 | loadingState: 'success' | 'error' | 'loading'; 73 | }>({ 74 | data: [], 75 | loadingState: 'success', 76 | }); 77 | ``` 78 | 79 | But it's currently typed to `any` - so our component has no idea about the shape of the objects it uses! 80 | 81 | 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! 82 | 83 | --- 84 | 85 | 8. Use the `@nx/workspace:move` generator to move the interface lib created above into the root `/libs` folder 86 | 87 | ⚠️  Make sure you use the `--dry-run` flag until you're confident your command is correct 88 | 89 |
90 | 🐳   Hint 1 91 | Nx generate cmd structure 92 |
93 | 94 |
95 | 🐳   Hint 2 96 | 97 | Use the `--help` command to figure out how to target a specific **project** 98 | Alternatively, check out the [docs](https://nx.dev/latest/react/react/move#move) 99 | 100 |
101 | 102 |
103 | 104 | 🐳   Hint 3 105 | 106 | Your library name is `api-util-interface` - to move it to root, its new name needs to be `util-interface` 107 | 108 |

109 | 110 | 9. We can now import it in the frontend components and use it when making the `http` request: 111 | 112 |
113 | 🐳   Hint 114 | 115 | Frontend store shell app: `apps/store/src/app/app.tsx` 116 | 117 | ```typescript 118 | import { Game } from '@bg-hoard/api-util-interface'; 119 | 120 | const [state, setState] = useState<{ 121 | data: Game[]; 122 | loadingState: 'success' | 'error' | 'loading'; 123 | }>({ 124 | data: [], 125 | loadingState: 'success', 126 | }); 127 | ``` 128 | 129 | *** 130 | 131 | Routed game detail component: `libs/store/feature-game-detail/src/lib/game-detail/game-detail.tsx` 132 | 133 | ```typescript 134 | const [state, setState] = useState<{ 135 | data: Partial; 136 | loadingState: 'success' | 'error' | 'loading'; 137 | }>({ 138 | data: {}, 139 | loadingState: 'success', 140 | }); 141 | ``` 142 | 143 |
144 | 145 | ⚠️  Notice how we didn't have to update the imports in the API. The `move` generator took care of that for us! 146 | 147 |
148 | 149 | 10. Trigger a build of both the store and the API projects and make sure it passes 150 |
151 | 152 | 11. Inspect the dependency graph 153 |
154 | 155 | 12. Inspect what changed from the last time you committed, then commit your changes 156 |
157 | 158 | --- 159 | 160 | 🎓  If you get stuck, check out [the solution](SOLUTION.md) 161 | 162 | --- 163 | 164 | [➡️  Next lab ➡️](../lab10/LAB.md) 165 | -------------------------------------------------------------------------------- /docs/lab9/SOLUTION.md: -------------------------------------------------------------------------------- 1 | ##### Generate a new type lib for the API 2 | 3 | ```shell 4 | nx generate @nx/js:lib api-util-interface --directory=libs/api/util-interface 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.module.scss: -------------------------------------------------------------------------------- 1 | .games-layout { 2 | display: flex; 3 | justify-content: space-between; 4 | margin-bottom: 20px; 5 | } 6 | 7 | .container { 8 | max-width: 800px; 9 | margin: 50px auto; 10 | } 11 | 12 | .game-card { 13 | max-width: 250px; 14 | display: flex; 15 | flex-direction: column; 16 | justify-content: space-between; 17 | 18 | .game-rating { 19 | padding-top: 10px; 20 | } 21 | } 22 | 23 | .center-content { 24 | display: flex; 25 | justify-content: center; 26 | } 27 | 28 | .game-details { 29 | display: flex; 30 | flex-direction: column; 31 | margin: 0; 32 | } 33 | 34 | .game-card-media { 35 | height: 140px; 36 | } 37 | -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import styles from './app.module.scss'; 2 | import { getAllGames } from '../fake-api'; 3 | 4 | import Card from '@mui/material/Card'; 5 | import CardActionArea from '@mui/material/CardActionArea'; 6 | import CardContent from '@mui/material/CardContent'; 7 | import CardMedia from '@mui/material/CardMedia'; 8 | import Typography from '@mui/material/Typography'; 9 | 10 | export const App = () => { 11 | return ( 12 |
13 |
14 | {getAllGames().map((x) => ( 15 | 16 | 17 | 22 | 23 | 24 | {x.name} 25 | 26 | 27 | {x.description} 28 | 29 | 35 | Rating: {x.rating} 36 | 37 | 38 | 39 | 40 | ))} 41 |
42 |
43 | ); 44 | }; 45 | 46 | export default App; 47 | -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/assets/beans.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/examples/lab2/apps/store/src/assets/beans.png -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/assets/cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/examples/lab2/apps/store/src/assets/cat.png -------------------------------------------------------------------------------- /examples/lab2/apps/store/src/assets/chess.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/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.tsx: -------------------------------------------------------------------------------- 1 | import AppBar from '@mui/material/AppBar'; 2 | import Toolbar from '@mui/material/Toolbar'; 3 | import Typography from '@mui/material/Typography'; 4 | 5 | export interface HeaderProps { 6 | title: string; 7 | } 8 | 9 | export const Header = ({ title }: HeaderProps) => { 10 | return ( 11 | 12 | 13 | 14 | {title} 15 | 16 | 17 | 18 | ); 19 | }; 20 | 21 | export default Header; 22 | -------------------------------------------------------------------------------- /examples/lab5/libs/store/util-formatters/src/lib/store-util-formatters.ts: -------------------------------------------------------------------------------- 1 | export function formatRating(rating = 0) { 2 | return `${Math.round(rating * 100) / 10} / 10`; 3 | } 4 | -------------------------------------------------------------------------------- /examples/lab6/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.module.scss: -------------------------------------------------------------------------------- 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/store-feature-game-detail/store-feature-game-detail.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from 'react-router-dom'; 2 | import styles from './store-feature-game-detail.module.scss'; 3 | 4 | import Card from '@mui/material/Card'; 5 | import CardActionArea from '@mui/material/CardActionArea'; 6 | import CardContent from '@mui/material/CardContent'; 7 | import Typography from '@mui/material/Typography'; 8 | 9 | /* eslint-disable-next-line */ 10 | export interface StoreFeatureGameDetailProps {} 11 | 12 | export function StoreFeatureGameDetail(props: StoreFeatureGameDetailProps) { 13 | const params = useParams(); 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | {params['id']} 21 | 22 | 23 | 24 | 25 |
26 | ); 27 | }; 28 | 29 | export default StoreFeatureGameDetail; 30 | -------------------------------------------------------------------------------- /examples/lab7/apps/api/src/app/games.repository.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/lab7/apps/api/src/main.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not a production server yet! 3 | * This is only a minimal backend to get started. 4 | */ 5 | 6 | import express from 'express'; 7 | import { getAllGames, getGame } from './app/games.repository'; 8 | 9 | const app = express(); 10 | 11 | app.get('/api/games', (req, res) => { 12 | res.send(getAllGames()); 13 | }); 14 | 15 | app.get('/api/games/:id', (req, res) => { 16 | return res.send(getGame(req.params.id)); 17 | }); 18 | 19 | const port = process.env.port || 3000; 20 | const server = app.listen(port, () => { 21 | console.log(`Listening at http://localhost:${port}/api`); 22 | }); 23 | server.on('error', console.error); 24 | -------------------------------------------------------------------------------- /examples/lab8/apps/store/src/app/app.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { Routes, Route, Link } from 'react-router-dom'; 3 | 4 | import Card from '@mui/material/Card'; 5 | import CardActionArea from '@mui/material/CardActionArea'; 6 | import CardContent from '@mui/material/CardContent'; 7 | import CardMedia from '@mui/material/CardMedia'; 8 | import Typography from '@mui/material/Typography'; 9 | 10 | import styles from './app.module.scss'; 11 | import { Header } from '@bg-hoard/store-ui-shared'; 12 | import { formatRating } from '@bg-hoard/store-util-formatters'; 13 | import { StoreFeatureGameDetail } from '@bg-hoard/store-feature-game-detail'; 14 | 15 | export const App = () => { 16 | const [state, setState] = useState<{ 17 | data: any[]; 18 | loadingState: 'success' | 'error' | 'loading'; 19 | }>({ 20 | data: [], 21 | loadingState: 'success', 22 | }); 23 | 24 | useEffect(() => { 25 | setState((state) => ({ 26 | ...state, 27 | loadingState: 'loading', 28 | })); 29 | fetch('/api/games') 30 | .then((x) => x.json()) 31 | .then((res) => { 32 | setState((state) => ({ 33 | ...state, 34 | data: res, 35 | loadingState: 'success', 36 | })); 37 | }) 38 | .catch((err) => { 39 | setState((state) => ({ 40 | ...state, 41 | loadingState: 'error', 42 | })); 43 | }); 44 | }, []); 45 | 46 | return ( 47 | <> 48 |
49 |
50 |
51 | {state.loadingState === 'loading' 52 | ? 'Loading...' 53 | : state.loadingState === 'error' 54 | ? '
Error retrieving data
' 55 | : state.data.map((x) => ( 56 | 57 | 58 | 59 | 64 | 65 | 66 | {x.name} 67 | 68 | 73 | {x.description} 74 | 75 | 81 | Rating: {formatRating(x.rating)} 82 | 83 | 84 | 85 | 86 | 87 | ))} 88 |
89 | 90 | } />; 91 | 92 |
93 | 94 | ); 95 | }; 96 | 97 | export default App; 98 | -------------------------------------------------------------------------------- /examples/lab8/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.module.scss: -------------------------------------------------------------------------------- 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 | 26 | .container { 27 | max-width: 800px; 28 | margin: 50px auto; 29 | } 30 | 31 | .game-rating { 32 | padding-top: 10px; 33 | } 34 | 35 | .game-card-media { 36 | height: 200px; 37 | float: left; 38 | width: 300px; 39 | border-radius: 10px; 40 | margin: 0 16px 20px; 41 | } 42 | -------------------------------------------------------------------------------- /examples/lab8/libs/store/feature-game-detail/src/lib/store-feature-game-detail/store-feature-game-detail.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react'; 2 | import { useParams } from 'react-router-dom'; 3 | import styles from './game-detail.module.scss'; 4 | 5 | import Button from '@mui/material/Button'; 6 | import Card from '@mui/material/Card'; 7 | import CardActions from '@mui/material/CardActions'; 8 | import CardHeader from '@mui/material/CardHeader'; 9 | import CardContent from '@mui/material/CardContent'; 10 | import Typography from '@mui/material/Typography'; 11 | import CardMedia from '@mui/material/CardMedia'; 12 | import { formatRating } from '@bg-hoard/store-util-formatters'; 13 | 14 | /* eslint-disable-next-line */ 15 | export interface StoreFeatureGameDetailProps {} 16 | 17 | export function StoreFeatureGameDetail(props: StoreFeatureGameDetailProps) { 18 | const [state, setState] = useState<{ 19 | data: any; 20 | loadingState: 'success' | 'error' | 'loading'; 21 | }>({ 22 | data: {}, 23 | loadingState: 'success', 24 | }); 25 | const params = useParams(); 26 | 27 | useEffect(() => { 28 | setState({ 29 | ...state, 30 | loadingState: 'loading', 31 | }); 32 | const gameId = params['id']; 33 | fetch(`/api/games/${gameId}`) 34 | .then((x) => x.json()) 35 | .then((res) => { 36 | setState({ 37 | ...state, 38 | data: res, 39 | loadingState: 'success', 40 | }); 41 | }) 42 | .catch((err) => { 43 | setState({ 44 | ...state, 45 | loadingState: 'error', 46 | }); 47 | }); 48 | }, [params['id']]); 49 | 50 | return ( 51 |
52 | {state.loadingState === 'loading' ? ( 53 | 'Loading...' 54 | ) : state.loadingState === 'error' ? ( 55 |
Error fetching data
56 | ) : state.data == null ? ( 57 | '' 58 | ) : ( 59 | 60 | 61 | {state.data.image ? ( 62 | 67 | ) : null} 68 | 69 | 70 | {state.data.description} 71 | 72 | 78 | Rating: {formatRating(state.data.rating)} 79 | 80 | 81 | 82 | 85 | 86 | 87 | 88 | )} 89 |
90 | ); 91 | } 92 | 93 | export default StoreFeatureGameDetail; 94 | -------------------------------------------------------------------------------- /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 | import { getJestProjects } from '@nx/jest'; 2 | 3 | export default { 4 | projects: [...getJestProjects(), '/apps/nx-workshop-e2e'], 5 | }; 6 | -------------------------------------------------------------------------------- /jest.preset.js: -------------------------------------------------------------------------------- 1 | const nxPreset = require('@nx/jest/preset').default; 2 | 3 | module.exports = { 4 | ...nxPreset, 5 | }; 6 | -------------------------------------------------------------------------------- /libs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nrwl/nx-react-workshop/3f15c53be9d9b4cacb0e44dc62d242393f7995cd/libs/.gitkeep -------------------------------------------------------------------------------- /libs/nx-react-workshop/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@nx/js/babel", 5 | { 6 | "useBuiltIns": "usage" 7 | } 8 | ] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /libs/nx-react-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-react-workshop/README.md: -------------------------------------------------------------------------------- 1 | # Nx React Workshop utility 2 | 3 | This utility is used to provide automatic lab completion during the [**Nx React Workshop**](https://github.com/nrwl/nx-react-workshop). 4 | 5 | ## Catching up with missed labs 6 | 7 | If you fall behind or join late, we provide migrations that would bring your repository up to date with desired lab. To use the migrarions follow the next steps: 8 | 9 | 1. Install `@nrwl/nx-react-workshop` package as dev dependency (e.g. `yarn add -D @nrwl/nx-react-workshop`). If you finished [Lab 3.1 - Migrations](docs/lab3.1/LAB.md) then you should already have it installed. 10 | 2. Run the generator with one of the following options: 11 | - Provide `lab` you want to complete: `nx g @nrwl/nx-react-workshop:complete-labs --lab=5` or 12 | - Use `from` range to finish until end: `nx g @nrwl/nx-react-workshop:complete-labs --from=2` 13 | - Use `to` range to catch up with given lab: `nx g @nrwl/nx-react-workshop:complete-labs --to=5` 14 | - Use `from/to` range to catch up with several labs in between: `nx g @nrwl/nx-react-workshop:complete-labs --from=2 --from=7` 15 | - Use `option` to specify if you want **track 1** or **track 2**: `nx g @nrwl/nx-react-workshop:complete-labs --from=19 --option=option2` (default option is track 2) 16 | 3. Finnally, run the the migrations `npx nx migrate --run-migrations` to have that code generated. 17 | 18 | --- 19 | 20 | This library was generated with [Nx](https://nx.dev). 21 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/generators.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | "name": "nx-react-workshop", 4 | "version": "0.0.1", 5 | "generators": { 6 | "complete-labs": { 7 | "factory": "./src/generators/complete-labs/generator", 8 | "schema": "./src/generators/complete-labs/schema.json", 9 | "description": "Completes the chosen labs" 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/jest.config.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default { 3 | displayName: 'nx-react-workshop', 4 | preset: '../../jest.preset.js', 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-react-workshop', 17 | }; 18 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/migrations.json: -------------------------------------------------------------------------------- 1 | { 2 | "generators": { 3 | "complete-lab-1": { 4 | "version": "0.1.1", 5 | "description": "Complete Lab 1", 6 | "cli": "nx", 7 | "implementation": "./src/migrations/complete-lab-1/complete-lab-1" 8 | }, 9 | "complete-lab-2": { 10 | "version": "0.1.2", 11 | "description": "Complete Lab 2", 12 | "cli": "nx", 13 | "implementation": "./src/migrations/complete-lab-2/complete-lab-2" 14 | }, 15 | "complete-lab-3": { 16 | "version": "0.1.3", 17 | "description": "Complete Lab 3", 18 | "cli": "nx", 19 | "implementation": "./src/migrations/complete-lab-3/complete-lab-3" 20 | }, 21 | "complete-lab-4": { 22 | "version": "0.1.4", 23 | "description": "Complete Lab 4", 24 | "cli": "nx", 25 | "implementation": "./src/migrations/complete-lab-4/complete-lab-4" 26 | }, 27 | "complete-lab-5": { 28 | "version": "0.1.5", 29 | "description": "Complete Lab 5", 30 | "cli": "nx", 31 | "implementation": "./src/migrations/complete-lab-5/complete-lab-5" 32 | }, 33 | "complete-lab-6": { 34 | "version": "0.1.6", 35 | "description": "Complete Lab 6", 36 | "cli": "nx", 37 | "implementation": "./src/migrations/complete-lab-6/complete-lab-6" 38 | }, 39 | "complete-lab-7": { 40 | "version": "0.1.7", 41 | "description": "Complete Lab 7", 42 | "cli": "nx", 43 | "implementation": "./src/migrations/complete-lab-7/complete-lab-7" 44 | }, 45 | "complete-lab-8": { 46 | "version": "0.1.8", 47 | "description": "Complete Lab 8", 48 | "cli": "nx", 49 | "implementation": "./src/migrations/complete-lab-8/complete-lab-8" 50 | }, 51 | "complete-lab-9": { 52 | "version": "0.1.9", 53 | "description": "Complete Lab 9", 54 | "cli": "nx", 55 | "implementation": "./src/migrations/complete-lab-9/complete-lab-9" 56 | }, 57 | "complete-lab-10": { 58 | "version": "0.1.10", 59 | "description": "Complete Lab 10", 60 | "cli": "nx", 61 | "implementation": "./src/migrations/complete-lab-10/complete-lab-10" 62 | }, 63 | "complete-lab-11": { 64 | "version": "0.1.11", 65 | "description": "Complete Lab 11", 66 | "cli": "nx", 67 | "implementation": "./src/migrations/complete-lab-11/complete-lab-11" 68 | }, 69 | "complete-lab-12": { 70 | "version": "0.1.12", 71 | "description": "Complete Lab 12", 72 | "cli": "nx", 73 | "implementation": "./src/migrations/complete-lab-12/complete-lab-12" 74 | }, 75 | "complete-lab-13a": { 76 | "version": "0.1.13", 77 | "description": "Complete Lab 13a", 78 | "cli": "nx", 79 | "implementation": "./src/migrations/complete-lab-13/complete-lab-13a" 80 | }, 81 | "complete-lab-13b": { 82 | "version": "0.1.13", 83 | "description": "Complete Lab 13b", 84 | "cli": "nx", 85 | "implementation": "./src/migrations/complete-lab-13/complete-lab-13b" 86 | }, 87 | "complete-lab-13c": { 88 | "version": "0.1.13", 89 | "description": "Complete Lab 13c", 90 | "cli": "nx", 91 | "implementation": "./src/migrations/complete-lab-13/complete-lab-13c" 92 | }, 93 | "complete-lab-14": { 94 | "version": "0.1.14", 95 | "description": "Complete Lab 14", 96 | "cli": "nx", 97 | "implementation": "./src/migrations/complete-lab-14/complete-lab-14" 98 | }, 99 | "complete-lab-15": { 100 | "version": "0.1.15", 101 | "description": "Complete Lab 15", 102 | "cli": "nx", 103 | "implementation": "./src/migrations/complete-lab-15/complete-lab-15" 104 | }, 105 | "complete-lab-16": { 106 | "version": "0.1.16", 107 | "description": "Complete Lab 16", 108 | "cli": "nx", 109 | "implementation": "./src/migrations/complete-lab-16/complete-lab-16" 110 | }, 111 | "complete-lab-17": { 112 | "version": "0.1.17", 113 | "description": "Complete Lab 17", 114 | "cli": "nx", 115 | "implementation": "./src/migrations/complete-lab-17/complete-lab-17" 116 | }, 117 | "complete-lab-18": { 118 | "version": "0.1.18", 119 | "description": "Complete Lab 18", 120 | "cli": "nx", 121 | "implementation": "./src/migrations/complete-lab-18/complete-lab-18" 122 | }, 123 | "complete-lab-19": { 124 | "version": "0.1.19", 125 | "description": "Complete Lab 19", 126 | "cli": "nx", 127 | "implementation": "./src/migrations/complete-lab-19/complete-lab-19" 128 | }, 129 | "complete-lab-19-alt": { 130 | "version": "0.1.19-alt", 131 | "description": "Complete Lab 19-alt", 132 | "cli": "nx", 133 | "implementation": "./src/migrations/complete-lab-19-alt/complete-lab-19-alt" 134 | }, 135 | "complete-lab-20": { 136 | "version": "0.1.20", 137 | "description": "Complete Lab 20", 138 | "cli": "nx", 139 | "implementation": "./src/migrations/complete-lab-20/complete-lab-20" 140 | }, 141 | "complete-lab-20-alt": { 142 | "version": "0.1.20-alt", 143 | "description": "Complete Lab 20-alt", 144 | "cli": "nx", 145 | "implementation": "./src/migrations/complete-lab-20-alt/complete-lab-20-alt" 146 | }, 147 | "complete-lab-21": { 148 | "version": "0.1.21", 149 | "description": "Complete Lab 21", 150 | "cli": "nx", 151 | "implementation": "./src/migrations/complete-lab-21/complete-lab-21" 152 | }, 153 | "complete-lab-21-alt": { 154 | "version": "0.1.21-alt", 155 | "description": "Complete Lab 21-alt", 156 | "cli": "nx", 157 | "implementation": "./src/migrations/complete-lab-21-alt/complete-lab-21-alt" 158 | }, 159 | "complete-lab-22": { 160 | "version": "0.1.22", 161 | "description": "Complete Lab 22", 162 | "cli": "nx", 163 | "implementation": "./src/migrations/complete-lab-22/complete-lab-22" 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nrwl/nx-react-workshop", 3 | "version": "0.0.0", 4 | "main": "src/index.js", 5 | "generators": "./generators.json", 6 | "nx-migrations": { 7 | "migrations": "./migrations.json" 8 | }, 9 | "dependencies": { 10 | "nx-cloud": "19.1.0", 11 | "cors": "*", 12 | "node-fetch": "^2.x", 13 | "surge": "*", 14 | "@nx/workspace": "19.7.2", 15 | "@nx/storybook": "19.7.2", 16 | "@nx/plugin": "19.7.2", 17 | "@nx/express": "19.7.2", 18 | "@nx/react": "19.7.2", 19 | "@nx/nest": "19.7.2", 20 | "@nx/next": "19.7.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nx-react-workshop", 3 | "$schema": "../../node_modules/nx/schemas/project-schema.json", 4 | "sourceRoot": "libs/nx-react-workshop/src", 5 | "projectType": "library", 6 | "tags": [], 7 | "targets": { 8 | "lint": { 9 | "executor": "@nx/eslint:lint" 10 | }, 11 | "test": { 12 | "executor": "@nx/jest:jest", 13 | "outputs": ["{workspaceRoot}/coverage/libs/nx-react-workshop"], 14 | "options": { 15 | "jestConfig": "libs/nx-react-workshop/jest.config.ts" 16 | } 17 | }, 18 | "build": { 19 | "executor": "@nx/js:tsc", 20 | "outputs": ["{options.outputPath}"], 21 | "options": { 22 | "outputPath": "dist/libs/nx-react-workshop", 23 | "tsConfig": "libs/nx-react-workshop/tsconfig.lib.json", 24 | "packageJson": "libs/nx-react-workshop/package.json", 25 | "main": "libs/nx-react-workshop/src/index.ts", 26 | "assets": [ 27 | "libs/nx-react-workshop/*.md", 28 | { 29 | "input": "./libs/nx-react-workshop/src", 30 | "glob": "**/!(*.ts)", 31 | "output": "./src" 32 | }, 33 | { 34 | "input": "./libs/nx-react-workshop/src", 35 | "glob": "**/*.d.ts", 36 | "output": "./src" 37 | }, 38 | { 39 | "input": "./libs/nx-react-workshop", 40 | "glob": "generators.json", 41 | "output": "." 42 | }, 43 | { 44 | "input": "./libs/nx-react-workshop", 45 | "glob": "executors.json", 46 | "output": "." 47 | }, 48 | { 49 | "input": "./libs/nx-react-workshop", 50 | "glob": "migrations.json", 51 | "output": "." 52 | } 53 | ], 54 | "updateBuildableProjectDepsInPackageJson": true 55 | } 56 | }, 57 | "release-dev": { 58 | "command": "nx release version prerelease && nx release publish" 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/generators/complete-labs/generator.ts: -------------------------------------------------------------------------------- 1 | import { formatFiles, readJsonFile, Tree } from '@nx/devkit'; 2 | import { initGenerator } from '@nx/js'; 3 | import { CompleteLabsGeneratorSchema } from './schema'; 4 | 5 | export default async function ( 6 | tree: Tree, 7 | options: CompleteLabsGeneratorSchema 8 | ) { 9 | initGenerator(tree, { skipPackageJson: true }); 10 | const { lab, from, to, option } = options; 11 | const migrationDefinitions = readJsonFile( 12 | 'node_modules/@nrwl/nx-react-workshop/migrations.json' 13 | ).generators; 14 | let migrations = Object.keys(migrationDefinitions).map((name) => { 15 | const { version, description, implementation, cli } = 16 | migrationDefinitions[name]; 17 | return { 18 | version, 19 | description, 20 | factory: implementation, 21 | cli, 22 | package: '@nrwl/nx-react-workshop', 23 | name, 24 | }; 25 | }); 26 | let including = false; 27 | migrations = migrations 28 | .filter((migration) => { 29 | const versionParts = migration.version.split('.'); 30 | const lastVersionPart = versionParts[versionParts.length - 1]; 31 | const optionSuffix = option === 'option1' ? '-alt' : ''; 32 | const firstLab = from || lab; 33 | const firstLabString = 34 | firstLab < 19 ? firstLab + '' : firstLab + optionSuffix; 35 | if (lastVersionPart === firstLabString) { 36 | including = true; 37 | } 38 | const lastLab = to || lab; 39 | const lastLabString = 40 | lastLab < 19 ? lastLab + '' : lastLab + optionSuffix; 41 | if (lastVersionPart === lastLabString) { 42 | including = false; 43 | return true; 44 | } 45 | return including; 46 | }) 47 | .filter((migration) => { 48 | const versionParts = migration.version.split('.'); 49 | const lastVersionPart = versionParts[versionParts.length - 1]; 50 | if (option == 'option2') { 51 | return !Number.isNaN(+lastVersionPart); 52 | } else { 53 | return +lastVersionPart < 19 || Number.isNaN(+lastVersionPart); 54 | } 55 | }); 56 | tree.write('migrations.json', JSON.stringify({ migrations }, undefined, 2)); 57 | await formatFiles(tree); 58 | console.log('Migration file generated, to complete the labs run:'); 59 | console.log('nx migrate --run-migrations=migrations.json'); 60 | } 61 | -------------------------------------------------------------------------------- /libs/nx-react-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-react-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-react-workshop/src/index.ts: -------------------------------------------------------------------------------- 1 | import completeLabsGenerator from './generators/complete-labs/generator'; 2 | 3 | export { completeLabsGenerator }; 4 | -------------------------------------------------------------------------------- /libs/nx-react-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 | installPackagesTask, 8 | readJson, 9 | } from '@nx/devkit'; 10 | import { removeGenerator } from '@nx/workspace'; 11 | import { execSync } from 'child_process'; 12 | 13 | export default async function update(tree: Tree) { 14 | // npx create-nx-workspace bg-hoard --preset=empty --no-nx-cloud 15 | const projects = getProjects(tree); 16 | const projectsToRemove = [ 17 | 'store-e2e', 18 | 'store', 19 | 'api', 20 | 'api-e2e', 21 | 'api-util-interface', 22 | 'util-interface', 23 | 'store-feature-game-detail', 24 | 'ui-shared', 25 | 'store-ui-shared', 26 | 'store-ui-shared-e2e', 27 | 'store-util-formatters', 28 | 'api-util-notifications', 29 | 'admin-ui', 30 | 'admin-ui-e2e', 31 | 'internal-plugin', 32 | 'internal-plugin-e2e', 33 | ].filter((removeProject) => projects.has(removeProject)); 34 | projectsToRemove.forEach( 35 | async (projectName) => 36 | await removeGenerator(tree, { 37 | projectName, 38 | skipFormat: true, 39 | forceRemove: true, 40 | }) 41 | ); 42 | // hack to fix remove generator 43 | updateJson(tree, 'tsconfig.base.json', (json) => { 44 | json.compilerOptions.paths = {}; 45 | return json; 46 | }); 47 | 48 | // Lab 2 49 | [ 50 | '.eslintignore', 51 | '.eslintrc.json', 52 | 'jest.config.ts', 53 | 'jest.preset.js', 54 | ].forEach((file) => tree.delete(file)); 55 | 56 | // Lab 13 57 | tree.delete('tools/generators/util-lib'); 58 | 59 | // Lab 14 60 | tree.delete('tools/generators/update-scope-schema'); 61 | tree.delete('.husky'); 62 | 63 | // Lab 15 64 | tree.delete('.github/workflows/ci.yml'); 65 | // Lab 19 66 | if (tree.exists('.nx-workshop.json')) { 67 | const { flyName } = readJson(tree, '.nx-workshop.json'); 68 | const flyApps = await execSync(`fly apps list`).toString(); 69 | if (flyApps.includes(flyName)) { 70 | execSync(`fly apps destroy ${flyName} --yes`); 71 | } 72 | tree.delete('.nx-workshop.json'); 73 | } 74 | // Lab 19-alt 75 | tree.delete('tools/generators/add-deploy-target'); 76 | // Lab 21 77 | tree.delete('.github/workflows/deploy.yml'); 78 | 79 | // Reset nx.json to default 80 | updateJson(tree, 'nx.json', (json) => { 81 | const newJson = { 82 | $schema: './node_modules/nx/schemas/nx-schema.json', 83 | namedInputs: { 84 | default: ['{projectRoot}/**/*', 'sharedGlobals'], 85 | production: ['default'], 86 | sharedGlobals: [], 87 | }, 88 | }; 89 | 90 | // Keep nxCloudAccessToken if they connected their repo to Nx Cloud 91 | if (json.nxCloudAccessToken) { 92 | newJson['nxCloudAccessToken'] = json.nxCloudAccessToken; 93 | } 94 | 95 | return newJson; 96 | }); 97 | 98 | // Reset package.json to default 99 | updateJson( 100 | tree, 101 | 'package.json', 102 | ({ name, version, license, dependencies, devDependencies }) => { 103 | const packagesToKeep = [ 104 | '@nx/js', 105 | '@nx/workspace', 106 | 'nx', 107 | '@nrwl/nx-react-workshop', 108 | ]; 109 | 110 | const filterDependencies = (d: Record = {}) => 111 | Object.fromEntries( 112 | Object.entries(d).filter(([packageName]) => 113 | packagesToKeep.includes(packageName) 114 | ) 115 | ); 116 | 117 | return { 118 | name, 119 | version, 120 | license, 121 | scripts: {}, 122 | private: true, 123 | dependencies: filterDependencies(dependencies), 124 | devDependencies: filterDependencies(devDependencies), 125 | }; 126 | } 127 | ); 128 | 129 | await formatFiles(tree); 130 | return () => { 131 | installPackagesTask(tree); 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-10/complete-lab-10.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { addDependenciesToPackageJson, Tree } from '@nx/devkit'; 3 | import { storybookConfigurationGenerator } from '@nx/react'; 4 | import { dependencies } from '../../../package.json'; 5 | 6 | export default async function update(host: Tree) { 7 | // yarn add @nx/storybook 8 | await addDependenciesToPackageJson( 9 | host, 10 | {}, 11 | { 12 | '@nx/storybook': dependencies['@nx/storybook'], 13 | vite: 'latest', 14 | } 15 | ); 16 | // nx generate @nx/react:storybook-configuration store-ui-shared 17 | await storybookConfigurationGenerator(host, { 18 | project: 'store-ui-shared', 19 | configureCypress: false, 20 | generateStories: true, 21 | interactionTests: true, 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-11/complete-lab-11.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | 4 | export default function update(host: Tree) { 5 | host.write( 6 | 'libs/store/ui-shared/src/lib/header/header.stories.tsx', 7 | `import type { Meta, StoryObj } from '@storybook/react'; 8 | import { Header } from './header'; 9 | 10 | import { within } from '@storybook/testing-library'; 11 | import { expect } from '@storybook/jest'; 12 | 13 | const meta: Meta = { 14 | component: Header, 15 | title: 'Header', 16 | }; 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const Primary = { 21 | args: {}, 22 | }; 23 | 24 | export const Heading: Story = { 25 | args: { 26 | title: 'Welcome to Board Game Hoard', 27 | }, 28 | play: async ({ canvasElement }) => { 29 | const canvas = within(canvasElement); 30 | expect(canvas.getByText(/Welcome to Board Game Hoard/gi)).toBeTruthy(); 31 | }, 32 | }; 33 | ` 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-12/complete-lab-12.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | readProjectConfiguration, 4 | Tree, 5 | updateJson, 6 | updateProjectConfiguration, 7 | } from '@nx/devkit'; 8 | 9 | export default function update(host: Tree) { 10 | const projectUpdates = { 11 | store: { 12 | tags: ['scope:store', 'type:app'], 13 | }, 14 | 'store-e2e': { 15 | tags: ['scope:store', 'type:e2e'], 16 | implicitDependencies: ['store'], 17 | }, 18 | 'store-ui-shared': { 19 | tags: ['scope:store', 'type:ui'], 20 | }, 21 | 'store-util-formatters': { 22 | tags: ['scope:store', 'type:util'], 23 | }, 24 | 'store-feature-game-detail': { 25 | tags: ['scope:store', 'type:feature'], 26 | }, 27 | api: { 28 | tags: ['scope:api', 'type:app'], 29 | }, 30 | 'util-interface': { 31 | tags: ['scope:shared', 'type:util'], 32 | }, 33 | }; 34 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 35 | Object.keys(projectUpdates).forEach((projectName) => { 36 | const config = readProjectConfiguration(host, projectName); 37 | config.tags = projectUpdates[projectName].tags; 38 | config.implicitDependencies = 39 | projectUpdates[projectName].implicitDependencies || []; 40 | updateProjectConfiguration(host, projectName, config); 41 | }); 42 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 43 | 44 | updateJson(host, '.eslintrc.json', (json) => { 45 | json.overrides[0].rules['@nx/enforce-module-boundaries'][1].depConstraints = 46 | [ 47 | { 48 | sourceTag: 'scope:store', 49 | onlyDependOnLibsWithTags: ['scope:store', 'scope:shared'], 50 | }, 51 | { 52 | sourceTag: 'scope:api', 53 | onlyDependOnLibsWithTags: ['scope:api', 'scope:shared'], 54 | }, 55 | { 56 | sourceTag: 'type:feature', 57 | onlyDependOnLibsWithTags: ['type:feature', 'type:ui', 'type:util'], 58 | }, 59 | { 60 | sourceTag: 'type:ui', 61 | onlyDependOnLibsWithTags: ['type:ui', 'type:util'], 62 | }, 63 | { 64 | sourceTag: 'type:util', 65 | onlyDependOnLibsWithTags: ['type:util'], 66 | }, 67 | ]; 68 | return json; 69 | }); 70 | 71 | host.write( 72 | 'apps/api-e2e/src/api/lint.spec.ts', 73 | ` 74 | import { execSync } from 'child_process'; 75 | import { writeFileSync } from 'node:fs'; 76 | 77 | describe('Dependencies', () => { 78 | it('should fail linting when tag rules are applied', async () => { 79 | writeFileSync( 80 | 'libs/util-interface/src/index.ts', 81 | \`import {} from '@bg-hoard/store-ui-shared'; 82 | 83 | export * from './lib/api-util-interface'; 84 | \` 85 | ); 86 | expect(() => execSync('nx lint util-interface')).toThrow(); 87 | }); 88 | afterAll(() => { 89 | writeFileSync( 90 | 'libs/util-interface/src/index.ts', 91 | \`export * from './lib/api-util-interface'; 92 | \` 93 | ); 94 | }); 95 | }); 96 | ` 97 | ); 98 | } 99 | -------------------------------------------------------------------------------- /libs/nx-react-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/eslint'; 5 | 6 | export default async function update(host: Tree) { 7 | // nx generate @nx/plugin:generator util-lib 8 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 9 | await pluginGenerator(host, { 10 | name: 'internal-plugin', 11 | directory: 'libs/internal-plugin', 12 | projectNameAndRootFormat: 'as-provided', 13 | skipTsConfig: false, 14 | unitTestRunner: 'jest', 15 | linter: Linter.EsLint, 16 | compiler: 'tsc', 17 | skipFormat: false, 18 | skipLintChecks: false, 19 | }); 20 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 21 | 22 | await generatorGenerator(host, { 23 | name: 'util-lib', 24 | nameAndDirectoryFormat: 'as-provided', 25 | directory: 'libs/internal-plugin/src/generators/util-lib', 26 | unitTestRunner: 'jest', 27 | }); 28 | 29 | // add js package for dependency checks 30 | updateJson(host, 'libs/internal-plugin/package.json', (json) => { 31 | json.dependencies['@nx/js'] = json.dependencies['@nx/devkit']; 32 | return json; 33 | }); 34 | 35 | host.write( 36 | 'libs/internal-plugin/src/generators/util-lib/generator.ts', 37 | `import { formatFiles, installPackagesTask, Tree } from '@nx/devkit'; 38 | import { libraryGenerator } from '@nx/js'; 39 | import { UtilLibGeneratorSchema } from './schema'; 40 | 41 | export default async function (tree: Tree, schema: UtilLibGeneratorSchema) { 42 | await libraryGenerator(tree, { 43 | name: \`util-\${schema.name}\`, 44 | directory: schema.directory, 45 | tags: \`type:util, scope:\${schema.directory}\`, 46 | }); 47 | await formatFiles(tree); 48 | return () => { 49 | installPackagesTask(tree); 50 | }; 51 | } 52 | ` 53 | ); 54 | host.write( 55 | 'libs/internal-plugin/src/generators/util-lib/schema.d.ts', 56 | `export interface UtilLibGeneratorSchema { 57 | name: string; 58 | directory: 'store' | 'api' | 'shared'; 59 | } 60 | ` 61 | ); 62 | updateJson( 63 | host, 64 | 'libs/internal-plugin/src/generators/util-lib/schema.json', 65 | (json) => { 66 | delete json.properties.tags; 67 | return { 68 | ...json, 69 | properties: { 70 | ...json.properties, 71 | directory: { 72 | type: 'string', 73 | description: 'The scope of your lib.', 74 | 'x-prompt': { 75 | message: 'Which directory do you want the lib to be in?', 76 | type: 'list', 77 | items: [ 78 | { 79 | value: 'store', 80 | label: 'store', 81 | }, 82 | { 83 | value: 'api', 84 | label: 'api', 85 | }, 86 | { 87 | value: 'shared', 88 | label: 'shared', 89 | }, 90 | ], 91 | }, 92 | }, 93 | }, 94 | }; 95 | } 96 | ); 97 | host.write( 98 | 'libs/internal-plugin/src/generators/util-lib/generator.spec.ts', 99 | `import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; 100 | import { Tree, readProjectConfiguration } from '@nx/devkit'; 101 | 102 | import generator from './generator'; 103 | import { UtilLibGeneratorSchema } from './schema'; 104 | 105 | describe('util-lib generator', () => { 106 | let appTree: Tree; 107 | const options: UtilLibGeneratorSchema = { name: 'foo', directory: 'store' }; 108 | 109 | beforeEach(() => { 110 | appTree = createTreeWithEmptyWorkspace(); 111 | }); 112 | 113 | it('should add util to the name and add appropriate tags', async () => { 114 | await generator(appTree, options); 115 | const config = readProjectConfiguration(appTree, 'store-util-foo'); 116 | expect(config).toBeDefined(); 117 | expect(config.tags).toEqual(['type:util', 'scope:store']); 118 | }); 119 | }); 120 | ` 121 | ); 122 | await formatFiles(host); 123 | } 124 | -------------------------------------------------------------------------------- /libs/nx-react-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-react-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-react-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 | directory: 'libs/internal-plugin/src/generators/update-scope-schema', 10 | nameAndDirectoryFormat: 'as-provided', 11 | unitTestRunner: 'jest', 12 | }); 13 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 14 | 15 | host.write( 16 | 'libs/internal-plugin/src/generators/update-scope-schema/generator.ts', 17 | `import { 18 | Tree, 19 | updateJson, 20 | formatFiles, 21 | ProjectConfiguration, 22 | getProjects, 23 | updateProjectConfiguration, 24 | } from '@nx/devkit'; 25 | 26 | export default async function (tree: Tree) { 27 | addScopeIfMissing(tree); 28 | const scopes = getScopes(getProjects(tree)); 29 | updateSchemaJson(tree, scopes); 30 | updateSchemaInterface(tree, scopes); 31 | await formatFiles(tree); 32 | } 33 | 34 | function addScopeIfMissing(tree: Tree) { 35 | const projectMap = getProjects(tree); 36 | Array.from(projectMap.keys()).forEach((projectName) => { 37 | const project = projectMap.get(projectName); 38 | if (!project.tags.some((tag) => tag.startsWith('scope:'))) { 39 | const scope = projectName.split('-')[0]; 40 | project.tags.push(\`scope:\${scope}\`); 41 | updateProjectConfiguration(tree, projectName, project); 42 | } 43 | }); 44 | } 45 | 46 | function getScopes(projectMap: Map) { 47 | const projects: any[] = Array.from(projectMap.values()); 48 | const allScopes: string[] = projects 49 | .map((project) => 50 | project.tags.filter((tag: string) => tag.startsWith('scope:')) 51 | ) 52 | .reduce((acc, tags) => [...acc, ...tags], []) 53 | .map((scope: string) => scope.slice(6)); 54 | return Array.from(new Set(allScopes)); 55 | } 56 | 57 | function updateSchemaJson(tree: Tree, scopes: string[]) { 58 | updateJson( 59 | tree, 60 | 'libs/internal-plugin/src/generators/util-lib/schema.json', 61 | (schemaJson) => { 62 | schemaJson.properties.directory['x-prompt'].items = scopes.map( 63 | (scope) => ({ 64 | value: scope, 65 | label: scope, 66 | }) 67 | ); 68 | return schemaJson; 69 | } 70 | ); 71 | } 72 | 73 | function updateSchemaInterface(tree: Tree, scopes: string[]) { 74 | const joinScopes = scopes.map((s) => \`'\${s}'\`).join(' | '); 75 | const interfaceDefinitionFilePath = 76 | 'libs/internal-plugin/src/generators/util-lib/schema.d.ts'; 77 | const newContent = \`export interface UtilLibGeneratorSchema { 78 | name: string; 79 | directory: \${joinScopes}; 80 | }\`; 81 | tree.write(interfaceDefinitionFilePath, newContent); 82 | } 83 | ` 84 | ); 85 | 86 | host.write( 87 | 'libs/internal-plugin/src/generators/update-scope-schema/schema.json', 88 | ` 89 | { 90 | "$schema": "http://json-schema.org/schema", 91 | "cli": "nx", 92 | "$id": "UpdateScopeSchema", 93 | "properties": {} 94 | } 95 | 96 | ` 97 | ); 98 | 99 | host.delete( 100 | 'libs/internal-plugin/src/generators/update-scope-schema/schema.d.ts' 101 | ); 102 | 103 | await formatFiles(host); 104 | } 105 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-15/complete-lab-15.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree, updateJson } from '@nx/devkit'; 3 | 4 | export default 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@v4 18 | - uses: bahmutov/npm-install@v1.4.5 19 | - run: npm run nx affected -- --target=build --base=origin/master --parallel 20 | test: 21 | runs-on: ubuntu-latest 22 | name: Testing affected apps 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: bahmutov/npm-install@v1.4.5 26 | - run: npm run nx affected -- --target=test --base=origin/master --parallel 27 | lint: 28 | runs-on: ubuntu-latest 29 | name: Linting affected apps 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: bahmutov/npm-install@v1.4.5 33 | - run: npm run nx affected -- --target=lint --base=origin/master --parallel 34 | e2e: 35 | runs-on: ubuntu-latest 36 | name: E2E testing affected apps 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: bahmutov/npm-install@v1.4.5 40 | - run: npm run nx affected -- --target=e2e --base=origin/master --parallel 41 | ` 42 | ); 43 | updateJson(host, 'nx.json', (json) => { 44 | const sharedGlobalsSet = new Set(json['namedInputs'].sharedGlobals || []); 45 | sharedGlobalsSet.add('{workspaceRoot}/.github/workflows/ci.yml'); 46 | json['namedInputs'].sharedGlobals = Array.from(sharedGlobalsSet); 47 | return json; 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /libs/nx-react-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 async function update(host: Tree) { 6 | execSync(`npx nx connect`, { 7 | stdio: [0, 1, 2], 8 | }); 9 | } 10 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-17/complete-lab-17.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | 4 | export default 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@v4 22 | - uses: bahmutov/npm-install@v1.4.5 23 | - run: npm run nx affected -- --target=build --base=origin/master --parallel 24 | test: 25 | runs-on: ubuntu-latest 26 | name: Testing affected apps 27 | steps: 28 | - uses: actions/checkout@v4 29 | - uses: bahmutov/npm-install@v1.4.5 30 | - run: npm run nx affected -- --target=test --base=origin/master --parallel 31 | lint: 32 | runs-on: ubuntu-latest 33 | name: Linting affected apps 34 | steps: 35 | - uses: actions/checkout@v4 36 | - uses: bahmutov/npm-install@v1.4.5 37 | - run: npm run nx affected -- --target=lint --base=origin/master --parallel 38 | e2e: 39 | runs-on: ubuntu-latest 40 | name: E2E testing affected apps 41 | steps: 42 | - uses: actions/checkout@v4 43 | - uses: bahmutov/npm-install@v1.4.5 44 | - run: npm run nx affected -- --target=e2e --base=origin/master --parallel 45 | ` 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-18/complete-lab-18.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { 3 | addDependenciesToPackageJson, 4 | Tree, 5 | updateJson, 6 | readJsonFile, 7 | } from '@nx/devkit'; 8 | import { uniq } from '@nx/plugin/testing'; 9 | import { runCommandsGenerator } from '@nx/workspace'; 10 | import { execSync } from 'child_process'; 11 | 12 | export default function update(host: Tree) { 13 | addDependenciesToPackageJson( 14 | host, 15 | {}, 16 | { 17 | surge: '*', 18 | } 19 | ); 20 | 21 | let surgeToken, surgeName; 22 | if (host.exists('.nx-workshop.json')) { 23 | const workshopConstants = readJsonFile('.nx-workshop.json'); 24 | surgeToken = workshopConstants.surgeToken; 25 | surgeName = workshopConstants.surgeName; 26 | } 27 | if (!surgeToken || !surgeName) { 28 | surgeToken = execSync('npx surge token').toString().trim(); 29 | surgeName = uniq(`prophetic-narwhal-`); 30 | if (host.exists('.nx-workshop.json')) { 31 | updateJson(host, '.nx-workshop.json', (json) => { 32 | json.surgeToken = surgeToken; 33 | json.surgeName = surgeName; 34 | return json; 35 | }); 36 | } else { 37 | host.write( 38 | '.nx-workshop.json', 39 | JSON.stringify({ surgeToken, surgeName }) 40 | ); 41 | } 42 | } 43 | 44 | // nx generate run-commands deploy --project=store --command="surge dist/apps/store https://.surge.sh --token " 45 | runCommandsGenerator(host, { 46 | name: 'deploy', 47 | project: 'store', 48 | command: `surge dist/apps/store https://${surgeName}.surge.sh --token ${surgeToken}`, 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /libs/nx-react-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 | updateJson, 8 | } from '@nx/devkit'; 9 | import { Linter } from '@nx/eslint'; 10 | import { applicationGenerator } from '@nx/next'; 11 | import { runCommandsGenerator } from '@nx/workspace'; 12 | import { generatorGenerator } from '@nx/plugin/generators'; 13 | import { dependencies } from '../../../package.json'; 14 | 15 | export default async function update(host: Tree) { 16 | // yarn add @nx/next # or "npm i -S @nx/next" 17 | addDependenciesToPackageJson( 18 | host, 19 | {}, 20 | { 21 | '@nx/next': dependencies['@nx/next'], 22 | } 23 | ); 24 | // nx g @nx/next:app admin-ui 25 | await applicationGenerator(host, { 26 | name: 'admin-ui', 27 | style: 'scss', 28 | directory: 'apps/admin-ui', 29 | e2eTestRunner: 'cypress', 30 | projectNameAndRootFormat: 'as-provided', 31 | skipFormat: true, 32 | linter: Linter.EsLint, 33 | unitTestRunner: 'jest', 34 | }); 35 | // nx generate run-commands deploy --project=admin-ui --command="surge dist/apps/admin-ui/exported \${SURGE_DOMAIN_ADMIN_UI} --token \${SURGE_TOKEN}" 36 | runCommandsGenerator(host, { 37 | name: 'deploy', 38 | project: 'admin-ui', 39 | command: 40 | 'surge dist/apps/admin-ui/exported ${SURGE_DOMAIN_ADMIN_UI} --token ${SURGE_TOKEN}', 41 | }); 42 | 43 | // nx g @nx/plugin/generator add-deploy-target 44 | generatorGenerator(host, { 45 | directory: 'libs/internal-plugin/src/generators/add-deploy-target', 46 | nameAndDirectoryFormat: 'as-provided', 47 | unitTestRunner: 'jest', 48 | name: 'add-deploy-target', 49 | skipFormat: true, 50 | }); 51 | 52 | host.write( 53 | `libs/internal-plugin/src/generators/add-deploy-target/files/.local.env`, 54 | ` 55 | SURGE_DOMAIN_<%= undercaps(project) %>=https://<%= subdomain %>.surge.sh 56 | ` 57 | ); 58 | 59 | // add js package for dependency checks 60 | updateJson(host, 'libs/internal-plugin/package.json', (json) => { 61 | json.dependencies['@nx/workspace'] = json.dependencies['@nx/devkit']; 62 | return json; 63 | }); 64 | 65 | host.write( 66 | `libs/internal-plugin/src/generators/add-deploy-target/index.ts`, 67 | ` 68 | import { 69 | Tree, 70 | formatFiles, 71 | installPackagesTask, 72 | generateFiles, 73 | } from '@nx/devkit'; 74 | import { runCommandsGenerator } from '@nx/workspace/generators'; 75 | import { join } from 'path'; 76 | 77 | interface Schema { 78 | project: string; 79 | subdomain: string; 80 | } 81 | 82 | export default async function (host: Tree, schema: Schema) { 83 | await runCommandsGenerator(host, { 84 | name: 'deploy', 85 | project: schema.project, 86 | command: \`surge dist/apps/\${ 87 | schema.project 88 | } \\\${SURGE_DOMAIN_\${underscoreWithCaps( 89 | schema.project 90 | )}} --token \\\${SURGE_TOKEN}\`, 91 | }); 92 | await generateFiles( 93 | host, 94 | join(__dirname, './files'), 95 | \`apps/\${schema.project}\`, 96 | { 97 | tmpl: '', 98 | project: schema.project, 99 | subdomain: schema.subdomain, 100 | undercaps: underscoreWithCaps, 101 | } 102 | ); 103 | await formatFiles(host); 104 | return () => { 105 | installPackagesTask(host); 106 | }; 107 | } 108 | 109 | export function underscoreWithCaps(value: string): string { 110 | return value.replace(/-/g, '_').toLocaleUpperCase(); 111 | } 112 | ` 113 | ); 114 | 115 | host.write( 116 | `libs/internal-plugin/src/generators/add-deploy-target/schema.json`, 117 | ` 118 | { 119 | "cli": "nx", 120 | "id": "add-deploy-target", 121 | "type": "object", 122 | "properties": { 123 | "project": { 124 | "type": "string", 125 | "description": "Project name to generate the deploy target for", 126 | "$default": { 127 | "$source": "argv", 128 | "index": 0 129 | } 130 | }, 131 | "subdomain": { 132 | "type": "string", 133 | "description": "Surge subdomain where you want it deployed.", 134 | "x-prompt": "What is the Surge subdomain you want it deployed under? (https://.surge.sh)" 135 | } 136 | }, 137 | "required": ["project", "subdomain"] 138 | } 139 | ` 140 | ); 141 | await formatFiles(host); 142 | return async () => { 143 | installPackagesTask(host); 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /libs/nx-react-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-react-workshop/src/migrations/complete-lab-2/complete-lab-2.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 { dependencies } from '../../../package.json'; 9 | import { applicationGenerator } from '@nx/react'; 10 | import { Linter } from '@nx/eslint'; 11 | import fetch from 'node-fetch'; 12 | 13 | export default async function update(tree: Tree) { 14 | await addDependenciesToPackageJson( 15 | tree, 16 | { 17 | '@mui/material': 'latest', 18 | '@emotion/react': 'latest', 19 | '@emotion/styled': 'latest', 20 | }, 21 | { 22 | '@nx/react': dependencies['@nx/react'], 23 | } 24 | ); 25 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 26 | await applicationGenerator(tree, { 27 | name: 'store', 28 | e2eTestRunner: 'cypress', 29 | directory: 'apps/store', 30 | projectNameAndRootFormat: 'as-provided', 31 | skipFormat: true, 32 | linter: Linter.EsLint, 33 | style: 'scss', 34 | unitTestRunner: 'jest', 35 | routing: true, 36 | }); 37 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 38 | tree.write( 39 | 'apps/store/src/fake-api.ts', 40 | `const games = [ 41 | { 42 | id: 'settlers-in-the-can', 43 | name: 'Settlers in the Can', 44 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 45 | description: 46 | 'Help your bug family claim the best real estate in a spilled can of beans.', 47 | price: 35, 48 | rating: Math.random() 49 | }, 50 | { 51 | id: 'chess-pie', 52 | name: 'Chess Pie', 53 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 54 | description: 'A circular game of Chess that you can eat as you play.', 55 | price: 15, 56 | rating: Math.random() 57 | }, 58 | { 59 | id: 'purrfection', 60 | name: 'Purrfection', 61 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 62 | description: 'A cat grooming contest goes horribly wrong.', 63 | price: 45, 64 | rating: Math.random() 65 | } 66 | ]; 67 | 68 | export const getAllGames = () => games; 69 | export const getGame = (id: string) => games.find(game => game.id === id); 70 | ` 71 | ); 72 | tree.write( 73 | 'apps/store/src/app/app.module.scss', 74 | `.games-layout { 75 | display: flex; 76 | justify-content: space-between; 77 | margin-bottom: 20px; 78 | } 79 | 80 | .container { 81 | max-width: 800px; 82 | margin: 50px auto; 83 | } 84 | 85 | .game-card { 86 | max-width: 250px; 87 | display: flex; 88 | flex-direction: column; 89 | justify-content: space-between; 90 | 91 | .game-rating { 92 | padding-top: 10px; 93 | } 94 | } 95 | 96 | .center-content { 97 | display: flex; 98 | justify-content: center; 99 | } 100 | 101 | .game-details { 102 | display: flex; 103 | flex-direction: column; 104 | margin: 0; 105 | } 106 | 107 | .game-card-media { 108 | height: 140px; 109 | } 110 | ` 111 | ); 112 | tree.write( 113 | 'apps/store/src/app/app.tsx', 114 | `import styles from './app.module.scss'; 115 | import { getAllGames } from '../fake-api'; 116 | 117 | import Card from '@mui/material/Card'; 118 | import CardActionArea from '@mui/material/CardActionArea'; 119 | import CardContent from '@mui/material/CardContent'; 120 | import CardMedia from '@mui/material/CardMedia'; 121 | import Typography from '@mui/material/Typography'; 122 | 123 | export const App = () => { 124 | return ( 125 |
126 |
127 | {getAllGames().map((x) => ( 128 | 129 | 130 | 135 | 136 | 137 | {x.name} 138 | 139 | 140 | {x.description} 141 | 142 | 148 | Rating: {x.rating} 149 | 150 | 151 | 152 | 153 | ))} 154 |
155 |
156 | ); 157 | }; 158 | 159 | export default App; 160 | ` 161 | ); 162 | tree.write( 163 | 'apps/store-e2e/src/e2e/app.cy.ts', 164 | `describe('store', () => { 165 | beforeEach(() => cy.visit('/')); 166 | 167 | it('should have 3 games', () => { 168 | cy.contains('Settlers in the Can'); 169 | cy.contains('Chess Pie'); 170 | cy.contains('Purrfection'); 171 | }); 172 | }); 173 | ` 174 | ); 175 | formatFiles(tree); 176 | async function download(uri: string, filename: string) { 177 | await fetch(uri) 178 | .then(async (res) => Buffer.from(await res.arrayBuffer())) 179 | .then((buffer) => { 180 | tree.write(filename, buffer); 181 | }); 182 | } 183 | await download( 184 | 'https://github.com/nrwl/nx-react-workshop/raw/main/examples/lab2/apps/store/src/assets/beans.png', 185 | 'apps/store/src/assets/beans.png' 186 | ); 187 | await download( 188 | 'https://github.com/nrwl/nx-react-workshop/raw/main/examples/lab2/apps/store/src/assets/cat.png', 189 | 'apps/store/src/assets/cat.png' 190 | ); 191 | await download( 192 | 'https://github.com/nrwl/nx-react-workshop/raw/main/examples/lab2/apps/store/src/assets/chess.png', 193 | 'apps/store/src/assets/chess.png' 194 | ); 195 | return async () => { 196 | installPackagesTask(tree); 197 | }; 198 | } 199 | -------------------------------------------------------------------------------- /libs/nx-react-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 { insertImport } from '../utils'; 4 | import { tsquery } from '@phenomnomnominal/tsquery'; 5 | 6 | export default function update(host: Tree) { 7 | host.write( 8 | 'apps/store/src/fake-api/index.ts', 9 | ` 10 | const games = [ 11 | { 12 | id: 'settlers-in-the-can', 13 | name: 'Settlers in the Can', 14 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 15 | description: 16 | 'Help your bug family claim the best real estate in a spilled can of beans.', 17 | price: 35, 18 | rating: Math.random() 19 | }, 20 | { 21 | id: 'chess-pie', 22 | name: 'Chess Pie', 23 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 24 | description: 'A circular game of Chess that you can eat as you play.', 25 | price: 15, 26 | rating: Math.random() 27 | }, 28 | { 29 | id: 'purrfection', 30 | name: 'Purrfection', 31 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 32 | description: 'A cat grooming contest goes horribly wrong.', 33 | price: 45, 34 | rating: Math.random() 35 | } 36 | ]; 37 | 38 | export const getAllGames = () => games; 39 | export const getGame = (id: string) => games.find(game => game.id === id); 40 | ` 41 | ); 42 | 43 | host.write( 44 | `apps/store/src/app/app.tsx`, 45 | tsquery.replace( 46 | host.read(`apps/store/src/app/app.tsx`).toString(), 47 | 'CallExpression:has(Identifier[name=useEffect])', 48 | () => ` 49 | useEffect(() => { 50 | setState((state) => ({ 51 | ...state, 52 | data: getAllGames(), 53 | loadingState: 'success', 54 | })); 55 | }, []); 56 | ` 57 | ) 58 | ); 59 | insertImport( 60 | host, 61 | `apps/store/src/app/app.tsx`, 62 | 'getAllGames', 63 | '../fake-api' 64 | ); 65 | 66 | formatFiles(host); 67 | } 68 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-20/complete-lab-20.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | 4 | function replaceInFile( 5 | host: Tree, 6 | path: string, 7 | find: string, 8 | replace: string 9 | ) { 10 | const newContent = host.read(path).toString().replace(find, replace); 11 | host.write(path, newContent); 12 | } 13 | 14 | export default function update(host: Tree) { 15 | replaceInFile( 16 | host, 17 | 'apps/store/src/app/app.tsx', 18 | `fetch('/api/games')`, 19 | `fetch((process.env.NX_API_URL ?? '') + '/api/games')` 20 | ); 21 | replaceInFile( 22 | host, 23 | `libs/store/feature-game-detail/src/lib/game-detail/game-detail.tsx`, 24 | 'fetch(`/api/games/${gameId}`)', 25 | `fetch((process.env.NX_API_URL ?? '') + \`/api/games/\${gameId}\`)` 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-21-alt/complete-lab-21-alt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | 4 | export default 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 | - master 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@v4 26 | - uses: bahmutov/npm-install@v1.4.5 27 | - run: npm run nx build store -- --configuration=production 28 | - run: npm run nx build admin-ui -- --configuration=production 29 | - run: npm run nx deploy store 30 | - run: npm run nx deploy admin-ui 31 | ` 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-21/complete-lab-21.ts: -------------------------------------------------------------------------------- 1 | import { readJsonFile, Tree } from '@nx/devkit'; 2 | 3 | export default function update(host: Tree) { 4 | const { flyName } = readJsonFile('.nx-workshop.json'); 5 | 6 | host.write( 7 | '.github/workflows/deploy.yml', 8 | ` 9 | name: Deploy Website 10 | 11 | on: 12 | push: 13 | branches: 14 | - master 15 | 16 | env: 17 | SURGE_DOMAIN: \${{ secrets.SURGE_DOMAIN }} 18 | SURGE_TOKEN: \${{ secrets.SURGE_TOKEN }} 19 | FLY_API_TOKEN: \${{ secrets.FLY_API_TOKEN }} 20 | NX_API_URL: https://${flyName}.fly.dev 21 | 22 | jobs: 23 | build: 24 | runs-on: ubuntu-latest 25 | name: Deploying apps 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: bahmutov/npm-install@v1.4.5 29 | - run: npm run nx build store -- --configuration=production 30 | - run: npm run nx build api -- --configuration=production 31 | - run: npm run nx deploy store 32 | - run: npm run nx deploy api 33 | ` 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-22/complete-lab-22.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree, readJsonFile } from '@nx/devkit'; 3 | 4 | export default function update(host: Tree) { 5 | const { flyName } = readJsonFile('.nx-workshop.json'); 6 | 7 | host.write( 8 | '.github/workflows/deploy.yml', 9 | ` 10 | name: Deploy Website 11 | 12 | on: 13 | push: 14 | branches: 15 | - master 16 | 17 | env: 18 | SURGE_DOMAIN: \${{ secrets.SURGE_DOMAIN }} 19 | SURGE_TOKEN: \${{ secrets.SURGE_TOKEN }} 20 | FLY_API_TOKEN: \${{ secrets.FLY_API_TOKEN }} 21 | NX_API_URL: https://${flyName}.fly.dev 22 | 23 | jobs: 24 | build: 25 | runs-on: ubuntu-latest 26 | name: Deploying apps 27 | steps: 28 | - uses: actions/checkout@v4 29 | with: 30 | fetch-depth: 0 31 | - uses: bahmutov/npm-install@v1 32 | - uses: nrwl/nx-set-shas@v4 33 | - run: npm run nx affected -- --target=build --base=\${{ env.NX_BASE }} --parallel --configuration=production 34 | - run: npm run nx affected -- --target=deploy --base=\${{ env.NX_BASE }} --parallel 35 | ` 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | 4 | export default function update(host: Tree) { 5 | // no file changes 6 | } 7 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-4/complete-lab-4.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | import { Linter } from '@nx/eslint'; 4 | import { componentGenerator, libraryGenerator } from '@nx/react'; 5 | 6 | export default async function update(tree: Tree) { 7 | // nx generate @nx/react:lib ui-shared --directory=store --no-component 8 | await libraryGenerator(tree, { 9 | name: 'store-ui-shared', 10 | directory: 'libs/store/ui-shared', 11 | projectNameAndRootFormat: 'as-provided', 12 | component: false, 13 | style: 'css', 14 | skipTsConfig: false, 15 | skipFormat: true, 16 | unitTestRunner: 'jest', 17 | linter: Linter.EsLint, 18 | }); 19 | // nx generate @nx/react:component libs/store/ui-shared/src/lib/header --export 20 | await componentGenerator(tree, { 21 | name: 'header', 22 | directory: 'libs/store/ui-shared/src/lib/header', 23 | nameAndDirectoryFormat: 'as-provided', 24 | style: 'css', 25 | export: true, 26 | }); 27 | 28 | tree.write( 29 | 'libs/store/ui-shared/src/lib/header/header.tsx', 30 | `import AppBar from '@mui/material/AppBar'; 31 | import Toolbar from '@mui/material/Toolbar'; 32 | import Typography from '@mui/material/Typography'; 33 | 34 | export interface HeaderProps { 35 | title: string; 36 | } 37 | 38 | export const Header = ({ title }: HeaderProps) => { 39 | return ( 40 | 41 | 42 | 43 | {title} 44 | 45 | 46 | 47 | ); 48 | }; 49 | 50 | export default Header; 51 | ` 52 | ); 53 | tree.write( 54 | 'apps/store/src/app/app.tsx', 55 | `import styles from './app.module.scss'; 56 | import { getAllGames } from '../fake-api'; 57 | 58 | import Card from '@mui/material/Card'; 59 | import CardActionArea from '@mui/material/CardActionArea'; 60 | import CardContent from '@mui/material/CardContent'; 61 | import CardMedia from '@mui/material/CardMedia'; 62 | import Typography from '@mui/material/Typography'; 63 | import { Header } from '@bg-hoard/store-ui-shared'; 64 | 65 | export const App = () => { 66 | return ( 67 | <> 68 |
69 |
70 |
71 | {getAllGames().map((x) => ( 72 | 73 | 74 | 79 | 80 | 81 | {x.name} 82 | 83 | 88 | {x.description} 89 | 90 | 96 | Rating: {x.rating} 97 | 98 | 99 | 100 | 101 | ))} 102 |
103 |
104 | 105 | ); 106 | }; 107 | 108 | export default App; 109 | ` 110 | ); 111 | tree.write( 112 | 'apps/store-e2e/src/e2e/app.cy.ts', 113 | `describe('store', () => { 114 | beforeEach(() => cy.visit('/')); 115 | 116 | it('should have 3 games', () => { 117 | cy.contains('Settlers in the Can'); 118 | cy.contains('Chess Pie'); 119 | cy.contains('Purrfection'); 120 | }); 121 | it('should have a header', () => { 122 | cy.contains('Board Game Hoard'); 123 | }); 124 | }); 125 | ` 126 | ); 127 | } 128 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-5/complete-lab-5.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | import { libraryGenerator } from '@nx/js'; 4 | 5 | export default async function update(host: Tree) { 6 | // nx generate @nx/js:lib util-formatters --directory=store 7 | await libraryGenerator(host, { 8 | name: 'store-util-formatters', 9 | directory: 'libs/store/util-formatters', 10 | projectNameAndRootFormat: 'as-provided', 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.tsx', 23 | `import styles from './app.module.scss'; 24 | import { getAllGames } from '../fake-api'; 25 | 26 | import Card from '@mui/material/Card'; 27 | import CardActionArea from '@mui/material/CardActionArea'; 28 | import CardContent from '@mui/material/CardContent'; 29 | import CardMedia from '@mui/material/CardMedia'; 30 | import Typography from '@mui/material/Typography'; 31 | import { Header } from '@bg-hoard/store-ui-shared'; 32 | import { formatRating } from '@bg-hoard/store-util-formatters'; 33 | 34 | export const App = () => { 35 | return ( 36 | <> 37 |
38 |
39 |
40 | {getAllGames().map((x) => ( 41 | 42 | 43 | 48 | 49 | 50 | {x.name} 51 | 52 | 57 | {x.description} 58 | 59 | 65 | Rating: {formatRating(x.rating)} 66 | 67 | 68 | 69 | 70 | ))} 71 |
72 |
73 | 74 | ); 75 | }; 76 | 77 | export default App; 78 | ` 79 | ); 80 | host.write( 81 | 'apps/store-e2e/cypress.config.ts', 82 | `import { defineConfig } from 'cypress'; 83 | import { nxE2EPreset } from '@nx/cypress/plugins/cypress-preset'; 84 | import { execSync } from 'child_process'; 85 | 86 | export default defineConfig({ 87 | e2e: { 88 | ...nxE2EPreset(__dirname), 89 | setupNodeEvents(on, config) { 90 | on('task', { 91 | showProjects() { 92 | return execSync('nx show projects').toString(); 93 | }, 94 | }); 95 | }, 96 | }, 97 | });` 98 | ); 99 | host.write( 100 | 'apps/store-e2e/src/e2e/app.cy.ts', 101 | `describe('store', () => { 102 | beforeEach(() => cy.visit('/')); 103 | 104 | it('should have 3 games', () => { 105 | cy.contains('Settlers in the Can'); 106 | cy.contains('Chess Pie'); 107 | cy.contains('Purrfection'); 108 | }); 109 | it('should have a header', () => { 110 | cy.contains('Board Game Hoard'); 111 | }); 112 | it('should have a store-util-formatters library', () => { 113 | cy.task('showProjects').should('contain', 'store-util-formatters'); 114 | }); 115 | }); 116 | ` 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-6/complete-lab-6.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree } from '@nx/devkit'; 3 | import { Linter } from '@nx/eslint'; 4 | import { libraryGenerator } from '@nx/react'; 5 | 6 | export default async function update(host: Tree) { 7 | // nx generate @nx/react:library feature-game-detail --directory=store --appProject=store --no-component 8 | process.env.NX_PROJECT_GLOB_CACHE = 'false'; 9 | await libraryGenerator(host, { 10 | name: 'store-feature-game-detail', 11 | directory: 'libs/store/feature-game-detail', 12 | projectNameAndRootFormat: 'as-provided', 13 | component: false, 14 | appProject: 'store', 15 | style: 'css', 16 | skipTsConfig: false, 17 | skipFormat: true, 18 | unitTestRunner: 'jest', 19 | linter: Linter.EsLint, 20 | }); 21 | host.write( 22 | 'apps/store/src/app/app.tsx', 23 | `import styles from './app.module.scss'; 24 | import { getAllGames } from '../fake-api'; 25 | 26 | import Card from '@mui/material/Card'; 27 | import CardActionArea from '@mui/material/CardActionArea'; 28 | import CardContent from '@mui/material/CardContent'; 29 | import CardMedia from '@mui/material/CardMedia'; 30 | import Typography from '@mui/material/Typography'; 31 | import { Header } from '@bg-hoard/store-ui-shared'; 32 | import { formatRating } from '@bg-hoard/store-util-formatters'; 33 | 34 | import { Routes, Route, useNavigate } from 'react-router-dom'; 35 | 36 | import { StoreFeatureGameDetail } from '@bg-hoard/store-feature-game-detail'; 37 | 38 | export const App = () => { 39 | const navigate = useNavigate(); 40 | return ( 41 | <> 42 |
43 |
44 |
45 | {getAllGames().map((x) => ( 46 | navigate(\`/game/\${x.id}\`)} 50 | > 51 | 52 | 57 | 58 | 59 | {x.name} 60 | 61 | 66 | {x.description} 67 | 68 | 74 | Rating: {formatRating(x.rating)} 75 | 76 | 77 | 78 | 79 | ))} 80 |
81 |
82 | 83 | {/* START: routes */} 84 | {/* These routes and navigation have been generated for you */} 85 | {/* Feel free to move and update them to fit your needs */} 86 | 87 | } />; 88 | 89 | {/* END: routes */} 90 | 91 | ); 92 | }; 93 | 94 | export default App; 95 | ` 96 | ); 97 | 98 | process.env.NX_PROJECT_GLOB_CACHE = 'true'; 99 | host.write( 100 | 'libs/store/feature-game-detail/src/lib/store-feature-game-detail.tsx', 101 | `import { useParams } from 'react-router-dom'; 102 | import styles from './store-feature-game-detail.module.scss'; 103 | 104 | import Card from '@mui/material/Card'; 105 | import CardActionArea from '@mui/material/CardActionArea'; 106 | import CardContent from '@mui/material/CardContent'; 107 | import Typography from '@mui/material/Typography'; 108 | 109 | /* eslint-disable-next-line */ 110 | export interface StoreFeatureGameDetailProps {} 111 | 112 | export function StoreFeatureGameDetail(props: StoreFeatureGameDetailProps) { 113 | const params = useParams(); 114 | return ( 115 |
116 | 117 | 118 | 119 | 120 | {params['id']} 121 | 122 | 123 | 124 | 125 |
126 | ); 127 | }; 128 | 129 | export default StoreFeatureGameDetail; 130 | ` 131 | ); 132 | host.write( 133 | 'libs/store/feature-game-detail/src/index.ts', 134 | `export * from './lib/store-feature-game-detail';` 135 | ); 136 | host.write( 137 | 'libs/store/feature-game-detail/src/lib/store-feature-game-detail.module.scss', 138 | `.game-image { 139 | width: 300px; 140 | border-radius: 20px; 141 | margin-right: 20px; 142 | } 143 | 144 | .content { 145 | display: flex; 146 | } 147 | 148 | .details { 149 | display: flex; 150 | flex-direction: column; 151 | justify-content: space-between; 152 | } 153 | 154 | .sell-info { 155 | display: flex; 156 | flex-direction: column; 157 | } 158 | 159 | .buy-button { 160 | margin-right: 20px; 161 | } 162 | ` 163 | ); 164 | host.write( 165 | 'apps/store-e2e/src/e2e/app.cy.ts', 166 | `describe('store', () => { 167 | beforeEach(() => cy.visit('/')); 168 | 169 | it('should have 3 games', () => { 170 | cy.contains('Settlers in the Can'); 171 | cy.contains('Chess Pie'); 172 | cy.contains('Purrfection'); 173 | }); 174 | it('should have a header', () => { 175 | cy.contains('Board Game Hoard'); 176 | }); 177 | it('should have a store-util-formatters library', () => { 178 | cy.task('showProjects').should('contain', 'store-util-formatters'); 179 | }); 180 | it('should navigate to game details', () => { 181 | cy.contains('Settlers in the Can').click(); 182 | cy.contains('settlers-in-the-can'); 183 | cy.location('pathname').should('contain', 'settlers-in-the-can'); 184 | }); 185 | }); 186 | 187 | ` 188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/src/migrations/complete-lab-7/complete-lab-7.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { Tree, addDependenciesToPackageJson } from '@nx/devkit'; 3 | import { applicationGenerator } from '@nx/express'; 4 | import { Linter } from '@nx/eslint'; 5 | import { dependencies } from '../../../package.json'; 6 | 7 | export default async function update(host: Tree) { 8 | await addDependenciesToPackageJson( 9 | host, 10 | {}, 11 | { 12 | '@nx/express': dependencies['@nx/express'], 13 | } 14 | ); 15 | 16 | // nx generate @nx/express:application api --frontendProject=store 17 | await applicationGenerator(host, { 18 | name: 'api', 19 | directory: 'apps/api', 20 | projectNameAndRootFormat: 'as-provided', 21 | frontendProject: 'store', 22 | skipFormat: true, 23 | skipPackageJson: false, 24 | unitTestRunner: 'jest', 25 | linter: Linter.EsLint, 26 | js: false, 27 | pascalCaseFiles: false, 28 | }); 29 | host.write( 30 | 'apps/api/src/app/games.repository.ts', 31 | `const games = [ 32 | { 33 | id: 'settlers-in-the-can', 34 | name: 'Settlers in the Can', 35 | image: '/assets/beans.png', // 'https://media.giphy.com/media/xUNda3pLJEsg4Nedji/giphy.gif', 36 | description: 37 | 'Help your bug family claim the best real estate in a spilled can of beans.', 38 | price: 35, 39 | rating: Math.random(), 40 | }, 41 | { 42 | id: 'chess-pie', 43 | name: 'Chess Pie', 44 | image: '/assets/chess.png', // 'https://media.giphy.com/media/iCZyBnPBLr0dy/giphy.gif', 45 | description: 'A circular game of Chess that you can eat as you play.', 46 | price: 15, 47 | rating: Math.random(), 48 | }, 49 | { 50 | id: 'purrfection', 51 | name: 'Purrfection', 52 | image: '/assets/cat.png', // 'https://media.giphy.com/media/12xMvwvQXJNx0k/giphy.gif', 53 | description: 'A cat grooming contest goes horribly wrong.', 54 | price: 45, 55 | rating: Math.random(), 56 | }, 57 | ]; 58 | 59 | export const getAllGames = () => games; 60 | export const getGame = (id: string) => games.find((game) => game.id === id); 61 | ` 62 | ); 63 | host.write( 64 | 'apps/api/src/main.ts', 65 | `/** 66 | * This is not a production server yet! 67 | * This is only a minimal backend to get started. 68 | */ 69 | 70 | import express from 'express'; 71 | import { getAllGames, getGame } from './app/games.repository'; 72 | 73 | const app = express(); 74 | 75 | app.get('/api/games', (req, res) => { 76 | res.send(getAllGames()); 77 | }); 78 | 79 | app.get('/api/games/:id', (req, res) => { 80 | return res.send(getGame(req.params.id)); 81 | }); 82 | 83 | const port = process.env.port || 3000; 84 | const server = app.listen(port, () => { 85 | console.log(\`Listening at http://localhost:\${port}/api\`); 86 | }); 87 | server.on('error', console.error); 88 | ` 89 | ); 90 | host.write( 91 | 'apps/api-e2e/src/api/api.spec.ts', 92 | `import axios from 'axios'; 93 | import { exec } from 'child_process'; 94 | 95 | describe('GET /api/games', () => { 96 | it('should return a list of games', async () => { 97 | exec('nx serve api'); 98 | const res = await axios.get(\`/api/games\`); 99 | 100 | expect(res.status).toBe(200); 101 | expect(res.data).toMatchObject([ 102 | { 103 | description: 104 | 'Help your bug family claim the best real estate in a spilled can of beans.', 105 | id: 'settlers-in-the-can', 106 | image: '/assets/beans.png', 107 | name: 'Settlers in the Can', 108 | price: 35, 109 | }, 110 | { 111 | description: 'A circular game of Chess that you can eat as you play.', 112 | id: 'chess-pie', 113 | image: '/assets/chess.png', 114 | name: 'Chess Pie', 115 | price: 15, 116 | }, 117 | { 118 | description: 'A cat grooming contest goes horribly wrong.', 119 | id: 'purrfection', 120 | image: '/assets/cat.png', 121 | name: 'Purrfection', 122 | price: 45, 123 | }, 124 | ]); 125 | }); 126 | }); 127 | ` 128 | ); 129 | } 130 | -------------------------------------------------------------------------------- /libs/nx-react-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-react-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-react-workshop/tsconfig.lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "../../dist/out-tsc", 6 | "resolveJsonModule": true, 7 | "declaration": true, 8 | "types": ["node"] 9 | }, 10 | "exclude": ["**/*.spec.ts", "jest.config.ts"], 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /libs/nx-react-workshop/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../dist/out-tsc", 5 | "module": "commonjs", 6 | "types": ["jest", "node"] 7 | }, 8 | "include": [ 9 | "**/*.spec.ts", 10 | "**/*.spec.tsx", 11 | "**/*.spec.js", 12 | "**/*.spec.jsx", 13 | "**/*.d.ts", 14 | "jest.config.ts" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /nx.json: -------------------------------------------------------------------------------- 1 | { 2 | "targetDefaults": { 3 | "build": { 4 | "dependsOn": ["^build"], 5 | "inputs": ["production", "^production"], 6 | "cache": true 7 | }, 8 | "@nx/jest:jest": { 9 | "cache": true, 10 | "inputs": ["default", "^production", "{workspaceRoot}/jest.preset.js"], 11 | "options": { 12 | "passWithNoTests": true 13 | }, 14 | "configurations": { 15 | "ci": { 16 | "ci": true, 17 | "codeCoverage": true 18 | } 19 | } 20 | }, 21 | "@nx/eslint:lint": { 22 | "inputs": ["default", "{workspaceRoot}/.eslintrc.json"], 23 | "cache": true 24 | }, 25 | "nx-release-publish": { 26 | "options": { 27 | "packageRoot": "dist/libs/{projectName}" 28 | } 29 | } 30 | }, 31 | "namedInputs": { 32 | "default": ["{projectRoot}/**/*", "sharedGlobals"], 33 | "sharedGlobals": ["{workspaceRoot}/.github/workflows/ci.yml"], 34 | "production": [ 35 | "default", 36 | "!{projectRoot}/**/?(*.)+(spec|test).[jt]s?(x)?(.snap)", 37 | "!{projectRoot}/tsconfig.spec.json", 38 | "!{projectRoot}/jest.config.[jt]s", 39 | "!{projectRoot}/.eslintrc.json", 40 | "!{projectRoot}/src/test-setup.[jt]s", 41 | "!{projectRoot}/**/*.stories.@(js|jsx|ts|tsx|mdx)", 42 | "!{projectRoot}/.storybook/**/*", 43 | "!{projectRoot}/tsconfig.storybook.json" 44 | ] 45 | }, 46 | "release": { 47 | "projects": ["nx-react-workshop"], 48 | "version": { 49 | "preVersionCommand": "npx nx build nx-react-workshop", 50 | "generatorOptions": { 51 | "packageRoot": "dist/libs/{projectName}", 52 | "currentVersionResolver": "registry" 53 | } 54 | } 55 | }, 56 | "nxCloudAccessToken": "NzdhYTRjNmQtNGYwMC00MGE0LTk2YzEtNWFlNGU1MDk3OTczfHJlYWQtd3JpdGU=", 57 | "nxCloudUrl": "https://staging.nx.app", 58 | "useInferencePlugins": false, 59 | "defaultBase": "main" 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nrwl/nx-react-workshop", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "nx": "nx" 7 | }, 8 | "private": true, 9 | "dependencies": { 10 | "@storybook/addon-interactions": "^8.2.9", 11 | "@storybook/builder-webpack5": "^8.2.9", 12 | "@storybook/core-server": "^8.2.9", 13 | "@storybook/manager-webpack5": "6.5.16", 14 | "react": "18.3.1", 15 | "react-dom": "18.3.1", 16 | "react-is": "18.3.1", 17 | "storybook": "^8.2.9", 18 | "tslib": "^2.3.0" 19 | }, 20 | "devDependencies": { 21 | "@chromatic-com/storybook": "^1", 22 | "@nx/devkit": "19.7.2", 23 | "@nx/eslint": "19.7.2", 24 | "@nx/eslint-plugin": "19.7.2", 25 | "@nx/express": "19.7.2", 26 | "@nx/jest": "19.7.2", 27 | "@nx/js": "19.7.2", 28 | "@nx/nest": "19.7.2", 29 | "@nx/next": "19.7.2", 30 | "@nx/node": "19.7.2", 31 | "@nx/plugin": "19.7.2", 32 | "@nx/react": "19.7.2", 33 | "@nx/storybook": "19.7.2", 34 | "@nx/workspace": "19.7.2", 35 | "@phenomnomnominal/tsquery": "^4.1.1", 36 | "@storybook/addon-webpack5-compiler-swc": "^1.0.5", 37 | "@swc-node/register": "~1.9.1", 38 | "@swc/core": "1.5.7", 39 | "@swc/helpers": "~0.5.11", 40 | "@types/fs-extra": "^11.0.4", 41 | "@types/jest": "29.5.12", 42 | "@types/node": "18.19.9", 43 | "@typescript-eslint/eslint-plugin": "7.18.0", 44 | "@typescript-eslint/parser": "7.18.0", 45 | "dir-compare": "^5.0.0", 46 | "dotenv": "10.0.0", 47 | "eslint": "8.57.0", 48 | "eslint-config-prettier": "9.0.0", 49 | "fs-extra": "^11.1.1", 50 | "jest": "29.7.0", 51 | "jest-environment-jsdom": "29.7.0", 52 | "node-fetch": "^2.6.1", 53 | "nx": "19.7.2", 54 | "prettier": "2.6.2", 55 | "ts-jest": "29.1.0", 56 | "ts-node": "10.9.1", 57 | "typescript": "5.5.4", 58 | "verdaccio": "^5.27.0" 59 | }, 60 | "packageManager": "yarn@4.0.1" 61 | } 62 | -------------------------------------------------------------------------------- /project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@nrwl/nx-react-workshop", 3 | "$schema": "node_modules/nx/schemas/project-schema.json", 4 | "targets": { 5 | "local-registry": { 6 | "executor": "@nx/js:verdaccio", 7 | "options": { 8 | "port": 4873, 9 | "config": ".verdaccio/config.yml", 10 | "storage": "dist/local-registry/storage" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tools/cherry-pick-all.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | hash=$1 5 | 6 | git checkout starting-lab2 7 | git cherry-pick "$hash" 8 | git checkout starting-lab3 9 | git cherry-pick "$hash" 10 | git checkout starting-lab3 11 | git cherry-pick "$hash" 12 | git checkout starting-lab4 13 | git cherry-pick "$hash" 14 | git checkout starting-lab5 15 | git cherry-pick "$hash" 16 | git checkout starting-lab6 17 | git cherry-pick "$hash" 18 | git checkout starting-lab7 19 | git cherry-pick "$hash" 20 | git checkout starting-lab8 21 | git cherry-pick "$hash" 22 | git checkout starting-lab9 23 | git cherry-pick "$hash" 24 | git checkout starting-lab10 25 | git cherry-pick "$hash" 26 | git checkout starting-lab11 27 | git cherry-pick "$hash" 28 | git checkout starting-lab12 29 | git cherry-pick "$hash" 30 | git checkout starting-lab13 31 | git cherry-pick "$hash" 32 | git checkout starting-lab14 33 | git cherry-pick "$hash" 34 | git checkout starting-lab15 35 | git cherry-pick "$hash" 36 | git checkout starting-lab16 37 | git cherry-pick "$hash" 38 | git checkout starting-lab17 39 | git cherry-pick "$hash" 40 | git checkout starting-lab18 41 | git cherry-pick "$hash" 42 | git checkout starting-lab19 43 | git cherry-pick "$hash" 44 | git checkout starting-lab20-path1 45 | git cherry-pick "$hash" 46 | git checkout starting-lab20-path2 47 | git cherry-pick "$hash" 48 | git checkout starting-lab21-path1 49 | git cherry-pick "$hash" 50 | git checkout starting-lab21-path2 51 | git cherry-pick "$hash" 52 | git checkout starting-lab22-path1 53 | git cherry-pick "$hash" 54 | git checkout starting-lab22-path2 55 | git cherry-pick "$hash" 56 | git checkout final-path1 57 | git cherry-pick "$hash" 58 | git checkout final-path2 59 | git cherry-pick "$hash" 60 | git checkout main 61 | 62 | -------------------------------------------------------------------------------- /tools/migrate-all.sh: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/bash 3 | 4 | version=$1 5 | 6 | update_to_version() { 7 | rm -rf node_modules 8 | yarn 9 | yarn nx migrate "$version" 10 | yarn 11 | if [[ -f "migrations.json" ]]; then 12 | yarn nx migrate --run-migrations=migrations.json 13 | fi 14 | git add -A && git commit -am "chore(deps): update nx to $version" 15 | } 16 | 17 | git checkout main 18 | update_to_version 19 | git checkout starting-lab2 20 | update_to_version 21 | git checkout starting-lab3 22 | update_to_version 23 | git checkout starting-lab3 24 | update_to_version 25 | git checkout starting-lab4 26 | update_to_version 27 | git checkout starting-lab5 28 | update_to_version 29 | git checkout starting-lab6 30 | update_to_version 31 | git checkout starting-lab7 32 | update_to_version 33 | git checkout starting-lab8 34 | update_to_version 35 | git checkout starting-lab9 36 | update_to_version 37 | git checkout starting-lab10 38 | update_to_version 39 | git checkout starting-lab11 40 | update_to_version 41 | git checkout starting-lab12 42 | update_to_version 43 | git checkout starting-lab13 44 | update_to_version 45 | git checkout starting-lab14 46 | update_to_version 47 | git checkout starting-lab15 48 | update_to_version 49 | git checkout starting-lab16 50 | update_to_version 51 | git checkout starting-lab17 52 | update_to_version 53 | git checkout starting-lab18 54 | update_to_version 55 | git checkout starting-lab19 56 | update_to_version 57 | git checkout starting-lab20-path1 58 | update_to_version 59 | git checkout starting-lab20-path2 60 | update_to_version 61 | git checkout starting-lab21-path1 62 | update_to_version 63 | git checkout starting-lab21-path2 64 | update_to_version 65 | git checkout starting-lab22-path1 66 | update_to_version 67 | git checkout starting-lab22-path2 68 | update_to_version 69 | git checkout final-path1 70 | update_to_version 71 | git checkout final-path2 72 | update_to_version 73 | 74 | # go back to main 75 | git checkout main 76 | 77 | # push all changes 78 | git push --all 79 | -------------------------------------------------------------------------------- /tools/rebase-all-master.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | git checkout starting-lab2 4 | git rebase main 5 | git checkout starting-lab3 6 | git rebase main 7 | git checkout starting-lab4 8 | git rebase main 9 | git checkout starting-lab5 10 | git rebase main 11 | git checkout starting-lab6 12 | git rebase main 13 | git checkout starting-lab7 14 | git rebase main 15 | git checkout starting-lab8 16 | git rebase main 17 | git checkout starting-lab9 18 | git rebase main 19 | git checkout starting-lab10 20 | git rebase main 21 | git checkout starting-lab11 22 | git rebase main 23 | git checkout starting-lab12 24 | git rebase main 25 | git checkout starting-lab13 26 | git rebase main 27 | git checkout starting-lab14 28 | git rebase main 29 | git checkout starting-lab15 30 | git rebase main 31 | git checkout starting-lab16 32 | git rebase main 33 | git checkout starting-lab17 34 | git rebase main 35 | git checkout starting-lab18 36 | git rebase main 37 | git checkout starting-lab19 38 | git rebase main 39 | git checkout starting-lab20-path1 40 | git rebase main 41 | git checkout starting-lab21-path1 42 | git rebase main 43 | git checkout starting-lab20-path2 44 | git rebase main 45 | git checkout starting-lab21-path2 46 | git rebase main 47 | git checkout starting-lab22-path1 48 | git rebase main 49 | git checkout starting-lab22-path2 50 | git rebase main 51 | git checkout final-path1 52 | git rebase main 53 | git checkout final-path2 54 | git rebase main 55 | git checkout main 56 | -------------------------------------------------------------------------------- /tools/scripts/start-local-registry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This script starts a local registry for e2e testing purposes. 3 | * It is meant to be called in jest's globalSetup. 4 | */ 5 | import { startLocalRegistry } from '@nx/js/plugins/jest/local-registry'; 6 | import { releasePublish, releaseVersion } from 'nx/release'; 7 | 8 | export default async () => { 9 | // local registry target to run 10 | const localRegistryTarget = '@nrwl/nx-react-workshop:local-registry'; 11 | // storage folder for the local registry 12 | const storage = './tmp/local-registry/storage'; 13 | 14 | global.stopLocalRegistry = await startLocalRegistry({ 15 | localRegistryTarget, 16 | storage, 17 | verbose: false, 18 | }); 19 | 20 | await releaseVersion({ 21 | specifier: '0.0.0-e2e', 22 | stageChanges: false, 23 | gitCommit: false, 24 | gitTag: false, 25 | firstRelease: true, 26 | generatorOptionsOverrides: { 27 | skipLockFileUpdate: true, 28 | }, 29 | }); 30 | await releasePublish({ 31 | tag: 'e2e', 32 | firstRelease: true, 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /tools/scripts/stop-local-registry.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This script stops the local registry for e2e testing purposes. 3 | * It is meant to be called in jest's globalTeardown. 4 | */ 5 | 6 | export default () => { 7 | if (global.stopLocalRegistry) { 8 | global.stopLocalRegistry(); 9 | } 10 | }; 11 | -------------------------------------------------------------------------------- /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 | "importHelpers": false 10 | }, 11 | "include": ["**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "sourceMap": true, 6 | "declaration": true, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "target": "ES2021", 12 | "module": "commonjs", 13 | "lib": ["ES2021", "dom"], 14 | "skipLibCheck": true, 15 | "skipDefaultLibCheck": true, 16 | "baseUrl": ".", 17 | "types": ["node", "jest"], 18 | "paths": { 19 | "@nrwl/nx-react-workshop": ["libs/nx-react-workshop/src/index.ts"] 20 | } 21 | } 22 | } 23 | --------------------------------------------------------------------------------