Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Angular cli - not removing dependencies of unused library components #13082

Closed
babeal opened this issue Nov 29, 2018 · 22 comments
Closed

Angular cli - not removing dependencies of unused library components #13082

babeal opened this issue Nov 29, 2018 · 22 comments

Comments

@babeal
Copy link

babeal commented Nov 29, 2018

Bug Report or Feature Request (mark with an x)

- [x ] bug report -> please search issues before submitting
- [ ] feature request

Command (mark with an x)

- [x] build

Versions

Npm: 6.4.1
Angular CLI: 7.1.0
Node: 10.14.0
OS: darwin x64
Angular: 7.1.1
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package Version

@angular-devkit/architect 0.11.0
@angular-devkit/build-angular 0.11.0
@angular-devkit/build-ng-packagr 0.11.0
@angular-devkit/build-optimizer 0.11.0
@angular-devkit/build-webpack 0.11.0
@angular-devkit/core 7.1.0
@angular-devkit/schematics 7.1.0
@angular/cli 7.1.0
@ngtools/json-schema 1.1.0
@ngtools/webpack 7.1.0
@schematics/angular 7.1.0
@schematics/update 0.11.0
ng-packagr 4.4.0
rxjs 6.3.3
typescript 3.1.6
webpack 4.23.1

Repro steps

The angular build/packaging process is unable to remove third party dependencies from unused components in a library. The example provided includes a library called components containing a component that imports moment.js and one that doesn't. When only the component that DOES NOT use moment is imported into the application the resulting application bundle will still contain moment. I have tried to force the build process to use esm5 instead of fesm 5 and this does not help. The only thing left referencing moment would be the metadata.json. This problem makes it difficult to build large component libraries using the cli due to the fact that all runtime transitive dependencies will be included in the final bundle whether they are used or not. I would like to know why this is and where the problem resides.

https://github.com/babeal/ng-tdp

clone, run npm install, then npm run analyze

The log given by the failure

The results of the webpack analysis will show moment in the main bundle as well as inspecting the minified main.js in the dist folder.

Desired functionality

Should not include unused external dependencies into the final bundle

Mention any other details that might be useful

@kzc
Copy link

kzc commented Nov 29, 2018

Assuming that the moment package is side effect free, ask them to add "sideEffects": false to their package.json file.

You can make the change locally in node_modules/moment/package.json to verify it works. You may need to clear the webpack build cache after the change.

@babeal
Copy link
Author

babeal commented Nov 30, 2018

@kzc even going into node_modules/moment and changing the package.json to include that property doesn't work. I have already tired that before I created this issue :-).

@filipesilva
Copy link
Contributor

Heya, I was looking at this today. I didn't use webpack-bundle-analyzer as you did, because in the past I had some trouble with it. Instead I used source-map-explorer. I just find it more reliable overall since it doesn't really do anything webpack specific and doesn't rely on webpack state.

This is how it looks like for the repro as is:
image

The important bit is moment.js with around 333kb.

The reason why moment is in the compilation is because:

  • src/app.module.ts imports SimpleComponentModule from components
  • components resolves to dist/components/fesm5/components.js
  • dist/components/fesm5/components.js imports moment

Running ng prod --verbose (I recommend piping it to a file) shows a lot more information.

One interesting bit is

 [wd/R] ./node_modules/moment/moment.js 140 KiB {1} [built]
     ModuleConcatenation bailout: Module is not an ECMAScript module

That means that ModuleConcatenation can't be used with moment because it isn't a ES module. This overall prevents the more advanced optimizations. I have written a bit more about that in a similar problem, #12646 (comment).

At this point it's reasonable to think moment would not end up in the final bundle because you only import SimpleComponentModule. Since that module is a direct export from components that doesn't actually have anything to do with moment (only MomentComponent does) , then there should never be a moment import at all.

