11

I'm using the latest versions of all Angular-related packages (so Angular 10).

I want to add some code to a component, but I only want this code to exist in dev, never in a production build. It needs to be completely stripped in prod builds. I found this comment, which indicates that environments do this automatically (because they're const).

I tried using that exact code in my app, but the dev code is still there in a production build. I copied the code over to a new test app that I made with ng new, and it does work properly there.

What things should I be looking for, how can I fix this? Is this possibly because I have CommonJS dependencies, and if so, can I do anything about that (since I can't remove those dependencies)?

Some notes:

  • An issue has been opened on the angular-cli repo here.
  • The environment object is never written to anywhere in the codebase, I've searched thoroughly. (It's only used in a few places anyway.)
  • Code bounded with if (false) { } is properly stripped.
  • Removing the services export from the end of environment{.prod}.ts does not fix the problem.
  • Removing all CommonJS dependencies does not fix the problem.

Here's environment.prod.ts (environment.ts is the same, just with false instead of true):

export const environment = {
  production: true
};

export * from './services/services';

Here's the main.ts that I'm testing with:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from 'environments/environment';
import { AppModule } from './app/app.module';

// tslint:disable:no-console

if (environment.production) {
  console.warn('this is a prod build');
  enableProdMode();
}

if (!environment.production) {
  console.warn('this is a dev build');
}

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Here's the relevant output code after running ng build -c my-prod-config:

o.X.production && (console.warn('this is a prod build'), Object(i.R) ()),
o.X.production || console.warn('this is a dev build'),
s.d().bootstrapModule(fi).catch (e=>console.error(e))

Here's the relevant part of angular.json:

"my-prod-config": {
  "optimization": true,
  "outputHashing": "all",
  "sourceMap": false,
  "extractCss": true,
  "namedChunks": false,
  "aot": true,
  "extractLicenses": true,
  "vendorChunk": false,
  "buildOptimizer": true,
  "stylePreprocessorOptions": {
    "includePaths": [
      "src/styles"
    ]
  },
  "fileReplacements": [
    {
      "replace": "src/environments/environment.ts",
      "with": "src/environments/environment.prod.ts"
    }
  ],
  "baseHref": "./"
}

Here's tsconfig.base.json:

{
  "compileOnSave": false,
  "compilerOptions": {
    "downlevelIteration": true,
    "importHelpers": true,
    "module": "es2020",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "moduleResolution": "node",
    "baseUrl": "src/",
    "experimentalDecorators": true,
    "allowJs": true,
    "target": "es2015",
    "lib": [
      "es2018",
      "dom"
    ],
    "paths": {
      "path1": [
        "app/modules/stripped-from-stack-overflow-example1"
      ],
      "path2": [
        "app/modules/stripped-from-stack-overflow-example2"
      ]
    }
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "strictTemplates": true,
    "strictInjectionParameters": true
  }
}

Here's package.json:

{
  "name": "my-app",
  "version": "0.0.0",
  "license": "MIT",
  "scripts": {
    "section stripped": "section stripped"
  },
  "private": true,
  "dependencies": {
    "@angular/animations": "10.0.8",
    "@angular/common": "10.0.8",
    "@angular/compiler": "10.0.8",
    "@angular/core": "10.0.8",
    "@angular/forms": "10.0.8",
    "@angular/platform-browser": "10.0.8",
    "@angular/platform-browser-dynamic": "10.0.8",
    "@angular/router": "10.0.8",
    "@ng-idle/core": "9.0.0-beta.1",
    "@ng-idle/keepalive": "9.0.0-beta.1",
    "@ngneat/until-destroy": "8.0.1",
    "angular-svg-icon": "10.0.0",
    "brace": "0.11.1",
    "caniuse-lite": "1.0.30001111",
    "chart.js": "2.9.3",
    "core-js": "3.6.5",
    "css-vars-ponyfill": "2.3.2",
    "detect-browser": "5.1.1",
    "element-closest-polyfill": "1.0.2",
    "file-saver": "2.0.2",
    "fomantic-ui": "2.8.6",
    "jsonexport": "3.0.1",
    "moment": "2.24.0",
    "ngx-drag-drop": "2.0.0",
    "rxjs": "6.6.2",
    "tslib": "^2.0.0",
    "typeface-roboto": "0.0.75",
    "uuid": "8.3.0",
    "zone.js": "0.10.3"
  },
  "devDependencies": {
    "@angular-devkit/build-angular": "0.1000.5",
    "@angular/cli": "10.0.5",
    "@angular/compiler-cli": "10.0.8",
    "@angular/language-service": "10.0.8",
    "@types/chart.js": "2.7.54",
    "@types/file-saver": "2.0.1",
    "@types/uuid": "8.0.1",
    "codelyzer": "^6.0.0",
    "rimraf": "3.0.2",
    "rxjs-tslint-rules": "4.34.0",
    "ts-node": "8.10.2",
    "tslint": "6.1.3",
    "tslint-angular": "3.0.2",
    "typescript": "3.9.7",
    "webpack-bundle-analyzer": "3.8.0"
  }
}
vaindil
  • 7,536
  • 21
  • 68
  • 127
  • Can you please include package.json? – yurzui Aug 17 '20 at 19:24
  • @yurzui Added to the question. – vaindil Aug 17 '20 at 20:35
  • Did you try to remove those CommonJS dependencies? Where is exactly in your code those conditions? – yurzui Aug 18 '20 at 03:53
  • What is the purpose of this `export * from './services/services';` Could you remove it and see if there is any difference – Nikolay Aug 18 '20 at 10:42
  • @yurzui I can't remove the CommonJS dependencies at this time, they don't have any alternatives that I'm aware of. This is a problem anywhere in my code, whether I try to add it to `main.ts` or inside a component. – vaindil Aug 18 '20 at 14:19
  • @Nikolay We have mock services and use the environment to switch between them. Our components import the services from the environment file, which automatically switches them out when built with our mock configuration. I can try refactoring to remove them later this week. – vaindil Aug 18 '20 at 14:20
  • I don't ask you to remove them completely. I want to understand what is the reason for broken tree-shaking – yurzui Aug 18 '20 at 14:21
  • I also faced similar issue in Angular 8 and it was related to how terser-plugin works – yurzui Aug 18 '20 at 14:22
  • @Nikolay Was finally able to test, removing that export did not fix the problem. – vaindil Aug 21 '20 at 21:17
  • @yurzui Also just tested removing the CommonJS dependencies. I was able to remove all of them, the Angular CLI no longer complains about any, but that did not fix the problem. Code is still not being stripped. – vaindil Aug 21 '20 at 22:08
  • In main.ts environment path should be `import { environment } from './environments/environment';` but you have used `import { environment } from 'environments/environment';` I really hope that is not the issue . unless you've made changes in the directory structure – Shaheer Khan Aug 22 '20 at 01:16
  • 1
    It would be great if you could reproduce it with minimum amount of code which you might share as a github repo. – yurzui Aug 22 '20 at 03:07
  • I just noticed the same issue in our project - will investigate further – amakhrov Aug 23 '20 at 18:36
  • @vaindil Any chance to reproduce it? – yurzui Aug 24 '20 at 15:22
  • @yurzui I'm actively working on that right now, but no luck yet. For what it's worth, I did open an issue on the angular-cli repo [here](https://github.com/angular/angular-cli/issues/18603). – vaindil Aug 24 '20 at 15:48

5 Answers5

6

You could apply the same logic as environment.ts; create main.prod.ts (without the dev specific code) and main.dev.ts (with dev specific code), then use fileReplacements in your config.

The config for prod would be:

 "fileReplacements": [
      ...
      {
        "replace": "src/main.ts",
        "with": "src/main.prod.ts"
      }
Albondi
  • 1,141
  • 1
  • 8
  • 19
  • I'll keep this in mind, but the dev code I want to apply is actually buried in a component. I know I can apply this same logic to a component, but I'd rather fix the tree shaking if possible because it's much cleaner and requires a lot less maintenance. – vaindil Aug 17 '20 at 20:37
  • 1
    I don't believe you can achieve that because it has to be done at a build level, and angular does not provide anything to help with that. Everything you do at an app level (like the if(production) ), has to be in the JS code to be interpreted by the browser so it can't be ommited, if you have an import of a class, that class gets entirely added to the final code. But good luck, maybe I'm missing something. – Albondi Aug 17 '20 at 21:15
  • [This recent comment](https://github.com/angular/angular-cli/issues/10999#issuecomment-650317497) from an Angular team member states that this should be happening, and it does work in a test app (created with `ng new`) with basically this exact same setup. It just doesn't work in my actual prod app. – vaindil Aug 17 '20 at 21:19
  • Just tested your configuration and main.ts and it works for me, the code gets properly stripped, how are you testing it? "ng serve -c my-prod-config" ? The files from the ng build do not have the dev code also. – Albondi Aug 18 '20 at 14:26
  • Yeah, that's the problem, the exact same configuration works in a test app but not my actual app. That's why I posted this, to try to figure out why. – vaindil Aug 21 '20 at 21:18
1

The post that you linked to specifically states that the tree-shaking occurs for 'Code gated by constants in if statements' . So you may need to alter your if statement to:

if (environment.production===true) {
  console.warn('this is a prod build');
  enableProdMode();
}
else    
{
  console.warn('this is a dev build');
}

to introduce the presence of a constant.

Boluc Papuccuoglu
  • 2,318
  • 1
  • 14
  • 24
1

This question was answered by an Angular team member here on GitHub. The answer is that this is a Webpack issue--if the environment file is imported into multiple output files, then Webpack is unable to optimize it properly. I've pasted the full response below for posterity.

Without a reproduction the definitive cause is hard to discern. However, a potential cause is the use of the environment JS module (environment.ts/environment.prod.ts) in more than one generated output file. This could be the case if the environment module is used in the main code and in the code for a lazy route. When this happens, Webpack cannot concatenate the environment module with the main module (as happens in a new project) because the environment module needs to be accessible to two different output modules. This then in turn prevents the optimizer from inlining the production property value since the environment object is now essentially an import from another module and not a local variable.

When this happens code similar to the following (which represents a separate Webpack module) should end up in the main output file for the application:

AytR: function (module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_require__.d(__webpack_exports__, "a", function () {
    return environment;
  });
  const environment = { production: !0 };
},
vaindil
  • 7,536
  • 21
  • 68
  • 127
0

I don't know what is wrong with your environment, but it seems that you don't need to do anything and that production build takes care of this.

For example I tested having a component with this code:

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

  constructor() {
    console.log('A');
    if (environment.production) {
      console.log('B');
    } else {
      console.log('C');
    }
    console.log('D');
    if (!environment.production) {
      console.log('E');
    } else {
      console.log('F');
    }
    console.log('G');
  }
}

Then I ran ng build --prod. This is how the constructor of the component was emitted uglified in output code:

{class t{constructor(){this.title="test1",console.log("A"),console.log("B"),console.log("D"),console.log("F"),console.log("G")}}

Note that the if conditions and console.log('C') and console.log('E') are not in the output.

And this is how it was emitted in the es5 output:

(Wu=function n(){v(this,n),this.title="test1",console.log("A"),console.log("B"),console.log("D"),console.log("F"),console.log("G")})

Again the if conditions and console.log('C') and console.log('E')

So just building with --prod flag will solve it unless something wrong in your environment.

Sherif Elmetainy
  • 4,034
  • 1
  • 13
  • 22
  • I build the app with `ng build -c my-prod-config`, and that config (included in the question) is virtually the same as the production config generated by `ng new`. I tested my method just to make sure, I renamed the config in that `ng new` app and built the same way as my real app, and code was stripped. Thanks though! – vaindil Aug 21 '20 at 13:01
  • I also tested having a config name my-prod-config and also the code got stripped. Actually --prod is just shorthand to -c production. And the name production is not treated specially in the build process. So it should be the same. – Sherif Elmetainy Aug 21 '20 at 14:28
0

as we know environment.ts file will get replace by environment.prod.ts file during prod build.you have written if else statements in the app.component.ts condition these condition will be evaluated during runtime & will not tree shakes.

I would like to suggest one alternate-native approach.Create two library projects called lib-dev & lib-prod. use ng g library lib-prod & ng g library lib-dev to create library project. create required module, components & services inside the library project.make sure component selector, module & services name should be same in both library projects.

name in package.json of lib-prod & lib-dev should be same.

{
  "name": "my-lib",
  "version": "0.0.1",
  "peerDependencies": {
    "@angular/common": "^10.0.0",
    "@angular/core": "^10.0.0"
  }
}

tsconfig.json

   ....
    "paths": {
      "my-lib": [
        "dist/my-lib"
      ],
      "extension/*": [
        "dist/my-lib/*"
      ]
    }

In your app.module.ts use compiled library project.

import { MyLibModule } from "dist/my-lib";

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    .....
    MyLibModule
  ],
  providers: [
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

package.json of main app

{
  "name": "demandfarm-ngweb",
  "version": "0.0.0",
  "scripts": {
    "ng": "ng",
    "start": "ng build lib-dev && ng serve",
    ...
    "build:prod": "ng build lib-prod && ng build --prod "
  },

For dev, npm run start command will first compile lib-dev library project & then runs ng serve. it will use compiled lib-dev in main app.

For prod, npm run build:prod command will first compile lib-prod library project & then runs ng build --prod.

  • Thanks for the answer. This hypothetically works, but I have code gated by compile-time constants across a few different components. This solution would very quickly get messy and difficult to manage, so I'm trying to fix the root issue, not use a workaround. Thank you though! – vaindil Aug 24 '20 at 16:38