UNPKG

prepack

Version:

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

545 lines (484 loc) 25.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Modules = exports.ModuleTracer = undefined; var _environment = require("../environment.js"); var _errors = require("../errors.js"); var _realm = require("../realm.js"); var _index = require("../methods/index.js"); var _completions = require("../completions.js"); var _singletons = require("../singletons.js"); var _index2 = require("../values/index.js"); var _babelTypes = require("babel-types"); var t = _interopRequireWildcard(_babelTypes); var _invariant = require("../invariant.js"); var _invariant2 = _interopRequireDefault(_invariant); var _logger = require("./logger.js"); var _types = require("../serializer/types.js"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 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 downgradeErrorsToWarnings(realm, f) { let savedHandler = realm.errorHandler; function handler(e) { e.severity = "Warning"; realm.errorHandler = savedHandler; try { return realm.handleError(e); } finally { realm.errorHandler = handler; } } realm.errorHandler = handler; try { return f(); } finally { realm.errorHandler = savedHandler; } } /** * 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. */ class ModuleTracer extends _realm.Tracer { constructor(modules, statistics, logModules) { super(); this.modules = modules; this.evaluateForEffectsNesting = 0; this.requireStack = []; this.requireSequence = []; this.logModules = logModules; this.uninitializedModuleIdsRequiredInEvaluateForEffects = new Set(); this.statistics = statistics; } // We can't say that a module has been initialized if it was initialized in a // evaluate for effects context until we know the effects are applied. log(message) { if (this.logModules) console.log(`[modules] ${this.requireStack.map(_ => " ").join("")}${message}`); } beginEvaluateForEffects(state) { if (state !== this) { this.log(">evaluate for effects"); this.evaluateForEffectsNesting++; this.requireStack.push(undefined); } } endEvaluateForEffects(state, effects) { if (state !== this) { let popped = this.requireStack.pop(); (0, _invariant2.default)(popped === undefined); this.evaluateForEffectsNesting--; this.log("<evaluate for effects"); } } // If we don't delay unsupported requires, we simply want to record here // when a module gets initialized, and then we return. _callRequireAndRecord(moduleIdValue, performCall) { let realm = this.modules.realm; if ((this.requireStack.length === 0 || this.requireStack[this.requireStack.length - 1] !== moduleIdValue) && this.modules.moduleIds.has(moduleIdValue)) { this.requireStack.push(moduleIdValue); try { let value = performCall(); // Make this into a join point by suppressing the conditional exception. // TODO: delete this code and let the caller deal with the conditional exception. let completion = _singletons.Functions.incorporateSavedCompletion(realm, value); if (completion instanceof _completions.PossiblyNormalCompletion) { realm.stopEffectCapture(completion); let warning = new _errors.CompilerDiagnostic("Module import may fail with an exception", completion.location, "PP0018", "Warning"); realm.handleError(warning); } else { this.modules.recordModuleInitialized(moduleIdValue, value); } return value; } finally { (0, _invariant2.default)(this.requireStack.pop() === moduleIdValue); } } return undefined; } _callRequireAndAccelerate(isTopLevelRequire, moduleIdValue, performCall) { let realm = this.modules.realm; let acceleratedModuleIds, effects; do { try { effects = realm.evaluateForEffects(() => performCall(), this, "_callRequireAndAccelerate"); } catch (e) { e; } acceleratedModuleIds = []; if (isTopLevelRequire && effects !== undefined && !(effects[0] instanceof _completions.AbruptCompletion)) { // We gathered all effects, but didn't apply them yet. // Let's check if there was any call to `require` in a // evaluate-for-effects context. If so, try to initialize // that module right now. Acceleration module initialization in this // way might not actually be desirable, but it works around // general prepack-limitations around joined abstract values involving // conditionals. Long term, Prepack needs to implement a notion of refinement // of conditional abstract values under the known path condition. // Example: // if (*) require(1); else require(2); // let x = require(1).X; // => // require(1); // require(2); // if (*) require(1); else require(2); // let x = require(1).X; for (let nestedModuleId of this.uninitializedModuleIdsRequiredInEvaluateForEffects) { let nestedEffects = this.modules.tryInitializeModule(nestedModuleId, `accelerated initialization of conditional module ${nestedModuleId} as it's required in an evaluate-for-effects context by module ${moduleIdValue}`); if (this.modules.accelerateUnsupportedRequires && nestedEffects !== undefined && nestedEffects[0] instanceof _index2.Value && this.modules.isModuleInitialized(nestedModuleId)) { acceleratedModuleIds.push(nestedModuleId); } } this.uninitializedModuleIdsRequiredInEvaluateForEffects.clear(); // Keep restarting for as long as we find additional modules to accelerate. if (acceleratedModuleIds.length > 0) { console.log(`restarting require(${moduleIdValue}) after accelerating conditional require calls for ${acceleratedModuleIds.join()}`); this.statistics.acceleratedModules += acceleratedModuleIds.length; } } } while (acceleratedModuleIds.length > 0); return effects; } // If a require fails, recover from it and delay the factory call until runtime // Also, only in this mode, consider "accelerating" require calls, see below. _callRequireAndDelayIfNeeded(moduleIdValue, performCall) { let realm = this.modules.realm; this.log(`>require(${moduleIdValue})`); let isTopLevelRequire = this.requireStack.length === 0; if (this.evaluateForEffectsNesting > 0) { if (isTopLevelRequire) { let diagnostic = new _errors.CompilerDiagnostic("Non-deterministically conditional top-level require not currently supported", realm.currentLocation, "PP0017", "FatalError"); realm.handleError(diagnostic); throw new _errors.FatalError(); } else if (!this.modules.isModuleInitialized(moduleIdValue)) // Nested require call: We record that this happened. Just so that // if we discover later this this require call needs to get delayed, // then we still know (some of) which modules it in turn required, // and then we'll later "accelerate" requiring them to preserve the // require ordering. See below for more details on acceleration. this.uninitializedModuleIdsRequiredInEvaluateForEffects.add(moduleIdValue); return undefined; } else { return downgradeErrorsToWarnings(realm, () => { let result; try { this.requireStack.push(moduleIdValue); let requireSequenceStart = this.requireSequence.length; this.requireSequence.push(moduleIdValue); const previousNumDelayedModules = this.statistics.delayedModules; let effects = this._callRequireAndAccelerate(isTopLevelRequire, moduleIdValue, performCall); if (effects === undefined || effects[0] instanceof _completions.AbruptCompletion) { console.log(`delaying require(${moduleIdValue})`); this.statistics.delayedModules = previousNumDelayedModules + 1; // So we are about to emit a delayed require(...) call. // However, before we do that, let's try to require all modules that we // know this delayed require call will require. // This way, we ensure that those modules will be fully initialized // before the require call executes. // TODO #690: More needs to be done to make the delayUnsupportedRequires // feature completely safe. Open issues are: // 1) Side-effects on the heap of delayed factory functions are not discovered or rejected. // 2) While we do process an appropriate list of transitively required modules here, // it's likely just a subset / prefix of all transivitely required modules, as // more modules would have been required if the Introspection exception had not been thrown. // To be correct, those modules would have to be prepacked here as well. // TODO #798: Watch out for an upcoming change to the __d module declaration where the statically known // list of dependencies will be announced, so we'll no longer have to guess. let nestedModulesIds = new Set(); for (let i = requireSequenceStart; i < this.requireSequence.length; i++) { let nestedModuleId = this.requireSequence[i]; if (nestedModulesIds.has(nestedModuleId)) continue; nestedModulesIds.add(nestedModuleId); this.modules.tryInitializeModule(nestedModuleId, `initialization of module ${nestedModuleId} as it's required by module ${moduleIdValue}`); } result = _index2.AbstractValue.createTemporalFromBuildFunction(realm, _index2.Value, [], ([]) => t.callExpression(t.identifier("require"), [t.valueToNode(moduleIdValue)])); } else { result = effects[0]; if (result instanceof _index2.Value) { realm.applyEffects(effects, `initialization of module ${moduleIdValue}`); this.modules.recordModuleInitialized(moduleIdValue, result); } else if (result instanceof _completions.PossiblyNormalCompletion) { let warning = new _errors.CompilerDiagnostic("Module import may fail with an exception", result.location, "PP0018", "Warning"); realm.handleError(warning); result = result.value; realm.applyEffects(effects, `initialization of module ${moduleIdValue}`); } else { (0, _invariant2.default)(false); } } } finally { let popped = this.requireStack.pop(); (0, _invariant2.default)(popped === moduleIdValue); this.log(`<require(${moduleIdValue})`); } (0, _invariant2.default)(result instanceof _index2.Value); return result; }); } } _tryExtractDependencies(value) { if (value === undefined || value instanceof _index2.NullValue || value instanceof _index2.UndefinedValue) return []; if (value instanceof _index2.ArrayValue) { const realm = this.modules.realm; const lengthValue = (0, _index.Get)(realm, value, "length"); if (lengthValue instanceof _index2.NumberValue) { const dependencies = []; const logger = this.modules.logger; for (let i = 0; i < lengthValue.value; i++) { const elementValue = logger.tryQuery(() => (0, _index.Get)(realm, value, "" + i), realm.intrinsics.undefined); dependencies.push(elementValue); } return dependencies; } } return undefined; } detourCall(F, thisArgument, argumentsList, newTarget, performCall) { if (F === this.modules.getRequire() && !this.modules.disallowDelayingRequiresOverride && argumentsList.length === 1) { // Here, we handle calls of the form // require(42) let moduleId = argumentsList[0]; let moduleIdValue; // Do some sanity checks and request require(...) calls with bad arguments if (moduleId instanceof _index2.NumberValue || moduleId instanceof _index2.StringValue) { moduleIdValue = moduleId.value; if (!this.modules.moduleIds.has(moduleIdValue) && this.modules.delayUnsupportedRequires) { this.modules.logger.logError(moduleId, "Module referenced by require call has not been defined."); } } else { if (this.modules.delayUnsupportedRequires) { this.modules.logger.logError(moduleId, "First argument to require function is not a number or string value."); } return undefined; } if (this.modules.delayUnsupportedRequires) return this._callRequireAndDelayIfNeeded(moduleIdValue, performCall);else return this._callRequireAndRecord(moduleIdValue, performCall); } else if (F === this.modules.getDefine()) { // Here, we handle calls of the form // __d(factoryFunction, moduleId, dependencyArray) if (this.evaluateForEffectsNesting !== 0) this.modules.logger.logError(F, "Defining a module in nested partial evaluation is not supported."); let factoryFunction = argumentsList[0]; if (factoryFunction instanceof _index2.FunctionValue) { let dependencies = this._tryExtractDependencies(argumentsList[2]); if (dependencies !== undefined) this.modules.factoryFunctionDependencies.set(factoryFunction, dependencies);else this.modules.logger.logError(argumentsList[2], "Third argument to define function is present but not a concrete array."); } else this.modules.logger.logError(factoryFunction, "First argument to define function is not a function value."); let moduleId = argumentsList[1]; if (moduleId instanceof _index2.NumberValue || moduleId instanceof _index2.StringValue) this.modules.moduleIds.add(moduleId.value);else this.modules.logger.logError(moduleId, "Second argument to define function is not a number or string value."); } return undefined; } } exports.ModuleTracer = ModuleTracer; class Modules { constructor(realm, logger, statistics, logModules, delayUnsupportedRequires, accelerateUnsupportedRequires) { this.realm = realm; this.logger = logger; this._require = realm.intrinsics.undefined; this._define = realm.intrinsics.undefined; this.factoryFunctionDependencies = new Map(); this.moduleIds = new Set(); this.initializedModules = new Map(); realm.tracers.push(this.moduleTracer = new ModuleTracer(this, statistics, logModules)); this.delayUnsupportedRequires = delayUnsupportedRequires; this.accelerateUnsupportedRequires = accelerateUnsupportedRequires; this.disallowDelayingRequiresOverride = false; } resolveInitializedModules() { this.initializedModules.clear(); let globalInitializedModulesMap = this._getGlobalProperty("__initializedModules"); (0, _invariant2.default)(globalInitializedModulesMap instanceof _index2.ObjectValue); for (let moduleId of globalInitializedModulesMap.properties.keys()) { let property = globalInitializedModulesMap.properties.get(moduleId); (0, _invariant2.default)(property); let moduleValue = property.descriptor && property.descriptor.value; if (moduleValue instanceof _index2.Value) this.initializedModules.set(moduleId, moduleValue); } } _getGlobalProperty(name) { if (this.active) return this.realm.intrinsics.undefined; this.active = true; try { let realm = this.realm; return this.logger.tryQuery(() => (0, _index.Get)(realm, realm.$GlobalObject, name), realm.intrinsics.undefined); } finally { this.active = false; } } getRequire() { if (!(this._require instanceof _index2.FunctionValue)) this._require = this._getGlobalProperty("require"); return this._require; } getDefine() { if (!(this._define instanceof _index2.FunctionValue)) this._define = this._getGlobalProperty("__d"); return this._define; } // Returns a function that checks if a call node represents a call to a // known require function, and if so, what module id that call indicates. // A known require function call is either of the form // ... require(42) ... // where require resolves to the global require function, or // factoryFunction(, require, , , dependencies) { // ... // ... require(dependencies[3]) ... // where factoryFunction and dependencies were announced as part of the // global code execution via a global module declaration call such as // global.__d(factoryFunction, , [0,2,4,6,8]) getGetModuleIdIfNodeIsRequireFunction(formalParameters, functions) { let realm = this.realm; let logger = this.logger; let modules = this; return (scope, node) => { // Are we calling a function that has a single name and a single argument? if (!t.isIdentifier(node.callee) || node.arguments.length !== 1) return undefined; let argument = node.arguments[0]; if (!argument) return undefined; if (!t.isNumericLiteral(argument) && !t.isStringLiteral(argument) && !t.isMemberExpression(argument)) return undefined; (0, _invariant2.default)(node.callee); let innerName = node.callee.name; let moduleId; // Helper function used to give up if we ever come up with different module ids for different functions let updateModuleId = newModuleId => { if (moduleId !== undefined && moduleId !== newModuleId) return false; moduleId = newModuleId; return true; }; // Helper function that retrieves module id from call argument, possibly chasing dependency array indirection const getModuleId = dependencies => { if (t.isMemberExpression(argument)) { if (dependencies !== undefined) { let memberExpression = argument; if (t.isIdentifier(memberExpression.object)) { let scopedBinding = scope.getBinding(memberExpression.object.name); if (scopedBinding && formalParameters[4] === scopedBinding.path.node) { if (t.isNumericLiteral(memberExpression.property)) { let dependencyIndex = memberExpression.property.value; if (Number.isInteger(dependencyIndex) && dependencyIndex >= 0 && dependencyIndex < dependencies.length) { let dependency = dependencies[dependencyIndex]; if (dependency instanceof _index2.NumberValue || dependency instanceof _index2.StringValue) return dependency.value; } } } } } } else { return argument.value; } }; // Let's consider each of the function instances (closures for the same code) for (let f of functions) { // 1. Let's check if we have a match for a factory function like // factoryFunction(, require, , , [dependencies]) // which is used with the Metro bundler let scopedBinding = scope.getBinding(innerName); if (scopedBinding) { let dependencies = modules.factoryFunctionDependencies.get(f); if (dependencies !== undefined && formalParameters[1] === scopedBinding.path.node) { (0, _invariant2.default)(scopedBinding.kind === "param"); let newModuleId = getModuleId(dependencies); if (newModuleId !== undefined && !updateModuleId(newModuleId)) return undefined; continue; } // The name binds to some local entity, but nothing we'd know what exactly it is return undefined; } // 2. Let's check if we can resolve the called function just by looking at the // function instance environment. // TODO: We should not do this if the current node is in a nested function! // We won't have a dependency map here, so this only works for literal arguments. if (!t.isNumericLiteral(argument) && !t.isStringLiteral(argument)) return undefined; let doesNotMatter = true; let reference = logger.tryQuery(() => _singletons.Environment.ResolveBinding(realm, innerName, doesNotMatter, f.$Environment), undefined); if (reference === undefined) { // We couldn't resolve as we came across some behavior that we cannot deal with abstractly return undefined; } if (_singletons.Environment.IsUnresolvableReference(realm, reference)) return undefined; let referencedBase = reference.base; let referencedName = reference.referencedName; if (typeof referencedName !== "string") return undefined; let value; if (reference.base instanceof _environment.GlobalEnvironmentRecord) { value = logger.tryQuery(() => (0, _index.Get)(realm, realm.$GlobalObject, innerName), realm.intrinsics.undefined); } else { (0, _invariant2.default)(referencedBase instanceof _environment.DeclarativeEnvironmentRecord); let binding = referencedBase.bindings[referencedName]; if (!binding.initialized) return undefined; value = binding.value; } if (value !== modules.getRequire()) return undefined; const newModuleId = getModuleId(); (0, _invariant2.default)(newModuleId !== undefined); if (!updateModuleId(newModuleId)) return undefined; } return moduleId; }; } recordModuleInitialized(moduleId, value) { this.realm.assignToGlobal(t.memberExpression(t.memberExpression(t.identifier("global"), t.identifier("__initializedModules")), t.identifier("" + moduleId)), value); } tryInitializeModule(moduleId, message) { let realm = this.realm; let previousDisallowDelayingRequiresOverride = this.disallowDelayingRequiresOverride; this.disallowDelayingRequiresOverride = true; return downgradeErrorsToWarnings(realm, () => { try { let node = t.callExpression(t.identifier("require"), [t.valueToNode(moduleId)]); let effects = realm.evaluateNodeForEffectsInGlobalEnv(node); realm.applyEffects(effects, message); return effects; } catch (err) { if (err instanceof _errors.FatalError) return undefined;else throw err; } finally { this.disallowDelayingRequiresOverride = previousDisallowDelayingRequiresOverride; } }); } initializeMoreModules() { // partially evaluate all factory methods by calling require let count = 0; for (let moduleId of this.moduleIds) { if (this.initializedModules.has(moduleId)) continue; let effects = this.tryInitializeModule(moduleId, `Speculative initialization of module ${moduleId}`); if (effects === undefined) continue; let result = effects[0]; if (!(result instanceof _index2.Value)) continue; // module might throw count++; this.initializedModules.set(moduleId, result); } if (count > 0) console.log(`=== speculatively initialized ${count} additional modules`); } isModuleInitialized(moduleId) { let realm = this.realm; let oldReadOnly = realm.setReadOnly(true); let oldDisallowDelayingRequiresOverride = this.disallowDelayingRequiresOverride; this.disallowDelayingRequiresOverride = true; try { let node = t.callExpression(t.identifier("require"), [t.valueToNode(moduleId)]); let [compl, generator, bindings, properties, createdObjects] = realm.evaluateNodeForEffectsInGlobalEnv(node); // for lint unused (0, _invariant2.default)(bindings); if (compl instanceof _completions.AbruptCompletion) return undefined; (0, _invariant2.default)(compl instanceof _index2.Value); if (!generator.empty() || compl instanceof _index2.ObjectValue && createdObjects.has(compl)) return undefined; // Check for escaping property assignments, if none escape, we got an existing object let escapes = false; for (let [binding] of properties) { let object = binding.object; (0, _invariant2.default)(object instanceof _index2.ObjectValue); if (!createdObjects.has(object)) escapes = true; } if (escapes) return undefined; return compl; } catch (err) { if (err instanceof _errors.FatalError) return undefined; throw err; } finally { realm.setReadOnly(oldReadOnly); this.disallowDelayingRequiresOverride = oldDisallowDelayingRequiresOverride; } } } exports.Modules = Modules; //# sourceMappingURL=modules.js.map