At another point in the verbose log we can find this:

     | ./dist/components/fesm5/components.js 1.89 KiB [built]
     |     [only some exports used: SimpleComponentModule, SimpleComponent, MomentComponentModule, MomentComponent]
     |     harmony side effect evaluation components  ./dist/components/components.ngfactory.js 8:0-33
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 9:73-97
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 9:296-320
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 9:322-346
     |     harmony side effect evaluation components  ./src/app/app.component.ngfactory.js 10:0-33
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 11:73-97
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 11:296-320
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 11:322-346
     |     harmony side effect evaluation components  ./src/app/app.module.ngfactory.js 16:0-33
     |     harmony import specifier components  ./src/app/app.component.ngfactory.js 16:233-251
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 17:235-253
     |     harmony import specifier components  ./src/app/app.module.ngfactory.js 17:4797-4821
     |     harmony import specifier components  ./src/app/app.module.ngfactory.js 17:4823-4847
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 18:91-109
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 24:235-253
     |     harmony import specifier components  ./dist/components/components.ngfactory.js 25:91-109

So for some reason, the MomentComponentModule and MomentComponent exports are actually used. So this means that MomentComponent (along with moment) get into the bundle just prior to Terser (the optimizer we use) doing its job.

At this point it is too late to remove moment, because of the ModuleConcatenation bailout I mentioned above. moment is in a different chunk and that indirection makes it impossible for Terser to remove it's code.

That list shows where the imports are used. I added some debug code to see the referenced ngfactories:

  • ng-tdp/dist/components/components.ngfactory.js
/**
 * @fileoverview This file was generated by the Angular template compiler. Do not edit.
 *
 * @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
 * tslint:disable
 */ 
import * as i0 from "@angular/core";
import * as i1 from "components";
var MomentComponentModuleNgFactory = i0.ɵcmf(i1.MomentComponentModule, [], function (_l) { return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, []], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]), i0.ɵmpd(1073742336, i1.MomentComponentModule, i1.MomentComponentModule, [])]); });
export { MomentComponentModuleNgFactory as MomentComponentModuleNgFactory };
var SimpleComponentModuleNgFactory = i0.ɵcmf(i1.SimpleComponentModule, [], function (_l) { return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, []], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]), i0.ɵmpd(1073742336, i1.SimpleComponentModule, i1.SimpleComponentModule, [])]); });
export { SimpleComponentModuleNgFactory as SimpleComponentModuleNgFactory };
var styles_MomentComponent = [];
var RenderType_MomentComponent = i0.ɵcrt({ encapsulation: 2, styles: styles_MomentComponent, data: {} });
export { RenderType_MomentComponent as RenderType_MomentComponent };
export function View_MomentComponent_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "p", [], null, null, null, null, null)), (_l()(), i0.ɵted(-1, null, [" Moment component! "]))], null, null); }
export function View_MomentComponent_Host_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "lib-moment-component", [], null, null, null, View_MomentComponent_0, RenderType_MomentComponent)), i0.ɵdid(1, 114688, null, 0, i1.MomentComponent, [], null, null)], function (_ck, _v) { _ck(_v, 1, 0); }, null); }
var MomentComponentNgFactory = i0.ɵccf("lib-moment-component", i1.MomentComponent, View_MomentComponent_Host_0, {}, {}, []);
export { MomentComponentNgFactory as MomentComponentNgFactory };
var styles_SimpleComponent = [];
var RenderType_SimpleComponent = i0.ɵcrt({ encapsulation: 2, styles: styles_SimpleComponent, data: {} });
export { RenderType_SimpleComponent as RenderType_SimpleComponent };
export function View_SimpleComponent_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "p", [], null, null, null, null, null)), (_l()(), i0.ɵted(-1, null, [" Simple component! "]))], null, null); }
export function View_SimpleComponent_Host_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "lib-simple-component", [], null, null, null, View_SimpleComponent_0, RenderType_SimpleComponent)), i0.ɵdid(1, 114688, null, 0, i1.SimpleComponent, [], null, null)], function (_ck, _v) { _ck(_v, 1, 0); }, null); }
var SimpleComponentNgFactory = i0.ɵccf("lib-simple-component", i1.SimpleComponent, View_SimpleComponent_Host_0, {}, {}, []);
export { SimpleComponentNgFactory as SimpleComponentNgFactory };
//# sourceMappingURL=components.ngfactory.js.map
  • ng-tdp/src/app/app.component.ngfactory.js
