diff --git a/tools/isobuild/linker.js b/tools/isobuild/linker.js index 276ec9e913..d1721d053b 100644 --- a/tools/isobuild/linker.js +++ b/tools/isobuild/linker.js @@ -162,13 +162,15 @@ _.extend(Module.prototype, { servePath: self.combinedServePath, }; + const results = [result]; + // An array of strings and SourceNode objects. let chunks = []; let fileCount = 0; // Emit each file if (self.meteorInstallOptions) { - const tree = self._buildModuleTree(); + const tree = self._buildModuleTree(results, sourceWidth); fileCount = self._chunkifyModuleTree(tree, chunks, sourceWidth); result.exportsName = self._chunkifyEagerRequires(chunks, fileCount, sourceWidth); @@ -216,19 +218,20 @@ _.extend(Module.prototype, { } ); - return [result]; + return results; }), // Builds a tree of nested objects where the properties are names of // files or directories, and the values are either nested objects // (representing directories) or File objects (representing modules). // Bare files and lazy files that are never imported are ignored. - _buildModuleTree() { + _buildModuleTree(results, sourceWidth) { assert.ok(this.meteorInstallOptions); + // Tree of File objects for all non-dynamic modules. const tree = {}; - _.each(this.files, function (file) { + _.each(this.files, file => { if (file.bare) { // Bare files will be added in between the synchronous require // calls in _chunkifyEagerRequires. @@ -242,22 +245,71 @@ _.extend(Module.prototype, { return; } - const parts = file.installPath.split("/"); - let t = tree; - _.each(parts, function (part, i) { - const isLastPart = i === parts.length - 1; - t = _.has(t, part) - ? t[part] - : t[part] = isLastPart ? file : {}; - }); + const dynamic = file.lazy && file.imported === "dynamic"; + + if (dynamic) { + const servePath = "dynamic/" + file.installPath; + const { code: source, map } = + file.getPrelinkedOutput({ + sourceWidth: sourceWidth, + noLineNumbers: this.noLineNumbers + }).toStringWithSourceMap({ + file: servePath, + }); + + results.push({ + source, + servePath, + sourceMap: map && map.toJSON(), + dynamic: true, + }); + + const entry = { + version: file.sourceHash, + }; + + if (! _.isEmpty(file.deps)) { + entry.deps = file.deps; + } + + if (file.installPath.endsWith("/package.json") && + file.jsonData) { + const main = file.jsonData.main; + if (_.isString(main)) { + entry.main = main; + } + + const browser = file.jsonData.browser; + if (_.isString(browser)) { + entry.browser = browser; + } + } + + this._addToTree([entry], file.installPath, tree); + + } else { + // If the file is not dynamic, then it should be included in the + // initial bundle, so we add it to the static tree. + this._addToTree(file, file.installPath, tree); + } }); return tree; }, - // Takes the tree generated by _buildModuleTree and populates the chunks + _addToTree(obj, path, tree) { + const parts = path.split("/"); + const lastIndex = parts.length - 1; + parts.forEach((part, i) => { + tree = _.has(tree, part) + ? tree[part] + : tree[part] = i < lastIndex ? {} : obj; + }); + }, + + // Take the tree generated in getPrelinkedFiles and populate the chunks // array with strings and SourceNode objects that can be combined into a - // single SourceNode object. Returns the count of modules in the tree. + // single SourceNode object. Return the count of modules in the tree. _chunkifyModuleTree(tree, chunks, sourceWidth) { const self = this; @@ -268,12 +320,18 @@ _.extend(Module.prototype, { let moduleCount = 0; function walk(t) { - if (t instanceof File) { + if (Array.isArray(t)) { ++moduleCount; + chunks.push(JSON.stringify(t, null, 2)); + + } else if (t instanceof File) { + ++moduleCount; + chunks.push(t.getPrelinkedOutput({ sourceWidth, noLineNumbers: self.noLineNumbers })); + } else if (_.isObject(t)) { chunks.push("{"); const keys = _.keys(t); @@ -424,20 +482,16 @@ var File = function (inputFile, module) { self.servePath = inputFile.servePath; // Module identifiers imported or required by this module, if any. - if (Array.isArray(inputFile.deps)) { - self.deps = inputFile.deps; - } else if (inputFile.deps && typeof inputFile.deps === "object") { - self.deps = Object.keys(inputFile.deps); - } else { - self.deps = []; - } + // Excludes dynamically imported dependencies, and may exclude + // dependencies already included in the non-dynamic initial bundle. + self.deps = getNonDynamicDeps(inputFile.deps); // True if the input file should not be evaluated eagerly. self.lazy = inputFile.lazy; // could be `true`, `false` or `undefined` - // True if the file is an eagerly evaluated entry point, or if some - // other file imports or requires it. - self.imported = !!inputFile.imported; + // True if the file is eagerly imported, "dynamic" if the file is + // dynamically imported. + self.imported = inputFile.imported; // Boolean indicating whether this file is the main entry point module // for its package. @@ -450,10 +504,28 @@ var File = function (inputFile, module) { // Is an Object, not a string. self.sourceMap = inputFile.sourceMap; + // If inputFile is a JSON file, its parsed data will be exposed via the + // .jsonData property. + self.jsonData = inputFile.jsonData || null; + // The Module containing this file. self.module = module; }; +function getNonDynamicDeps(inputFileDeps) { + const nonDynamicDeps = Object.create(null); + + if (! _.isEmpty(inputFileDeps)) { + _.each(inputFileDeps, (info, id) => { + if (! info.dynamic) { + nonDynamicDeps[id] = info; + } + }); + } + + return Object.keys(nonDynamicDeps); +} + _.extend(File.prototype, { // Return the globals in this file as an array of symbol names. For // example: if the code references 'Foo.bar.baz' and 'Quux', and @@ -513,19 +585,12 @@ _.extend(File.prototype, { _getClosureHeader() { if (this._useMeteorInstall()) { - var header = ""; - - if (this.deps.length > 0) { - header += "["; - _.each(this.deps, dep => { - header += JSON.stringify(dep) + ","; - }); - } - - const headerParts = [ - header, - "function(" - ]; + // The wrapper function is named "module" so that the UglifyJS + // minifier will parse it as a function declaration, because + // UglifyJS has trouble parsing single function expressions. If the + // module refers to `module`, however, it will be referring to the + // parameter of that name, rather than the function name. + const headerParts = ["function module("]; if (this.source.match(/\b__dirname\b/)) { headerParts.push("require,exports,module,__filename,__dirname"); @@ -548,14 +613,9 @@ _.extend(File.prototype, { }, _getClosureFooter() { - if (this._useMeteorInstall()) { - var footer = "}"; - if (this.deps.length > 0) { - footer += "]"; - } - return footer; - } - return "}).call(this);\n"; + return this._useMeteorInstall() + ? "}" + : "}).call(this);\n"; }, // Options: @@ -1057,6 +1117,10 @@ export var fullLink = Profile("linker.fullLink", function (inputFiles, { var headerContent = (new Array(headerLines + 1).join(';')); return _.map(prelinkedFiles, function (file) { + if (file.dynamic) { + return file; + } + if (file.sourceMap) { var sourceMap = file.sourceMap; sourceMap.mappings = headerContent + sourceMap.mappings;