Skip to content

Tree shaking removes dynamically created NgModule #5275

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

Closed
SebastianFree opened this issue Mar 7, 2017 · 10 comments
Closed

Tree shaking removes dynamically created NgModule #5275

SebastianFree opened this issue Mar 7, 2017 · 10 comments

Comments

@SebastianFree
Copy link

I am taking the below information from the following StackOverflow question: http://stackoverflow.com/questions/42537138/angular2-cli-tree-shaking-removing-dynamically-created-ngmodule (I am the author)

OS?

any

Versions.

@angular/cli: 1.0.0-rc.1
node: 6.10.0
os: linux x64
@angular/common: 2.4.9
@angular/compiler: 2.4.9
@angular/core: 2.4.9
@angular/forms: 2.4.9
@angular/http: 2.4.9
@angular/platform-browser: 2.4.9
@angular/platform-browser-dynamic: 2.4.9
@angular/router: 3.4.9
@angular/upgrade: 2.4.9
@angular/cli: 1.0.0-rc.1
@angular/compiler-cli: 2.4.9
typescript: 2.1.6

Repro steps.

Compiling my app with a non-production environment works just fine. However, when I run ng build --prod -e prod the transpiling/compiling process removes all dynamically created modules.

The log given by the failure.

Immediately when trying to load a dynamically created component:

EXCEPTION: No NgModule metadata found for 'e'.
ORIGINAL STACKTRACE:
main.dc05ae9….bundle.js:formatted:4731
Error: No NgModule metadata found for 'e'.
at f (vendor.c18e6df….bundle.js:formatted:76051)
at t.resolve (vendor.c18e6df….bundle.js:formatted:20624)
at t.getNgModuleMetadata (vendor.c18e6df….bundle.js:formatted:20169)
at t._loadModules (vendor.c18e6df….bundle.js:formatted:40474)
at t._compileModuleAndAllComponents (vendor.c18e6df….bundle.js:formatted:40462)
at t.compileModuleAndAllComponentsSync (vendor.c18e6df….bundle.js:formatted:40436)
at e.createComponentFactory (main.dc05ae9….bundle.js:formatted:4789)

Mention any other details that might be useful.

My dynamic component builder (the problem is in the createComponentModule function which dynamically creates a new module):

@Injectable()
export class DynamicTypeBuilder {    
  constructor() {
  }

  private _cacheOfFactories: {[templateKey: string]: ComponentFactory<any>} = {};
  private compiler: Compiler = new JitCompilerFactory([{useDebug: false, useJit: true}]).createCompiler();

  public createComponentFactory<COMPONENT_TYPE>(type: any, template: string, additionalModules: any[] = []): Observable<ComponentFactory<COMPONENT_TYPE>> {

    let factory = this._cacheOfFactories[template];
    if (factory) {
      return Observable.of(factory);
    }

    // unknown template ... let's create a Type for it
    let module = this.createComponentModule(type, additionalModules);

    // compiles and adds the created factory to the cache
    return Observable.of(this.compiler.compileModuleAndAllComponentsSync(module))
                     .map((moduleWithFactories: ModuleWithComponentFactories<COMPONENT_TYPE>) => {
                       factory = moduleWithFactories.componentFactories.find(value => value.componentType == type);
                       this._cacheOfFactories[template] = factory;                           
                       return factory;
                     });
  }

  protected createComponentModule(componentType: any, additionalModules: any[]): Type<any> {
    @NgModule({
      imports: [
        FormsModule,
        ReactiveFormsModule,
        BrowserModule,
        PipesModule,
        ...additionalModules
      ],
      declarations: [
        componentType
      ],
      schemas:[CUSTOM_ELEMENTS_SCHEMA]
    })
    class RuntimeComponentModule {
    }

    return RuntimeComponentModule;
  }
}

And its transpiled/compiled result:

var _ = function() {
    function e() {
        this._cacheOfFactories = {},
        this.compiler = new i.a([{
            useDebug: !1,
            useJit: !0
        }]).createCompiler()
    }
    return e.prototype.createComponentFactory = function(e, t, n) {
        var i = this;
        var _ = this._cacheOfFactories[t];
        if (_)
            r.Observable.of(_);
        var a = this.createComponentModule(e, n);
        return r.Observable.of(this.compiler.compileModuleAndAllComponentsSync(a)).map(function(n) {
            return _ = n.componentFactories.find(function(t) {
                return t.componentType == e
            }),
            i._cacheOfFactories[t] = _,
            _
        })
    }
    ,
    e.prototype.createComponentModule = function(e, t) {
        var n = function() {
            function e() {}
            return e
        }();
        return n
    }
    ,
    e.ctorParameters = function() {
        return []
    }
    ,
    e
}()

The 'e' in the error message is the function e() from createComponentModule which, as you can see, is empty even though is should contain the @NgModule content.

This is the transpiled/compiled content with a non-production setting, still containing all the information about the removed NgModule:

