Skip to content

Lazy loading module from library package located in node_modules fails to build. #6373

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
thekhoi opened this issue May 18, 2017 · 80 comments
Closed

Comments

@thekhoi
Copy link

thekhoi commented May 18, 2017

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

@thekhoi thekhoi changed the title Lazy loading module from library package located in node_modules fails. Lazy loading module from library package located in node_modules fails to build. May 18, 2017
@Brocco Brocco added P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent severity3: broken labels May 18, 2017
@mdewaelebe
Copy link

mdewaelebe commented May 19, 2017

I'm having the same issue with version:

@angular/cli: 1.0.4 
@angular: 4.0.0

Hopefully this can be fixed soon, I'll continue by using --no-aot for the moment.

@thekhoi
Copy link
Author

thekhoi commented May 19, 2017

@mdewaelebe doesn't the CLI default to JIT? This error happens for me even if I use --aot false. I don't see a --no-aot option.

@mdewaelebe
Copy link

@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.

@thekhoi
Copy link
Author

thekhoi commented May 25, 2017

@mdewaelebe I think that is a different issue. I get the same error for a lazy route that happens to be local to my project directory or not.

ERROR in ./src/$$_gendir async
Module not found: Error: Can't resolve 'C:\Workspace\scratch\Ng4Starter\host\src\$$_gendir\app\foo\index.ngfactory.ts' in 'C:\Workspace\scratch\Ng4Starter\host\src\$$_gendir'
 @ ./src/$$_gendir 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.

@thekhoi
Copy link
Author

thekhoi commented May 30, 2017

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.

@filipesilva
Copy link
Contributor

#6755 contains a nice repro of this issue.

@Chabane
Copy link

Chabane commented Jul 20, 2017

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" }
        ]
    }
];

@filipesilva filipesilva added P1 Impacts a large percentage of users; if a workaround exists it is partial or overly painful type: bug/fix and removed P3 An issue that is relevant to core functions, but does not impede progress. Important, but not urgent labels Jul 24, 2017
@adamlubek
Copy link

adamlubek commented Jul 31, 2017

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 :)

@Chabane
Copy link

Chabane commented Aug 8, 2017

@adamlubek it works! Thanks!

@babeal
Copy link

babeal commented Aug 14, 2017

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?

@adamlubek
Copy link

@babeal yes, instead of default Angular router you can use https://github.com/ui-router/angular for pure Angular project or https://github.com/ui-router/angular-hybrid for hybrid AngularJs/Angular project

That said, it looks like @Chabane is using default router and got it working. @Chabane , is that correct?

@Chabane
Copy link

Chabane commented Aug 17, 2017

@adamlubek yes perfect!

@filipesilva
Copy link
Contributor

Related to #5986.

@thekhoi
Copy link
Author

thekhoi commented Aug 28, 2017

The key to work around from @adamlubek is the 'local' module, QuickstartLibLazyLoader, imports the installed 'LibModule'.

This mostly works however it breaks if you try to do a production build.

ng build --prod

Turning off AOT will suppress the issue

ng build --prod --aot false

@adamlubek
Copy link

adamlubek commented Aug 29, 2017

@thekhoi , that's strange as my workaround works for me in production build with AOT. Does your library work with AOT when it's not lazy loaded using my work around?

Maybe you have non AOT compliant code somewhere in your library?

@thekhoi
Copy link
Author

thekhoi commented Aug 29, 2017

@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".

@e-davidson
Copy link

I have the same issue.
@adamlubek s workaround worked for me.
Is this issue going to get resolved?

@jcimoch
Copy link

jcimoch commented Oct 4, 2017

Is it going to be fixed?

@Etchelon
Copy link

Etchelon commented Dec 3, 2018

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?

@totkeks
Copy link

totkeks commented Dec 3, 2018

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.

@Etchelon
Copy link

Etchelon commented Dec 3, 2018

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.

@ngbot ngbot bot added this to the Backlog milestone Dec 10, 2018
@sqonde
Copy link

sqonde commented Dec 12, 2018

