📜 ⬆️ ⬇️

We update Angular to the 6th version in the project without use of CLI

In this article I will talk about the thorny path of updating Angular with a custom Webpack config that our team had to go a week ago. Perhaps our experience will be useful for those who use Angular with their Webpack configuration, and the rest is interesting as an illustration of where the modern frontend can lead and how to live with it.



Our team is working on the BILLmanager 6 interface. So that you have a general idea of ​​the project before the update, I will inform you that the number of files in it has already exceeded 67 thousand. Architecturally, there are two subprojects: the registration module and the main user interface. The technologies are based on Angular components, directives and modules written in TypeScript. There are several components on the Web components. For styling, we use SASS / SCSS and use CSS variables to theme the application without recompiling.

Prerequisites


Everything has a reason, and our current difficulties got their start a year and a half ago. Then only Angular 2 appeared. Programmers in the company had experience creating applications on Angular 1, ReactJS and their own small framework. Angular 2 at that time absorbed the pluses from the first version and ReactJS. Therefore, it was chosen because of its prospects (like Google), the success of Angular 1 and the formalization that TypeScript provides. We do not write small SPA sites that you can give to the customer and forget, our applications live for a long time and they need constant support and development. BILLmanager is used by providers to sell hosting and work with clients. Therefore, both it and other ISPsystem products need to be constantly maintained and developed. In principle, the Angular 2 team has already written everywhere that now there will be just Angular and the development of the framework will happen evolutionarily, which is suitable for our internal processes.
')
As I already wrote, our projects are large and long-lived. They have complex configs with flexible settings for individual assemblies. And Webpack has long been a kind of standard for assembling large and small projects in the frontend world, so the choice here was unequivocal.

As a result, the common part of the config in the project looked as follows:

