Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(legacy-javascript): fix core-js 3 detection #10852

Merged
merged 4 commits into from May 27, 2020
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
182 changes: 99 additions & 83 deletions lighthouse-core/audits/legacy-javascript.js
Expand Up @@ -149,9 +149,14 @@ class LegacyJavascript extends Audit {
// TODO: perhaps this is the wrong place to check for a CDN polyfill. Remove?
// expression += `|;e\\([^,]+,${qt(objectWithoutPrototype)},{${property}:`;

// Minified pattern.
// core-js@2 minified pattern.
// $export($export.S,"Date",{now:function
expression += `|\\$export\\([^,]+,${qt(objectWithoutPrototype)},{${property}:`;

// core-js@3 minified pattern.
// {target:"Array",proto:true},{fill:fill
// {target:"Array",proto:true,forced:!HAS_SPECIES_SUPPORT||!USES_TO_LENGTH},{filter:
expression += `|{target:${qt(objectWithoutPrototype)}\\S*},{${property}:`;
} else {
// WeakSet, etc.
expression += `|function ${property}\\(`;
Expand All @@ -161,88 +166,98 @@ class LegacyJavascript extends Audit {
}

static getPolyfillData() {
/** @param {string} coreJs2Module */
const defineModules = (coreJs2Module) => {
return {
coreJs2Module,
coreJs3Module: coreJs2Module
.replace('es6.', 'es.')
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a somewhat nice transformation from core js 2 -> core js 3 module names. I'm confident that all of these are accurate–otherwise the all-legacy-polyfills variants would error during bundling.

.replace('es7.', 'es.')
.replace('typed.', 'typed-array.'),
};
};
return [
/* eslint-disable max-len */
{module: 'es6.array.fill', name: 'Array.prototype.fill'},
{module: 'es6.array.filter', name: 'Array.prototype.filter'},
{module: 'es6.array.find', name: 'Array.prototype.find'},
{module: 'es6.array.find-index', name: 'Array.prototype.findIndex'},
{module: 'es6.array.for-each', name: 'Array.prototype.forEach'},
{module: 'es6.array.from', name: 'Array.from'},
{module: 'es6.array.is-array', name: 'Array.isArray'},
{module: 'es6.array.last-index-of', name: 'Array.prototype.lastIndexOf'},
{module: 'es6.array.map', name: 'Array.prototype.map'},
{module: 'es6.array.of', name: 'Array.of'},
{module: 'es6.array.reduce', name: 'Array.prototype.reduce'},
{module: 'es6.array.reduce-right', name: 'Array.prototype.reduceRight'},
{module: 'es6.array.some', name: 'Array.prototype.some'},
{module: 'es6.date.now', name: 'Date.now'},
{module: 'es6.date.to-iso-string', name: 'Date.prototype.toISOString'},
{module: 'es6.date.to-json', name: 'Date.prototype.toJSON'},
{module: 'es6.date.to-string', name: 'Date.prototype.toString'},
{module: 'es6.function.name', name: 'Function.prototype.name'},
{module: 'es6.map', name: 'Map'},
{module: 'es6.number.is-integer', name: 'Number.isInteger'},
{module: 'es6.number.is-safe-integer', name: 'Number.isSafeInteger'},
{module: 'es6.number.parse-float', name: 'Number.parseFloat'},
{module: 'es6.number.parse-int', name: 'Number.parseInt'},
{module: 'es6.object.assign', name: 'Object.assign'},
{module: 'es6.object.create', name: 'Object.create'},
{module: 'es6.object.define-properties', name: 'Object.defineProperties'},
{module: 'es6.object.define-property', name: 'Object.defineProperty'},
{module: 'es6.object.freeze', name: 'Object.freeze'},
{module: 'es6.object.get-own-property-descriptor', name: 'Object.getOwnPropertyDescriptor'},
{module: 'es6.object.get-own-property-names', name: 'Object.getOwnPropertyNames'},
{module: 'es6.object.get-prototype-of', name: 'Object.getPrototypeOf'},
{module: 'es6.object.is-extensible', name: 'Object.isExtensible'},
{module: 'es6.object.is-frozen', name: 'Object.isFrozen'},
{module: 'es6.object.is-sealed', name: 'Object.isSealed'},
{module: 'es6.object.keys', name: 'Object.keys'},
{module: 'es6.object.prevent-extensions', name: 'Object.preventExtensions'},
{module: 'es6.object.seal', name: 'Object.seal'},
{module: 'es6.object.set-prototype-of', name: 'Object.setPrototypeOf'},
{module: 'es6.promise', name: 'Promise'},
{module: 'es6.reflect.apply', name: 'Reflect.apply'},
{module: 'es6.reflect.construct', name: 'Reflect.construct'},
{module: 'es6.reflect.define-property', name: 'Reflect.defineProperty'},
{module: 'es6.reflect.delete-property', name: 'Reflect.deleteProperty'},
{module: 'es6.reflect.get', name: 'Reflect.get'},
{module: 'es6.reflect.get-own-property-descriptor', name: 'Reflect.getOwnPropertyDescriptor'},
{module: 'es6.reflect.get-prototype-of', name: 'Reflect.getPrototypeOf'},
{module: 'es6.reflect.has', name: 'Reflect.has'},
{module: 'es6.reflect.is-extensible', name: 'Reflect.isExtensible'},
{module: 'es6.reflect.own-keys', name: 'Reflect.ownKeys'},
{module: 'es6.reflect.prevent-extensions', name: 'Reflect.preventExtensions'},
{module: 'es6.reflect.set', name: 'Reflect.set'},
{module: 'es6.reflect.set-prototype-of', name: 'Reflect.setPrototypeOf'},
{module: 'es6.set', name: 'Set'},
{module: 'es6.string.code-point-at', name: 'String.prototype.codePointAt'},
{module: 'es6.string.ends-with', name: 'String.prototype.endsWith'},
{module: 'es6.string.from-code-point', name: 'String.fromCodePoint'},
{module: 'es6.string.includes', name: 'String.prototype.includes'},
{module: 'es6.string.raw', name: 'String.raw'},
{module: 'es6.string.repeat', name: 'String.prototype.repeat'},
{module: 'es6.string.starts-with', name: 'String.prototype.startsWith'},
{module: 'es6.string.trim', name: 'String.prototype.trim'},
{module: 'es6.typed.array-buffer', name: 'ArrayBuffer'},
{module: 'es6.typed.data-view', name: 'DataView'},
{module: 'es6.typed.float32-array', name: 'Float32Array'},
{module: 'es6.typed.float64-array', name: 'Float64Array'},
{module: 'es6.typed.int16-array', name: 'Int16Array'},
{module: 'es6.typed.int32-array', name: 'Int32Array'},
{module: 'es6.typed.int8-array', name: 'Int8Array'},
{module: 'es6.typed.uint16-array', name: 'Uint16Array'},
{module: 'es6.typed.uint32-array', name: 'Uint32Array'},
{module: 'es6.typed.uint8-array', name: 'Uint8Array'},
{module: 'es6.typed.uint8-clamped-array', name: 'Uint8ClampedArray'},
{module: 'es6.weak-map', name: 'WeakMap'},
{module: 'es6.weak-set', name: 'WeakSet'},
{module: 'es7.array.includes', name: 'Array.prototype.includes'},
{module: 'es7.object.entries', name: 'Object.entries'},
{module: 'es7.object.get-own-property-descriptors', name: 'Object.getOwnPropertyDescriptors'},
{module: 'es7.object.values', name: 'Object.values'},
{module: 'es7.string.pad-end', name: 'String.prototype.padEnd'},
{module: 'es7.string.pad-start', name: 'String.prototype.padStart'},
{...defineModules('es6.array.fill'), name: 'Array.prototype.fill'},
{...defineModules('es6.array.filter'), name: 'Array.prototype.filter'},
{...defineModules('es6.array.find'), name: 'Array.prototype.find'},
{...defineModules('es6.array.find-index'), name: 'Array.prototype.findIndex'},
{...defineModules('es6.array.for-each'), name: 'Array.prototype.forEach'},
{...defineModules('es6.array.from'), name: 'Array.from'},
{...defineModules('es6.array.is-array'), name: 'Array.isArray'},
{...defineModules('es6.array.last-index-of'), name: 'Array.prototype.lastIndexOf'},
{...defineModules('es6.array.map'), name: 'Array.prototype.map'},
{...defineModules('es6.array.of'), name: 'Array.of'},
{...defineModules('es6.array.reduce'), name: 'Array.prototype.reduce'},
{...defineModules('es6.array.reduce-right'), name: 'Array.prototype.reduceRight'},
{...defineModules('es6.array.some'), name: 'Array.prototype.some'},
{...defineModules('es6.date.now'), name: 'Date.now'},
{...defineModules('es6.date.to-iso-string'), name: 'Date.prototype.toISOString'},
{...defineModules('es6.date.to-json'), name: 'Date.prototype.toJSON'},
{...defineModules('es6.date.to-string'), name: 'Date.prototype.toString'},
{...defineModules('es6.function.name'), name: 'Function.prototype.name'},
{...defineModules('es6.map'), name: 'Map'},
{...defineModules('es6.number.is-integer'), name: 'Number.isInteger'},
{...defineModules('es6.number.is-safe-integer'), name: 'Number.isSafeInteger'},
{...defineModules('es6.number.parse-float'), name: 'Number.parseFloat'},
{...defineModules('es6.number.parse-int'), name: 'Number.parseInt'},
{...defineModules('es6.object.assign'), name: 'Object.assign'},
{...defineModules('es6.object.create'), name: 'Object.create'},
{...defineModules('es6.object.define-properties'), name: 'Object.defineProperties'},
{...defineModules('es6.object.define-property'), name: 'Object.defineProperty'},
{...defineModules('es6.object.freeze'), name: 'Object.freeze'},
{...defineModules('es6.object.get-own-property-descriptor'), name: 'Object.getOwnPropertyDescriptor'},
{...defineModules('es6.object.get-own-property-names'), name: 'Object.getOwnPropertyNames'},
{...defineModules('es6.object.get-prototype-of'), name: 'Object.getPrototypeOf'},
{...defineModules('es6.object.is-extensible'), name: 'Object.isExtensible'},
{...defineModules('es6.object.is-frozen'), name: 'Object.isFrozen'},
{...defineModules('es6.object.is-sealed'), name: 'Object.isSealed'},
{...defineModules('es6.object.keys'), name: 'Object.keys'},
{...defineModules('es6.object.prevent-extensions'), name: 'Object.preventExtensions'},
{...defineModules('es6.object.seal'), name: 'Object.seal'},
{...defineModules('es6.object.set-prototype-of'), name: 'Object.setPrototypeOf'},
{...defineModules('es6.promise'), name: 'Promise'},
{...defineModules('es6.reflect.apply'), name: 'Reflect.apply'},
{...defineModules('es6.reflect.construct'), name: 'Reflect.construct'},
{...defineModules('es6.reflect.define-property'), name: 'Reflect.defineProperty'},
{...defineModules('es6.reflect.delete-property'), name: 'Reflect.deleteProperty'},
{...defineModules('es6.reflect.get'), name: 'Reflect.get'},
{...defineModules('es6.reflect.get-own-property-descriptor'), name: 'Reflect.getOwnPropertyDescriptor'},
{...defineModules('es6.reflect.get-prototype-of'), name: 'Reflect.getPrototypeOf'},
{...defineModules('es6.reflect.has'), name: 'Reflect.has'},
{...defineModules('es6.reflect.is-extensible'), name: 'Reflect.isExtensible'},
{...defineModules('es6.reflect.own-keys'), name: 'Reflect.ownKeys'},
{...defineModules('es6.reflect.prevent-extensions'), name: 'Reflect.preventExtensions'},
{...defineModules('es6.reflect.set'), name: 'Reflect.set'},
{...defineModules('es6.reflect.set-prototype-of'), name: 'Reflect.setPrototypeOf'},
{...defineModules('es6.set'), name: 'Set'},
{...defineModules('es6.string.code-point-at'), name: 'String.prototype.codePointAt'},
{...defineModules('es6.string.ends-with'), name: 'String.prototype.endsWith'},
{...defineModules('es6.string.from-code-point'), name: 'String.fromCodePoint'},
{...defineModules('es6.string.includes'), name: 'String.prototype.includes'},
{...defineModules('es6.string.raw'), name: 'String.raw'},
{...defineModules('es6.string.repeat'), name: 'String.prototype.repeat'},
{...defineModules('es6.string.starts-with'), name: 'String.prototype.startsWith'},
{...defineModules('es6.string.trim'), name: 'String.prototype.trim'},
{coreJs2Module: 'es6.typed.array-buffer', coreJs3Module: 'es.array-buffer.constructor', name: 'ArrayBuffer'},
{coreJs2Module: 'es6.typed.data-view', coreJs3Module: 'es.data-view', name: 'DataView'},
{...defineModules('es6.typed.float32-array'), name: 'Float32Array'},
{...defineModules('es6.typed.float64-array'), name: 'Float64Array'},
{...defineModules('es6.typed.int16-array'), name: 'Int16Array'},
{...defineModules('es6.typed.int32-array'), name: 'Int32Array'},
{...defineModules('es6.typed.int8-array'), name: 'Int8Array'},
{...defineModules('es6.typed.uint16-array'), name: 'Uint16Array'},
{...defineModules('es6.typed.uint32-array'), name: 'Uint32Array'},
{...defineModules('es6.typed.uint8-array'), name: 'Uint8Array'},
{...defineModules('es6.typed.uint8-clamped-array'), name: 'Uint8ClampedArray'},
{...defineModules('es6.weak-map'), name: 'WeakMap'},
{...defineModules('es6.weak-set'), name: 'WeakSet'},
{...defineModules('es7.array.includes'), name: 'Array.prototype.includes'},
{...defineModules('es7.object.entries'), name: 'Object.entries'},
{...defineModules('es7.object.get-own-property-descriptors'), name: 'Object.getOwnPropertyDescriptors'},
{...defineModules('es7.object.values'), name: 'Object.values'},
{...defineModules('es7.string.pad-end'), name: 'String.prototype.padEnd'},
{...defineModules('es7.string.pad-start'), name: 'String.prototype.padStart'},
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

drive by: a simpler array and then a .map() into a array of {name: string, coreJs2Module: string, coreJs3Module: string} might read more straightforwardly here.

Something like

static getPolyfillData() {
  return [
    /* eslint-disable max-len */
    ['Array.prototype.fill', 'es6.array.fill'],
    ['Array.prototype.filter', 'es6.array.filter'],
    // ...
    ['String.prototype.trim', 'es6.string.trim'],
    // These break the coreJs2/coreJs3 naming pattern so are set explicitly.
    {name: 'ArrayBuffer', coreJs2Module: 'es6.typed.array-buffer', coreJs3Module: 'es.array-buffer.constructor'},
    {name: 'DataView', coreJs2Module: 'es6.typed.data-view', coreJs3Module: 'es.data-view'},
    ['Float32Array', 'es6.typed.float32-array'],
    // ...
    ['String.prototype.padStart', 'es7.string.pad-start'],
    /* eslint-enable max-len */
  ].map(info => {
    if (!Array.isArray(info)) return info;

    const [name, coreJs2Module] = info;
    return {
      name,
      coreJs2Module,
      coreJs3Module: coreJs2Module
        .replace('es6.', 'es.')
        .replace('es7.', 'es.')
        .replace('typed.', 'typed-array.'),
    };
  });
}

(could also do an in check or whatever instead of isArray if want to keep named properties)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice :)

/* eslint-enable max-len */
];
}
Expand Down Expand Up @@ -307,11 +322,12 @@ class LegacyJavascript extends Audit {
// If it's a bundle with source maps, add in the polyfill modules by name too.
const bundle = bundles.find(b => b.script.src === networkRecord.url);
if (bundle) {
for (const {module, name} of polyfillData) {
for (const {coreJs2Module, coreJs3Module, name} of polyfillData) {
// Skip if the pattern matching found a match for this polyfill.
if (matches.some(m => m.name === name)) continue;

const source = bundle.rawMap.sources.find(source => source.endsWith(`${module}.js`));
const source = bundle.rawMap.sources.find(source =>
source.endsWith(`${coreJs2Module}.js`) || source.endsWith(`${coreJs3Module}.js`));
if (!source) continue;

const mapping = bundle.map.mappings().find(m => m.sourceURL === source);
Expand Down
40 changes: 32 additions & 8 deletions lighthouse-core/scripts/legacy-javascript/run.js
Expand Up @@ -25,6 +25,7 @@ const hash = crypto
.update(fs.readFileSync(`${__dirname}/yarn.lock`, 'utf8'))
.update(fs.readFileSync(`${__dirname}/run.js`, 'utf8'))
.update(fs.readFileSync(`${__dirname}/main.js`, 'utf8'))
.update(fs.readFileSync(require.resolve('../../audits/legacy-javascript.js'), 'utf8'))
.digest('hex');
const VARIANT_DIR = `${__dirname}/variants/${hash}`;

Expand All @@ -34,7 +35,7 @@ const STAGE = process.env.STAGE || 'all';
const mainCode = fs.readFileSync(`${__dirname}/main.js`, 'utf-8');

const plugins = LegacyJavascript.getTransformPatterns().map(pattern => pattern.name);
const polyfills = LegacyJavascript.getPolyfillData().map(d => d.module);
const polyfills = LegacyJavascript.getPolyfillData();

/**
* @param {string} command
Expand Down Expand Up @@ -95,6 +96,7 @@ async function createVariant(options) {
`${dir}/main.transpiled.js`,
'-o', `${dir}/main.bundle.js`,
'--debug', // source maps
'--full-paths=false',
]);

// Minify.
Expand Down Expand Up @@ -217,6 +219,13 @@ function createSummarySizes() {
fs.writeFileSync(`${__dirname}/summary-sizes.txt`, lines.join('\n'));
}

/**
* @param {string} module
*/
function makeRequireCodeForPolyfill(module) {
return `require("../../../../node_modules/core-js/modules/${module}")`;
}

async function main() {
for (const plugin of plugins) {
await createVariant({
Expand Down Expand Up @@ -254,12 +263,23 @@ async function main() {
}

for (const polyfill of polyfills) {
const module = coreJsVersion === 2 ? polyfill.coreJs2Module : polyfill.coreJs3Module;
await createVariant({
group: `core-js-${coreJsVersion}-only-polyfill`,
name: polyfill,
code: `require("core-js/modules/${polyfill}")`,
name: module,
code: makeRequireCodeForPolyfill(module),
});
}

const allPolyfillCode = polyfills.map(polyfill => {
const module = coreJsVersion === 2 ? polyfill.coreJs2Module : polyfill.coreJs3Module;
return makeRequireCodeForPolyfill(module);
}).join('\n');
await createVariant({
group: 'all-legacy-polyfills',
name: `all-legacy-polyfills-core-js-${coreJsVersion}`,
code: allPolyfillCode,
});
}

removeCoreJs();
Expand All @@ -269,15 +289,19 @@ async function main() {
// Summary of using source maps and pattern matching.
summary = makeSummary('legacy-javascript.json');
fs.writeFileSync(`${__dirname}/summary-signals.json`, JSON.stringify(summary, null, 2));
console.log({
totalSignals: summary.totalSignals,
variantsMissingSignals: summary.variantsMissingSignals,
});
console.table(summary.variants);

// Summary of using only pattern matching.
summary = makeSummary('legacy-javascript-nomaps.json');
fs.writeFileSync(`${__dirname}/summary-signals-nomaps.json`, JSON.stringify(summary, null, 2));
console.log({
totalSignals: summary.totalSignals,
variantsMissingSignals: summary.variantsMissingSignals,
});
console.table(summary.variants.filter(variant => {
// Too many signals, break layout.
if (variant.name.includes('all-legacy-polyfills')) return false;
return true;
}));

createSummarySizes();
}
Expand Down