angular-cli: Lazy loading module from library package located in node_modules fails to build.

Bug Report or Feature Request (mark with an x)

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

Versions.

@angular/cli: 1.0.4
node: 6.10.2
os: win32 x64
@angular/cli: 1.0.4
@angular/common: 4.1.3
@angular/compiler: 4.1.3
@angular/core: 4.1.3
@angular/forms: 4.1.3
@angular/http: 4.1.3
@angular/platform-browser: 4.1.3
@angular/platform-browser-dynamic: 4.1.3
@angular/router: 4.1.3
@angular/compiler-cli: 4.1.3

Repro steps.

I’ve modified the quick start library from @filipesilva and copied the output folder to my node_modules/quickstart-lib directory.

https://github.com/filipesilva/angular-quickstart-lib

Modification includes a routing module to add a child route.

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { LibComponent } from './component/lib.component';
const childRoutes: Routes = [
  {
    path: '',
    component: LibComponent
  }
];

@NgModule({
  imports: [
    RouterModule.forChild(childRoutes)
  ],
  exports: [
    RouterModule
  ]
})
export class LibRoutingModule { }

which is then imported into the main LibModule file.

import { NgModule } from '@angular/core';
import { LibRoutingModule } from './lib-routing.module';
import { LibComponent } from './component/lib.component';
import { LibService } from './service/lib.service';

@NgModule({
  declarations: [LibComponent],
  imports: [LibRoutingModule],
  providers: [LibService],
  exports: [LibComponent]
})
export class LibModule { }

In my host application, I created a simple route to lazy load the module from the library

const routes: Routes = [
  {
    path: 'quickstart',
    loadChildren: 'quickstart-lib#LibModule'
  },
  {
    path: 'preferences',
    loadChildren: 'app/preferences#PreferencesModule'
  },
  ...HomeRoutes
];


The log given by the failure.

PS C:\Workspace\scratch\Ng4Starter\host> ng serve
** NG Live Development Server is listening on localhost:4200, open your browser on http://localhost:4200 **
 11% building modules 14/23 modules 9 active ...gular\platform-browser-dynamic.es5.js(node:19124) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic,
 see https://github.com/webpack/loader-utils/issues/56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.
 11% building modules 15/23 modules 8 active ...gular\platform-browser-dynamic.es5.js(node:19124) DeprecationWarning: loaderUtils.parseQuery() received a non-string value which can be problematic,
 see https://github.com/webpack/loader-utils/issues/56
parseQuery() will be replaced with getOptions() in the next major version of loader-utils.
Hash: c46342c7aafb5ce0f53e
Time: 14214ms
chunk    {0} 0.chunk.js, 0.chunk.js.map 20.5 kB {3} [rendered]
chunk    {1} 1.chunk.js, 1.chunk.js.map 793 bytes {0} {3} [rendered]
chunk    {2} polyfills.bundle.js, polyfills.bundle.js.map (polyfills) 157 kB {7} [initial] [rendered]
chunk    {3} main.bundle.js, main.bundle.js.map (main) 72.5 kB {6} [initial] [rendered]
chunk    {4} styles.bundle.js, styles.bundle.js.map (styles) 229 kB {7} [initial] [rendered]
chunk    {5} scripts.bundle.js, scripts.bundle.js.map (scripts) 162 kB {7} [initial] [rendered]
chunk    {6} vendor.bundle.js, vendor.bundle.js.map (vendor) 3.36 MB [initial] [rendered]
chunk    {7} inline.bundle.js, inline.bundle.js.map (inline) 0 bytes [entry] [rendered]