/**
 * @fileoverview This file was generated by the Angular template compiler. Do not edit.
 *
 * @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
 * tslint:disable
 */ 
import * as i0 from "./app.component.scss.shim.ngstyle";
import * as i1 from "@angular/core";
import * as i2 from "../../dist/components/components.ngfactory";
import * as i3 from "components";
import * as i4 from "@angular/router";
import * as i5 from "./app.component";
var styles_AppComponent = [i0.styles];
var RenderType_AppComponent = i1.ɵcrt({ encapsulation: 0, styles: styles_AppComponent, data: {} });
export { RenderType_AppComponent as RenderType_AppComponent };
export function View_AppComponent_0(_l) { return i1.ɵvid(0, [(_l()(), i1.ɵeld(0, 0, null, null, 1, "lib-simple-component", [], null, null, null, i2.View_SimpleComponent_0, i2.RenderType_SimpleComponent)), i1.ɵdid(1, 114688, null, 0, i3.SimpleComponent, [], null, null), (_l()(), i1.ɵeld(2, 16777216, null, null, 1, "router-outlet", [], null, null, null, null, null)), i1.ɵdid(3, 212992, null, 0, i4.RouterOutlet, [i4.ChildrenOutletContexts, i1.ViewContainerRef, i1.ComponentFactoryResolver, [8, null], i1.ChangeDetectorRef], null, null)], function (_ck, _v) { _ck(_v, 1, 0); _ck(_v, 3, 0); }, null); }
export function View_AppComponent_Host_0(_l) { return i1.ɵvid(0, [(_l()(), i1.ɵeld(0, 0, null, null, 1, "app-root", [], null, null, null, View_AppComponent_0, RenderType_AppComponent)), i1.ɵdid(1, 49152, null, 0, i5.AppComponent, [], null, null)], null, null); }
var AppComponentNgFactory = i1.ɵccf("app-root", i5.AppComponent, View_AppComponent_Host_0, {}, {}, []);
export { AppComponentNgFactory as AppComponentNgFactory };
//# sourceMappingURL=app.component.ngfactory.js.map
  • ng-tdp/src/app/app.module.ngfactory.js
/**
 * @fileoverview This file was generated by the Angular template compiler. Do not edit.
 *
 * @suppress {suspiciousCode,uselessCode,missingProperties,missingOverride,checkTypes}
 * tslint:disable
 */ 
