diff --git a/docs/assets/lab3_build_cmds.png b/docs/assets/lab3_build_cmds.png deleted file mode 100644 index 02f47c5..0000000 Binary files a/docs/assets/lab3_build_cmds.png and /dev/null differ diff --git a/docs/assets/lab3_cmds.png b/docs/assets/lab3_cmds.png new file mode 100644 index 0000000..4a75eb9 Binary files /dev/null and b/docs/assets/lab3_cmds.png differ diff --git a/docs/assets/lab3_cmds.svg b/docs/assets/lab3_cmds.svg new file mode 100644 index 0000000..1b7266b --- /dev/null +++ b/docs/assets/lab3_cmds.svg @@ -0,0 +1,244 @@ + + + +projectNametargetName.....nxInvoke the Nx CLIas shown in project detailsoptions to send to the targetuse "--help" to see themopen details using "nx show project <project-name> diff --git a/docs/lab3/LAB.md b/docs/lab3/LAB.md index 37621a2..b94afa2 100644 --- a/docs/lab3/LAB.md +++ b/docs/lab3/LAB.md @@ -7,9 +7,9 @@ We'll build the app we just created, and look at what executors are and how to c ## 📚 Learning outcomes: - **Understand what a `target` and `executor` are** -- **Invoking executors** +- **Understand how to view Project Details** +- **Invoke executors** - **Configure executors by passing them different options** -- **Understand how an executor can invoke another executor** #### 📲 After this workshop, you should have: @@ -24,35 +24,75 @@ We'll build the app we just created, and look at what executors are and how to c
🐳   Hint - Nx executor command structure + Nx executor command structure

