MrFrankel / share-loader

Share modules between webpack applications
64 stars 25 forks source link

Share-loader with ng-zorro-antd #23

Open EvandroApSantos opened 5 years ago

EvandroApSantos commented 5 years ago

Hi.

First of all, congrats for the project. What a great idea!

I'm using ng-zorro-antd (https://github.com/NG-ZORRO/ng-zorro-antd) in my project and I'm not able to share it's modules between shell and ext apps. I've created a repro with this scenario: https://github.com/EvandroApSantos/share-loader-ng-zorro

When I run ext app an error is thrown on console: Uncaught TypeError: Cannot read property 'ng-zorro-antd' of undefined

The error occurs at this point: e["container-app"].node_modules["ng-zorro-antd"].dropdown

Any idea how to solve this?

Thank you.

MrFrankel commented 5 years ago

@EvandroApSantos, thanks for the repro, ill look into it.

RobM-ADP commented 5 years ago

@EvandroApSantos Are you seeing that issue all the time? I get that issue but only when using AOT and the error seems to always be pointing at a factory:

e["container-app"].node_modules["@van-genesis"].core.address["van-genesis-core-address"].ngfactory

the window['container-app'] object doesn't have a property called node_modules at all as the error seems to indicate, but even if it did, the ['van-genesis'].core.address object that is in the window['container-app'] doesn't have an ngfactory in it anyway.

alexej-strelzow commented 5 years ago

Hi @RobM-ADP : I had a similar problem with clarity-design and solved it as follows

In the CL (Shell):

module.exports = {
  module: {
    rules: [
      {
        test: /\.js?$/,
        use:
          [{
            loader: 'share-loader',
            options: {
                modules: [/@angular/,/@clr\/angular/,/@clr\/ui/,/rxjs/],
                exclude: [/@angular-devkit/],
                namespace: 'xxx'
            }
          }]
      }]
  }
};

In the MF:

externals: [
    Externals({
      namespace: 'xxx',
      modules: [/@angular/,/@clr\/ui/,/@clr\/angular/,/rxjs/],
      exclude: [/router.ngfactory/,/clr-angular.ngfactory/]
    })
  ],

Maybe you can identify the lib you have to share by inspecting your webpack bundle:

In package.json (install "webpack-bundle-analyzer": "~3.4.1"):

    "analyze:gen": "ng run lucy-web-app:build-prod --stats-json",
    "analyze:stats": "node_modules/.bin/webpack-bundle-analyzer ./dist/stats.json",
    "analyze": "npm run analyze:gen && npm run analyze:stats",
RobM-ADP commented 5 years ago

Thanks @alexej-strelzow. I think the missing piece for me was adding the ngfactory part to the exclude array. With this approach though, all the ngfactories still end up in the "child" app which I guess is to be expected. It would be nice if that too could come from the shell.

Also, you have to have imported the modules used in the child into the shell in order for them to be in the window when the child loads. Again, I guess this is also to be expected. The downside is that, when making changes to the child, you have to be aware of what is available in the parent.

alexej-strelzow commented 5 years ago

@RobM-ADP : yes, that is a tricky one and I just came by it (excluding the factory) by chance. The next two points (what is to be expected) - I agree. Also your 3rd point, e.g. when you want to use Animations or lets just say the HttpClient in your child-app, you have to import the respective modules in the parent.

One quick tip (for free): You can have an app-wide interceptor by just providing HttpClientModule in the shell and injecting the services in the child-apps (not providing the HttpClientModule in there).

Another tip: You can write you own Angular library (e.g. @app/shared) and import it in shell + children (via package.json) AND provide @app/shared in the webpack.config from your shell in order to have an application wide singleton (e.g. session-service or private event-bus) ;-)

I really enjoy share-loader, once you've got the hang of it you can do great things with minimal bandwidth (because of sharing stuff). But it is for sure an advanced tool, for devs who know what they are doing.

But I have one suggestion for improvement (@MrFrankel ) - currently with AOT it is not possible to use source-maps, because the LastCallWebpackPlugin manipulates the bundle and then the @angular/cli cannot generate the source-map properly, hence you cannot use the "hidden sourcemap" feature. I found a workaround for this by disabling uglification in angular.json (optimization: false) and doing the string-replacement (which is done in LastCallWebpackPlugin) in a gulp-taks that runs after the prod build. Then however you'd have to uglify the main.js manually. Is there a better approach to this?

RobM-ADP commented 5 years ago

@alexej-strelzow One thing that I did was to put this into my webpack config in the child. When I build the child it outputs a script to the console that can be executed in the running parent app to tell whether or not anything externalized in the child is missing in the parent.

Right now running it is a manual process but I think you could use pupeteer to actually fireup the real app and run it automatically

const NAMESPACE = 'container-app';
...
new LastCallWebpackPlugin({
      assetProcessors: [
        {
          regExp: /main.js$/,
          phase: 'compilation.optimize-chunk-assets',
          processor: (assetName, asset) => {
            const regx = new RegExp('(require\\()([^\\)]*)(\\))', 'gi');
            const src = asset.source();
            let results;
            let externalPackages = [];
            let packageName;
            while ((results = regx.exec(src)) !== null) {
              packageName = results[2].substring(1, results[2].length - 1);
              if (externalPackages.indexOf(packageName) === -1) {
                externalPackages.push(packageName);
              }
            }
            console.log('\nThe following packages must exist in the shell app:');
            console.log('\t' + externalPackages.sort().join('\n\t'));
            console.log('\nTo verify that this bundle will work in your app, run the following command in the console of your running application.')
            let code = `
            (function() {
              let packages = ['${externalPackages.join("', '")}'];
              let missing = [];
              packages.forEach((pkg) => {
                if (pkg.charAt(0) === '@') {
                      pkg = pkg.substr(1);
                  }
                  let parts = pkg.split('/');
                let root = window['${NAMESPACE}'];
                  for (let i = 0; i < parts.length; i++) {
                    let part = parts[i];
                    if (!root[part]) {
                      missing.push(parts.join('/'));
                      break;
                    }
                    root = root[part];
                  }
              });
              if (missing.length > 0) {
                throw new Error('Missing bundles: ' + missing.join(', '));
              } else {
                console.log('No missing bundles detected');
              }
            })();
            `;
            console.log(code);
            return Promise.resolve(asset.source().replace('var AppModuleNgFactory', `window.extapp = {};\nvar AppModuleNgFactory = window.extapp.AppModule`))
          }
        }
      ],
      canPrint: true
    })
gesanchez commented 4 years ago

@EvandroApSantos Are you seeing that issue all the time? I get that issue but only when using AOT and the error seems to always be pointing at a factory:

e["container-app"].node_modules["@van-genesis"].core.address["van-genesis-core-address"].ngfactory

the window['container-app'] object doesn't have a property called node_modules at all as the error seems to indicate, but even if it did, the ['van-genesis'].core.address object that is in the window['container-app'] doesn't have an ngfactory in it anyway.

@RobM-ADP I have the same issue with angular material when i use AOT, Do you have you is a solution for this ?