ERROR in ./~/quickstart-lib/quickstart-lib.d.ts
Module build failed: Error: Debug Failure. False expression: Output generation failed:=>C:/Workspace/scratch/Ng4Starter/host/node_modules/quickstart-lib/quickstart-lib.d.ts
    at Object.assert (C:\Workspace\scratch\Ng4Starter\host\node_modules\typescript\lib\typescript.js:3259:23)
    at Object.transpileModule (C:\Workspace\scratch\Ng4Starter\host\node_modules\typescript\lib\typescript.js:76181:18)
    at TypeScriptFileRefactor.transpile (C:\Workspace\scratch\Ng4Starter\host\node_modules\@ngtools\webpack\src\refactor.js:161:27)
    at Promise.resolve.then.then.then.then (C:\Workspace\scratch\Ng4Starter\host\node_modules\@ngtools\webpack\src\loader.js:361:37)
    at process._tickCallback (internal/process/next_tick.js:109:7)
 @ ./src async
 @ ./~/@angular/core/@angular/core.es5.js
 @ ./src/main.ts
 @ multi webpack-dev-server/client?http://localhost:4200 ./src/main.ts
webpack: Failed to compile.

Desired functionality.

I would like to distribute my code as an NPM library and have the CLI support lazy loading the module that is located in the ‘node_modules’ directory.

Modules located in the same application directory are lazy loaded just fine.

Mention any other details that might be useful.

https://github.com/filipesilva/angular-quickstart-lib

About this issue

  • Original URL
  • State: closed
  • Created 7 years ago
  • Reactions: 38
  • Comments: 80 (4 by maintainers)

Commits related to this issue

Most upvoted comments

I’ve got work around for this which works with uirouter/angular-hybrid (in AOT mode too) but might work with angular router too.

So, let’s say I’ve got a library based on angular-quickstart-lib (let’s call it quickstart-lib) which has LibModule that I want to lazy load. To do so, I added wrapper around library which lives in Angular CLI based host project.

So, in app.module.ts of host, instead of:

@NgModule({
  imports: [
    UIRouterModule.forChild({
    states: [
        {
          name: 'quickstart.**',
          url: 'quickstart',
          loadChildren: 'quickstart-lib#LibModule'
        }
    ]}),
  ]
})
export class AppModule

I’ve got:

libModuleLazyLoader.ts

import { NgModule } from "@angular/core";
import { LibModule } from 'quickstart-lib';

@NgModule({
  imports: [
      LibModule
  ]
})
export class QuickstartLibLazyLoader {}

and then, in app.module.ts, I’ve got:

@NgModule({
  imports: [
    UIRouterModule.forChild({
    states: [
        {
          name: 'quickstart.**',
          url: 'quickstart',
          loadChildren: './libModuleLazyLoader#QuickstartLibLazyLoader'
        }
    ]}),
  ]
})
export class AppModule

Downside is that you’d need wrapper per library with this work around.

hope this helps someone 😃

@mnicic yeah, @tiagoa solution is not lazy loading. If there’s an import in your routing you will bring all the source together. Currently the only working solution is the proxy, and seeing the lack of interest from the development team on this issue, until someone develops another routing solution for angular, there won’t be alternative.

What’s the problem with the working solution? The proxy module is lazy loaded by the router. Your only overhead is the proxy module around your actual module, that you wanted to lazy load.

The problem with the working solution is that it’s a hack to make it work around an issue, that it’s not being able to lazy load modules installed through npm. This produces inconsistent source code on applications, where you have local modules without the need of a proxy module, and node modules with a proxy requirement. But things get worse, this issue, is not mentioned on official angular documentation, so if you try to do this at first time, you lose time trying to make work something that should work until you google it and find this issue growing with comments from people who find this issue in their projects over and over again. Try to keep the “modular” philosophy angular proposes and then publish your reusable routed modules into npm or your private node repository, and now you have to create a proxy for every “reusable” module that isn’t reusable without it after all.

@filipesilva @Brocco any idea, if this issue is going to be considered or it would be great if you let us know if you would be fixing this issue anytime sooner

I understand there could be a reason, would you please give us Justification, why this is not considered, as this is open since a year back

Eagerly waiting for the response from angular cli team

Why hasn’t this issue been a high priority for a fix? Are we not supposed to lazy load modules from Angular 6 libraries?

