single-spa-angular: Sharing Angular with Ivy throws: "Class constructor BrowserPlatformLocation cannot be invoked without 'new'"

While preparing for the upgrade to Angular 12, I tried to change our shared Angular 11 setup that is working with ViewEngine to Ivy activated like in the example at the angular-microfrontends repo. However, if I keep sharing single-spa-angular in order to workaround the problem discussed at #288 (see), I receive the following error:

Uncaught TypeError: Class constructor BrowserPlatformLocation cannot be invoked without 'new'
    at new SingleSpaPlatformLocation (extra-providers.ts:13)

If I, like the angular-microfrontends example, stop sharing single-spa-angular I get the same error as in #288:

Uncaught Error: 
      single-spa-angular: could not retrieve extra providers from the platform injector. Did you call platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule()

Demonstration

I’ll try to make a demo later. Meanwhile, my config looks like this:

Angular on the package.json:

  "dependencies": {
    "@angular/common": "~11.1.1",
    "@angular/compiler": "~11.1.1",
    "@angular/core": "~11.1.1",
    "@angular/forms": "~11.1.1",
    "@angular/platform-browser": "~11.1.1",
    "@angular/platform-browser-dynamic": "~11.1.1",
    "@angular/router": "~11.1.1",
    "angular-i18next": "~10.2.0",
    "i18next": "^20.3.1",
    "rxjs": "^6.6.0",
    "single-spa": "~5.8.0",
    "single-spa-angular": "~4.9.0",
    "zone.js": "~0.11.4"
  },