-2. You should now have a `dist` folder - let's open it up! +2. There should be a `dist` folder in the root of the workspace -- lets open it up! - - This is your whole app's output! If we wanted we could push this now to a server and it would all work. - - Notice how we generated a `3rdpartylicenses.txt` file and how all files have hashes in suffix - - Open one of the files, for example `main.{hash}.js` and look at it's contents. Notice how it's minified. -
+ - This is the whole app's output! If we wanted to, we could push this to a server, and it would all work. + - Notice how all files have hashes in their suffix. + - Open one of the files, for example, `main.{hash}.js`, and look at its contents. Notice how it's minified. -3. **Open up `apps/store/project.json`** and look at the object under `targets/build` +
- - this is the **target**, and it has an **executor** option, that points to `@nx/webpack:webpack` - - 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! -
+3. Open the **Project Details** for the `store` app and expand the `build` section listed under "Targets." -4. Send a flag to the executor so that it builds for development + - This is a **target** that uses the [`nx:run-commands`](https://nx.dev/nx-api/nx/executors/run-commands#nxruncommands) **executor** to call `webpack-cli` to build the app. + - Since the build target uses the [`webpack-cli`](https://webpack.js.org/api/cli/), webpack can be configured using the `webpack.config.js` file in the project root. +
🐳   Hint + The easiest way to open the Project Details is by using the Nx Console from within VS Code or a JetBrains IDE. Once installed, the Project Details Views can be accessed in multiple ways without leaving the editor. +

- `--configuration=development` - + If the CLI is preferred, or editor without Nx Console support is being used, the project details can be opened in the browser by running `nx show project --web`.

-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. +4. Configure license extraction during production builds + + - Explore the `webpack.config.js` file for the `store` app. + - Remember how we copied some images into our `/assets` folder earlier? Look through the webpack config and try to find how it knows to include them in the final build! + - Configure webpack to `extractLicenses` into a `3rdpartylicenses.txt` file during the build, but only when the node environment is `production`. +
+
+ 🐳   Hint + + The `NxAppWebpackPlugin` takes an `extractLicenses` option. + +

+ +5. Build the app again + + - Notice how we now have a `3rdpartylicenses.txt` file in the `dist` folder. + +
+ +6. Add a `development` configuration to the `build` target that changes the `node-env` argument to `development`. + + - Nx detects the presence of tooling configuration, in this case `webpack.config.js`, and automatically [infers targets](https://nx.dev/concepts/inferred-tasks) needed to run that tool with a set of common defaults (`build`, `preview`, `serve` in this case). + - These targets can be modified by adding additional configuration to the `targets` key in the `project.json`. + - Targets can have multiple configurations that allow for the execution of the same tool with different options. + +
+
+ 🐳   Hint + + - The key to add to the `project.json` is `targets.build.configurations.development.args`. + - Use the Project Details view to see how the environment is being set to production as an example. + +

+ +7. Build the app one more time, but this time using the development configuration we just created. +
+ 🐳   Hint + + `--configuration=development` + +

-6. The **serve** target (located a bit lower in `project.json`) also contains a executor, that _uses_ the output from the **build** target +8. 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.
--- diff --git a/docs/lab3/SOLUTION.md b/docs/lab3/SOLUTION.md index 8fc07b2..80b8cd2 100644 --- a/docs/lab3/SOLUTION.md +++ b/docs/lab3/SOLUTION.md @@ -1,7 +1,49 @@ -##### To build the app for production: +##### 1. To build the app: -`nx build store` +```sh +nx build store +``` -##### To build the app for development: +##### 4. To configure webpack to extract licenses only during production builds: -`nx build store --configuration=development` +In `apps/store/webpack.config.js`: + +```js +module.exports = { + // ... + plugins: [ + // ... + new NxAppWebpackPlugin({ + // ... + extractLicenses: process.env['NODE_ENV'] === 'production', + }), + // ... + ], + // ... +}; +``` + +##### 6. To add a development configuration to the build target: + +In `apps/store/project.json`: + +```json +{ + // ... + "targets": { + "build": { + "configurations": { + "development": { + "args": ["--node-env=development"] + } + } + } + } +} +``` + +##### 7. To build the app using the development configuration: + +```sh +nx build store --configuration=development +``` diff --git a/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.spec.ts b/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.spec.ts new file mode 100644 index 0000000..a103027 --- /dev/null +++ b/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.spec.ts @@ -0,0 +1,54 @@ +import { addProjectConfiguration, readJson, Tree } from '@nx/devkit'; +import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing'; +import update from './complete-lab-3'; + +function exampleWebpackConfig(additionalConfig = '') { + return ` +module.exports = { + plugins: [ + new NxAppWebpackPlugin({ + foo: 'bar', + optimization: process.env['NODE_ENV'] === 'production', + ${additionalConfig} + }), + ], +}; +`; +} + +describe('complete-lab-3', () => { + let tree: Tree; + + beforeEach(() => { + tree = createTreeWithEmptyWorkspace(); + addProjectConfiguration(tree, 'store', { root: 'apps/store', targets: {} }); + }); + + it('should add extractLicenses to the NxAppWebpackPlugin configuration', async () => { + tree.write('apps/store/webpack.config.js', exampleWebpackConfig()); + + await update(tree); + + expect(readJson(tree, 'apps/store/project.json')).toHaveProperty( + 'targets.build.configurations.development.args', + ['--node-env=development'] + ); + + expect(tree.read('apps/store/webpack.config.js', 'utf-8')).toContain( + `extractLicenses: process.env['NODE_ENV'] === 'production'` + ); + }); + + it('should replace extractLicenses to NxAppWebpackPlugin if it already exists', async () => { + tree.write( + 'apps/store/webpack.config.js', + exampleWebpackConfig('extractLicenses: true') + ); + + await update(tree); + + expect(tree.read('apps/store/webpack.config.js', 'utf-8')).toContain( + `extractLicenses: process.env['NODE_ENV'] === 'production'` + ); + }); +}); diff --git a/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.ts b/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.ts index 7cd9881..50fd21f 100644 --- a/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.ts +++ b/libs/nx-react-workshop/src/migrations/complete-lab-3/complete-lab-3.ts @@ -1,6 +1,101 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { Tree } from '@nx/devkit'; +import { tsquery } from '@phenomnomnominal/tsquery'; +import { formatFiles, Tree, updateJson } from '@nx/devkit'; +import ts = require('typescript'); -export default function update(host: Tree) { - // no file changes +// deeply set a property where parent properties are created if they don't exist +function setProperty(obj: Record, path: string, value: any) { + return path + .split('.') + .reduce( + (acc, key, index, arr) => + (acc[key] = index === arr.length - 1 ? value : acc[key] || {}), + obj + ); +} + +const addOrUpdateProperty = + (propertyName: string | ts.PropertyName, propertyValue: ts.Expression) => + (objectToUpdate: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression => { + const newProperty = ts.factory.createPropertyAssignment( + propertyName, + propertyValue + ); + + const existingPropertyIndex = objectToUpdate.properties.findIndex( + (prop) => prop.name === propertyName + ); + + const updatedProperties = [...objectToUpdate.properties]; + if (existingPropertyIndex === -1) { + updatedProperties.push(newProperty); + } else { + updatedProperties[existingPropertyIndex] = newProperty; + } + + return ts.factory.updateObjectLiteralExpression( + objectToUpdate, + updatedProperties + ); + }; + +const updateMatchingObjectLiteral = + ( + objectToUpdate: ts.ObjectLiteralExpression, + transform: (o: ts.ObjectLiteralExpression) => ts.ObjectLiteralExpression + ): ts.TransformerFactory => + (context) => + (sourceFile) => { + const visit: ts.Visitor = (node) => + ts.isObjectLiteralExpression(node) && node === objectToUpdate + ? transform(node) + : ts.visitEachChild(node, visit, context); + + return ts.visitNode(sourceFile, visit) as ts.SourceFile; + }; + +function addExtractLicensesProperty(webpackConfig: string) { + const ast = tsquery.ast(webpackConfig); + const query = `NewExpression:has(Identifier[name="NxAppWebpackPlugin"]) ObjectLiteralExpression`; + const [config] = tsquery(ast, query); + + if (!config) { + throw new Error('NxAppWebpackPlugin configuration not found'); + } + + // process.env['NODE_ENV'] === 'production' + const newPropertyValue = ts.factory.createBinaryExpression( + ts.factory.createElementAccessExpression( + ts.factory.createIdentifier('process.env'), + ts.factory.createStringLiteral('NODE_ENV') + ), + ts.SyntaxKind.EqualsEqualsEqualsToken, + ts.factory.createStringLiteral('production') + ); + + const result = ts.transform(ast, [ + updateMatchingObjectLiteral( + config, + addOrUpdateProperty('extractLicenses', newPropertyValue) + ), + ]); + + return ts.createPrinter().printFile(result.transformed[0]); +} + +export default async function update(tree: Tree) { + const webpackConfig = tree.read('apps/store/webpack.config.js', 'utf-8'); + tree.write( + 'apps/store/webpack.config.js', + addExtractLicensesProperty(webpackConfig) + ); + + updateJson(tree, 'apps/store/project.json', (json) => { + setProperty(json, 'targets.build.configurations.development.args', [ + '--node-env=development', + ]); + + return json; + }); + + await formatFiles(tree); }