Skip to content

Instantly share code, notes, and snippets.

@connorjclark
Last active November 1, 2019 19:40
Show Gist options
  • Save connorjclark/cc583554ff07cba7cdc416c06721fd6a to your computer and use it in GitHub Desktop.
Save connorjclark/cc583554ff07cba7cdc416c06721fd6a to your computer and use it in GitHub Desktop.
detecting core-js polyfills

Context: https://github.com/GoogleChrome/lighthouse/pull/6730/files#diff-4f9810692cc44a0098f356a1dc35cca3R133

Oh boy :) cracks knuckles tl;dr no, also I overlooked so much, thanks for pointing this out.

The minified JS I linked to is every polyfill that @babel/present-env would provide. The recommended distribution of these polyfills is not the module I linked (@babel/polyfill), but instead via @babel/preset-env. Regardless, they both use the polyfills as defined by core-js. You can see that the polyfills module has a standard bundling and minifying script: https://github.com/babel/babel/blob/fced5cea430cc00e916876b663a8d2a84a5dad1f/packages/babel-polyfill/scripts/build-dist.sh, which is what I linked to before. The question is: does the pattern I'm looking for hold for these core-js polyfills regardless of the minifier used? Additionally: where does this pattern come from? I'll focus on that first.

To further complicate, there's core-js@2 and core-js@3, and both seem to be used (or at least, supported in @babel/preset-env).

Let's start with core-js@2.

core-js@2

There's a few different mechanisms for defining polyfills.

_export

Here's a typical polyfill: https://github.com/zloirock/core-js/blob/6a3fe85136aaae0e3b099c96a05a5ceb1f515a50/modules/es6.array.fill.js

// 22.1.3.6 Array.prototype.fill(value, start = 0, end = this.length)
var $export = require('./_export');

$export($export.P, 'Array', { fill: require('./_array-fill') });

require('./_add-to-unscopables')('fill');

Here's another: https://github.com/zloirock/core-js/blob/6a3fe85136aaae0e3b099c96a05a5ceb1f515a50/modules/es6.array.find-index.js

'use strict';
// 22.1.3.9 Array.prototype.findIndex(predicate, thisArg = undefined)
var $export = require('./_export');
var $find = require('./_array-methods')(6);
var KEY = 'findIndex';
var forced = true;
// Shouldn't skip holes
if (KEY in []) Array(1)[KEY](function () { forced = false; });
$export($export.P + $export.F * forced, 'Array', {
  findIndex: function findIndex(callbackfn /* , that = undefined */) {
    return $find(this, callbackfn, arguments.length > 1 ? arguments[1] : undefined);
  }
});
require('./_add-to-unscopables')(KEY);

$export is a helper function to define an extension to the provided class. The first parameter is a bitmap.

// type bitmap
$export.F = 1;   // forced
$export.G = 2;   // global
$export.S = 4;   // static
$export.P = 8;   // proto
$export.B = 16;  // bind
$export.W = 32;  // wrap
$export.U = 64;  // safe
$export.R = 128; // real proto method for `library`

So any number of these flags, but at least one, are passed in as the first parameter. I recall seeing this before, which explains why the regex has [^,]+.

The second parameter the name of the class. The third is an object whose keys are properties/methods that will be added.

_redefine

There's also a redefine helper function, that looks pretty simple: https://github.com/zloirock/core-js/blob/6a3fe85136aaae0e3b099c96a05a5ceb1f515a50/modules/es6.regexp.to-string.js#L10

require('./_redefine')(RegExp.prototype, TO_STRING, fn, true);

That constant TO_STRING is interesting - I'm seeing that some polyfills use constants for the method name, others don't. Should be the same code post-minification (it'd get inlined), but without minifying the difference is maybe something to consider.

_collection

Polyfills that create a new collection class uses _collection.

https://github.com/zloirock/core-js/blob/6a3fe85136aaae0e3b099c96a05a5ceb1f515a50/modules/es6.set.js

'use strict';
var strong = require('./_collection-strong');
var validate = require('./_validate-collection');
var SET = 'Set';

// 23.2 Set Objects
module.exports = require('./_collection')(SET, function (get) {
  return function Set() { return get(this, arguments.length > 0 ? arguments[0] : undefined); };
}, {
  // 23.2.3.1 Set.prototype.add(value)
  add: function add(value) {
    return strong.def(validate(this, SET), value = value === 0 ? 0 : value, value);
  }
}, strong);

_typed-array

https://github.com/zloirock/core-js/blob/6a3fe85136aaae0e3b099c96a05a5ceb1f515a50/modules/es6.typed.float32-array.js

require('./_typed-array')('Float32', 4, function (init) {
  return function Float32Array(data, byteOffset, length) {
    return init(this, data, byteOffset, length);
  };
});

For these, we could combine each typed-array variant into one check by looking for code you'd find in require('./_typed-array').

core-js@3

Different patterns here.

export

https://github.com/zloirock/core-js/blob/5adfcd91edfce629ced1eab76a303f65a97e50cb/packages/core-js/modules/es.array.fill.js

var $ = require('../internals/export');
var fill = require('../internals/array-fill');
var addToUnscopables = require('../internals/add-to-unscopables');

// `Array.prototype.fill` method
// https://tc39.github.io/ecma262/#sec-array.prototype.fill
$({ target: 'Array', proto: true }, {
  fill: fill
});

// https://tc39.github.io/ecma262/#sec-array.prototype-@@unscopables
addToUnscopables('fill');

collection, redefine, typed-array.

No change really.

quicky ditty on minifying

This command: npx browserify node_modules/core-js/modules/es6.array.find.js | npx uglify-js --mangle keep_fnames

will transform this:

$export($export.P + $export.F * forced, 'Array', {
  find: function find(

to this:

e(e.P+e.F*a,"Array",{find:function find(

Drop the mangling, and you get:

$export($export.P+$export.F*forced,"Array",{find:function find(

I'm really curious how uglifyjs picks the short name it can use, and why it seems to commonly be e. The answer lies somewhere in here: https://github.com/mishoo/UglifyJS2/blob/e8a2c0b5bf18659a3e1285b6038ea755a290220d/lib/scope.js

so...

With this extra domain knowledge, I'd like to create the regex that'd match each of these few different patterns. And the pattern should survive typical minification. I'd want to verify these patterns too - could bundle each polyfill individually, minify, and apply the pattern.

@developit
Copy link

Just a tip: the overwhelming majority of projects do not have property mangling enabled, which means any "tell-tale" properties in core-js or other polyfills can be used as relatively reliable indicators of their presence.

For example, core-js can be detected by looking for a few properties used in the _export helper mentioned above:

const hasCoreJs source => /\.noTargetGet/.test(source) && /\.sham/.test(source);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment