Skip to content

Latest commit

 

History

History
343 lines (251 loc) · 12.9 KB

polyfill-provider.md

File metadata and controls

343 lines (251 loc) · 12.9 KB

"Polyfill providers" are the packages which contain the actual polyfills data. Their job is to provide the correct import paths for every functionality that their underling polyfill can handle.

You can find some examples in the packages folder of this repository

A "polyfill provider" is defined passing a factory function to @babel/helper-define-polyfill-provider. The factory function takes two parameters (api and options) and returns an object with the provider implementation.

function polyfillProvider(api: ProviderApi, options: Options): Provider;

The Provider result object.

The provider object exposes different properties and methods:

provider.name: string

It's the name of the polyfill providers, similarly Babel plugins' name property. It's mostly used when the debug option is enabled, and in error messages.

function myProvider(api) {
  return {
    name: "my-polyfill",
    // ...
  };
}

provider.polyfills: string[] | { [name: string]: Support }

Is the list of polyfills supported by the polyfill provider. The names don't have a predefined format, but it's important that you are consistent. For example, core-js always uses es.OBJECT.METHOD, like in es.array.includes. These names will be used for two main purpuses to automatically validate and apply the include and exclude options specified by the user. If you use the recommended object version of this option, you can specify which browsers natively support the feature provided by the polyfill: this will make your provider support the target option of @babel/helper-define-polyfill-provider.

function myProvider(api) {
  return {
    // ...
    polyfills: {
      "es.array.from": { chrome: "51", firefox: "53", node: "6.5" },
      "es.array.includes": { chrome: "53", firefox: "48", node: "7.0" },
    },
  };
}

provider.filterPolyfills: (name: string) => boolean

Sometimes you might want to conditionally include a polyfill based on some additional options that your provider takes. For example, you might want to only include some non compliant shams if the sham: true option is enabled.

This is similar to directly filtering the polyfills option, but it is applied after validating the include and exclude options. Since only polyfills defined with polyfills can be used in those option, filterPolyfills prevents errors in case someone is excluding a polyfill which would be excluded by this method anyway.

function myProvider(api, options) {
  return {
    // ...
    polyfills: ["Object.assign", "Object.create"],
    filterPolyfills(name) {
      if (name === "Object.create" && !options.sham) return false;
      return true;
    },
  };
}

provider.visitor

If a provider needs to handle special cases which aren't supported by @babel/helper-define-polyfill-provider, it can provide a normal visitor like any Babel plugin.

provider.entryGlobal, provider.usageGlobal and provider.usagePure

These three functions are the core of any polyfill provider. They correspond, respectively, to the entry-global, usage-global and usage-pure values of the method option of @babel/helper-define-polyfill-provider. They are all optional, but you must specify at least one of them.

They all take three parameters. entryGlobal and usagePure return nothing, while usageGlobal can return a boolean to indicate if the polyfill was injected or not. This boolean is used, in case of object properties, to avoid injecting a polyfill for the object in case the static property has been already polyfilled.

function entryGlobal(meta, utils, path): void;
function usageGlobal(meta, utils, path): undefined | boolean;
function usagePure(meta, utils, path): void;

meta