import * as i0 from "@angular/core";
import * as i1 from "./app.module";
import * as i2 from "./app.component";
import * as i3 from "../../node_modules/@angular/router/router.ngfactory";
import * as i4 from "./app.component.ngfactory";
import * as i5 from "@angular/common";
import * as i6 from "@angular/platform-browser";
import * as i7 from "@angular/router";
import * as i8 from "./app-routing.module";
import * as i9 from "components";
var AppModuleNgFactory = i0.ɵcmf(i1.AppModule, [i2.AppComponent], function (_l) { return i0.ɵmod([i0.ɵmpd(512, i0.ComponentFactoryResolver, i0.ɵCodegenComponentFactoryResolver, [[8, [i3.ɵEmptyOutletComponentNgFactory, i4.AppComponentNgFactory]], [3, i0.ComponentFactoryResolver], i0.NgModuleRef]), i0.ɵmpd(5120, i0.LOCALE_ID, i0.ɵangular_packages_core_core_o, [[3, i0.LOCALE_ID]]), i0.ɵmpd(4608, i5.NgLocalization, i5.NgLocaleLocalization, [i0.LOCALE_ID, [2, i5.ɵangular_packages_common_common_a]]), i0.ɵmpd(5120, i0.APP_ID, i0.ɵangular_packages_core_core_g, []), i0.ɵmpd(5120, i0.IterableDiffers, i0.ɵangular_packages_core_core_m, []), i0.ɵmpd(5120, i0.KeyValueDiffers, i0.ɵangular_packages_core_core_n, []), i0.ɵmpd(4608, i6.DomSanitizer, i6.ɵDomSanitizerImpl, [i5.DOCUMENT]), i0.ɵmpd(6144, i0.Sanitizer, null, [i6.DomSanitizer]), i0.ɵmpd(4608, i6.HAMMER_GESTURE_CONFIG, i6.HammerGestureConfig, []), i0.ɵmpd(5120, i6.EVENT_MANAGER_PLUGINS, function (p0_0, p0_1, p0_2, p1_0, p2_0, p2_1, p2_2, p2_3) { return [new i6.ɵDomEventsPlugin(p0_0, p0_1, p0_2), new i6.ɵKeyEventsPlugin(p1_0), new i6.ɵHammerGesturesPlugin(p2_0, p2_1, p2_2, p2_3)]; }, [i5.DOCUMENT, i0.NgZone, i0.PLATFORM_ID, i5.DOCUMENT, i5.DOCUMENT, i6.HAMMER_GESTURE_CONFIG, i0.ɵConsole, [2, i6.HAMMER_LOADER]]), i0.ɵmpd(4608, i6.EventManager, i6.EventManager, [i6.EVENT_MANAGER_PLUGINS, i0.NgZone]), i0.ɵmpd(135680, i6.ɵDomSharedStylesHost, i6.ɵDomSharedStylesHost, [i5.DOCUMENT]), i0.ɵmpd(4608, i6.ɵDomRendererFactory2, i6.ɵDomRendererFactory2, [i6.EventManager, i6.ɵDomSharedStylesHost]), i0.ɵmpd(6144, i0.RendererFactory2, null, [i6.ɵDomRendererFactory2]), i0.ɵmpd(6144, i6.ɵSharedStylesHost, null, [i6.ɵDomSharedStylesHost]), i0.ɵmpd(4608, i0.Testability, i0.Testability, [i0.NgZone]), i0.ɵmpd(5120, i7.ActivatedRoute, i7.ɵangular_packages_router_router_g, [i7.Router]), i0.ɵmpd(4608, i7.NoPreloading, i7.NoPreloading, []), i0.ɵmpd(6144, i7.PreloadingStrategy, null, [i7.NoPreloading]), i0.ɵmpd(135680, i7.RouterPreloader, i7.RouterPreloader, [i7.Router, i0.NgModuleFactoryLoader, i0.Compiler, i0.Injector, i7.PreloadingStrategy]), i0.ɵmpd(4608, i7.PreloadAllModules, i7.PreloadAllModules, []), i0.ɵmpd(5120, i7.ɵangular_packages_router_router_n, i7.ɵangular_packages_router_router_c, [i7.Router, i5.ViewportScroller, i7.ROUTER_CONFIGURATION]), i0.ɵmpd(5120, i7.ROUTER_INITIALIZER, i7.ɵangular_packages_router_router_j, [i7.ɵangular_packages_router_router_h]), i0.ɵmpd(5120, i0.APP_BOOTSTRAP_LISTENER, function (p0_0) { return [p0_0]; }, [i7.ROUTER_INITIALIZER]), i0.ɵmpd(1073742336, i5.CommonModule, i5.CommonModule, []), i0.ɵmpd(1024, i0.ErrorHandler, i6.ɵangular_packages_platform_browser_platform_browser_a, []), i0.ɵmpd(1024, i0.NgProbeToken, function () { return [i7.ɵangular_packages_router_router_b()]; }, []), i0.ɵmpd(512, i7.ɵangular_packages_router_router_h, i7.ɵangular_packages_router_router_h, [i0.Injector]), i0.ɵmpd(1024, i0.APP_INITIALIZER, function (p0_0, p1_0) { return [i6.ɵangular_packages_platform_browser_platform_browser_j(p0_0), i7.ɵangular_packages_router_router_i(p1_0)]; }, [[2, i0.NgProbeToken], i7.ɵangular_packages_router_router_h]), i0.ɵmpd(512, i0.ApplicationInitStatus, i0.ApplicationInitStatus, [[2, i0.APP_INITIALIZER]]), i0.ɵmpd(131584, i0.ApplicationRef, i0.ApplicationRef, [i0.NgZone, i0.ɵConsole, i0.Injector, i0.ErrorHandler, i0.ComponentFactoryResolver, i0.ApplicationInitStatus]), i0.ɵmpd(1073742336, i0.ApplicationModule, i0.ApplicationModule, [i0.ApplicationRef]), i0.ɵmpd(1073742336, i6.BrowserModule, i6.BrowserModule, [[3, i6.BrowserModule]]), i0.ɵmpd(1024, i7.ɵangular_packages_router_router_a, i7.ɵangular_packages_router_router_e, [[3, i7.Router]]), i0.ɵmpd(512, i7.UrlSerializer, i7.DefaultUrlSerializer, []), i0.ɵmpd(512, i7.ChildrenOutletContexts, i7.ChildrenOutletContexts, []), i0.ɵmpd(256, i7.ROUTER_CONFIGURATION, {}, []), i0.ɵmpd(1024, i5.LocationStrategy, i7.ɵangular_packages_router_router_d, [i5.PlatformLocation, [2, i5.APP_BASE_HREF], i7.ROUTER_CONFIGURATION]), i0.ɵmpd(512, i5.Location, i5.Location, [i5.LocationStrategy]), i0.ɵmpd(512, i0.Compiler, i0.Compiler, []), i0.ɵmpd(512, i0.NgModuleFactoryLoader, i0.SystemJsNgModuleLoader, [i0.Compiler, [2, i0.SystemJsNgModuleLoaderConfig]]), i0.ɵmpd(1024, i7.ROUTES, function () { return [[]]; }, []), i0.ɵmpd(1024, i7.Router, i7.ɵangular_packages_router_router_f, [i0.ApplicationRef, i7.UrlSerializer, i7.ChildrenOutletContexts, i5.Location, i0.Injector, i0.NgModuleFactoryLoader, i0.Compiler, i7.ROUTES, i7.ROUTER_CONFIGURATION, [2, i7.UrlHandlingStrategy], [2, i7.RouteReuseStrategy]]), i0.ɵmpd(1073742336, i7.RouterModule, i7.RouterModule, [[2, i7.ɵangular_packages_router_router_a], [2, i7.Router]]), i0.ɵmpd(1073742336, i8.AppRoutingModule, i8.AppRoutingModule, []), i0.ɵmpd(1073742336, i9.SimpleComponentModule, i9.SimpleComponentModule, []), i0.ɵmpd(1073742336, i1.AppModule, i1.AppModule, []), i0.ɵmpd(256, i0.ɵAPP_ROOT, true, [])]); });
export { AppModuleNgFactory as AppModuleNgFactory };
//# sourceMappingURL=app.module.ngfactory.js.map