The webpack config:

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

  singleSpaWebpackConfig.externals.push(
    /^@apps\/*/,
    /^single-spa$/,
    /^single-spa-angular$/,
    /^single-spa-angular\/internals$/,
    /^rxjs$/,
    /^rxjs\/operators$/,
    /^@angular\/core$/,
    /^@angular\/common$/,
    /^@angular\/router$/,
    /^@angular\/platform-browser$/,
    /^zone\.js$/,
    /^i18next$/,
  );
 return singleSpaWebpackConfig
}

Shared dependencies on importmap:

        "@angular/core": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__core@11.1.1/system/es2015/ivy/angular-core.js",
        "@angular/common": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__common@11.1.1/system/es2015/ivy/angular-common.js",
        "@angular/router": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__router@11.1.1/system/es2015/ivy/angular-router.js",
        "@angular/platform-browser": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__platform-browser@11.1.1/system/es2015/ivy/angular-platform-browser.js",
       
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@5.9.3/lib/umd/single-spa.dev.js",
        "single-spa-angular": "https://cdn.jsdelivr.net/npm/single-spa-angular@4.9.2/bundles/single-spa-angular.umd.js",
        "single-spa-angular/internals": "https://cdn.jsdelivr.net/npm/single-spa-angular@4.9.2/bundles/single-spa-angular-internals.umd.js",
    
        "rxjs": "https://cdn.jsdelivr.net/npm/rxjs@6.6.6/bundles/rxjs.umd.js",
        "rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs@6.6.6/system/es5/rxjs-operators.min.js",

        "i18next": "https://unpkg.com/i18next/dist/umd/i18next.js",
        "i18next-browser-languagedetector": "https://cdnjs.cloudflare.com/ajax/libs/i18next-browser-languagedetector/4.2.0/i18nextBrowserLanguageDetector.js"

and main.single-spa.ts:

const lifecycles = singleSpaAngular({
  bootstrapFunction: singleSpaProps => {
    singleSpaPropsSubject.next(singleSpaProps);

    i18next.loadNamespaces('login').then(() => {
      i18next.addResourceBundle('en', 'login', resources.en, true, true);
      i18next.addResourceBundle('es', 'login', resources.es, true, true);
    });

    return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule);
  },
  domElementGetter,
  template: '<login-root />',
  Router,
  NgZone,
  NavigationStart
});

Expected Behavior

Being able to share Angular 11-12 with Ivy enabled between multiple microfrontends like in the angular-microfrontends example.

Actual Behavior

App crashes on load with BrowserPlatformLocation error or it crashes when loading the second Angular app if single-spa-angular is not being shared.

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 1
  • Comments: 25 (14 by maintainers)

Most upvoted comments

@OriolInvernonLlaneza Managed to somehow get it working in a hacky way, but wouldn’t want to use this for production since I dont really understand what else could go wrong with this:

I managed to isolate the “issue” to ng-packagr which seems to be used to create the single-spa-angular bundles. The exact issue is in these lines:

  1. https://github.com/ng-packagr/ng-packagr/blob/6e22549aa32ffe43ca8a12e120fcdc4c0e89e63d/src/lib/ng-package/entry-point/write-bundles.transform.ts#L65

image

  1. https://github.com/ng-packagr/ng-packagr/blob/6e22549aa32ffe43ca8a12e120fcdc4c0e89e63d/src/lib/flatten/downlevel-plugin.ts#L25

image

This seems to be explicitly transpiling the ES2015 UMD bundle to ES5.

In my local node_modules, I simply commented the transform in ng_packagr/src/lib/ng-package/entry-point/write-bundles.transform.ts, like this: image

After this, I built the single-spa-angular repo again and used the bundles generated from this in my demo code, which fixed the issue.

However, I am not really sure it is a good idea to do this or if this will cause something else to break. I am okay with my code only supporting browsers that support ES2015, but not sure if some other library can potentially break because of this.

Was wondering whether to raise an issue in ng-packagr to make the transformation to ES5 optional, but then saw some comments over there in other issues saying they have deprecated support for UMD bundles and will soon be removing that from ng-packagr as well.

I then proceeded to check if SystemJS would work with fesm2015 bundles generated by ng-packagr, but apparently that is not supported either (I hope I am wrong about this).

@arturovt @joeldenning I know you guys must be busy, but your input on this would be super valuable. Any ideas about what could be done here?

Thanks in advance!

I found the same error as in #288 and here on the angular-microfrontends repo: image

Assuming X is BrowserPlatformLocation (same file, same line), I also get the same error while sharing single-spa-angular. image

Should I make a different issue on that repo?

@joeldenning I have added PRs for both 4.x as well as master branches. Please let me know if this works or if any changes are required.

I commented in the esm-bundle issue - I’d rather fix single-spa-angular than create an esm-bundle workaround for it.

@OriolInvernonLlaneza I think i understand why the umd bundles for Angular from unpkg seem to be working fine with single-spa-angular when ivy is disabled. The umd bundles seem to be compiled down to ES5 and the single-spa-angular umd bundle too is is compiled down to ES5, so the two were mutually compatible with each other.

This is not the case with the Ivy esm bundles since they are using ES2015 syntax. Maybe down leveling the esm bundles for Angular Ivy to ES5 will also solve the issue - not verified though.

However, this would probably also mean that all of my angular applications too would need to be downleveled to ES5. This is likely to impact my bundle sizes non-trivially, so would like to avoid this and use ES2015 itself.

Since ng-packagr doesn’t want to support es2015 umd bundles, perhaps it’s time to publish a systemjs version that is in es2015 format. I don’t think ng-packagr supports changing rollup’s output.format to "system", but that’s what we’d want. So we’d probably need to write our own rollup configuration to do this. I think I’d prefer that approach over using patch-package to manually modify ng-packagr. @ajaykrishnan33 are you interested in giving that a shot? I’m willing to merge #374 and #375 as a last resort if needed.

I don’t spend much time maintaining single-spa-angular and do not know angular or ng-packagr very well, but my understanding is that this is just a matter of ng-packagr and/or typescript configuration. It should not be compiling tjhe SingleSpaPlatformLocation class down to es5, but keeping it as an es class. The single-spa-angular project is not supposed to publish any es5 bundles anymore (same goes for Angular itself), so we don’t have the es2015 or esm folders for differential loading. This file should not have var SingleSpaPlatformLocation but rather class SingleSpaPlatformLocation, and it’s ng-packagr/typescript that control that. I’d be happy to review a pull request fixing it.

Thanks @OriolInvernonLlaneza

Just posting another screenshot from the generated single-spa-angular.umd.js code which shows that the SingleSpaPlatformLocation class got compiled down to ES5 syntax.

image

I captured the SingleSpaPlatformLocation class into a variable in the console and noticed that the class itself has been converted into a function (ES5 syntax), whereas the parent class seems to be in ES6 syntax.

From some reading around on the net about this same error, this error usually seems to be happening when the child class is transpiled down to ES5 syntax but it is trying to extend a class which is in ES6 syntax.

Yesterday looking back at the problems I had when we shared Angular for the first time (v8), I found that I had the very same issues and got help from StackOverflow. See:

That time I had to import rxjs from an UMD bundle and rxjs/operators as system from the esm-bundle and it worked. (I have no idea why both as system didn’t work).

I am facing the same issue too. We are currently evaluating whether single-spa is a good fit for our usecase, but we dont want to be restricted from using the Angular Ivy compiler or from using future versions of Angular because of this choice.

We really like single-spa and would like to use it. Sharing components and services between applications is an important capability for us. Hence it would be great if this issue could be resolved since it is directly blocking us from adopting single-spa.