7

I'm using Angular 9 and I have some code like this:

(features.ts, autogenerated:)
// AUTO-GENERTATED FILE. DO NOT EDIT!
export const Features = {
  // Whether to reveal our Secret New Feature to the world
  ENABLE_SECRET_FEATURE: 1
};

(mycode.ts, app code)
import { Features } from 'generated/features.ts';

function doSomething() {
  if (Features.ENABLE_SECRET_FEATURE) {
    doAIBlockChainARThing();
  } else {
    doSomeBoringOldCRUDThing();
  }
}

I'd like the code emitted to be EITHER

function doSomething() {
    doAIBlockChainARThing();
}

OR

function doSomething() {
    doSomeBoringOldCRUDThing();
}

but not both.

Is there an invocation of ng build that would make this happen? I know uglify can sort of do this and Closure Compiler certainly can. My current invocation is: ng build --aot=true --stats-json --buildOptimizer=true --optimization=true --prod

After some experimentation I have see that the terser settings in Angular prod builds do what I want if I have code like:

const SECRET_FEATURE = true;
if (SECRET_FEATURE) {
  // code here is emitted
} else {
  // code here is NOT emitted
}

However if I try to do something like:

import {SECRET_FEATURE} from 'my-features';

if (SECRET_FEATURE) { // this conditional is emitted
  // this code is emitted
} else {
  // this code is also emitted
}

My thought is I'll have to use something like https://www.npmjs.com/package/tsickle for better dead-code elimination, along with a custom WebPack config to call it. I was hoping for a more Angular-centric path just so I don't have to create/document a lot of custom machinery for future engineers.

Gavin
  • 6,495
  • 3
  • 21
  • 22
  • Are you trying to do something like feature flags? – mwilson Mar 03 '20 at 01:54
  • 2
    I'm exactly trying to do feature flags. – Gavin Mar 04 '20 at 13:28
  • did you try: `import {SECRET_FEATURE} from 'my-features'; const secretFeature = SECRET_FEATURE; if (secretFeature) { /* code here is emitted */ } else { /* code here is NOT emitted */ }` ? – DoronG Mar 04 '20 at 15:02
  • That doesn't work because terser isn't wise enough to follow the code elision all the way back to the constant declared in another file (closure compiler can do it). If I *could* tune the terser settings in my Angular builds it's possible to use their defines, but Angular doesn't expose those. – Gavin Mar 04 '20 at 15:28
  • As I understood, you are looking for feature flagging solution that will actually exclude the "turned off" code from the bundle. Are you OK with actually building the app for each Feature (in this case twice)? Or will it be enough to lazy load that code? – Vojtech Mar 05 '20 at 20:32
  • 1
    We'd be actually building the app with some set of features enabled/disabled depending on environment (Features.SECRET_FEAURE=1 for staging, Features.SECRET_FEATURE=0 for production, etc.). The goal is to not "leak" features or pay the price for their code size until they ship. – Gavin Mar 06 '20 at 18:35

3 Answers3

3

I've just tried on a fresh angular 9 project build with ng build --prod

With the following code, in a component's class

import { Features } from 'generated/features.ts';

doSomething() {
  if (Features.ENABLE_SECRET_FEATURE) {
    console.log('AAAA');
  } else {
    console.log('BBBB')();
  }
}

the compiled code is what you expect (the else part is not emitted)

doSomething() {console.log('AAAA');}

If you have other methods calls in you if/else, this is a bit different

For instance, with the code below

import { Features } from 'generated/features.ts';

   doSomething() {
    if (Features.ENABLE_SECRET_FEATURE) {
      this.doAIBlockChainARThing();
    } else {
      this.doSomeBoringOldCRUDThing();
    }
  }

  private doAIBlockChainARThing()
  {
    console.log('AAAAAAAAA');
  }

  private doSomeBoringOldCRUDThing()
  {
    console.log('BBBBB');
  }

the compiled code is

doSomething(){this.doAIBlockChainARThing()}
doAIBlockChainARThing(){console.log("AAAAAAAAA")}
doSomeBoringOldCRUDThing(){console.log("BBBBB")}}

So the else part is not generated, but unused private methods are not dropped by terser either.

You could use your if test again in the specific method to have no secret code generated at all, but it's not very convenient

  private doAIBlockChainARThing()
  {
    if (Features.ENABLE_SECRET_FEATURE)
    console.log('AAAAAAAAA');
  }
David
  • 33,444
  • 11
  • 80
  • 118
  • For this code: `import { Features } from '../generated/featureFlags'; if (Features.SECRET_FEATURE) { console.log('+root'); } else { console.log('-root'); } ` I get: `a.a.SECRET_FEATURE ? console.log('+root') : console.log('-root'); ` – Gavin Mar 06 '20 at 19:41
  • Where is your code exactly? In a ts class? In a ts file without a class? – David Mar 10 '20 at 10:09
1

For build specific changes you should use Angular.json fileReplacements.

See related answer @ngrx/store-devtools for production mode

MTJ
  • 1,059
  • 1
  • 10
  • 23
  • 1
    For a feature-flagging system you want something much more fine-grained than file replacements. – Gavin Mar 04 '20 at 13:30
  • @Gavin, no you don't need more fine-grained for feature-flagging, fileReplacements should fullfill any feature flagging requirement... otherwise it might mean that your are mixing unfinished code with production released code. If needed, your architecture should be able to "exclude" all the unfinished code from production release. – luiscla27 Mar 10 '20 at 00:01
  • 1
    @LuisLimas, depends on why you use feature flagging. For shared application base producing white-label products with different set / overridden features file replacements is problematic as it quickly becomes very complex. That being said with file replacements you could easily prepare each project to have their own set of replacements. – MTJ Mar 11 '20 at 07:06
  • Your architecture should be able to "exclude" all the unfinished code from production release, that is a MUST BE design pattern while considering the use of fileReplacements. Production builds should be prepared by a continuous integration software like Jenkins... Your architecture may be the one to handle what is unfinished and what's not. Making trusty builds should never rely on the developer experience. – luiscla27 Aug 20 '20 at 16:32
0

You could concentrate your features in Angular 9's lazy loaded components (ivy), and give them a webpackChunkName to match and delete from you build files (if you really want it to be very secret features). Then use your feature flags to prevent lazy loading one way or another. See https://netbasal.com/welcome-to-the-ivy-league-lazy-loading-components-in-angular-v9-e76f0ee2854a for example.

user3791775
  • 426
  • 3
  • 5