The important bits seem to be:

  • ng-tdp/dist/components/components.ngfactory.js imports and uses MomentComponentModule and MomentComponent to make MomentComponentModuleNgFactory, MomentComponentNgFactory, RenderType_MomentComponent and View_MomentComponent_0
  • ng-tdp/src/app/app.component.ngfactory.js imports ../../dist/components/components.ngfactory but only uses View_SimpleComponent_0 and RenderType_SimpleComponent
  • ng-tdp/src/app/app.module.ngfactory.js imports app.component.ngfactory

At this point I am a bit confused. The ng-tdp/dist/components/package.json file does have "sideEffects": false and at the end of the day none of MomentComponentModuleNgFactory, MomentComponentNgFactory, RenderType_MomentComponent and View_MomentComponent_0 are actually used.

In the verbose log we also see:

WARNING in Terser Plugin: Dropping unused variable MomentComponentModuleNgFactory [./dist/components/components.ngfactory.js.pre-build-optimizer.js:9,4]
WARNING in Terser Plugin: Dropping unused variable MomentComponentNgFactory [./dist/components/components.ngfactory.js.pre-build-optimizer.js:18,4]

But no such statements for RenderType_MomentComponent and View_MomentComponent_0.

Those two are both exported and used in:

export function View_MomentComponent_Host_0(_l) { return i0.ɵvid(0, [(_l()(), i0.ɵeld(0, 0, null, null, 1, "lib-moment-component", [], null, null, null, View_MomentComponent_0, RenderType_MomentComponent)), i0.ɵdid(1, 114688, null, 0, i1.MomentComponent, [], null, null)], function (_ck, _v) { _ck(_v, 1, 0); }, null); }

