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

Application not working with webpack externals #204

Closed
lukszy opened this issue May 28, 2020 · 17 comments · Fixed by #299
Closed

Application not working with webpack externals #204

lukszy opened this issue May 28, 2020 · 17 comments · Fixed by #299

Comments

@lukszy
Copy link

lukszy commented May 28, 2020

Hi there,
i'm trying to use single-spa-angular combined with webpack externals to have a single vendor bundle at host app level that can be shared between all microfrontend apps.

How to reporoduce?

  1. create an two angular apps (hostApp, microApp) within your workspace (i'm using nx extension)
  2. add single-spa and single-spa-angular as dependencies and follow the docs
  3. in webpack config file (in microApp) add externals like
module.exports = (angularWebpackConfig, options) => {
  const singleSpaWebpackConfig = singleSpaAngularWebpack(
    angularWebpackConfig,
    options
  );

  singleSpaWebpackConfig.externals = {
    rxjs: 'rxjs',
    '@angular/core': 'ng.core',
    '@angular/common': 'ng.common',
    '@angular/common/http': 'ng.common.http',
    '@angular/platform-browser': 'ng.platformBrowser',
    '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic',
    '@angular/compiler': 'ng.compiler',
    '@angular/animations': 'ng.animations',
    '@angular/router': 'ng.router',
    '@angular/forms': 'ng.forms',
  };

  return singleSpaWebpackConfig;
};
  1. in angular.json file, in hostapp -> architect -> build -> options -> scripts add following
"scripts": [
    "node_modules/rxjs/bundles/rxjs.umd.js",
    "node_modules/@angular/core/bundles/core.umd.js",
    "node_modules/@angular/common/bundles/common.umd.js",
    "node_modules/@angular/common/bundles/common-http.umd.js",
    "node_modules/@angular/compiler/bundles/compiler.umd.js",
    "node_modules/@angular/platform-browser/bundles/platform-browser.umd.js",
    "node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js",
    "node_modules/@angular/router/bundles/router.umd.js",
    "node_modules/@angular/forms/bundles/forms.umd.js",
    "node_modules/@angular/animations/bundles/animations.umd.js",
]
  1. start both apps and register microApp using single-spa methods.
  2. microApp throws import error
app.component.ts:8 Uncaught TypeError: Cannot read property 'ɵɵdefineComponent' of undefined
    at Module../src/app/app.component.ts (app.component.ts:8)
    at __webpack_require__ (bootstrap:19)
    at Module../src/app/app.module.ts (app.module.ts:1)
    at __webpack_require__ (bootstrap:19)
    at Module../src/main.ts (main.ts:1)
    at __webpack_require__ (bootstrap:19)
    at Object.0 (main.js:10365)
    at __webpack_require__ (bootstrap:19)
    at bootstrap:83
    at bootstrap:83

The problem.
All the imports are swpped with the externals from webpack config. However it looks like main.js file is bundled before using original import with internal module references. (snippet below)

AppComponent.ɵfac = function AppComponent_Factory(t) { return new (t || AppComponent)(); };
AppComponent.ɵcmp = _angular_core__WEBPACK_IMPORTED_MODULE_0__["ɵɵdefineComponent"]({ type: AppComponent, selectors: [["angular-application"]], decls: 14, vars: 0, consts: [[1, "flex"], ["href", ""]], template: function AppComponent_Template(rf, ctx) { if (rf & 1) {

Any advaice how to resolve this issue?

@arturovt
Copy link
Collaborator

Sharing dependencies is not possible in Angular 9.

@joeldenning
Copy link
Member

Angular 9 with the Ivy Compiler enabled does not support sharing dependencies between separate microfrontends. You can read more about that at https://twitter.com/Joelbdenning/status/1253781652486017024.

My understanding is that Angular 10's Ivy compiler may support this.

If you'd like to share dependencies with Angular 9, it is possible but only if you turn off the Ivy Compiler and go back to View Engine.

@arturovt
Copy link
Collaborator

Why didn't you provide a minimal reproducible example? I can't always spend my time going through steps manually. I'd rather spend this time fixing some bug or helping you to resolve the issue than trying to reproduce it manually 😞


microApp throws import error - this is falsy. hostApp throws this error. As you didn't provide reproducible example I had to guess what's wrong with your example. I was wondering why did you provide scripts in angular.json -> host-app? Basically your dependencies will be bundled twice since you don't have webpack.config.externals in host-app.

You have to expose dependencies to window object in the host-app, this can be done via expose-loader or other tools, just google. I went the fastest way:

// apps/host-app/src/polyfills.ts

import * as rxjs from 'rxjs';
import * as compiler from '@angular/compiler';
import * as core from '@angular/core';
import * as common from '@angular/common';
import * as commonHttp from '@angular/common/http';
import * as platformBrowser from '@angular/platform-browser';
import * as platformBrowserDynamic from '@angular/platform-browser-dynamic';
import * as router from '@angular/router';
import * as singleSpaAngular from 'single-spa-angular';

const win = window as any;

win.rxjs = rxjs;
win['ng.compiler'] = compiler;
win['ng.core'] = core;
win['ng.common'] = common;
win['ng.common.http'] = commonHttp;
win['ng.platformBrowser'] = platformBrowser;
win['ng.platformBrowserDynamic'] = platformBrowserDynamic;
win['ng.router'] = router;
win.singleSpaAngular = singleSpaAngular;

And this is my apps/micro-app/webpack.config.js:

module.exports = config => {
  config = require('single-spa-angular/lib/webpack').default(config);

  config.output.library = 'microApp';

  config.externals = {
    rxjs: 'rxjs',
    '@angular/core': 'ng.core',
    '@angular/common': 'ng.common',
    '@angular/common/http': 'ng.common.http',
    '@angular/platform-browser': 'ng.platformBrowser',
    '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic',
    '@angular/compiler': 'ng.compiler',
    '@angular/animations': 'ng.animations',
    '@angular/router': 'ng.router',
    '@angular/forms': 'ng.forms',
    'single-spa-angular': 'singleSpaAngular',
  };

  return config;
};

This is my apps/host-app/src/main.ts:

import { registerApplication, start } from 'single-spa';

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

function loadScript() {
  return new Promise(resolve => {
    const script = document.createElement('script');
    script.src = 'http://localhost:8080/main.js';
    script.addEventListener('load', resolve);
    document.head.appendChild(script);
  });
}

registerApplication({
  name: 'microApp',
  app: () => loadScript().then(() => window.microApp),
  activeWhen: () => true,
});

start();

platformBrowserDynamic().bootstrapModule(AppModule);

This is my apps/host-app/src/app/app.component.html:

<div class="micro-app-host"></div>

This is my apps/micro-app/src/main.ts:

import { enableProdMode, NgZone } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { singleSpaAngular } from 'single-spa-angular';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

if (environment.production) {
  enableProdMode();
}

const lifecycles = singleSpaAngular({
  template: '<micro-app-nx-root />',
  bootstrapFunction: () => platformBrowserDynamic().bootstrapModule(AppModule),
  NgZone,
  domElementGetter: () => document.querySelector('.micro-app-host'),
});

export const bootstrap = lifecycles.bootstrap;
export const mount = lifecycles.mount;
export const unmount = lifecycles.unmount;

Result:

image

micro-app/main.js size is 46kb:

image

@lukszy
Copy link
Author

lukszy commented Jun 1, 2020

Thanks for a quick reponse.
Sorry for not providing an example, my bad. Here's a repository https://github.com/lukszymanski/single-spa-angular-nx-demo

In my case host application is not an angular app. It's just some html with vendor files that's why i've provided vendors in angular.json. Adding vendor files (UMD version) in angular.json to host-app we gonna have all of them accesable from window.

Having this setup the error is thrown when loading microApp/microApp1 but the soruce of it is placed in micro application bundle file with bad replaced imports for @angular/core - described in the first post.

@arturovt
Copy link
Collaborator

arturovt commented Nov 8, 2020

@lukszymanski I'm sorry for the delay. You issue is really informative and I've finally found out why it didn't work.

The problem was that our package single-spa-angular/internals was generated with the wrong UMD id by ng-packagr.

I was able to clone your git repo. Your Webpack config for micro-apps is not correct, you've got such externals:

return {
  ...singleSpaWebpackConfig,
  externals: {
    rxjs: 'rxjs',
    '@angular/core': 'ng.core',
    '@angular/common': 'ng.common',
    '@angular/common/http': 'ng.common.http',
    '@angular/platform-browser': 'ng.platformBrowser',
    '@angular/platform-browser-dynamic': 'ng.platformBrowserDynamic',
    '@angular/compiler': 'ng.compiler',
    '@angular/animations': 'ng.animations',
    // '@angular/elements': 'ng.elements',
    // '@angular/router': 'ng.router',
    // '@angular/forms': 'ng.forms',
    'single-spa-angular': 'single-spa-angular',
  },
};

ng.{PACKAGE_NAME} is not a correct UMD id for Angular packages, if you type in console:

console.log(window['ng.core'])

It will log undefined, since ng is an object with the core property. The below code is correct and works:

module.exports = (angularWebpackConfig, options) => {
  const singleSpaWebpackConfig = singleSpaAngularWebpack(
    angularWebpackConfig,
    options
  );

  singleSpaWebpackConfig.output.library = 'microApp';

  const mappings = {
    '@angular/core': 'core',
    '@angular/common': 'common',
    '@angular/common/http': 'common.http',
    '@angular/platform-browser': 'platformBrowser',
    '@angular/platform-browser-dynamic': 'platformBrowserDynamic',
    '@angular/compiler': 'compiler',
    '@angular/animations': 'animations',
  };

  const angularExternals = Object.keys(mappings).reduce(
    (accumulator, mapping) => {
      const request = ['ng', mappings[mapping]];

      accumulator[mapping] = {
        root: request,
        commonjs: request,
        commonjs2: request,
        amd: request,
      };

      return accumulator;
    }
  );

  return {
    ...singleSpaWebpackConfig,
    externals: {
      rxjs: 'rxjs',
      ...angularExternals,
      'single-spa-angular': 'single-spa-angular',
    },
  };
};

Also, you've added single-spa-angular as an external dependency but you don't bundle it in scripts of the host app, so you also should've added UMD packages to scripts:

"scripts": [
  "node_modules/rxjs/bundles/rxjs.umd.js",
  "node_modules/@angular/core/bundles/core.umd.js",
  "node_modules/@angular/common/bundles/common.umd.js",
  "node_modules/@angular/common/bundles/common-http.umd.js",
  "node_modules/@angular/compiler/bundles/compiler.umd.js",
  "node_modules/@angular/platform-browser/bundles/platform-browser.umd.js",
  "node_modules/@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js",
  "node_modules/@angular/router/bundles/router.umd.js",
  "node_modules/@angular/forms/bundles/forms.umd.js",
  "node_modules/single-spa-angular/bundles/single-spa-angular-internals.umd.js",
  "node_modules/single-spa-angular/bundles/single-spa-angular.umd.js"
]

The result is:
image

@lukszy
Copy link
Author

lukszy commented Nov 9, 2020

cool! thank you for helping out! 💪

@joeldenning
Copy link
Member

@tommueller
Copy link

@arturovt are you sure that the code you provided works? I added it to my custom-webpack.js and everything works fine from the application perspective, but the bundle size stays the same. Shouldn't it decrease the bundle size quite immensely?

@wudith
Copy link

wudith commented Feb 25, 2021

@arturovt are you sure that the code you provided works? I added it to my custom-webpack.js and everything works fine from the application perspective, but the bundle size stays the same. Shouldn't it decrease the bundle size quite immensely?

Yes, I agree. The size does not change much.

@ayyappamaddi
Copy link

@arturovt , @wudith , sharing modules works in Angular 8 as well?

@arturovt
Copy link
Collaborator

arturovt commented Mar 9, 2021

@tommueller @wudith well, I've decided to check.

This is before:

image

This is after:

image

This navbar app is inside this repository, you can clone it and try it out.

@tommueller
Copy link

tommueller commented Mar 9, 2021

Thanks @arturovt . I can confirm that with the code in the image (webpack.config.ts) it does work for me as well. With the js code from you last post, it does actually not work for me.

@arturovt
Copy link
Collaborator

arturovt commented Mar 9, 2021

@tommueller I just made a typo and missed {} after reduce accumulator :D

@tommueller
Copy link

tommueller commented Mar 9, 2021

@arturovt When I now load the application with the externals configured (and the small bundle size) I run into:

http://localhost:4201/main.js module could not be loaded! Error: Unable to resolve bare specifier 'ng,core' from http://localhost:4201/main.js (SystemJS Error#8 https://git.io/JvFET#8)
    at system.min.js:4

@nmccrack37
Copy link

@tommueller just wondering - did you ever resolve the bare specifier issue above? Running into the same problem myself

@tommueller
Copy link

@nmccrack37 no unfortunately not. For us it's luckily such a big issue, because it is only a very small part of our application, so I never took the time to investigate further.

@parasharsh
Copy link

Is anyone able to get this working with Angular 14?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants