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 @@
+
+
+
+
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
-
+
-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);
}