var DynamicTypeBuilder = (function () {
    function DynamicTypeBuilder() {
        // since this object is a singleton, we can have the cache and the compiler here
        this._cacheOfFactories = {};
        this.compiler = new __WEBPACK_IMPORTED_MODULE_1__angular_compiler__["a" /* JitCompilerFactory */]([{ useDebug: false, useJit: true }]).createCompiler();
    }
    DynamicTypeBuilder.prototype.createComponentFactory = function (type, template, additionalModules) {
        var _this = this;
        if (additionalModules === void 0) { additionalModules = []; }
        var factory = this._cacheOfFactories[template];
        if (factory) {
            return __WEBPACK_IMPORTED_MODULE_2_rxjs__["Observable"].of(factory);
        }
        var module = this.createComponentModule(type, additionalModules);
        return __WEBPACK_IMPORTED_MODULE_2_rxjs__["Observable"].of(this.compiler.compileModuleAndAllComponentsSync(module))
            .map(function (moduleWithFactories) {
            factory = moduleWithFactories.componentFactories.find(function (value) { return value.componentType == type; });
            _this._cacheOfFactories[template] = factory;
            return factory;
        });
    };
    DynamicTypeBuilder.prototype.createComponentModule = function (componentType, additionalModules) {
        var RuntimeComponentModule = (function () {
            function RuntimeComponentModule() {
            }
            return RuntimeComponentModule;
        }());
        RuntimeComponentModule = __decorate([
            __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__angular_core__["NgModule"])({
                imports: [
                    __WEBPACK_IMPORTED_MODULE_3__angular_forms__["FormsModule"],
                    __WEBPACK_IMPORTED_MODULE_3__angular_forms__["ReactiveFormsModule"],
                    __WEBPACK_IMPORTED_MODULE_4__angular_platform_browser__["BrowserModule"],
                    __WEBPACK_IMPORTED_MODULE_5_app_modules_pipes_module__["a" /* PipesModule */]
                ].concat(additionalModules),
                declarations: [
                    componentType
                ],
                schemas: [__WEBPACK_IMPORTED_MODULE_0__angular_core__["CUSTOM_ELEMENTS_SCHEMA"]]
            })
        ], RuntimeComponentModule);
        // a module for just this Type
        return RuntimeComponentModule;
    };
    return DynamicTypeBuilder;
}());
DynamicTypeBuilder = __decorate([
    __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__angular_core__["Injectable"])(),
    __metadata("design:paramtypes", [])
], DynamicTypeBuilder);

I assume tree shaking removes this dynamically created NgModule.
How can I create a new NgModule like this and still use the production mode of Angular CLI?

@SebastianFree
Copy link
Author

SebastianFree commented Mar 9, 2017

Please, anyone?
I don't seem to be the only one having this issue according to some StackOverflow questions I've seen.

@mcgraphix
Copy link

+1

@antonberezan
Copy link

+1 Have the same issue when build in prod mode, whereas dev mode works fine

@antonberezan
Copy link

The issue arises when I'm using ng build -aot, even id dev mode. For some reason it removes TypeScript metadata. See the results

main.bundle.js when ng build

...
var AppModule = (function () {
    function AppModule() {
    }
    return AppModule;
}());
AppModule = __decorate([
    __webpack_require__.i(__WEBPACK_IMPORTED_MODULE_0__angular_core__["c" /* NgModule */])({
        imports: [
            __WEBPACK_IMPORTED_MODULE_1__angular_platform_browser__["a" /* BrowserModule */],
            __WEBPACK_IMPORTED_MODULE_3__angular_http__["a" /* HttpModule */],
            __WEBPACK_IMPORTED_MODULE_2__angular_router__["a" /* RouterModule */],
            __WEBPACK_IMPORTED_MODULE_5__routing_module__["a" /* AppRoutingModule */]
        ],
        declarations: [__WEBPACK_IMPORTED_MODULE_4__app_component__["a" /* AppComponent */]],
        exports: [],
        bootstrap: [__WEBPACK_IMPORTED_MODULE_4__app_component__["a" /* AppComponent */]],
    })
], AppModule);
...

main.bundle.js when ng build -aot

...
var AppModule = (function () {
    function AppModule() {
    }
    return AppModule;
}());
...

@filipesilva
Copy link
Contributor

AFAIK this will never work on AOT, because it requires code to be statically analyzable.

You can still build for production without AOT by doing ng prod --prod --no-aot.

@richardsengers
Copy link

richardsengers commented Apr 12, 2017

You can still build for production without AOT by doing ng prod --prod --no-aot

But why should we want that? I've got a quite large application and aot is ten times faster (initial load only has a performance boost of 6 to 8 seconds).

Are you saying we need to stick to JIT compiler if we need to dynamically add component template html and no AOT will be possible....ever?

@maxbellec
Copy link

Some people have managed to use AOT and have the JitCompiler be shipped with it to be able to compile components at runtime, see

angular/angular#15510 (comment)
https://github.com/shlomiassaf/lazy-jit/

I think this issue (or a new one more specifically on making it possible to create an app supporting AOT + JIT) should be reopened.

@SebastianFree
Copy link
Author

SebastianFree commented Apr 21, 2017

To be fair the user who came up with the solution over there did not use the CLI but handcrafted it using Webpack.
So, now the task is to transfer his solution to the CLI!

@maxbellec Your second link is not about AOT compilation, please remove it.

@KingMario
Copy link

KingMario commented Oct 10, 2017

I managed to workthrough this by using the AOT compiled componentNgFactory instead of the componentFactories element of the compileModuleAndAllComponentsSync call result as the createComponent's componentFactory parameter.

https://github.com/KingMario/leaflet-popup, a demo of interactive dynamic Angular component content for Leaflet popup created by componentFactory.

Here is the demo. When you click the marker, the popup content will change randomly.

The git diff for the corresponding file: KingMario/leaflet-popup@1fc6793#diff-5c26d2c8f8838f32ace87fdecd6344ea

Other changes in this commit are for my own AOT compilation case study only. You may run ng build --prod -e prod directly after you generated the componentNgFactories.

Hope it will help you out.

@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 7, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants