webpack: JsonpMainTemplatePlugin.js uses Promises to resolve dependencies which is not working in Internet Explorer 9-11

Code splitting with require.ensure for webpack 2.2.0-rc.1 generates loader that uses Promise

https://github.com/webpack/webpack/blob/f9e57d9a0c4b06d0bc3db5c67721827aa9cf6fcd/lib/JsonpMainTemplatePlugin.js#L102

 "",
"var promise = new Promise(function(resolve, reject) {",
this.indent([

These prevents splitted modules to run normally in any internet explorer 9-11 cause IE doesn’t have Promise implementation and polyfill is loaded within one of splitted modules.

My generated entry point module looks like this. It loads 2 other modules (1-vendor with Promise implementation, 2 - app code)

var library = function (n) {
	function e(t) {
		if (r[t])return r[t].exports;
		var o = r[t] = {i: t, l: !1, exports: {}};
		return n[t].call(o.exports, o, o.exports, e), o.l = !0, o.exports
	}

	var t = window.webpackJsonp;
	window.webpackJsonp = function (e, r, u) {
		for (var c, i, a = 0, s = []; a < e.length; a++)i = e[a], o[i] && s.push(o[i][0]), o[i] = 0;
		for (c in r)Object.prototype.hasOwnProperty.call(r, c) && (n[c] = r[c]);
		for (t && t(e, r, u); s.length;)s.shift()()
	};
	var r = {}, o = {2: 0};
	return e.e = function (n) {
		function t() {
			u.onerror = u.onload = null, clearTimeout(c);
			var e = o[n];
			0 !== e && (e && e[1](new Error("Loading chunk " + n + " failed.")), o[n] = void 0)
		}

		if (0 === o[n])return Promise.resolve();
		if (o[n])return o[n][2];
		var r = document.getElementsByTagName("head")[0], u = document.createElement("script");
		u.type = "text/javascript", u.charset = "utf-8", u.async = !0, u.timeout = 12e4, u.crossOrigin = "anonymous", e.nc && u.setAttribute("nonce", e.nc), u.src = e.p + "main." + {
				0: "cdb3",
				1: "caf4"
			}[n] + ".js";
		var c = setTimeout(t, 12e4);
		u.onerror = u.onload = t, r.appendChild(u);
		var i = new Promise(function (e, t) {
			o[n] = [e, t]
		});
		return o[n][2] = i
	}, e.m = n, e.c = r, e.i = function (n) {
		return n
	}, e.d = function (n, t, r) {
		e.o(n, t) || Object.defineProperty(n, t, {configurable: !1, enumerable: !0, get: r})
	}, e.n = function (n) {
		var t = n && n.__esModule ? function () {
				return n.default
			} : function () {
				return n
			};
		return e.d(t, "a", t), t
	}, e.o = function (n, e) {
		return Object.prototype.hasOwnProperty.call(n, e)
	}, e.p = "", e.oe = function (n) {
		throw n
	}, e(e.s = 4)
}({
	4: function (n, e, t) {
		"use strict";
		function r(n, e, r) {
			void 0 === n && (n = document.body);
			var o = [], u = null, c = function () {
				for (var n = 0, e = o; n < e.length; n++) {
					var t = e[n];
					u.update(t)
				}
				o.length = 0
			};
			return t.e(0).then(function () {
				t.e(1).then(function (o) {
					u = t(0).run(n, e), c(), "function" == typeof r && r(u)
				}.bind(null, t)).catch(t.oe)
			}.bind(null, t)).catch(t.oe), {
				update: function (n) {
					u ? u.update(n) : o.push(n)
				}
			}
		}

		n.exports = r
	}
});

About this issue

  • Original URL
  • State: closed
  • Created 8 years ago
  • Reactions: 8
  • Comments: 44 (20 by maintainers)

Most upvoted comments

Here’s a “real world” use case where this seems to be an issue:

Suppose you’re writing a JavaScript plugin that gets included on thousands of websites or more. The plugin provides simple “out of the box” functionality, so that non-technical users can just add a script tag for the plugin to their pages (or even add the tag with the click of a button, via a WordPress plugin) and be done with it. Many of these users won’t know what a Promise polyfill is or how to add one to their sites. At the same time, many of these users might be using various other plugins, some of them quite old – especially if they are using WordPress, where plugins can be added very quickly and almost indiscriminately – or catering to users whom they expect not to be using the most cutting edge browsers.

In a scenario of that sort, including a global polyfill with the plugin tends to be bad for the users: it bloats the plugin and risks breaking pages in the wild, since the polyfill will sometimes modify objects in the global scope, and there may be other plugins on those pages that will expect those global objects to behave slightly differently (especially if they are older plugins), or that will overwrite the polyfill later, and/or there may be users visiting these sites who have browsers that will throw errors under certain conditions when the polyfill is present (and so on, and so on). But because of the number of websites using the plugin, and because of the non-technical nature of many of the users, it also won’t be reasonable for developers of the plugin to expect that these users themselves will add a Promise polyfill to their pages (even if asked politely!). So, if the developers of this plugin rely on code-splitting at all, and want to provide the best experience for their users, they seemingly can’t use webpack 2+, given its reliance on Promise in the bootstrap code related to code-splitting. They’re stuck using webpack 1 or forking.

That may be the intention, but I figured I’d put that scenario out there as one where webpack 2+'s behavior can be problematic. (Even so, thanks to the maintainers/contributors for all of the hard work!)

I think part of the issue is that the tools that webpack provides for handling polyfills in the bundle, such as ProvidePlugin, don’t work on the bootstrap code. This might not normally be an issue, but if you’re using a polyfill that doesn’t pollute the global namespace (like babel-runtime), then you either need to load two polyfills (one global and one not) or manually handle mapping the global yourself (eg, window.Promise = require('my-polyfill-of-choice')).

If ProvidePlugin also worked for the bootstrap code that would probably go a long way toward alleviating this issue.

I am writing a library that makes use of code-splitting. I therefore don’t want to pollute the global namespace and to make it as easy as possible for my users to include the library I also don’t want to force my users to include a specific Polyfill.

What would be arguments against letting babel-plugin-transform-runtime transform bootstrap code as well (or alternatively allowing ProvidePlugin to manipulate bootstrap code)?

This is still a big problem

This was an intentional change done since the very early betas (https://gist.github.com/sokra/27b24881210b56bbaff7#promise-polyfill).

I have written a simple plugin to solve this problem :

https://github.com/mc-zone/webpack2-polyfill-plugin

Welcome to try! 😃

The problem for me is that “babel-plugin-transform-runtime” can’t transform it. I found some site use the following code. <script>window.Promise||document.write('<script src="/js/es6-promise.js"><\/script>')</script> This may be a suitable solution.

@alexander-akait Regardless of IE9/IE10/IE11, there are still use cases when bundling embeddable software and when you don’t want to reuse host website’s Promise which might be polyfilled with some mumbo-jumbo broken Promise implementation.

Just a warning for folks that need to support IE 8-11, using webpack is not a good idea. Webpack needs Promise and Function.bind to be available for webpackJsonp loading, and will break when loading your async loaded code if it is not present.

In this way users could Babel transform that module

If your code is a third party library you really shouldn’t pollute the global namespace.

Kirk Elliott 202.733.0317

On Oct 15, 2019, at 8:13 AM, Evilebot Tnawi notifications@github.com wrote:

@graingert why don’t use Promise polyfill as part of entry?

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or unsubscribe.

Because I do not want to mutate the environment. I already use a promise Ponyfill via Babel transform-runtime and core-js

This is still a huge problem!

On Tue, 15 Oct 2019, 08:43 webpack bot, notifications@github.com wrote:

This issue had no activity for at least three months.

It’s subject to automatic issue closing if there is no activity in the next 15 days.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/webpack/webpack/issues/3531?email_source=notifications&email_token=AADFATEX7IGWYG33NK42R4DQOVYALA5CNFSM4C2IFKV2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEBHYPVQ#issuecomment-542083030, or unsubscribe https://github.com/notifications/unsubscribe-auth/AADFATBI5TWPAWAWIKYU73TQOVYALANCNFSM4C2IFKVQ .

Any developments on this issue? I’m in exactly the same situation that @jmrog describes (thanks for the eloquent example, by the way!). My code is running on thousands of third party sites, and I absolutely can’t polyfill in any globals. It would be great if ProvidePlugin worked for this use-case.

I was able to solve this with an approach largely inspired by webpack’s BannerPlugin:

Purpose: Allow application developers to provide their own Promise ponyfill to the webpack runtime without modifying the global Polyfill. This is especially important for 3rd party scripts that need to function on another party’s site without affecting the native environment.

How it works: Adds a local Promise variable to the end of the outermost IIFE in the webpack output for each file matching the given test/include/exclude option(s). These are exactly the same as BannerPlugin’s:

{
  entryOnly: boolean, // if true, the Promise var will only be added to the entry chunks
  test: string | RegExp | [string, RegExp], // Include all modules that pass test assertion.
  include: string | RegExp | [string, RegExp], // Include all modules matching any of these conditions.
  exclude: string | RegExp | [string, RegExp], // Exclude all modules matching any of these conditions.
}

In your webpack config:

module.exports = (env, argv) => ({
  // ...
  plugins: [
    new RuntimePromisePlugin({
      entryOnly: true,
      test: /.*(?<!osano-ui)\.legacy\./
    })
  ]
})

At the top of any entry/chunk that will be affected:

import PromisePolyfill from 'your-promise-polyfill'
Promise = window.Promise || PromisePolyfill

The solution:

const { Compilation, ModuleFilenameHelpers } = require('webpack');

module.exports = class RuntimePromisePlugin {
    constructor(options) {
        this.options = options;
        this.insertion = ';var Promise';
    }

    apply(compiler) {
        const PLUGIN_NAME = RuntimePromisePlugin.name;
        const options = this.options;
        const insertion = this.insertion;
        const matchObject = ModuleFilenameHelpers.matchObject.bind(undefined, options);
        const { webpack } = compiler;

        compiler.hooks.compilation.tap(PLUGIN_NAME, compilation => {
            const logger = compilation.getLogger(PLUGIN_NAME);
            compilation.hooks.processAssets.tap(
                {
                    name: PLUGIN_NAME,
                    stage: Compilation.PROCESS_ASSETS_STAGE_SUMMARIZE,
                },
                () => {
                    for (const chunk of compilation.chunks) {
                        if (options.entryOnly && !chunk.canBeInitial()) {
                            continue;
                        }

                        for (const file of chunk.files) {
                            if (!matchObject(file)) {
                                continue;
                            }

                            logger.warn(`Added "${insertion}" to end of outer IIFE for: ${file}`);

                            compilation.updateAsset(file, old => {
                                const raw = old.source();
                                const i = raw.lastIndexOf('}()');
                                const start = raw.slice(0, i);
                                const end = raw.slice(i);
                                return new webpack.sources.RawSource(`${start}${insertion}${end}`);
                            });
                        }
                    }
                }
            );
        });
    }
};

Caveats: I tried to find a better way than converting to RawSource. I imagine it may slow the build down a little. This also relies heavily on the last characters (}()) remaining correct. I by trying to add a custom runtime module which would contain something like __webpack_require__.Promise = __webpack_require("your-ponyfill") and then hook into the rest of the RuntimePlugin templates to replace Promise with ${RuntimeGlobals.require}.Promise. Unfortunately, I didn’t come up with anything that wouldn’t require extending each template individually.

That said, it works for every scenario I’ve used it in thus far.

I’ve managed to overcome this issue by providing minimal promise like function to bootstrap. This “Promise” (actually it’s not) will only be used in bootstrap and actual modules will use polyfill provided by ProvidePlugin, this “Promise” won’t pollute global scope.

Sample plugin

/* fake "promise" polyfill. It can be only resolved or rejected
no states, context or anything promise compliant */ 
function PromiseLike(fn) {
	var callbacks = [],
		promise = {
			then: then,
			'catch': then.bind(this, null),
		}

	function complete(type) {
		return function (result) {
			callbacks.map(function (cb) {
				cb[type] && cb[type](result)
			})
                       callbacks = [] 
		}
	}

	function then(resolve, reject) {
		callbacks.push([resolve, reject])
		return promise
	}

	fn(complete(0), complete(1))
	return promise
}

class JsonpMainTemplatePromisePlugin {
	apply(compiler) {
		compiler.plugin("this-compilation", (compilation) => {
			compilation.mainTemplate.apply(this)
		})

		compiler.plugin("local-vars", function (source, chunk) {
			if (chunk.chunks.length > 0) {
				return this.asString([
					source,
                                        /* Add 'Promise' variable to loader scope 
                                            so it will use our implementation instead of global */
					`var Promise = ${PromiseLike.toString()}`,
				]);
			}
			return source
		})
	}
}

module.exports = JsonpMainTemplatePromisePlugin

For me it’s not an issue anymore so feel free to close it. Also I think that ProvidePluginshould also work with bootstrap code, though it’s really complicated

This was an intentional change done since the very early betas (https://gist.github.com/sokra/27b24881210b56bbaff7#promise-polyfill).

Apparently it’s missing from the https://webpack.js.org migration guide, but a Promise polyfill is mandatory when using code splitting. babel-polyfill and core-js include one. You can avoid including the rest of core-js by only requiring core-js/fn/promise, or using another polyfill such as es6-promise/auto.

Polyfills also should always be loaded synchronously, and run before any other code. You can ensure webpack generates an entry point that synchronously loads the polyfill by giving an array for the entry point; e.g.

entry: {
  library: [ 'core-js/fn/promise', path.resolve(__dirname, 'src/library') ]
}