From 479fa0bb1664ec618d482c15f0e52ba9d58acb07 Mon Sep 17 00:00:00 2001 From: Artur Androsovych Date: Tue, 19 Nov 2019 13:33:58 +0200 Subject: [PATCH] feat(custom-webpack): support Webpack config written in TS (#626) --- packages/custom-webpack/README.md | 77 +++++++++++++------ .../examples/full-cycle-app/angular.json | 4 +- ...pack.config.js => extra-webpack.config.ts} | 8 +- packages/custom-webpack/package.json | 3 +- .../src/custom-webpack-builder.ts | 28 ++++++- 5 files changed, 86 insertions(+), 34 deletions(-) rename packages/custom-webpack/examples/full-cycle-app/{extra-webpack.config.js => extra-webpack.config.ts} (65%) diff --git a/packages/custom-webpack/README.md b/packages/custom-webpack/README.md index e3382a942..afbd31bd7 100644 --- a/packages/custom-webpack/README.md +++ b/packages/custom-webpack/README.md @@ -25,7 +25,7 @@ Allow customizing build configuration without ejecting webpack configuration (`n "[architect-target]": { "builder": "@angular-builders/custom-webpack:[browser|server|karma|dev-server]" "options": { - ... + ... } ``` Where: @@ -36,7 +36,7 @@ Allow customizing build configuration without ejecting webpack configuration (`n `ng run [project]:[architect-target]` If it is one of the predefined targets, you can run it with `ng [architect-target]` -## For example +## For Example - angular.json: ```js @@ -49,7 +49,7 @@ Allow customizing build configuration without ejecting webpack configuration (`n "build": { "builder": "@angular-builders/custom-webpack:browser" "options": { - ... + ... } ``` - Run the build: `ng build` @@ -61,7 +61,7 @@ Allow customizing build configuration without ejecting webpack configuration (`n - [@angular-builders/custom-webpack:karma](#Custom-webpack-Karma) - [@angular-builders/custom-webpack:dev-server](#Custom-webpack-dev-server) -## Custom webpack browser +## Custom Webpack `browser` Extended `@angular-devkit/build-angular:browser` builder that allows to specify additional webpack configuration (on top of the existing under the hood) and `index.html` tranformations. The builder will run the same build as `@angular-devkit/build-angular:browser` does with extra parameters that are specified in the provided webpack configuration. It will also run transformation on `index.html` if specified. @@ -82,7 +82,9 @@ Builder options: "options": { "customWebpackConfig": { "path": "./extra-webpack.config.js", - "mergeStrategies": { "externals": "replace" } + "mergeStrategies": { + "externals": "replace" + } }, "indexTransform": "./index-html-transform.js", "outputPath": "dist/my-cool-client", @@ -95,7 +97,7 @@ Builder options: In this example `externals` entry from `extra-webpack.config.js` will replace `externals` entry from Angular CLI underlying webpack config while all the rest will be appended. In addition `index.html` will be modified by the function exported from `./index-html-transform.js`. -## Custom webpack dev-server +## Custom Webpack `dev-server` Enhanced `@angular-devkit/build-angular:dev-server` builder that leverages the custom webpack builder to get webpack configuration. @@ -111,25 +113,25 @@ Thus, if you use `@angular-builders/custom-webpack:dev-server` along with `@angu "architect": { ... "build": { - "builder": "@angular-builders/custom-webpack:browser" + "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { - path: "./extra-webpack.config.js" - } + "path": "./extra-webpack.config.js" + }, ... } }, "serve": { "builder": "@angular-builders/custom-webpack:dev-server", "options": { - "browserTarget": "my-project:build" + "browserTarget": "my-project:build" } } ``` In this example `dev-server` will use `custom-webpack:browser` builder, hence modified webpack config, when invoking the serve target. -## Custom webpack server +## Custom Webpack `server` Extended `@angular-devkit/build-angular:server` builder that allows to specify additional webpack configuration (on top of the existing under the hood) and `index.html` tranformations. The builder will run the same build as `@angular-devkit/build-angular:server` does with extra parameters that are specified in the provided webpack configuration. @@ -149,7 +151,9 @@ Builder options: "options": { "customWebpackConfig": { "path": "./extra-webpack.config.js", - "mergeStrategies": { "module.rules": "prepend" }, + "mergeStrategies": { + "module.rules": "prepend" + }, "replaceDuplicatePlugins": true }, "outputPath": "dist/my-cool-server", @@ -161,7 +165,7 @@ Builder options: In this example `module.rules` entry from `extra-webpack.config.js` will be prepended to `module.rules` entry from Angular CLI underlying webpack config while all the rest will be appended. Since loaders are evaluated [from right to left](https://webpack.js.org/concepts/loaders/#configuration) this will effectively mean that the loaders you define in your custom configuration will be applied **after** the loaders defined by Angular CLI. -## Custom webpack Karma +## Custom Webpack `karma` Extended `@angular-devkit/build-angular:karma` builder that allows to specify additional webpack configuration (on top of the existing under the hood) and `index.html` tranformations. The builder will run the same build as `@angular-devkit/build-angular:karma` does with extra parameters that are specified in the provided webpack configuration. @@ -186,11 +190,10 @@ Builder options: "polyfills": "src/polyfills.ts", "tsConfig": "src/tsconfig.spec.json", "karmaConfig": "src/karma.conf.js", - ... } ``` -# Custom webpack config object +# Custom Webpack Config Object This option defines your custom webpack configuration. If not specified at all, plain Angular build will run. The following properties are available: @@ -221,7 +224,29 @@ The following properties are available: - `replaceDuplicatePlugins`: Defaults to `false`. If `true`, the plugins in custom webpack config will replace the corresponding plugins in default Angular CLI webpack configuration. If `false`, the [default behavior](#merging-plugins-configuration) will be applied. **Note that if `true`, this option will override `mergeStrategies` for `plugins` field.** -## Merging plugins configuration: +Webpack configuration can be also written in TypeScript. Given the following example: + +```ts +// extra-webpack.config.ts +import * as webpack from 'webpack'; + +export default { + output: { + library: 'shop', + libraryTarget: 'umd', + }, +} as webpack.Configuration; +``` + +Do not forget to specify the correct path to this file: + +```js +"customWebpackConfig": { + "path": "./extra-webpack.config.ts" +}, +``` + +## Merging Plugins Configuration: If in your custom configuration you specify a plugin that is already added by Angular CLI then by default the two instances will be merged. In case of the conflicts your configuration will override the existing one. @@ -229,17 +254,19 @@ Thus, if you'd like to modify an existing plugin configuration, all you have to For example, if you'd like to allow cyclic dependencies that include dynamic imports you only have to specify this single entry: ```js -plugins: [ - new CircularDependencyPlugin({ - allowAsyncCycles: true, - }), -]; +module.exports = { + plugins: [ + new CircularDependencyPlugin({ + allowAsyncCycles: true, + }), + ], +}; ``` Keep in mind though that if there are default values in the plugin's constructor, they would override the corresponding values in the existing instance. So these you have to set explicitly to the same values Angular sets. You can check out an example for plugins merge in the [unit tests](./src/webpack-config-merger.spec.ts) and in [this](https://github.com/meltedspark/angular-builders/issues/13) issue. -## Custom Webpack promisified config +## Custom Webpack Promisified Config Webpack config can also export a `Promise` object that resolves custom config. Given the following example: @@ -260,7 +287,7 @@ module.exports = readFile('./LICENSE', { In this case, the behavior will be the same as when exporting a plain object — the resolved configuration will be merged with the base one. -## Custom Webpack config function +## Custom Webpack Config Function If `customWebpackConfig.path` file exports a function, the behaviour of the builder changes : no more automatic merge is applied, instead the function is called with the base Webpack configuration and must return the new configuration. @@ -311,7 +338,7 @@ module.exports = async config => { }; ``` -# Index transform +# Index Transform ### Important: @@ -369,6 +396,6 @@ In the example we add a paragraph with build configuration to your `index.html`. Full example [here](https://github.com/meltedspark/angular-builders/tree/master/packages/custom-webpack/examples/full-cycle-app). -# Further reading +# Further Reading - [Customizing Angular CLI 6 build  -  an alternative to ng eject](https://medium.com/@meltedspark/customizing-angular-cli-6-build-an-alternative-to-ng-eject-a48304cd3b21) diff --git a/packages/custom-webpack/examples/full-cycle-app/angular.json b/packages/custom-webpack/examples/full-cycle-app/angular.json index ec2e4ee3e..738663f24 100644 --- a/packages/custom-webpack/examples/full-cycle-app/angular.json +++ b/packages/custom-webpack/examples/full-cycle-app/angular.json @@ -18,7 +18,7 @@ "builder": "@angular-builders/custom-webpack:browser", "options": { "customWebpackConfig": { - "path": "./extra-webpack.config.js" + "path": "./extra-webpack.config.ts" }, "indexTransform": "./index-html.transform.js", "outputPath": "dist/full-cycle-app", @@ -137,4 +137,4 @@ "cli": { "packageManager": "yarn" } -} \ No newline at end of file +} diff --git a/packages/custom-webpack/examples/full-cycle-app/extra-webpack.config.js b/packages/custom-webpack/examples/full-cycle-app/extra-webpack.config.ts similarity index 65% rename from packages/custom-webpack/examples/full-cycle-app/extra-webpack.config.js rename to packages/custom-webpack/examples/full-cycle-app/extra-webpack.config.ts index 085336510..9f143b3aa 100644 --- a/packages/custom-webpack/examples/full-cycle-app/extra-webpack.config.js +++ b/packages/custom-webpack/examples/full-cycle-app/extra-webpack.config.ts @@ -1,13 +1,15 @@ -const HtmlWebpackPlugin = require('html-webpack-plugin'); +import * as webpack from 'webpack'; +import * as HtmlWebpackPlugin from 'html-webpack-plugin'; + /** * This is where you define your additional webpack configuration items to be appended to * the end of the webpack config. */ -module.exports = { +export default { plugins: [ new HtmlWebpackPlugin({ filename: 'footer.html', template: 'src/footer-template.html', }), ], -}; +} as webpack.Configuration; diff --git a/packages/custom-webpack/package.json b/packages/custom-webpack/package.json index d13c8731b..cb7d17383 100644 --- a/packages/custom-webpack/package.json +++ b/packages/custom-webpack/package.json @@ -37,7 +37,8 @@ "builders": "builders.json", "dependencies": { "lodash": "^4.17.10", - "webpack-merge": "^4.2.1" + "webpack-merge": "^4.2.1", + "ts-node": "^8.5.2" }, "peerDependencies": { "@angular-devkit/build-angular": ">=0.801.0" diff --git a/packages/custom-webpack/src/custom-webpack-builder.ts b/packages/custom-webpack/src/custom-webpack-builder.ts index 31ba51ede..11657fed5 100644 --- a/packages/custom-webpack/src/custom-webpack-builder.ts +++ b/packages/custom-webpack/src/custom-webpack-builder.ts @@ -24,9 +24,8 @@ export class CustomWebpackBuilder { } const webpackConfigPath = config.path || defaultWebpackConfigPath; - const configOrFactoryOrPromise: CustomWebpackConfig = require(`${getSystemPath( - root - )}/${webpackConfigPath}`); + const path = `${getSystemPath(root)}/${webpackConfigPath}`; + const configOrFactoryOrPromise = resolveCustomWebpackConfig(path); if (typeof configOrFactoryOrPromise === 'function') { // That exported function can be synchronous either @@ -51,3 +50,26 @@ export class CustomWebpackBuilder { ); } } + +function resolveCustomWebpackConfig(path: string): CustomWebpackConfig { + if (path.endsWith('.ts')) { + // Register TS compiler lazily + require('ts-node').register({ + compilerOptions: { + module: 'commonjs', + }, + }); + } + + const customWebpackConfig = require(path); + // If the user provides a configuration in TS file + // then there are 2 cases for exporing an object. The first one is: + // `module.exports = { ... }`. And the second one is: + // `export default { ... }`. The ESM format is compiled into: + // `{ default: { ... } }` + if (typeof customWebpackConfig.default === 'object') { + return customWebpackConfig.default; + } + + return customWebpackConfig; +}