But at this point I don't think that matters since MomentComponentModule and MomentComponent shouldn't have gotten to Terser at all. Once it gets there it's too late to drop non-ES libraries like moment.

So now we can look at how webpack deals with the side-effects: https://github.com/webpack/webpack/blob/f6d67b6/examples/side-effects/README.md

The "sideEffects": false flag in big-module's package.json indicates that the package's modules have no side effects (on evaluation) and only expose exports. This allows tools like webpack to optimize re-exports. In the case import { a, b } from "big-module-with-flag" is rewritten to import { a } from "big-module-with-flag/a"; import { b } from "big-module-with-flag/b".

This part only talks about re-exports. I think what's happening is that ng-tdp/dist/components/components.js is just re-exporting MomentComponentModule and MomentComponent, but ng-tdp/dist/components/components.ngfactory.js is importing and using them. It might also be related to the import * as i2 from "../../dist/components/components.ngfactory"; syntax though.

@filipesilva
Copy link
Contributor

I tested those two hypothesis and as far as I can tell:

  • the import * as i2 syntax doesn't stop exports from being dropped
  • using an import in the same file that exports it doesn't stop it from being dropped
  • using an import in another file stops it from being dropped

I opened an issue for the difference in the last two behaviours in webpack webpack/webpack#8597.

@filipesilva
Copy link
Contributor

Looked a bit further into that webpack issue with @clydin and he pointed out that another-file.js (in the example) could never be removed from the compilation since it's exports are used. Thus c will never be dropped either.

This is analogous to what is happening today with ngfactories. As long as an import from a ngfactory is used, all components it uses will remain. Will discuss with the team what can be done about it.

@babeal
Copy link
Author

babeal commented Jan 7, 2019

@filipesilva, dude you’re awesome!!! Your explanation really helps my understanding. This is something that I would love to find a solution for even if we have to write a specialized post processor to remove the code. We are going to start investigating as well to see what can be done in the meantime. I’ll add what we’ve been able to discover here. Thanks so much for taking a look at this.

@kzc
Copy link

kzc commented Jan 7, 2019

Tree shaking in Webpack is generally not as sophisticated as Rollup (*).

For best Webpack results one should export one item per source file (default or named) and have the main library file re-export everything. lodash-es does exactly this:

(*) With one notable exception - Rollup doesn't support the package sideEffects hint: rollup/rollup#2593

@babeal
Copy link
Author

babeal commented Jan 7, 2019

@kzc, lodash only reexports or uses code that is first party which is why it properly gets removed, however, if they were to import moment or any other third party library in one of their functions that third party code will get included even if you don’t use the function that uses it. I am not sure what would happen if the third party was also in esm format. “Own code” gets properly handled if it is exported that way. I believe I have tried it with both fesm and esm formats and either way it includes transitive dependencies that aren’t used.

@mgechev mgechev added the needs: discussion On the agenda for team meeting to determine next steps label Jan 8, 2019
@mgechev
Copy link
Member

mgechev commented Jan 8, 2019

@filipesilva, I just added a label for the triage meeting this week.

@kzc
Copy link

kzc commented Jan 8, 2019

My previous comment was not directed towards moment in particular, but would be a workaround for the webpack deficiency seen in https://github.com/filipesilva/webpack-side-effects.

@mgechev mgechev removed the needs: discussion On the agenda for team meeting to determine next steps label Jan 10, 2019
@filipesilva
Copy link
Contributor

Heya, just wanted to mention that I brought this up with the team. It's working as intended insofar as that's the way that NgFactories have always behaved, and why Angular Material ships many individual modules.

