UNPKG

prepack

Version:

Execute a JS bundle, serialize global state and side effects to a snapshot that can be quickly restored.

244 lines (209 loc) 9.61 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClosureRefVisitor = exports.ClosureRefReplacer = undefined; var _realm = require("../realm.js"); var _index = require("../values/index.js"); var _babelTypes = require("babel-types"); var t = _interopRequireWildcard(_babelTypes); var _jsx = require("../react/jsx"); var _internalizer = require("../utils/internalizer.js"); function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function markVisited(node, data) { node._renamedOnce = data; } /** * Copyright (c) 2017-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. */ function shouldVisit(node, data) { return node._renamedOnce !== data; } // replaceWith causes the node to be re-analyzed, so to prevent double replacement // we add this property on the node to mark it such that it does not get replaced // again on this pass // TODO: Make this work when replacing with arbitrary BabelNodeExpressions. Currently // if the node that we're substituting contains identifiers as children, // they will be visited again and possibly transformed. // If necessary we could implement this by following node.parentPath and checking // if any parent nodes are marked visited, but that seem unnecessary right now.let closureRefReplacer = { function replaceName(path, residualFunctionBinding, name, data) { // Let's skip names that are bound if (path.scope.hasBinding(name, /*noGlobals*/true)) return; // Let's skip bindings that are referring to // 1) something global (without an environment record), and // 2) have not been assigned a value (which would mean that they have a var/let binding and Prepack will take the liberty to rename them). if (residualFunctionBinding.declarativeEnvironmentRecord === null && residualFunctionBinding.value === undefined) return; if (shouldVisit(path.node, data)) { markVisited(residualFunctionBinding.serializedValue, data); let serializedValue = residualFunctionBinding.serializedValue; if (path.node.type === "JSXIdentifier" || path.node.type === "JSXMemberIdentifier") { path.replaceWith((0, _jsx.convertExpressionToJSXIdentifier)(serializedValue, true)); } else { path.replaceWith(serializedValue); } } } function getLiteralTruthiness(node) { // In the return value, 'known' is true only if this is a literal of known truthiness and with no side effects; if 'known' is true, 'value' is its truthiness. if (t.isBooleanLiteral(node) || t.isNumericLiteral(node) || t.isStringLiteral(node)) { return { known: true, value: !!node.value }; } if (t.isFunctionExpression(node) || t.isArrowFunctionExpression(node) || t.isRegExpLiteral(node) || t.isClassExpression(node) && node.superClass === null && node.body.body.length === 0 || t.isObjectExpression(node) && node.properties.length === 0 || t.isArrayExpression(node) && node.elements.length === 0) { return { known: true, value: true }; } if (t.isNullLiteral(node)) { return { known: true, value: false }; } return { known: false }; } function canShareFunctionBody(duplicateFunctionInfo) { // Only share function when: // 1. it does not access any free variables. // 2. it does not use "this". const { unbound, modified, usesThis } = duplicateFunctionInfo.functionInfo; return unbound.size === 0 && modified.size === 0 && !usesThis; } let ClosureRefReplacer = exports.ClosureRefReplacer = { ReferencedIdentifier(path, state) { if (ignorePath(path)) return; let residualFunctionBindings = state.residualFunctionBindings; let name = path.node.name; let residualFunctionBinding = residualFunctionBindings.get(name); if (residualFunctionBinding) replaceName(path, residualFunctionBinding, name, residualFunctionBindings); }, CallExpression(path, state) { // Here we apply the require optimization by replacing require calls with their // corresponding initialized modules. let requireReturns = state.requireReturns; if (state.getModuleIdIfNodeIsRequireFunction === undefined) return; let moduleId = state.getModuleIdIfNodeIsRequireFunction(path.scope, path.node); if (moduleId === undefined) return; state.requireStatistics.count++; if (state.modified.has(path.node.callee.name)) return; let new_node = requireReturns.get("" + moduleId); if (new_node !== undefined) { markVisited(new_node, state.residualFunctionBindings); path.replaceWith(new_node); state.requireStatistics.replaced++; } }, "AssignmentExpression|UpdateExpression"(path, state) { let residualFunctionBindings = state.residualFunctionBindings; let ids = path.getBindingIdentifierPaths(); for (let name in ids) { let residualFunctionBinding = residualFunctionBindings.get(name); if (residualFunctionBinding) { let nestedPath = ids[name]; replaceName(nestedPath, residualFunctionBinding, name, residualFunctionBindings); } } }, // TODO: handle FunctionDeclaration. // Replace "function () {}" ==> "factory_id.bind(null)". FunctionExpression(path, state) { if (t.isProgram(path.parentPath.parentPath.node)) { // Our goal is replacing duplicate nested function so skip root residual function itself. // This assumes the root function is wrapped with: t.file(t.program([t.expressionStatement(rootFunction). return; } const functionExpression = path.node; const functionTag = functionExpression.body.uniqueOrderedTag; if (!functionTag) { // Un-interpreted nested function. return; } const duplicateFunctionInfo = state.factoryFunctionInfos.get(functionTag); if (duplicateFunctionInfo && canShareFunctionBody(duplicateFunctionInfo)) { const { factoryId } = duplicateFunctionInfo; path.replaceWith(t.callExpression(t.memberExpression(factoryId, t.identifier("bind")), [_internalizer.nullExpression])); } }, // A few very simple dead code elimination helpers. Eventually these should be subsumed by the partial evaluators. IfStatement: { exit: function (path, state) { let node = path.node; let testTruthiness = getLiteralTruthiness(node.test); if (testTruthiness.known) { if (testTruthiness.value) { // Strictly speaking this is not safe: Annex B.3.4 allows FunctionDeclarations as the body of IfStatements in sloppy mode, // which have weird hoisting behavior: `console.log(typeof f); if (true) function f(){} console.log(typeof f)` will print 'undefined', 'function', but // `console.log(typeof f); function f(){} console.log(typeof f)` will print 'function', 'function'. // However, Babylon can't parse these, so it doesn't come up. path.replaceWith(node.consequent); } else { if (node.alternate !== null) { path.replaceWith(node.alternate); } else { path.remove(); } } } } }, ConditionalExpression: { exit: function (path, state) { let node = path.node; let testTruthiness = getLiteralTruthiness(node.test); if (testTruthiness.known) { path.replaceWith(testTruthiness.value ? node.consequent : node.alternate); } } }, LogicalExpression: { exit: function (path, state) { let node = path.node; let leftTruthiness = getLiteralTruthiness(node.left); if (node.operator === "&&" && leftTruthiness.known) { path.replaceWith(leftTruthiness.value ? node.right : node.left); } else if (node.operator === "||" && leftTruthiness.known) { path.replaceWith(leftTruthiness.value ? node.left : node.right); } } }, WhileStatement: { exit: function (path, state) { let node = path.node; let testTruthiness = getLiteralTruthiness(node.test); if (testTruthiness.known && !testTruthiness.value) { path.remove(); } } } }; function visitName(path, state, name, modified) { // Is the name bound to some local identifier? If so, we don't need to do anything if (path.scope.hasBinding(name, /*noGlobals*/true)) return; // Otherwise, let's record that there's an unbound identifier state.functionInfo.unbound.add(name); if (modified) state.functionInfo.modified.add(name); } function ignorePath(path) { let parent = path.parent; return t.isLabeledStatement(parent) || t.isBreakStatement(parent) || t.isContinueStatement(parent); } // TODO #886: doesn't check that `arguments` and `this` is in top function let ClosureRefVisitor = exports.ClosureRefVisitor = { ReferencedIdentifier(path, state) { if (ignorePath(path)) return; let innerName = path.node.name; if (innerName === "arguments") { state.functionInfo.usesArguments = true; return; } visitName(path, state, innerName, false); }, ThisExpression(path, state) { state.functionInfo.usesThis = true; }, "AssignmentExpression|UpdateExpression"(path, state) { for (let name in path.getBindingIdentifiers()) { visitName(path, state, name, true); } } }; //# sourceMappingURL=visitors.js.map