the contents of the file webpack.config.common.js before updating
module.exports = { context: PATHS.root, target: 'web', entry, resolve: { extensions: ['.ts', '.js', '.json'], modules: [PATHS.src, PATHS.node_modules], }, module: { rules: [{ test: /\.ts$/, loaders: [{ loader: 'awesome-typescript-loader', options: { transpileOnly: process.env.NODE_ENV !== 'production' } }, 'angular2-template-loader', 'angular2-router-loader' ], exclude: [/\.(spec|e2e)\.ts$/], }, { test: /\.ts$/, include: [/\.(spec|e2e)\.ts$/], loaders: ['awesome-typescript-loader', 'angular2-template-loader'] }, { test: /\.json$/, use: 'json-loader' }, { test: /\.html$/, use: [{ loader: 'html-loader', }], }, { test: /\.(eot|woff|woff2|ttf|png|jpg|gif|svg|ico)(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader', options: { context: PATHS.assets, name: '[path][name].[ext]' }, }, { test: /\.css$/, loader: extractSASS.extract({ fallback: 'style-loader', use: 'css-loader?sourceMap' }), exclude: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], }, { test: /\.css$/, include: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], use: [{ loader: "raw-loader" // creates style nodes from JS strings }], }, { test: /\.(scss|sass)$/, loader: extractSASS.extract({ use: [{ loader: "css-loader", }, { loader: "sass-loader", options: { sourceMap: true, } } ], // use style-loader in development fallback: "style-loader" }), exclude: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], }, { test: /\.(scss|sass)$/, use: [{ loader: "raw-loader" // creates style nodes from JS strings }, { loader: "sass-loader", // compiles Sass to CSS options: { sourceMap: true } } ], include: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], } ] }, plugins: [ extractSASS, new webpack.IgnorePlugin(/vertx/), new webpack.ContextReplacementPlugin( // The (\\|\/) piece accounts for path separators in *nix and Windows /\@angular(\\|\/)core(\\|\/)esm5/, PATHS.projectPath, // location of your src {} // a map of your routes ), new webpack.optimize.CommonsChunkPlugin({ name: ['vendor', 'polyfills'], // minChunks: Infinity }), ] }; 


This is very similar to what is described now in the Angular angular.io/guide/webpack documentation. The most interesting of this is the part about compiling .ts files.
 { test: /\.ts$/, loaders: [{ loader: 'awesome-typescript-loader', options: { transpileOnly: process.env.NODE_ENV !== 'production' } }, 'angular2-template-loader', 'angular2-router-loader' ], exclude: [/\.(spec|e2e)\.ts$/], }, { test: /\.ts$/, include: [/\.(spec|e2e)\.ts$/], loaders: ['awesome-typescript-loader', 'angular2-template-loader'] }, 

As you see, we use the angular2-template-loader and angular2-router-loader loaders to build our Angular components. The official documentation is written. And this is extremely strange, since both loaders are not written by the Angular team and are stored in user repositories on GitHub. One of the reasons for choosing Angular as the main framework was that it works like a combine - everything comes out of the box, unlike ReactJS. But here we see that the tool with which our project will be assembled out of the box does not go.

Well, okay, this config worked from the second to the fifth version, and there was no cause for concern. Although no, there was one. At ng-conf 2017, Brad Green spoke about trying to build an Angular application using Bazel and Closure. Who worked with large projects on Angular, he will understand me - builds take a very long time. Our first build of development mode on the fifth version of Angular with the second webpack takes more than 4 minutes. And the desire of the framework developers to make assemblies faster is quite reasonable. Although there is another look at this situation. As my colleague said:
"It was necessary to make a slowly gathering framework, and then start accelerating it."

Angular upgrade to version 6


Our company is well aware of what happens when you don’t update tools. We know what this may eventually lead to. Revolutions do not go off painlessly, and it is better to foresee them, moving in an evolutionary way. Therefore, when there is news about the release of a stable version of Angular 6, we decided to update our project. But it did not work out painlessly.

As with the previous version update, we moved to the site with update.angular.io update guide . Here we were in for the first surprise.



If you do not specify the item “I use ngUpgrade”, then the manual will still offer to execute the ng update @angular/core command.



In the update of previous versions of this was not, but now, it turns out, without the CLI and can not be updated? Perhaps this is a bug and it will be fixed, but today, like a week ago, it was not possible to get clear instructions for updating without the CLI.

If, like us, you still want to continue updating the project, then it is here that our thorny path begins. First you need to determine the direction:

  1. Install the CLI and update the steps of the official manual.
  2. Update packages separately and edit configs yourself.

The first one seemed simpler to us and we went over it.
But after installing, updating the CLI and executing the ng update @angular/core command, we were disappointed.

 $ ng update @angular/core Package "@angular/compiler-cli" has an incompatible peer dependency to "typescript" (requires ">=2.7.2 <2.8", would install "2.8.3") Invalid range: ">=2.3.0 <3.0.0||>=4.0.0" 

The issues on GitHub can be found at github.com/angular/angular- quad/ issues/ 10621 . To date, this error seems to be corrected (judging by github.com/angular/devkit/pull/901 ), but at that time we decided not to go into the jungle of the update utility and updated the packages manually.

After updating the packages, the project stopped running, which, in fact, was expected. Angular 6 uses Webpack 4 (this can be seen if you install it via the CLI). Therefore, in the next step, we updated the Webpack and related packages to the latest versions. The story about updating Webpack pulls into a separate article, so here I will only write that if you use extract-text-webpack-plugin , replace it with mini-css-extract-plugin , and this will save you nerves and strength. You can read about how good the fourth webpack is, here , well, and, actually, an article on migration .

In addition to updating Angular and Webpack, you need to update RxJS to the sixth version, otherwise the project simply will not start. This is a prerequisite and it’s easy to do; you just need to follow the migration documentation . There should be no significant difficulties, RxJS provides a utility that independently makes the necessary changes to the project.

In the meantime, we are returning to the upgrade to Angular 6. The project is still not going to, and gives a lot of unintelligible errors. Here is the time to pay attention to the loader, which handles .ts files. We use a bunch of angular2-template-loader and angular2-router-loader . If you go to the angular2-template-loader repository, you can see that it has not been updated for a year already (it’s strange that we are still offered to use it in the official documentation).



It seems that the problem is how this loader handles our code. We started looking for a replacement and found a plugin for the Ahead-of-Time (AoT) compilation @ ngtools / webpack . This is not an equivalent replacement, as before we used only JIT compilation. But, on the other hand, the Angular team has long been talking about plans to make AoT compilation by default. @ ngtools / webpack is the official tool from Angular DevKit , it is updated constantly and has been redesigned for the sixth version of the framework. To be fair, I can say that you can build Angular 6 project with angular2-template-loader and angular2-router-loader plugins. A bunch of these plugins may be suitable for development, but for production assemblies it is better not to use them due to the lack of additional checks of the executable code. It was precisely this that did not allow us to easily catch all the necessary corrections immediately for switching to the sixth version.

AOT compilation is, by and large, different in that templates are compiled when the application is built, and not after it is launched in the user's browser. On the one hand, it speeds up the application and reduces its size, on the other, it incurs additional costs when compiling and requires more rigor to writing components.

Switching to AOT compilation of a project is a separate big task. To accomplish it, you will have to redo a large part of the project, because AOT has very strict code requirements and if you didn’t observe them right away, it will be difficult. But there is a way out. You can use the @ ngtools / webpack plugin with JIT compilation. To do this, add the parameter skipCodeGeneration=true to the plugin settings.

I will outline the main points that had to be corrected when switching to the @ ngtools / webpack plugin:

  1. In templates, all private variables are replaced by public .
  2. In inheritance, it is undesirable to inherit one component from another (with the same directives). In principle, this is logical, but angular2-template-loader - skipped, and @ ngtools / webpack began to swear on incorrectly created modules.
  3. If we ignore the recommendation above, then you can get an error when using the component component in the designer variables of simple types. This is the most strange mistake. The component is as follows:

 @Component({ selector: '[form-component]', template: '' }) export class FormComponent extends BaseComponent implements OnInit { constructor( public formService: FormService, public formFunc: string, public formParams: Array<any> = [] ) { super(); } ... 

In the logs we see about the following:

 ERROR in : Can't resolve all parameters for FormComponent in form.component.ts: ( [object Object], ?, ?) 

I recommend to fulfill the second rule, but if for some reason it does not work, you can make a small hack https://stackoverflow.com/a/48748942/4778628 , replacing the code above with:

 @Component({ selector: '[form-component]', template: '' }) export class FormComponent extends BaseComponent implements OnInit { constructor( public formService: FormService, @Inject('') public formFunc: string, @Inject('') public formParams: Array<any> = [] ) { super(); } ... 

Unfortunately, the errors did not end there, and we got it in the Angular compiler itself:

error text
  [0] building modules 「wds」: Project is running at http://localhost:8080/ 「wds」: webpack output is served from / 「wds」: Content not from webpack is served from /home/dsumbaev/DEVELOPMENT/bill-client-front/dist 「wds」: 404s will fallback to /index.html [0] building modules/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/@ngtools/webpack/src/angular_compiler_plugin.js:509 if (this.done && (request.request.endsWith('.ts') ^ TypeError: Cannot read property 'request' of null at nmf.hooks.beforeResolve.tapAsync (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/@ngtools/webpack/src/angular_compiler_plugin.js:509:47) at _fn1 (eval at create (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/node_modules/tapable/lib/HookCodeFactory.js:24:12), <anonymous>:27:1) at Object.resolveWithPaths (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/@ngtools/webpack/src/paths-plugin.js:14:9) at nmf.hooks.beforeResolve.tapAsync (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/@ngtools/webpack/src/angular_compiler_plugin.js:521:32) at AsyncSeriesWaterfallHook.eval [as callAsync] (eval at create (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/node_modules/tapable/lib/HookCodeFactory.js:24:12), <anonymous>:19:1) at NormalModuleFactory.create (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/NormalModuleFactory.js:338:28) at semaphore.acquire (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:494:14) at Semaphore.acquire (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/util/Semaphore.js:17:4) at asyncLib.forEach (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:492:15) at arrayEach (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/neo-async/async.js:2400:9) at Object.each (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/neo-async/async.js:2835:9) at Compilation.addModuleDependencies (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:471:12) at Compilation.processModuleDependencies (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:450:8) at afterBuild (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:556:15) at buildModule.err (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:600:11) at callback (/home/dsumbaev/DEVELOPMENT/bill-client-front/node_modules/webpack/lib/Compilation.js:358:35) 


At first, we thought that we were giving the compiler packages from node_modules, but he could not process them, but adding exceptions had no effect on the error. There was nowhere to go and turn late, so a small PR appeared in @ ngtools / webpack. These changes are included in version 6.0.1 of the package. After that, the build was successful and the project started!

BUT! It turned out that all modules except the main one were not pulled up. Let's look at the plugin's @ ngtools / webpack setting.

  new AngularCompilerPlugin({ platform: 0, sourceMap: true, tsConfigPath: path.join(PATHS.root, 'tsconfig.json'), skipCodeGeneration: true, }) 

At first glance, everything is in the documentation . Note that the entryModule parameter is marked as optional. By trial and error, it was found that if the parameter was not specified, the assembly did not go beyond the main module, which is why we received a non-working application. Fixing the problem is easy, you need to add the entryModule .

  new AngularCompilerPlugin({ platform: 0, entryModule: path.join(PATHS.src, 'apps/client/app/app.module#AppModule'), sourceMap: true, tsConfigPath: path.join(PATHS.root, 'tsconfig.json'), skipCodeGeneration: true, }) 

If you remember, at the beginning I wrote that we have two subprojects in the project, but only one can be specified in the entryModule. Here we are lucky, because the second application does not contain nested modules. If you have a different situation: several complex projects within one, then you have to make separate configs for each one or wait for this PR to pass in the Angular DevKit repository.

As a result, the common part of the config in the project was as follows:

the final content of the webpack.config.common.js file
 const path = require('path'); const merge = require('webpack-merge'); const webpack = require('webpack'); const ProgressPlugin = require('webpack/lib/ProgressPlugin'); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); const {AngularCompilerPlugin} = require('@ngtools/webpack'); const { PATHS, PARAMS } = require('./helpers.js'); const devMode = process.env.NODE_ENV === 'development'; let entry = { 'polyfills': path.join(PATHS.src, 'polyfills.browser.ts'), 'main': path.join(PATHS.projectPath, 'main.ts'), 'extform': path.join(PATHS.apps, 'extform/main.ts'), 'style': path.join(PATHS.assets, 'sass', 'app.sass') }; PARAMS.themes.forEach(theme => { entry['themes/' + theme + '/theme'] = path.join(PATHS.themes, theme, 'theme.scss') }); module.exports = { context: PATHS.root, target: 'web', entry, resolve: { extensions: ['.ts', '.js', '.json'], modules: [PATHS.src, PATHS.node_modules], }, mode: process.env.NODE_ENV, stats: 'errors-only', module: { rules: [{ test: /\.ts$/, loader: '@ngtools/webpack', exclude: [/\.(spec|e2e)\.ts$/, /node_modules/], }, { test: /\.ts$/, loader: 'null-loader', include: [/\.(spec|e2e)\.ts$/], }, { test: /\.json$/, use: 'json-loader' }, { test: /\.html$/, use: [{ loader: 'html-loader', }], }, { test: /\.(eot|woff|woff2|ttf|png|jpg|gif|svg|ico)(\?v=\d+\.\d+\.\d+)?$/, loader: 'file-loader', options: { context: PATHS.assets, name: '[path][name].[ext]' }, }, { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, "css-loader" ], exclude: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], }, { test: /\.css$/, include: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], use: [{ loader: "raw-loader" // creates style nodes from JS strings }], }, { test: /\.(scss|sass)$/, use: [ devMode ? 'style-loader' : MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader', ], exclude: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], }, { test: /\.(scss|sass)$/, use: [{ loader: "raw-loader" // creates style nodes from JS strings }, { loader: "sass-loader", // compiles Sass to CSS options: { sourceMap: true } } ], include: [path.join(PATHS.projectPath), path.join(PATHS.src, 'common'), path.join(PATHS.src, 'common-bill')], } ] }, optimization: { splitChunks: { cacheGroups: { commons: { test: /[\\/]node_modules[\\/]/, name: "vendors", chunks: "all" } } } }, plugins: [ new MiniCssExtractPlugin({ filename: '[name].[hash].css', }), new webpack.IgnorePlugin(/vertx/), new ProgressPlugin(), new AngularCompilerPlugin({ platform: 0, entryModule: path.join(PATHS.src, 'apps/client/app/app.module#AppModule'), sourceMap: true, tsConfigPath: path.join(PATHS.root, 'tsconfig.json'), skipCodeGeneration: true, }) ] }; 


Conclusion


After all the above manipulations, we got a working application with Angular 6 and its own Webpack config. During the work 180 project files were corrected, and it took about one week.

Angular is a monolithic framework, and with the advent of the sixth version it becomes even more visible. Now, not only additional tools in the form of a router or a library of HTTP requests, but also build tools go out of the box. And it is better not to touch them, not to make changes that were not conceived by the Angular developers. Only in this case, you can easily update the project and may not be faced with the need to change hundreds of files after the updates. Otherwise, the hard way awaits you and you will have to like negative reviews on the new version in a sea of ​​positive and general joy of those around you.

This does not mean that Angular is bad or good, it just requires special treatment and is not suitable for everyone. If you work with it through the CLI, build the ng project with the utility, test and create modules and components for it, then you will be happy. In our team, we would also like it, but, alas, too much is already tied to our Webpack config. As they say in a good Russian proverb: "I would know where you fall, I would spread straws." A year ago, CLI was not such an indispensable tool in Angular projects, but today, even in the documentation on updating, there is no guide on how to update without it.

Source: https://habr.com/ru/post/358696/


All Articles