Search code examples
angulartypescriptservicebundletree-shaking

How to make Angular mock service tree-shakeable


Context

In an Angular 9 project, I am working with two environments: production & mock.

In the Core Module, I check for mock environment.

  • If build is made with mock configuration I inject mocked services that return mocked data, so no external http requests are made.

  • If build is made with prod configuration, real services are injected.

I do it like this:

 core.module.ts

@NgModule({
  declarations: [],
  providers: [],
  imports: [BrowserModule, HttpClientModule],
  exports: [],
})
export class CoreModule {}

country.service.proxy.ts

const countryServiceFactory = (
  _http: HttpClient,
  _errorUtil: ErrorUtilService
) => {
  return isMock
    ? new ServiceMock()
    : new Service(_http, _errorUtil);
};

@Injectable({
  providedIn: CoreModule,
  useFactory: countryServiceFactory,
})
export abstract class CountryServiceProxy {
  abstract getCountries(): Observable<CountryWithLanguages[]>;
}

Where ServiceMock and Service implement the same interface.

This works.

Problem

Code is not tree shakeable. The result is that in my bundle (when I run ng build --prod) even the mock services are included.

I want to switch each service from mock to prod during development.

Goal

How can I make Angular to bundle only the service that it is going to be used?


I am using:

Angular CLI: 9.0.4
Node: 13.6.0
OS: darwin x64

Ivy Workspace: Yes

Thank you! :)


Solution

  • I have just tried one approach that seems to work:

    • Declare the relevant service factory in your environment.{env}.ts files
    • Use the environment factory as your service provider

    My test setup:

    base class

    @Injectable()
    export abstract class TestService {
      abstract environment: string;
    }
    

    dev service

    @Injectable()
    export class DevTestService extends TestService {
      environment = 'qwertydev';
    }
    

    prod service

    @Injectable()
    export class ProdTestService extends TestService {
      environment = 'qwertyprod';
    }
    

    environment.ts

    export const environment = {
      testServiceFactory: () => new DevTestService()
    };
    

    environment.production.ts

    export const environment = {
      testServiceFactory: () => new ProdTestService()
    };
    

    app.module.ts

    providers: [
      { provide: TestService, useFactory: environment.testServiceFactory }
    ],
    

    app.component.ts

    constructor(private testService: TestService) {}
    
    ngOnInit() {
      console.log(this.testService.get());
    }
    

    When I inspect my build files, I only find qwertydev in the dev build, and qwertprod in the prod build, which suggests that they have been tree-shaken.

    I used the strings qwerty* to make it easy to search the build files after minification.

    Declaring services in the module

    I have declared the provider in the module to avoid circular references. It is easy to introduce a circular reference by declaring a service as providedIn: Module.

    You can work around this by declaring a third-party module, but this seems overkill.

    I have demonstrated this in an older answer: @Injectable() decorator and providers array

    Alternative approaches

    It doesn't quite feel right declaring service factories in the environment files. I did it for testing just for simplicity. You could create you own set of environment-specific files that are overwritten at build time in the same way as the environment files, but quite frankly this sounds like a maintenance nightmare.