3

The documentation for @babel/polyfill has the following note:

If you are looking for something that won't modify globals to be used in a tool/library, checkout the transform-runtime plugin.

On the transform-runtime documentation, it says the following:

While this [@babel/polyfill usage] might be ok for an app or a command line tool, it becomes a problem if your code is a library which you intend to publish for others to use or if you can't exactly control the environment in which your code will run.

More generally speaking, a lot of articles explaining the use of polyfills say that you may want to use a different solution if you care about polluting the global namespace.

In my understanding, most polyfills are loaded conditionally. If an implementation already exists, the polyfill will not overwrite it. My question is: under what kind of circumstances can polyfills in an external script cause an existing website to break? The only reason that I have been able to find so far is that the external script might load a polyfill earlier than the code in the website itself. This may lead to problems, but when these polyfills are based on web standards, their behavior should be the same. What is the likelihood of there still being serious conflicts?

I found an interesting discussion about this on a github issue. This talks mostly about modules in the NPM ecosystem though, whereas I'm mostly interested in external scripts that facilitate things like widgets or embeds.

Any personal experience or links to discussions and articles on the subject is appreciated!

UPDATE: One of the main reasons for this question is that there were some issues with transform-runtime. With the new release of core-js and babel these issues seem to have been addressed. Regardless I am still interested in answers to the original question above.

NeoZoom.lua
  • 2,269
  • 4
  • 30
  • 64
flut1
  • 195
  • 1
  • 10

1 Answers1

3

Well, polyfills are rarely perfects, and as you say, they almost all work conditionally.

Let's say library-1 injects its own polyfill (polyfill-A) for a feature called Interface.
This polyfill-A could very well implement only a few methods of the full API of Interface, for instance, the official API could be something like

interface Interface {
  constructor(optional (Interface or DOMString) foo);
  undefined doSomething();
  undefined doSomethingElse();
};

But passing an Interface instance in the constructor may have been added only later on in the specs, or doSomethingElse may have been omitted by that polyfill, or simply not tested correctly and all these little omissions may have been fine for library-1 because they don't use any of these.
Now when library-2's own polyfill will check if there is already an Instance constructor available, it will see that yes, it's already defined, and will thus not re-implement it.
However, library-2 may need to pass an Interface in the constructor, or it may need to call its doSomethingElse() method. And when it will try to do so, the code will crash, because even though the author of library-2 did include a polyfill that does correctly implement both features, library-1's polyfill's implementation is the one running and accessible.

<script>
  // library-1.js
  (function polyfillInterface() {
    if (typeof Interface !== "function") {
      class Interface {
        constructor(foo) {
          this.foo = foo.toUpperCase();
        }
        doSomething() {
          return this.foo + "-bar";
        }
      }
      globalThis.Interface = Interface;
    }
  })();
  {
    // for library-1, everything works well
    const instance = new Interface("bla");
    console.log(instance.doSomething());
  }
</script>

<script>
  // library-2.js
  (function polyfillInterface() {
    if (typeof Interface !== "function") {
      class Interface {
        constructor(foo) {
          if (foo instanceof Interface) {
            this.foo = foo.foo;
          }
          else if (typeof foo === "string") {
            this.foo = foo.toUpperCase();            
          }
          else {
            throw new TypeError("neither an Interface nor a DOMSrting");
          }
        }
        doSomething() {
          return this.foo + "-bar";
        }
        doSomethingElse() {
          return this.foo.toLowerCase() + "-bar";
        }
      }
      globalThis.Interface = Interface;
    }
  })();
  {
    // for library-2, everything is broken
    const instance_1 = new Interface("bla");
    try {
      console.log(instance_1.doSomethingElse());
    }
    catch(err) {
      // instance_1.doSomethingElse is not a function
      console.error(err);
    }
    // TypeError: foo.toUpperCase is not a function
    const instance_2 = new Interface(instance_1);
  }
</script>

And it may be things very hard to determine, for instance Promise.then() should fire in the same event loop than they resolved (in the microtask-queue), rather than in the next one like a normal task would, and numerous Promise libraries may have been using setTimeout(fn, 0) to make the asynchronicity instead of e.g using a MutationObserver when available.

That's why when writing a library, it's good to link to polyfills, but not to include them yourself.

Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • Thanks for the response. Besides avoiding to include a global polyfill altogether, are there other things you can do to avoid or minimize the chances of conflict? One issue I ran into is that webpack import() [relies on Promise](https://webpack.js.org/api/module-methods#import-1), and that it's not possible to link your own Promise implementation to the webpack _runtime_ (at least not easily) – flut1 Mar 19 '19 at 13:28
  • Whilst looking into this I also found that Promise was one of the most common polyfills causing problems. Probably this is likely due to the relative complexity of Promises, and the order at which code executes. Do you have any experience with polyfill methods that are relatively more straightforward, like Object.assign or Array.includes? Would these be 'safe' to include in your opinion? – flut1 Mar 19 '19 at 13:31
  • @flut1 ah.. I'm really not into webpack sorry. But seeing that their docs asks you to add such a polyfill, I fail to see how it would be **impossible** to do so. Though it might warrant its own Q/A if not yet asked. For Object.assign and Array.includes, these are very easy to implement with an es5 base, however other methods like Array.from are almost impossible to polyfill, because they rely on new concepts. And I'm sure you can find buggy polyfills even for Array.includes on github ;) So that really depends on what is polyfilled and by whom (and sometimes when). – Kaiido Mar 19 '19 at 13:32
  • I'm still experimenting to make this work with webpack. For now it seems like it requires some tooling from outside webpack, like doing post-processing on whatever webpack outputs. I definitely agree it _should_ be possible though, so if I can't figure it out I'll make sure to post another question here on SO or on their Github. – flut1 Mar 19 '19 at 13:41
  • I upvoted but then I found I don't understand your example. A client of the _lib1_ won't know about the implementation of _lib1_, so how can it depend on the implementation details of it? As long as the input-output be the same, changing _lib1_ to some other library should not break the code. – NeoZoom.lua Aug 20 '21 at 14:45
  • 1
    @Rainning, I admit my wording is not that clear, I'll have to come back to that later (it's a bit late now in here). The point is that lib1 will load its own version of the polyfill, this first polyfill may implement the wanted interface only partially, for instance, it may implement the method `foo()` but not `bar()`, or it may not accept the input `baz` in these methods. This may not be a problem for lib1, because they don't need `bar()` or don't pass `baz`. However lib2 may need these, but its own polyfill will see that the interface is already defined, so it won't polyfill it again. – Kaiido Aug 20 '21 at 15:11
  • And now when lib2 will try to access the interface's `bar()` method it will crash, because the interface is the one polyfiled by the first polyfill, not the one from lib2. – Kaiido Aug 20 '21 at 15:11