For my current project it is necessary to reuse feature modules. So i´m looking forward for a solution because usinge the provided workaround is a huge overhead if you have multiple modules.

I got lazy loading working with cli, standard angular router (not ui), lazy loading and… a couple of workarounds.

Please find a working example here: https://github.com/danielecanteri/angular-lazy-loading-modules.

Basically I had to create a wrapper module (as suggested in comments above). In the wrapper module I had to re-register the child routes (exported from the module).

import {ExternalModuleLibModule, routes} from 'external-module-lib';
...
@NgModule({
  imports: [
    CommonModule,
    ExternalModuleLibModule,
    // hack: re-register routes for imported module
    RouterModule.forChild(routes)
  ],
  declarations: []
})
export class ExternalModuleWrapperModule { }

After that I can use lazy loading routes int the application as follows:

{
  path: 'external-module',
  loadChildren: './external-module-wrapper/external-module-wrapper.module#ExternalModuleWrapperModule'
},

Is it going to be fixed?

@Nodarii It is @angular/compiler-cli that delegates to Typescript to resolve the my-lib module, and this assumes that there is a my-lib.ts file at node_modules/my-lib/my-lib.ts for us using ng-packagr, but instead there is only the typings. That’s the error you are seeing, it is a red herring. It seems that @angular/compiler-cli should support resolving modules that support the Angular Package Format, and it currently does not.

I was able to track down the issue. It appears that the AoT plugin discovers lazy routes and passes them to the angular compiler language service to resolve the file name of a given route string. Assuming your library is following the angular package format, this resolves to a definition file, e.g. c:.…\node_modules\lib-name\lib-name.d.ts.

The file @ngtools\webpack\src\loader.js then uses this path to transpile the source code to es2015 which fails. I modified this file to check for the *.d.ts extension. If there is a match I just use the contents of the pre-compiled .js and .map files for the library. This lets me get past the compilation error in the CLI and when watching the network traffic, I can see the module is loaded on demand.

However, this is another hickup, the library is already bundled in the vendor.js file since the webpack configuration simply bundles everything in the node_modules directory into a the vendors file.

The only way to get around this, is to eject out of the CLI to exclude the library from the common vendor chunk.

All these fixes seem to be a hack, hopefully the cli team can come up with something more elegant.

Hello, I believe Angular applications are going to be bigger and bigger. So this issue becomes more and more important. Is there any solution for this yet from angular-cli?

@danielecanteri I have tried your suggestion and I see it is working fine with 1 external lib module. I have multiple external lib modules and when I try what you have suggested I always get routed to the first imported module from the imports section.

Do you know if I am supposed to have a lazy wrapper for each module? From the other comments I have understand that only 1 wrapper is needed per external library but I have not managed to figure that part out.

import { ExternalModule1, ExternalModule2 } from "@namespace/external-lib";

@NgModule({
    imports: [
        ExternalModule1,
        ExternalModule2
    ]
  })
  export class LazyWrapperModule {}

I was able to make it work use this function to load the module

lazyModuleLoad = function <T>(cl: Type<T>): any { return function (): Type<any> | NgModuleFactory<any> | Promise<Type<any>> | Observable<Type<any>> { return new Promise<Type<any>>(resolve => resolve(cl)); }; }; And in path I call the function like that:

path: '', loadChildren: lazyModuleLoad(SomeModule)

Your solution will compile the module at runtime and won´t load it lazy. And it won´t work with AOT compile.

The interesting thing here is that I have different error:

With routing like:

const routes: Routes = [
  {
    path: 'libContent',
    loadChildren: 'my-lib#MyLibModule'
  }
];
...
  imports: [
    RouterModule.forRoot(routes),
...

it throws error like this:

my-lib\my-lib.d.ts is missing from the TypeScript compilation. Please make sure it is in your tsconfig via the ‘files’ or ‘include’ property.

but it works if I pre-build it in the main bundle like so:

import { MyLibModule } from 'my-lib';

const routes: Routes = [
  {
    path: 'libContent',
    loadChildren: () => MyLibModule 
  }
];
...

It works just like expected

So why is this not a priority? Lets say we have a bigger business application where all pages are located in a lib. For each customer we create a custom app out of the pages. When you have so many pages Lazy Loading is the way to go because the startup time would be too high otherwise. Now I have to create a wrapper module for 50 pages to make it work correctly?? That is not a reasonable solution at all…

@mnicic yeah, @tiagoa solution is not lazy loading. If there’s an import in your routing you will bring all the source together. Currently the only working solution is the proxy, and seeing the lack of interest from the development team on this issue, until someone develops another routing solution for angular, there won’t be alternative.

What’s the problem with the working solution? The proxy module is lazy loaded by the router. Your only overhead is the proxy module around your actual module, that you wanted to lazy load.

I see this as a seriously problematic issue for enterprise projects. Currently one of few issues keeping me away from using angular-cli.

Is there any chance at all for this issue to get resolved?

@adamlubek it works! Thanks!

@Nodarii @vanslly I have same issue with angular cli v6.0.3. Did you guys solved that? or created issue?

@hansl Can #7659 be merged? This looks like the missing piece to fix this issue.

@Chabane here confirmed that he got it working with default angular router too (see #6373 (comment)) also just to double check, are you using RouterModule.forChild(APP_ROUTES); ?

@adamlubek … I can confirm that this is working by creating a wrapper module and with RouterModule.forChild

But whats the thought process towards accepting a direct link from node_modules in loadChildren? I feel this seems to be more friendly…

@adamlubek For my situation the workaround is not working. I have a local module which imports another module from the node_modules folder. The chunking works however my chunk only contains the import. The code of the imported module ends up in the vendor.bundle.js.

Am I doing something wrong?

@thekhoi I tried the approach described in #5153. It seems you’re right, the --no-aot option is not found anywhere, but it does the trick for me.

ng build --prod : fails
ng build --prod --no-aot : succeeds

I did no further investigation on why or how this works.

With the recent version of Angular CLI (7.1.4), I might have found another workaround, which is much cleaner. You can find it in angular-demo-lazy-route-libraries repository.

Before I had a workaround with wrapper module, that statically imported lazy module and exported it. Then I used that wrapper module to configure loadChildren property in my routes.

But I accidentally discovered that (at least with 7.1.4) I do not need a wrapper module, instead, I can just statically import it in some file, but I do not need to import this file anywhere.
This file in the shared repo is called __fake-imports.ts in the application. Note the comment, it should never be imported.

By doing this I can keep imports in my routes simple:

{
  path: 'lazy',
  loadChildren: 'lazy-component#LazyComponentModule',
}

Can anyone test the repo as well (more info in README.md)? I tested it on OS X Mojave and Manjaro Linux with latest npm, node and Angular CLI.


If that indeed works and I didn’t miss something, that means Angular CLI is already close to supporting this feature. The question is if they will go the extra mile.

Hi, I’m developing a dashboard like application. I am developing widgets in angular cli libraries, then compiling these libraries and publishing them to our private NPM registry. These libraries export a NgModule which declares its widgets and makes them entryComponents for dynamic rendering. Then, in the host app, which installs these NPM packages, I am trying to dynamically load these modules, if requested via configuration, to get their widgets’ component factories. The problem I’m facing is that I can’t seem to be able to lazy load these precompiled NgModules in any way. I’ve tried following many good guides I found in stackoverflow but those always talk about loading source files that export an NgModule, not something that is already built in AOT mode from an NPM package. Reading this topic it seems it is not doable… and that I should publish NPM packages containing the source code instead of a compiled library. Is it the case or is there a workaround/official fix after 1 and a half years this topic was open?

I already posted this in a similar issue (could be a duplicate, but not entirely sure). #12859 (comment)

The loading from webpack is actually working fine, as in it found the correct modules to load via lazy loaded references, but the actual ngfactories for those modules have not been build in the process.

You can try my workaround. Basically what you want is to somehow force the compiler to build your factory, but don’t have it bundled into the main chunk but into their respective separate chunks.

I don’t think the issue is the same. I am trying to load modules from NPM packages without really knowing if these modules are even installed in the host app. This app, at startup, gets a configuration object from the server which tells which plugins should be enabled. The app then tries to load these plugins via http request (the paths to the NgModules contained in the NPM packages come with the configuration and aren’t written anywhere in the code, so webpack can’t know them) and use the ngfactory to then extract its components. All the examples I’ve seen just load the lib.module.ts#LibModule and then feed that file to the angular compiler. I want to download via HTTP the already compiled ngfactory (compiled by the external developer that publishes the NPM package with its widgets) and extract its components.

A proxy module works for now, but looking forward to a solution in 7.0.

Don’t know why angular guys have become so irresponsive, the issue is causing us problems.

On Sun, Jul 22, 2018, 7:06 AM Anthony Prado notifications@github.com wrote:

Why hasn’t this issue been a high priority for a fix? Are we not supposed to lazy load modules from Angular 6 libraries?

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/angular/angular-cli/issues/6373#issuecomment-406834793, or mute the thread https://github.com/notifications/unsubscribe-auth/AcrHe3XaEUo-b0mkisndaGexq8uEJTIZks5uI9cYgaJpZM4Nfpev .

@batdelger yes I am doing the same now… Publishing modules without bundling, or using Angular CLI 6 libraries, but again I do not bundle them. This of course works, but it is still just a workaround.

I just decided to publish my lazy modules without bundling ( to our local npm registry). Actually it is not very bad thing. Building them at once is more safe. To do that you must add your lazy libraries to tsconfig.app.json

On Да, 2018 6-р сар 11 05:08 Klemen Oslaj notifications@github.com wrote:

I see this as a seriously problematic issue for enterprise projects. Currently one of few issues keeping me away from using angular-cli.

Is there any chance at all this issue will get resolved?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/angular/angular-cli/issues/6373#issuecomment-396082110, or mute the thread https://github.com/notifications/unsubscribe-auth/AC70Cu6qeRA8EyG0Xv8fjcTg3cBMcafVks5t7YrigaJpZM4Nfpev .

@batdelger, for now I do not use lazy loading, just load moule like so loadChildren: () => MyLibModule and will replace it when this issue will be fixed

@appienvanveen how do you pass your APP_ROUTES to RouterModule? Are you using:

RouterModule.forChild(APP_ROUTES);

?

@adamlubek , you’re right. It looks like there was an issue with the node package I created for testing. My package name was scoped @foo/bar but the “importAs” attribute in my bar.metadata.json file was set to “bar”.

Using a different router isn’t helpful and I didn’t think the CLI supported anything other than the default router. What is the solution to this problem?

Hello @filipesilva, I have the same issue. Is there any news about a workaround?

Thanks,

ERROR in ./~/@project/myLib/lib/index.d.ts
Module build failed: Error: Debug Failure. False expression: Output generation failed
    at Object.assert (<path>\node_modules\typescript\lib\typescript.js:3329:23)
    at Object.transpileModule (<path>\node_modules\typescript\lib\typescript.js:80449:18)
    at TypeScriptFileRefactor.transpile (<path>\node_modules\@ngtools\webpack\src\refactor.js:161:27)
    at Promise.resolve.then.then.then.then (<path>\node_modules\@ngtools\webpack\src\loader.js:378:37)
    at process._tickCallback (internal/process/next_tick.js:103:7)
 @ ./src async
 @ ./~/@angular/core/@angular/core.es5.js
 @ ./src/main.ts
 @ multi ./src/main.ts

@angular/cli@1.1.0 typescript@~2.3.3

const routes: Routes = [
    {
        path: 'myRoute', component: MyComponent,
        children: [
            { path: "user", loadChildren: "@project/myLib#MyModule" }
        ]
    }
];