A meta object describes the statement or expression which triggered the call to the polyfill provider. It always has a kind property which can be used to differentiate between the possible polyfill types.

  •  import "my-polyfill"`;
    type Meta = { kind: "import", source: string };

    This can only happen when using method: "entry-global". It is needed to replace the generic entry point of the plugin with the optimized entry points based on the targets.

  • Promise;
    type Meta = { kind: "global", name: string };

    Where, in this example, name is "Promise"

  • obj.prop;
    type Meta = {
      kind: "property",
      placement: "static" | "prototype" | null,
      object: string | null,
      key: string,
    };

    @babel/plugin-inject-polyfill tries to detect the origin of obj, to infer the property placement and the real name of obj.

    • If obj is a global binding not declared in the current file, obj.prop is assumed to be a static property of a built-in object:

      Array.from;
      type Meta = {
        kind: "property",
        placement: "static",
        object: "Array",
        key: "from",
      };

      Note that @babel/helper-define-polyfill-provider will also track assignments as much as possible. For example, this code is represented by the same Meta:

      var MyArray = Array;
      var propName = "from";
      
      MyArray[propName];
    • If obj can be inferred to be a native JS type (e.g. an array), or it comes from a Something.prototype access, it is considered a prototype property:

      [].includes;
      type Meta = {
        kind: "property",
        placement: "prototype",
        object: "Array",
        key: "includes",
      };
    • If it isn't possible to detect where obj comes from, for example when it is generated by a complex expression, both placement and obj are set to null:

      getThing().includes;
      type Meta = {
        kind: "property",
        placement: null,
        object: null,
        key: "includes",
      };

    The "property" kind is also used when destructuring, for example in { from } = Array.

    Symbol.* property names are considered as an unit. For example, in Array.prototype[Symbol.iterator], meta.key is "Symbol.iterator".

  • Symbol.iterator in [];

    Many dynamic checks for the presence of a polyfilled property can be statically replaced. For example, an "is iterable" check could be statically replaced with a functions which checks if the lhs is an array, arguments, or any other polyfilled built-in which should be handled as if it was iterable.

    type Meta = {
      kind: "in",
      placement: ?("static" | "prototype"),
      object: ?string,
      key: string,
    };

    It follows the same rules as the "property" kind.

utils

When calling a provider function (e.g. usageGlobal), @babel/helper-define-polyfill-provider will provide it a few utilities to easily inject the necessary import statements or require calls, depending on the source type. Polyfill providers shouldn't worry about which AST represents an import, or about the source type of the file being transpiled.

  • utils.injectGlobalImport(url: string, polyfillName?: string) can be used to inject side-effectful global imports. It is usually called when injecting global polyfills. For example, utils.injectGlobalImport("my-polyfill") would generate this code:

    import "my-polyfill";

    If polyfillName is specified, imports are injected respecting the order defined in provider.polyfills.

  • utils.injectNamedImport(url: string, name: string, hint?: string, polyfillName?: string) and utils.injectDefaultImport(url: string, hint?: string, polyfillName?: string) are used to inject named or defaults import. They both return an identifier referencing the imported value. The optional hint parameter can be used to generate a nice-looking alias for the import. For example, utils.injectNamedImport("array-polyfills", "from, "Array.from") would generate this code:

    import { from as _ArrayFrom } from "array-polyfills";

    and return this AST node:

    {
      "type": "Identifier",
      "name": "_ArrayFrom"
    }

    If polyfillName is specified, imports are injected respecting the order defined in provider.polyfills.

The ProviderApi parameter

While some utilities are provided in the utils object, some of them are provided in the api object. The main different is that utils methods act on a specific input source file, while api methods provide info about how the plugin was configured and are not directly related to the transformed source code.

api.method

It represents the method option passed to @babel/helper-define-polyfill-provider, and it can be one of "entry-global", "usage-global", or "usage-pure".

api.targets

It represents the resolved engines which the user is targeting. Regardless of how they were specified (i.e. via browserslist), they are always normalized to an object mapping from engine names to versions.

Example:

{
  "chrome": "74.0.0",
  "firefox": "67.0.0",
  "ios": "12.2.0"
}

api.shouldInjectPolyfill(name: string): boolean

This methods returns wether or not a polyfill is needed, using data from the targets, include and exclude options, and from the engines support specified in the provider.polyfills property returned by the polyfill provider.

api.debug(name: string | null)

This method is used to log information about the injected polyfills, which will then be reported to the user if the debug option is enabled.

It should be called passing the polyfill name as a string whenever a polyfill is injected. When, in "entry-global" mode, the polyfill entry point has been found but removed becase no polyfill was needed, this method should be called passing null as a parameter to indicate that nothing has been injected.

api.assertDependency(name: string, version?: string)

This method is used to verify that a polyfill package that is going to be used is installed. If it's not installed, it will log a message asking the user to install it.

api.getUtils(path: NodePath): Utils

Sometimes you might need to inject an import outside of the entryGlobal/usageGlobal/usagePure methods. You could manually create the AST representing it, after checking which source type the current file is using (if script or module), but it is way more complex than the out-of-the-box support provided by the utils object.

You can use this method to create a new utils object with all its utilities, attached to the file the current NodePath belongs to.

export default function ({ getUtils }) {
  return {
    // ...
    visitor: {
      YieldExpression(path) {
        if (!path.node.star) return;
        getUtils(path).injectGlobalImport("iterators-polyfill");
      },
    },
  };
}

api.createMetaResolver

function createMetaResolver<T>(definitions: Definitions<T>): (meta: Meta) => T;

From a syntactic point of view, window.Promise is a static property access: we are accessing the "Promise" property of the window object. For this reason, when polyfilling the ES2015 Promise constructor, we need to check both for static properties and for global variables. Following the same pattern, theFoo.includes has the same problem: it could be a static property access on the theFoo global object, or theFoo could be a global instance of an array and includes would then be a prototype property.

To avoid having to manually check for all these cases, the createMetaResolver factory creates a function which does it for you.

The definitions parameter is an object containing mappings of global values, and of static and instance properties. It has the following shape:

type Definitions<T> = {
  global: {
    [variableName: string]: T,
  },
  instance: {
    [propertyName: string]: T,
  },
  static: {
    [objectName: string]: {
      [propertyName: string]: T,
    },
  },
};

Instance properties aren't categorized by their object, because often it's not possible to statically determine it. For this reason, it is highly recommended that pure polyfills for instance properties, if supported, check their argument type at runtime. For example, a *.includes polyfill might look like this:

import * as array from "array-methods";
import * as string from "string-methods";

export default includes(thisValue, value) {
  if (typeof thisValue === "string") {
    return string.includes(thisValue, value);
  }
  if (Array.isArray(thisValue)) {
    return array.includes(thisValue, value);
  }

  // This is not an array or a string, so we call the original method
  return arr.includes(value);
}

Naming convention

Polyfill providers follow a naming convention similar to Babel plugins and presets:

  • @babel/plugin-polyfill-POLYFILL-NAME
  • @ORG/babel-plugin-polyfill-POLYFILL-NAME
  • @ORG/babel-plugin-polyfill