8

I'm creating an Angular 2 application that can be dynamically injected into a running website. The point of this app is to be able to modify the website content visually.

Everything works as expected when the website in question does not also run using Angular 2. In particular, when the original website uses Angular 2 and Zone.js, my application also tries to load Zone.js but it crashes on error: Zone already loaded. I'm using Webpack as a build system and I tried to solve the problem by splitting the build into 3 parts: manifest, polyfill, and app. Manifest contains only the Webpack manifest, polyfill contains core-js and zone.js, and the rest is in the app. There's also a fourth chunk called index. This is the one placed into the website and it first checks if window.Zone is defined. If it is, only manifest and app are added. Otherwise also polyfill.

The problem is that when polyfill chunk is not loaded, all modules in that chunk are missing from Webpack. So when the code reaches some import from the app chunk that requires either core-js or Zone.js (that's what I think is happening), I get this error:

TypeError: Cannot read property 'call' of undefined
    at __webpack_require__ (manifest.js:55)
    at Object.<anonymous> (app.js:15016)
    at __webpack_require__ (manifest.js:55)
    at Object.<anonymous> (app.js:65567)
    at __webpack_require__ (manifest.js:55)
    at Object.<anonymous> (app.js:65163)
    at __webpack_require__ (manifest.js:55)
    at Object.defineProperty.value (app.js:65137)
    at __webpack_require__ (manifest.js:55)
    at webpackJsonpCallback (manifest.js:26)

Somewhere in the manifest file:

/******/   // Execute the module function
/******/   modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

QUESTION

How do I configure Webpack or Angular to not import zone.js as a dependency but to use that one already registered on the window? I want to be able to conditionally load core-js and zone.js only if it's not yet loaded on the website.

UPDATE

I modified my build (Webpack) to only use a single bundle. In that bundle, I tried using Webpack's dynamic imports like this:

// import reflect metadata shim
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';


// Angular and application imports
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app.module';
import { ViewEncapsulation } from '@angular/core';


// load the editor in the next frame
requestAnimationFrame(() => {

    if (window.Zone) {
        bootstrap();
    }
    else {
        import('zone.js/dist/zone') // <-- PROBLEM HERE
            .then(bootstrap);
    }

});


// bootstraps the editor
function bootstrap() {

    // add the root component to the DOM
    const root = document.createElement('efe-root');
    document.body.appendChild(root);

    // bootstrap the Angular app
    platformBrowserDynamic()
        .bootstrapModule(AppModule, {
            defaultEncapsulation: ViewEncapsulation.Native
        })
        .then(() => console.debug('Exponea Free Editor has bootstrapped'));

}

I'm using Typescript and that requires definition files. The problem is that Zone.js is an ambient dependency so I get an error about missing definition files when I use it like this. How do I solve this?

martindzejky
  • 389
  • 1
  • 3
  • 15

5 Answers5

13

If you don't want to load zone.js into your bundle, simply remove the import from polyfills.ts:

import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
import 'zone.js/dist/zone';  <---- remove this line

If you want to check the existence of Zone.js in runtime and decide based on the result use this:

declare var System;
if (!window['Zone']) {
    System.import('zone.js/dist/zone');  // Included with Angular CLI.
}
Max Koretskyi
  • 101,079
  • 60
  • 333
  • 488
  • The point is I want to decided whether to load it based on whether it is already loaded on the website. See the updated question. – martindzejky Aug 19 '17 at 14:02
  • @chuckeles, udpated my answer – Max Koretskyi Aug 19 '17 at 14:23
  • I forgot to mention that I use Typescript and that's the problem. As I mention in the updated question (BTW it's not `System.import`, just `import` now), the problem is that Typescript is missing definition files for that import. – martindzejky Aug 19 '17 at 19:10
  • @chuckeles, the answer I provided works for TypeScript, I checked. That's why I added `declare var System;` there. _ just `import` now_ - yeah, but no TS release yet support `import` call – Max Koretskyi Aug 19 '17 at 19:13
  • Oh I see, so you use `System.import` on purpose. It compiles and works, thanks. – martindzejky Aug 20 '17 at 09:12
  • So I should use written lines of code in polyfills.ts only? – YASH DAVE May 22 '18 at 14:30
  • @YASHDAVE, I don't understand what you mean. Can't you rephrase your question? – Max Koretskyi May 22 '18 at 15:16
  • 2
    In your answer you havent mentioned where to write ```declare var System; if (!window['Zone']) { System.import('zone.js/dist/zone'); // Included with Angular CLI. }``` So I got confused, but I updated it in polyfills.ts and it ssolved my issue. Thanks. – YASH DAVE May 22 '18 at 15:18
  • In my case this produces 0.*.chunk.js, which is loaded from other bundles. And because I host my app from different domain 0 is not found. I don't know how to swap its path. – Mike Jul 13 '18 at 12:24
6

I had to put together the accepted answer and some of its comments to get this to work in tsc 2.8.1, so here's everything in one:

First, make sure that tsconfig.app.json has module version that supports dynamic loading:

"module": "esnext" 

Second, prevent the automatic import of zone.js in the polyfills.ts by commenting out this line:

// import 'zone.js/dist/zone';  // Included with Angular CLI.

Third, add the following in the main.ts:

// if the zone has already been loaded, go ahead an bootstrap the app
if (window['Zone']) {
    bootstrap();

// otherwise, wait to bootstrap the app until zone.js is imported
} else {
    import('zone.js/dist/zone')
        .then(() => bootstrap());
}

function bootstrap() {
    platformBrowserDynamic().bootstrapModule(AppModule)
        .catch(err => console.log(err));
}
Dr. Hilarius
  • 1,134
  • 1
  • 14
  • 20
  • 1
    Yes, basically you are using dynamic imports instead of `System.import`, otherwise it is the same solution. Nice! – martindzejky Jun 01 '18 at 06:34
3

Another approach would be to replace the below statement in polyfills.ts:

import 'zone.js/dist/zone';  // Included with Angular CLI.

With:

declare var require: any
if (!window['Zone']) {
  require('zone.js/dist/zone'); // Included with Angular CLI.
}

Note: You can add the declaration of require on top of all import statements. This way it will be available through out the file.

Hamzeen Hameem
  • 2,360
  • 1
  • 27
  • 28
0

I faced the same situation and changed my build system from commonjs / webpack to systemjs build and run system

in systemjs you can load individual files and configure where it will look to get that file.

In the environment where multiple technologies, multiple team working to push code to same page in a single page architecture, systemjs is given me good experience.

Aniruddha Das
  • 20,520
  • 23
  • 96
  • 132
0

Here's my solution and the story for this problem after a couple of months. Basically, for a long time I used the accepted answer's solution in my project and it worked quite well. It could detect whether Zone is needed and load it only if it is not loaded yet.

However, problems arose when the underlying application would load slowly and it my application would bootstrap before the underlying one. In this case, my app would thing that Zone is not loaded and loaded it, only to clash with the underlying app as it kept loading.

There were other problems too, not related to this question in particular. For example, I found out that Angular clashes with Prototype and a lot of websites use Prototype!

All in all, I had to completely switch my approach. I moved my application to an iframe that has access to the parent website but does not inherit its CSS nor JS scope. This way I can safely load my application, Angular, and all its dependencies withou worrying that something might break the underlying website / app.

martindzejky
  • 389
  • 1
  • 3
  • 15