@angular-devkit/architect          0.10.7
@angular-devkit/build-angular      0.10.7
@angular-devkit/build-ng-packagr   0.10.7
@angular-devkit/build-optimizer    0.10.7
@angular-devkit/build-webpack      0.10.7
@angular-devkit/core               7.0.7
@angular-devkit/schematics         7.0.7
@angular/cli                       7.0.7
@ngtools/json-schema               1.1.0
@ngtools/webpack                   7.0.7
@schematics/angular                7.0.7
@schematics/update                 0.10.7
ng-packagr                         4.4.5
rxjs                               6.3.3
typescript                         3.1.6
webpack                            4.19.1

Both workarounds worked for me:
#6373 (comment) << preferred
#6373 (comment)

@digaus
Copy link

digaus commented Dec 14, 2018

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...

@woppa684
Copy link

I'm guessing because the Ivy renderer will fix this issue (and change the way to deal with these things anyway) and they don't want to spend time fixing the "old" behavior.

@klemenoslaj
Copy link

I'm guessing because the Ivy renderer will fix this issue (and change the way to deal with these things anyway) and they don't want to spend time fixing the "old" behavior.

Well, that is a valid and reasonable explanation, however, this issue and other related issues are hunting angular community since quite a while now, while having no official statement(apart from Ivy recently).

Sure, Ivy will fix that, but why didn't fix happened earlier(without Ivy) given the fact the issue really puts a stain on Angular itself IMHO.

@woppa684
Copy link

@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".

I know this is a while ago, but how did you manage to solve this? If I change the importAs attributes manually in the generated metadata file for my built libraries indeed it works, but how can I instruct Angular CLI to use the correct (scoped) importAs statement?

@klemenoslaj
Copy link

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.

@tobi-or-not-tobi
Copy link

@klemenoslaj Nice catch! Much cleaner then wrapping lib modules into app modules. For jit compilation, you can make that work with setting up the path in your tsconfig. I'm used to to this during development to build both app and lib source with webpack, see #10369. While this is generally not recommended, it's quite convenient during development. I have a pipeline running production builds on every commit (using a separate tsconfig), so the risk for libs no being build correctly is mitigated.

I was able to test the repo and further enhance it with several (ng-packagr) sub modules. It's even working with the route config in the lib code.

@klemenoslaj
Copy link

@tobi-or-not-tobi, great ;) And feel free to make a pull request to my repo adding JIT and other enhancement, so that people could benefit 👍

@alan-agius4
Copy link
Collaborator

Hi all,

This thread was opened a while ago. Having so comments makes it hard for people to find information in anything but the latest comments. But on the other hand I don't think most people would go through all of the comments anyway. New users that see this thread mostly read the first and latest comments and lose things said in between.

So for the reasons stated above (high volume of comments, hidden comments, hard to report and inform people) I'm locking this issue to prevent this comment from being lost as new comments come in.

Thank you to everyone that reported and helped diagnose the problems and provided workarounds.

Our recommended approach to lazy load a library within an angular workspace or node modules, is to use a proxy/wrapper module. With this approach lazy loading will works in both JIT and AOT mode. There are other solutions that work solely in AOT mode such as tsconfig imports however for a better development experience we don't encourage this.

Below find an example of the recommended approach;

lazy/lazy.module.ts

import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { LibModule, LazyComponent } from 'my-lib';

@NgModule({
  imports: [
    LibModule,
    RouterModule.forChild([
      { path: '', component: LazyComponent, pathMatch: 'full' }
    ])
  ]
})
export class LazyModule { }

app.module.ts

import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {RouterModule} from '@angular/router';

import { AppComponent } from './app.component';
import { HomeComponent } from './home/home.component';

@NgModule({
  declarations: [
    AppComponent,
    HomeComponent,
  ],
  imports: [
    RouterModule.forRoot([
      { path: '', component: HomeComponent, pathMatch: 'full'},
      { path: 'lazy', loadChildren: './lazy/lazy.module#LazyModule'},
    ]),
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

@angular angular locked and limited conversation to collaborators Jan 10, 2019
@alan-agius4
Copy link
Collaborator

Hi all,

I am going to close this as following a convo with @alxhub. Lazy loading should happen at an application and not a library level. Hence the above approach is the recommended and the supported way to lazy load a library module in your application.

Related to: angular/angular#31893 (comment)

@ngbot ngbot bot modified the milestones: Backlog, needsTriage Sep 29, 2020
@alan-agius4 alan-agius4 removed this from the needsTriage milestone Sep 29, 2020
@ngbot ngbot bot added this to the needsTriage milestone Sep 29, 2020
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