assetgraph
Version:
An auto discovery dependency graph based optimization framework for web pages and applications
402 lines (390 loc) • 11.7 kB
JavaScript
const _ = require('lodash');
const symbolNames = {
' ': 'SPACE',
'!': 'BANG',
'.': 'DOT',
'=': 'EQUALS',
'~': 'TILDE',
$: 'DOLLAR',
'#': 'HASH',
'%': 'PERCENT',
'/': 'SLASH',
'<': 'LESSTHAN',
'>': 'GREATERTHAN',
'&': 'AMPERSAND',
'?': 'QUESTIONMARK',
"'": 'SINGLEQUOTE',
'"': 'DOUBLEQUOTE',
'+': 'PLUS',
',': 'COMMA',
';': 'SEMICOLON',
':': 'COLON',
'(': 'LPAREN',
')': 'RPAREN',
'{': 'LBRACE',
'}': 'RBRACE',
'[': 'LSQBRACE',
']': 'RSQBRACE'
};
const escodegen = require('escodegen');
const replaceDescendantNode = require('../replaceDescendantNode');
const estraverse = require('estraverse-fb');
module.exports = (
queryObj,
{ globalNames = [], wrapInFunction = false, stringLiterals = false } = {}
) => {
const allGlobalNames = [
...globalNames,
'eval',
'clearInterval',
'clearTimeout',
'document',
'event',
'frames',
'history',
'Image',
'localStorage',
'location',
'name',
'navigator',
'Option',
'parent',
'screen',
'sessionStorage',
'setInterval',
'setTimeout',
'Storage',
'window',
'XMLHttpRequest',
'Math',
'Math.min',
'Math.max',
'Math.round',
'Function',
'Date',
'Date.prototype',
'Math.E',
'Math.LN2',
'Math.LN10',
'Math.LOG2E',
'Math.LOG10E',
'Math.PI',
'Math.SQRT1_2',
'Math.SQRT2',
'Math.abs',
'Math.acos',
'Math.asin',
'Math.atan',
'Math.atan2',
'Math.ceil',
'Math.cos',
'Math.exp',
'Math.floor',
'Math.log',
'Math.max',
'Math.min',
'Math.pow',
'Math.random',
'Math.round',
'Math.sin',
'Math.sqrt',
'Math.tan',
'parseInt',
'parseFloat',
'isNaN',
'NaN',
'RegExp',
'RegExp.prototype',
'RegExp.prototype.compile',
'RegExp.prototype.test',
'RegExp.prototype.exec',
'String',
'String.fromCharCode',
'String.prototype',
'String.prototype.charAt',
'String.prototype.charCodeAt',
'String.prototype.indexOf',
'String.prototype.match',
'String.prototype.replace',
'String.prototype.slice',
'String.prototype.split',
'String.prototype.substr',
'String.prototype.substring',
'String.prototype.toLowerCase',
'String.prototype.toUpperCase',
'Array',
'Array.prototype',
'Array.prototype.concat',
'Array.prototype.indexOf',
'Array.prototype.join',
'Array.prototype.pop',
'Array.prototype.push',
'Array.prototype.reverse',
'Array.prototype.shift',
'Array.prototype.slice',
'Array.prototype.sort',
'Array.prototype.splice',
'Array.prototype.unshift',
'Number',
'Number.prototype',
'Number.prototype.toFixed',
'Number.MAX_VALUE',
'Number.MIN_VALUE',
'Number.NEGATIVE_INFINITY',
'Number.NaN',
'Number.POSITIVE_INFINITY',
'Number.prototype',
'Boolean',
'Boolean.prototype',
'Error',
'Error.prototype',
'EvalError',
'EvalError.prototype',
'Infinity',
'JSON',
'JSON.stringify',
'JSON.parse',
'Object',
'Object.prototype',
'Object.prototype.toString',
'RangeError',
'RangeError.prototype',
'ReferenceError',
'ReferenceError.prototype',
'SyntaxError',
'SyntaxError.prototype',
'TypeError',
'TypeError.prototype',
'URIError',
'URIError.prototype',
'decodeURI',
'decodeURIComponent',
'encodeURI',
'encodeURIComponent',
'isFinite',
'undefined'
];
const globalsObj = {};
for (const globalName of allGlobalNames) {
globalsObj[globalName] = true;
}
return function pullGlobalsIntoVariables(assetGraph) {
for (const javaScriptAsset of assetGraph.findAssets(
Object.assign({ type: 'JavaScript' }, queryObj)
)) {
const occurrencesByGlobalName = {};
const occurrencesByString = {};
const ast = javaScriptAsset.parseTree;
const seenNames = {}; // To avoid collisions when introducing new vars
const seenLocals = {}; // To avoid aliasing globals that are shadowed by a local var somewhere
estraverse.traverse(ast, {
enter(node, parentNode) {
if (node.type === 'MemberExpression' && !node.computed) {
const name = escodegen.generate(node);
if (typeof globalsObj[name] !== 'undefined') {
if (
!Object.prototype.hasOwnProperty.call(
occurrencesByGlobalName,
name
)
) {
occurrencesByGlobalName[name] = [];
}
occurrencesByGlobalName[name].push({ node, parentNode });
} else if (node.property.name.length > 2) {
// .foo() => [a]() won't save anything if the method name is 2 chars or less
if (
!Object.prototype.hasOwnProperty.call(
occurrencesByString,
node.property.name
)
) {
occurrencesByString[node.property.name] = [];
}
occurrencesByString[node.property.name].push({
node,
parentNode
});
}
} else if (node.type === 'FunctionDeclaration') {
seenLocals[node.id.name] = true;
} else if (node.type === 'FunctionExpression') {
if (node.id) {
seenLocals[node.id.name] = true;
}
} else if (node.type === 'VariableDeclaration') {
for (const declaration of node.declarations) {
seenNames[declaration.id.name] = true;
seenLocals[declaration.id.name] = true;
}
} else if (
node.type === 'Literal' &&
typeof node.value === 'string'
) {
if (
!Object.prototype.hasOwnProperty.call(
occurrencesByString,
node.value
)
) {
occurrencesByString[node.value] = [];
}
occurrencesByString[node.value].push({ node, parentNode });
} else if (node.type === 'Identifier') {
seenNames[node.name] = true;
if (Object.prototype.hasOwnProperty.call(globalsObj, node.name)) {
if (
!Object.prototype.hasOwnProperty.call(
occurrencesByGlobalName,
node.name
)
) {
occurrencesByGlobalName[node.name] = [];
}
occurrencesByGlobalName[node.name].push({ node, parentNode });
}
}
}
});
// Order by number of dots ascending so e.g. Math is considered before Math.min:
const globalNames = Object.keys(occurrencesByGlobalName).sort((a, b) => {
return a.split('.').length - b.split('.').length;
});
const aliasDeclarations = [];
const aliasByGlobalName = {};
/* eslint-disable no-inner-declarations */
function nameToAst(name) {
name =
(!wrapInFunction &&
Object.prototype.hasOwnProperty.call(aliasByGlobalName, name) &&
aliasByGlobalName[name]) ||
name;
const nameFragments = name.split('.');
if (nameFragments.length > 1) {
return {
type: 'MemberExpression',
computed: false,
object:
nameToAst(
nameFragments.slice(0, nameFragments.length - 1).join('.')
) || 'wat1',
property: {
type: 'Identifier',
name: nameFragments[nameFragments.length - 1]
}
};
} else {
return { type: 'Identifier', name: nameFragments[0] };
}
}
for (const globalName of globalNames) {
if (
!Object.prototype.hasOwnProperty.call(seenLocals, globalName) &&
occurrencesByGlobalName[globalName].length > 1
) {
let alias = globalName.replace(/\./g, '').toUpperCase();
while (Object.prototype.hasOwnProperty.call(seenNames, alias)) {
alias += '_';
}
seenNames[alias] = true;
aliasDeclarations.push({
name: alias,
valueAst: nameToAst(globalName)
});
for (const occurrence of occurrencesByGlobalName[globalName]) {
replaceDescendantNode(occurrence.parentNode, occurrence.node, {
type: 'Identifier',
name: alias
});
}
aliasByGlobalName[globalName] = alias;
} else {
occurrencesByGlobalName[globalName] = undefined;
}
}
if (stringLiterals) {
for (const string of Object.keys(occurrencesByString)) {
const occurrences = occurrencesByString[string];
if (occurrences.length >= 2) {
let alias = string || 'EMPTY';
for (const symbolName of Object.keys(symbolNames)) {
while (alias.includes(symbolName)) {
alias = alias.replace(symbolName, symbolNames[symbolName]);
}
}
if (/^[0-9]/.test(alias)) {
alias = `_${alias}`;
}
alias = alias.replace(/[^a-z0-9_]/gi, '').toUpperCase();
while (
!alias ||
Object.prototype.hasOwnProperty.call(seenNames, alias)
) {
alias += '_';
}
seenNames[alias] = true;
aliasDeclarations.push({
name: alias,
valueAst: { type: 'Literal', value: string }
});
for (const occurrence of occurrences) {
if (
occurrence.node.type === 'Literal' &&
typeof occurrence.node.value === 'string'
) {
replaceDescendantNode(occurrence.parentNode, occurrence.node, {
type: 'Identifier',
name: alias
});
} else {
// parent is AST_Dot
replaceDescendantNode(occurrence.parentNode, occurrence.node, {
type: 'MemberExpression',
computed: true,
property: { type: 'Identifier', name: alias },
object: occurrence.node.object
});
}
}
}
}
}
if (aliasDeclarations.length) {
if (wrapInFunction) {
ast.body = [
{
type: 'ExpressionStatement',
expression: {
type: 'CallExpression',
arguments: _.map(aliasDeclarations, 'valueAst'),
callee: {
type: 'FunctionExpression',
params: aliasDeclarations.map(aliasDeclaration => {
return { type: 'Identifier', name: aliasDeclaration.name };
}),
body: {
type: 'BlockStatement',
body: ast.body
}
}
}
}
];
} else {
ast.body.unshift({
type: 'VariableDeclaration',
kind: 'var',
declarations: aliasDeclarations.map(aliasDeclaration => {
return {
type: 'VariableDeclarator',
id: { type: 'Identifier', name: aliasDeclaration.name },
init: aliasDeclaration.valueAst
};
})
});
}
javaScriptAsset.markDirty();
}
}
};
};