diff --git a/packages/meteor/define-package.js b/packages/meteor/define-package.js index c5d327b44c..fccae3c1fe 100644 --- a/packages/meteor/define-package.js +++ b/packages/meteor/define-package.js @@ -1,9 +1,28 @@ function PackageRegistry() { this._promiseInfoMap = Object.create(null); + this._packageQueue = []; + this._running = false; } var PRp = PackageRegistry.prototype; +var ASYNC_MAIN_MODULE = {}; + +// If potentialPromise is a promise, calls callback with the resolved value +// Otherwise, synchronously calls the callback with the value +PRp._waitForModule = function (potentialPromise, callback) { + if ( + isThenable(potentialPromise) && + potentialPromise.asyncMainModule === ASYNC_MAIN_MODULE + ) { + potentialPromise.then((results) => { + callback(results); + }); + } else { + callback(potentialPromise); + } +} + // Set global.Package[name] = pkg || {}. If additional arguments are // supplied, their keys will be copied into pkg if not already present. // This method is defined on the prototype of global.Package so that it @@ -28,6 +47,12 @@ PRp._define = function definePackage(name, pkg) { info.resolve(pkg); } + // if (this._packageQueue.length > 0) { + // this._packageQueue.unshift().load(); + // } else { + // this._running = false; + // } + return pkg; }; @@ -60,6 +85,95 @@ PRp._promise = function promise(name) { return info.promise; }; +// On the server, load is run immediately +// If it has async modules that are eagerly evaluated, it will return a +// promise that resolves after the package has been fully loaded. +PRp.load = function (name, deps, load) { + // console.log('start load', name); + var self = this; + + if (typeof Meteor === 'undefined' || Meteor.isServer) { + var result = load() || {}; + + var mainModule = result.mainModule; + var exports = result.exports; + + if (mainModule && mainModule.asyncMainModule === ASYNC_MAIN_MODULE) { + return mainModule.then(function (mainExports) { + console.log('defineAsync', name, mainModule, exports); + self._define(name, mainExports, exports); + }); + } + + self._define(name, mainModule, exports); + // console.log('define', name, mainModule, exports); + return; + // this._packageQueue.push({ + // name: name, + // load: load, + // deps: deps + // }); + + // if (!this._running) { + // this._running = true; + // this._packageQueue.unshift().load(); + // } + } + + // TODO: implement + +} + +function isThenable(value) { + return typeof value === 'object' && value !== null && + typeof value.then === 'function'; +} + +PRp._evaluateEagerModules = function(require, paths, mainModuleIndex) { + let index = -1; + let result; + let promise; + let resolve; + + function evaluateNext() { + index += 1; + + if (index === paths.length) { + if (resolve) { + resolve(result); + } + return; + } + + let path = paths[index]; + let exports = require(path); + + if (isThenable(exports)) { + if (!promise) { + promise = new Promise(_resolve => resolve = _resolve); + promise.asyncMainModule = ASYNC_MAIN_MODULE; + } + + exports.then(resolvedExports => { + if (index === mainModuleIndex) { + result = resolvedExports; + } + evaluateNext(); + }); + // TODO: handle error + } else { + if (index === mainModuleIndex) { + result = exports; + } + evaluateNext(); + } + } + + evaluateNext(); + + return promise ? promise : result; +} + // Initialize the Package namespace used by all Meteor packages. global.Package = new PackageRegistry(); diff --git a/tools/isobuild/linker.js b/tools/isobuild/linker.js index 8edd4fc117..76fe2202ba 100644 --- a/tools/isobuild/linker.js +++ b/tools/isobuild/linker.js @@ -439,7 +439,7 @@ Object.assign(Module.prototype, { assert.ok(_.isNumber(moduleCount)); assert.ok(_.isNumber(sourceWidth)); - let exportsName; + let exportsIndex = -1; // Now that we have installed everything in this package or // application, first evaluate the bare files, then require the @@ -458,21 +458,33 @@ Object.assign(Module.prototype, { }); if (eagerModuleFiles.length > 0) { - _.each(eagerModuleFiles, file => { + let code = 'Package._evaluateEagerModules(require,['; + let paths = eagerModuleFiles.map((file, index) => { if (file.mainModule) { - exportsName = "exports"; + exportsIndex = index; } - chunks.push( - file.mainModule ? "\nvar " + exportsName + " = " : "\n", - "require(", - JSON.stringify(file.absModuleId), - ");" - ); + return JSON.stringify(file.absModuleId); }); + + code += paths.join(',\n '); + code += ']'; + + if (exportsIndex !== -1) { + code += `, ${exportsIndex}`; + } + + code += ');'; + + if (exportsIndex !== -1) { + code = '\nvar exports = ' + code; + } + + chunks.push(code); } - return exportsName; + + return exportsIndex === -1 ? undefined : 'exports'; } }); @@ -964,8 +976,22 @@ var SOURCE_MAP_INSTRUCTIONS_COMMENT = banner([ var getHeader = function (options) { var chunks = []; + // TODO: find a better check that also works for packages that + // load before the Meteor package + if (options.name !== 'meteor') { + let depsCode = Object.values(options.imports).map(k => JSON.stringify(k)).join(', '); + + chunks.push( + `Package.load("${options.name}", [`, + depsCode, + '], function () {' + ); + + } else { + chunks.push("(function() {\n\n") + } + chunks.push( - "(function () {\n\n", getImportCode(options.imports, "/* Imports */\n", false), ); @@ -1015,33 +1041,47 @@ var getFooter = function ({ name, exported, exportsName, + imports }) { var chunks = []; - if (name && exported) { + if (name === 'meteor') { + chunks.push("Package._define(" + JSON.stringify(name) + ", "); + if (!_.isEmpty(exported)) { + const scratch = {}; + _.each(exported, symbol => scratch[symbol] = symbol); + const symbolTree = writeSymbolTree(buildSymbolTree(scratch)); + chunks.push(symbolTree); + } + chunks.push(');\n'); + + } else if (exported || exportsName) { chunks.push("\n\n/* Exports */\n"); + chunks.push('return {\n'); + + if (exportsName) { + chunks.push(` mainModule: ${exportsName},`); + } // Even if there are no exports, we need to define Package.foo, // because the existence of Package.foo is how another package // (e.g., one that weakly depends on foo) can tell if foo is loaded. - chunks.push("Package._define(" + JSON.stringify(name)); - - if (exportsName) { - // If we have an exports object, use it as Package[name]. - chunks.push(", ", exportsName); - } if (! _.isEmpty(exported)) { const scratch = {}; _.each(exported, symbol => scratch[symbol] = symbol); const symbolTree = writeSymbolTree(buildSymbolTree(scratch)); - chunks.push(", ", symbolTree); + chunks.push("exports: ", symbolTree); } - - chunks.push(");\n"); + chunks.push('};\n'); + + } + if (name !== 'meteor') { + chunks.push("\n});\n"); + } else { + chunks.push("\n})();\n"); } - chunks.push("\n})();\n"); return chunks.join(''); }; @@ -1150,6 +1190,7 @@ export var fullLink = Profile("linker.fullLink", function (inputFiles, { // Otherwise we're making a package and we have to actually combine the files // into a single scope. var header = getHeader({ + name, imports, packageVariables: _.union(assignedVariables, declaredExports) }); @@ -1164,7 +1205,8 @@ export var fullLink = Profile("linker.fullLink", function (inputFiles, { var footer = getFooter({ exported: declaredExports, exportsName, - name + name, + imports }); if (includeSourceMapInstructions) {