DEV Community

Raúl Julián López Caña
Raúl Julián López Caña

Posted on • Edited on

Angular Tips | Environmental Providers using CLI Builders

Providers that are injected depending on the configuration.

Have you ever thought about configuring different Angular development environments to consume resources or services depending on the configuration? Generate builds of the same application that consume resources from different sources? Then read on!

In some Web application development scenarios that work with APIs, we may not have access to them at will. In these cases, solutions such as proxies, fake/mock servers… are often used.

All of them are good solutions, but… Can we take advantage of Angular Dependency Injection and the CLI Builders to create and provide services depending on the environment? Let’s see it :)

First of all, if you are not familiar with Angular configurations, I advise you to take a look at the official documentation section.

Our goal is to achieve the following behavior:

# Should init dev-server with HTTP Providers*
ng serve

# Should init dev-server with Faker Providers*
ng serve --configuration faker
Enter fullscreen mode Exit fullscreen mode

Setup Environment

# Create a new Angular Project
ng new env-providers

# Install needed dependences
npm install --save-dev @angular-builders/custom-webpack
npm install --save faker
Enter fullscreen mode Exit fullscreen mode

Modeling Providers

We need to get the following structure:

  • FooInterface: defines the interface to be implemented by the different services (Http & Faker)

foo-service/foo-service.interface.ts

import { Observable } from 'rxjs';

export interface IFooService {
  retrieve(id: number): Observable<Foo>;
}
Enter fullscreen mode Exit fullscreen mode
  • FooService (HTTP impl.): concrete implementation of the service with access to data by HTTP protocol.

foo-service/foo.servicice.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { IFooService } from './foo-service.interface';

import { HttpClient } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class FooService implements IFooService {

  constructor(
    private http: HttpClient
  ) {}

  retrieve(id: number): Observable<Foo> {
    return this.http.get(`${environment.API_URL}/foo`);
  }
}
Enter fullscreen mode Exit fullscreen mode
  • FooService (Faker impl.): concrete implementation of the service of objects generated by Faker.

foo-service/foo*.servicice.faker.ts

import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { IFooService } from './foo-service.interface';

import * as faker from 'faker';

@Injectable({
  providedIn: 'root'
})
export class FooService implements IFooService {

  retrieve(id: number): Observable<Foo> {
    return of(new Foo({
      id: id,
      name: faker.name
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we have our providers ready to use, so … Let’s do it!

app.component.ts

import { Component, OnInit } from '@angular/core';
import { FooService } from './foo-service/foo.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {

  public foo$: Observable<Foo>;

  constructor(
    private *fooService*: FooService
  ) {}

  ngOnInit() {
    this.foo$ = this.fooService.retrieve(1);
  }
}
Enter fullscreen mode Exit fullscreen mode

Note that the service is being imported from /foo.service, that is, the service that will be used by default when running ng serve without additional parameters.

Configuring & Hacking CLI Builders

Now that we have our services created and are being consumed from a component of our application, we will configure the project so that the Angular CLI works as described at the beginning of the article.

Artifact & builder configuration

We must modify the angular.json file with a configuration similar to the one I show below:

angular.json

"projects": {
  . . .
  "architect": {
    /* <build> artifact config */
    "build": {
      /* Custom artifact builder */
      "builder": "@angular-builders/custom-webpack:browser",
        "options": {
          /* Our custom builder adjustment */
          "customWebpackConfig": {
            "path": "./providers.config.ts"
          },
            . . .
          },
          "configurations": {
            "production": {
              . . .
            },
            /* Our build configuration */
            "faker": {
              "tsConfig": "tsconfig.app.faker.json"
            }
         }
      }    
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

In this file, we perform 3 important changes:

  • We changed the default Angular builder to the one we installed from @angular-builders/custom-webpack.

  • With the customWebpackConfig property we indicate the file that will perform the necessary modifications to inject some or other providers. We will build later.

  • We create a new configuration called faker. This will perform the TypeScript transpilation with the configuration defined in tsconfig.app.faker.json .

The new tsconfig file:

tsconfig.app.faker.json

{
  "extends": "./tsconfig.app.json",
  "include": [
    "src//*.d.ts",
    "src/**/*.service.faker.ts"
  ]
}
Enter fullscreen mode Exit fullscreen mode

What we are doing with this configuration is explicitly telling Typescript to transpile the files ending with .service.faker.ts. This is because **if imports of the files are not found, the TypeScript compiler will not compile them unless explicitly specified* (includeoption).

Extending Builder behavior

Finally, we will extend the functionality of the CLI Builder with the following configuration.

providers.config.ts

import * as webpack from 'webpack';

const PROVIDER_FILE_CASING = '.service';

// Retrieve '--configuration' parameter*
const configurationIndex =
  process.argv.indexOf('--configuration') > -1
    ? process.argv.indexOf('--configuration') + 1
    : 0;
const configuration = configurationIndex
    ? `${process.argv[configurationIndex]}`
    : '';

export default (*config*: webpack.Configuration) => {
  config.plugins.push(
  /*
   * Using webpack plugin to replacement:
   *   We are searching all '.service' occurrences
   *   & replace it to `.service.<configuration>`
   */
     new webpack.NormalModuleReplacementPlugin(
       new RegExp(`(.*)${PROVIDER_FILE_CASING}(\.*)`),
         resource => {
           resource.request = resource.request.replace(
             new RegExp(`${PROVIDER_FILE_CASING}`),
             `${PROVIDER_FILE_CASING}.${configuration}`
           );
         }
      )
   );
   // New config is returned to Builder
  return config;
};
Enter fullscreen mode Exit fullscreen mode

We are creating a replacement rule that replaces:

  • .service => *.service.faker

That is, replace HTTP providers with Faker providers.

And that’s it! Now, when we execute the ng serve command, the application will be built on the development server with the HTTP providers, while if we run ng serve --configuration faker, it will do so with the Faker providers.

Applicability

We can use this approach if…

  • We need to configure different development environments that access to data from different sources or comunication protocols.

  • We need to generate different builds of our application based on the Data Access Layer.

  • We want to reduce test execution time due to server latency in test environments or we cannot access it.

Top comments (0)