This might change with Ivy (the new compiler engine) though. I tried using it now and moment wasn't included. By v8 we should have an opt-in version of Ivy for everyone to try.

@p0ma
Copy link

p0ma commented Feb 27, 2019

Not only that, I have a similar problem where all of my transitive dependencies are included even when importing a single class, re-exported in public_api.ts of an Angular library project into my main project.
Any workarounds for this?

Edit:
Adding an example to clarify

src/lib/component/model.ts
export class InputModel {};

src/public_api.ts
export * from './lib/component/model.ts';

consuming InputModel:
import { InputModel } from 'shared-lib;'

Library's package.json contains sideEffects: false. As it's built with a default @angular-devkit/build-ng-packagr:build pipeline:

"@angular-devkit/build-angular": "0.13.0", "@angular-devkit/build-ng-packagr": "0.13.0", "@angular/cli": "7.3.2", "@angular/compiler-cli": "7.2.5", "ng-packagr": "4.7.0"

@filipesilva
Copy link
Contributor

@p0ma unless your library is free from side effects, and your transitive dependencies are also free from side effects, it is unlikely that they will all be tree shaken. This is how ES modules work.

You must exercise caution when adding dependencies to a library because you are passing them on to your consumers.

@p0ma
Copy link

p0ma commented Feb 27, 2019

@filipesilva Thanks for your reply. I got confused even more though. Because not only I get my transitive dependencies included, but also all the unused shared components go into the bundle as well.

image

Should I remove that class import, I get whole another picture.

image

@yansokalau
Copy link

Hello @filipesilva @p0ma,
Guys, I spent 3 days looking into the issue with tree-shaking of compiled and imported angular library without any results. For me it's not clear have you come up with smth in this thread or not?

My case is the following:
I created angular library using ng-packagr with several independent modules in it, those modules are exported in public_api.ts like:

export * from './lib/modules/iq-navigator/iqnavigator.module';
export * from './lib/modules/iq-nav-bar/iq-nav-bar.module';
export * from './lib/modules/iq-dev-login/login.module';

In the consumer angular project I import only 1 module like:
import {IQDevLoginComponent, IQDevLoginModule} from "iq-components";

But prod build with the following config:

"production": {
              "optimization": true,
              "outputHashing": "all",
              "sourceMap": false,
              "extractCss": true,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": true,
              "vendorChunk": false,
              "buildOptimizer": true,
              "serviceWorker": true
            }

does include all code from angular library. Is there a solution for this?

"@angular/common": "^7.0.0",
 "@angular/compiler": "^7.0.0",
 "@angular/core": "^7.0.0",
"@angular-devkit/build-ng-packagr": "~0.8.0",

Please advice what can be wrong.

@filipesilva
Copy link
Contributor

@p0ma @yansokalau if you provide a repro I can look into it, but without one it's very hard to say anything concrete.

@yansokalau
Copy link

yansokalau commented Mar 19, 2019

Hi @filipesilva
Here is an example of simpliest project where issue is reproduced, everything is described in the README. Please let me know if anything isn't clear.
https://github.com/yansokalau/angular-library-test

Thanks in advance, the problem does really mean a lot, so me and my team looking forward for resolving it.

@yansokalau
Copy link

hi @babeal, do you have solution for the issue?

@yansokalau
Copy link

Hello @filipesilva,
Had a chance to checkout the repo I shared?

@schallus
Copy link

Hello @yansokalau

I have the exact same problem as you.

Even though I'm using only one module from a library, my application imports all the components, modules and external dependencies.

Did you find a solution for your project ?

image

Thanks in advance

@yansokalau
Copy link

Hi @schallus,
Unfortunatelly nothing to share. Waiting for @filipesilva response.
Will wait for some time and raise new ticket there.

@angular-automatic-lock-bot
Copy link

This issue has been automatically locked due to inactivity.
Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.

@angular-automatic-lock-bot angular-automatic-lock-bot bot locked and limited conversation to collaborators Sep 9, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

8 participants