/
githubmirror
/
meteor
Обзор
Документация
Войти
/
githubmirror
/
meteor
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
devel
tools/isobuild/compiler-plugin.js
1 907 строк
63 KB
Italo José
Release 3.5.1 ⌛ (#14555)
06 авг 2026, 02:08
Не верифицирован
06 авг 2026, 02:08
efef960
Код
Авторство
О чём код?
var archinfo = require('../utils/archinfo'); var buildmessage = require('../utils/buildmessage.js'); var buildPluginModule = require('./build-plugin.js'); var colonConverter = require('../utils/colon-converter.js'); var files = require('../fs/files'); var compiler = require('./compiler.js'); var linker = require('./linker.js'); var _ = require('underscore'); var Profile = require('../tool-env/profile').Profile; import assert from "assert"; import {readAndWatchFileWithHash, sha1, WatchSet,} from '../fs/watch'; import LRUCache from 'lru-cache'; import {sourceMapLength} from '../utils/utils.js'; import {Console} from '../console/console.js'; import ImportScanner from './import-scanner'; import {cssToCommonJS} from "./css-modules"; import Resolver from "./resolver"; import {optimisticHashOrNull, optimisticStatOrNull,} from "../fs/optimistic"; import {isTestFilePath} from './test-files.js'; const hasOwn = Object.prototype.hasOwnProperty; // This file implements the new compiler plugins added in Meteor 1.2, which are // registered with the Plugin.registerCompiler API. // // Unlike legacy source handlers (Plugin.registerSourceHandler), compilers run // in the context of an entire app. That is to say, they don't run when you run // `meteor publish`; whenever they run, they have access to all the files of // their type across all packages as well as the app. This allows them to // implement cross-file and cross-package inclusion, or config files in the app // that affect how packages are processed, among other possibilities. // // Compilers can specify which extensions or filenames they process. They only // process files in packages (or the app) that directly use the plugin's package // (or that use it indirectly via the "imply" directive); just because compiler // plugins act on multiple packages at a time doesn't mean they automatically // act on all packages in your app. // // The CompilerPluginProcessor is the main entry point to this file; it is used // by the bundler to run all plugins on a target. It doesn't have much // interesting state and perhaps could have just been a function. // // It receives an ordered list of unibuilds (essentially, packages) from the // bundler. It turns them into an ordered list of PackageSourceBatch objects, // each of which represents the source files in a single package. Each // PackageSourceBatch consists of an ordered list of ResourceSlots representing // the resources in that package. The idea here is that, because Meteor executes // all JS files in the order produced by the bundler, we need to make sure to // maintain the order of packages from the bundler and the order of source files // within a package. Each ResourceSlot represents a resource (either a 'source' // resource which will be processed by a compiler plugin, or something else like // a static asset or some JavaScript produced by a legacy source handler), and // when the compiler plugin calls something like `inputFile.addJavaScript` on a // file, we replace that source file with the resource produced by the plugin. // // InputFile is a wrapper around ResourceSlot that is the object presented to // the compiler in the plugin. It is part of the documented registerCompiler // API. // Cache the (slightly post-processed) results of linker.fullLink. const CACHE_SIZE = process.env.METEOR_LINKER_CACHE_SIZE || 1024*1024*100; const CACHE_DEBUG = !! process.env.METEOR_TEST_PRINT_LINKER_CACHE_DEBUG; const LINKER_CACHE_SALT = 26; // Increment this number to force relinking. const LINKER_CACHE = new LRUCache({ max: CACHE_SIZE, // Cache is measured in bytes. We don't care about servePath. // Key is JSONification of all options plus all hashes. length (files) { return files.reduce((soFar, current) => { return soFar + current.data.length + sourceMapLength(current.sourceMap); }, 0); } }); const serverLibPackages = { // Make sure fibers is defined, if nothing else. fibers: true }; function populateServerLibPackages() { const devBundlePath = files.getDevBundle(); const nodeModulesPath = files.pathJoin( devBundlePath, "server-lib", "node_modules" ); files.readdir(nodeModulesPath).forEach(packageName => { const packagePath = files.pathJoin(nodeModulesPath, packageName); const packageStat = files.statOrNull(packagePath); if (packageStat && packageStat.isDirectory()) { serverLibPackages[packageName] = true; } }); } try { populateServerLibPackages(); } catch (e) { // At least we tried! } export class CompilerPluginProcessor { constructor({ unibuilds, arch, sourceRoot, buildMode, isopackCache, linkerCacheDir, scannerCacheDir, minifyCssResource, }) { Object.assign(this, { unibuilds, arch, sourceRoot, buildMode, isopackCache, linkerCacheDir, scannerCacheDir, minifyCssResource, }); if (linkerCacheDir) { files.mkdir_p(linkerCacheDir); } if (scannerCacheDir) { files.mkdir_p(scannerCacheDir); } } async runCompilerPlugins() { const self = this; buildmessage.assertInJob(); // plugin id -> {sourceProcessor, resourceSlots} var sourceProcessorsWithSlots = {}; const sourceBatches = []; for (const unibuild of self.unibuilds) { const { pkg: { name }, arch } = unibuild; const sourceRoot = name && self.isopackCache.getSourceRoot(name, arch) || self.sourceRoot; const batch = new PackageSourceBatch(unibuild, self, { sourceRoot, linkerCacheDir: self.linkerCacheDir, scannerCacheDir: self.scannerCacheDir, }); await batch.init(); sourceBatches.push(batch); } // If we failed to match sources with processors, we're done. if (buildmessage.jobHasMessages()) { return []; } // Find out which files go with which CompilerPlugins. _.each(sourceBatches, function (sourceBatch) { _.each(sourceBatch.resourceSlots, function (resourceSlot) { var sourceProcessor = resourceSlot.sourceProcessor; // Skip non-sources. if (! sourceProcessor) { return; } if (! _.has(sourceProcessorsWithSlots, sourceProcessor.id)) { sourceProcessorsWithSlots[sourceProcessor.id] = { sourceProcessor: sourceProcessor, resourceSlots: [] }; } sourceProcessorsWithSlots[sourceProcessor.id].resourceSlots.push( resourceSlot); }); }); // Now actually run the handlers for (const [id, data] of Object.entries(sourceProcessorsWithSlots)) { var sourceProcessor = data.sourceProcessor; var resourceSlots = data.resourceSlots; var jobTitle = [ "processing files with ", sourceProcessor.isopack.name, " (for target ", self.arch, ")" ].join(''); await Profile.time("plugin "+sourceProcessor.isopack.name, async () => { await buildmessage.enterJob({ title: jobTitle }, async function () { var inputFiles = resourceSlots.map(resourceSlot => new InputFile(resourceSlot)); const markedMethod = buildmessage.markBoundary( sourceProcessor.userPlugin.processFilesForTarget, sourceProcessor.userPlugin ); try { await markedMethod(inputFiles); } catch (e) { buildmessage.exception(e); } }); }); } return sourceBatches; } } class InputFile extends buildPluginModule.InputFile { constructor(resourceSlot) { super(); // We use underscored attributes here because this is user-visible // code and we don't want users to be accessing anything that we don't // document. this._resourceSlot = resourceSlot; // Map from absolute paths to stat objects (or null if the file does // not exist). this._statCache = Object.create(null); // Map from control file names (e.g. package.json, .babelrc) to // absolute paths, or null to indicate absence. this._controlFileCache = Object.create(null); // Map from imported module identifier strings (possibly relative) to // fully require.resolve'd module identifiers. this._resolveCache = Object.create(null); // Communicate to compiler plugins that methods like addJavaScript // accept a lazy finalizer function as a second argument, so that // compilation can be avoided until/unless absolutely necessary. this.supportsLazyCompilation = true; // Communicate to compiler plugins that this version of Meteor // is able to support top level await // TODO: maybe this should also check if the file and package meet the // minimum requirements to use top level await (file isn't bare, and // package uses core-runtime and modules) this.supportsTopLevelAwait = true; } getContentsAsBuffer() { var self = this; return self._resourceSlot.inputResource.data; } getPackageName() { var self = this; return self._resourceSlot.packageSourceBatch.unibuild.pkg.name; } isPackageFile() { return !! this.getPackageName(); } isApplicationFile() { return ! this.getPackageName(); } getSourceRoot(tolerant = false) { const sourceRoot = this._resourceSlot.packageSourceBatch.sourceRoot; if (_.isString(sourceRoot)) { return sourceRoot; } if (! tolerant) { const name = this.getPackageName(); throw new Error( "Unknown source root for " + ( name ? "package " + name : "app")); } return null; } getPathInPackage() { var self = this; return self._resourceSlot.inputResource.path; } getFileOptions() { // XXX fileOptions only exists on some resources (of type "source"). The JS // resources might not have this property. const { inputResource } = this._resourceSlot; return inputResource.fileOptions || (inputResource.fileOptions = {}); } hmrAvailable() { const fileOptions = this.getFileOptions() || {}; return this._resourceSlot.hmrAvailable() && !fileOptions.bare; } readAndWatchFileWithHash(path) { const sourceBatch = this._resourceSlot.packageSourceBatch; return readAndWatchFileWithHash( sourceBatch.unibuild.watchSet, files.convertToPosixPath(path), ); } readAndWatchFile(path) { return this.readAndWatchFileWithHash(path).contents; } _stat(absPath) { return _.has(this._statCache, absPath) ? this._statCache[absPath] : this._statCache[absPath] = optimisticStatOrNull(absPath); } // Search ancestor directories for control files (e.g. package.json, // .babelrc), and return the absolute path of the first one found, or // null if the search failed. findControlFile(basename) { let absPath = this._controlFileCache[basename]; if (typeof absPath === "string") { return absPath; } const sourceRoot = this.getSourceRoot(true); if (! _.isString(sourceRoot)) { return this._controlFileCache[basename] = null; } let dir = files.pathDirname( files.pathJoin(sourceRoot, this.getPathInPackage())); while (true) { absPath = files.pathJoin(dir, basename); const stat = this._stat(absPath); if (stat && stat.isFile()) { return this._controlFileCache[basename] = absPath; } if (files.pathBasename(dir) === "node_modules") { // The search for control files should not escape node_modules. return this._controlFileCache[basename] = null; } if (dir === sourceRoot) break; let parentDir = files.pathDirname(dir); if (parentDir === dir) break; dir = parentDir; } return this._controlFileCache[basename] = null; } _resolveCacheLookup(id, parentPath) { const byId = this._resolveCache[id]; return byId && byId[parentPath]; } _resolveCacheStore(id, parentPath, resolved) { let byId = this._resolveCache[id]; if (! byId) { byId = this._resolveCache[id] = Object.create(null); } return byId[parentPath] = resolved; } resolve(id, parentPath) { parentPath = parentPath || files.pathJoin( this.getSourceRoot(), this.getPathInPackage() ); const resId = this._resolveCacheLookup(id, parentPath); if (resId) { return resId; } const parentStat = optimisticStatOrNull(parentPath); if (! parentStat || ! parentStat.isFile()) { throw new Error("Not a file: " + parentPath); } const batch = this._resourceSlot.packageSourceBatch; const resolver = batch.getResolver({ // Make sure we use a server architecture when resolving, so that we // don't accidentally use package.json "browser" fields. // https://github.com/meteor/meteor/issues/9870 targetArch: archinfo.host(), }); const resolved = resolver.resolve(id, parentPath); if (resolved === "missing") { const error = new Error("Cannot find module '" + id + "'"); error.code = "MODULE_NOT_FOUND"; throw error; } return this._resolveCacheStore(id, parentPath, resolved.id); } require(id, parentPath) { return this._require(id, parentPath); } // This private helper method exists to prevent ambiguity between the // module-global `require` function and the method name. _require(id, parentPath) { return require(this.resolve(id, parentPath)); } getArch() { return this._resourceSlot.packageSourceBatch.processor.arch; } getSourceHash() { return this._resourceSlot.inputResource.hash; } /** * @summary Returns the extension that matched the compiler plugin. * The longest prefix is preferred. * @returns {String} */ getExtension() { return this._resourceSlot.inputResource.extension; } /** * @summary Returns a list of symbols declared as exports in this target. The * result of `api.export('symbol')` calls in target's control file such as * package.js. * @memberof InputFile * @returns {String[]} */ getDeclaredExports() { var self = this; return self._resourceSlot.packageSourceBatch.unibuild.declaredExports; } /** * @summary Returns a relative path that can be used to form error messages or * other display properties. Can be used as an input to a source map. * @memberof InputFile * @returns {String} */ getDisplayPath() { var self = this; return self._resourceSlot.packageSourceBatch.unibuild.pkg._getServePath(self.getPathInPackage()); } /** * @summary Web targets only. Add a stylesheet to the document. Not available * for linter build plugins. * @param {Object} options * @param {String} options.path The requested path for the added CSS, may not * be satisfied if there are path conflicts. * @param {String} options.data The content of the stylesheet that should be * added. * @param {String|Object} options.sourceMap A stringified JSON * sourcemap, in case the stylesheet was generated from a different * file. * @param {Function} lazyFinalizer Optional function that can be called * to obtain any remaining options that may be * expensive to compute, and thus should only be * computed if/when we are sure this CSS will be used * by the application. * @memberOf InputFile * @instance */ addStylesheet(options, lazyFinalizer) { this._resourceSlot.addStylesheet(options, lazyFinalizer); } /** * @summary Add JavaScript code. The code added will only see the * namespaces imported by this package as runtime dependencies using * ['api.use'](#PackageAPI-use). If the file being compiled was added * with the bare flag, the resulting JavaScript won't be wrapped in a * closure. * @param {Object} options * @param {String} options.path The path at which the JavaScript file * should be inserted, may not be honored in case of path conflicts. * @param {String} options.data The code to be added. * @param {String|Object} options.sourceMap A stringified JSON * sourcemap, in case the JavaScript file was generated from a * different file. * @param {Function} lazyFinalizer Optional function that can be called * to obtain any remaining options that may be * expensive to compute, and thus should only be * computed if/when we are sure this JavaScript will * be used by the application. * @memberOf InputFile * @instance */ addJavaScript(options, lazyFinalizer) { this._resourceSlot.addJavaScript(options, lazyFinalizer); } /** * @summary Add a file to serve as-is to the browser or to include on * the browser, depending on the target. On the web, it will be served * at the exact path requested. For server targets, it can be retrieved * using `Assets.getText` or `Assets.getBinary`. * @param {Object} options * @param {String} options.path The path at which to serve the asset. * @param {Buffer|String} options.data The data that should be placed in the * file. * @param {String} [options.hash] Optionally, supply a hash for the output * file. * @param {Function} lazyFinalizer Optional function that can be called * to obtain any remaining options that may be * expensive to compute, and thus should only be * computed if/when we are sure this asset will be * used by the application. * @memberOf InputFile * @instance */ addAsset(options, lazyFinalizer) { this._resourceSlot.addAsset(options, lazyFinalizer); } /** * @summary Works in web targets only. Add markup to the `head` or `body` * section of the document. * @param {Object} options * @param {String} options.section Which section of the document should * be appended to. Can only be "head" or "body". * @param {String} options.data The content to append. * @param {Function} lazyFinalizer Optional function that can be called * to obtain any remaining options that may be * expensive to compute, and thus should only be * computed if/when we are sure this HTML will be used * by the application. * @memberOf InputFile * @instance */ async addHtml(options, lazyFinalizer) { if (typeof lazyFinalizer === "function") { // For now, just call the lazyFinalizer function immediately. Since // HTML is not compiled, this immediate invocation is probably // permanently appropriate for addHtml, whereas methods like // addJavaScript benefit from waiting to call lazyFinalizer. Object.assign(options, await lazyFinalizer()); } this._resourceSlot.addHtml(options); } _reportError(message, info) { this._resourceSlot.addError(message, info); if (! this.getFileOptions().lazy) { super._reportError(message, info); } } } class ResourceSlot { constructor(unibuildResourceInfo, sourceProcessor, packageSourceBatch) { const self = this; // XXX ideally this should be an classy object, but it's not. self.inputResource = unibuildResourceInfo; // Everything but JS. self.outputResources = []; // JS, which gets linked together at the end. self.jsOutputResources = []; // Errors encountered while processing this resource. self.errors = []; self.sourceProcessor = sourceProcessor; self.packageSourceBatch = packageSourceBatch; if (self.inputResource.type === "source") { if (self.sourceProcessor) { // If we have a sourceProcessor, it will handle the adding of the // final processed JavaScript. } else if (self.inputResource.extension === "js") { self._addDirectlyToJsOutputResources(); } } else { if (self.sourceProcessor) { throw Error("sourceProcessor for non-source? " + JSON.stringify(self.inputResource)); } // Any resource that isn't handled by compiler plugins just gets passed // through. if (self.inputResource.type === "js") { self._addDirectlyToJsOutputResources(); } else { self.outputResources.push(self.inputResource); } } } // Add this resource directly to jsOutputResources without modifying the // original data. #HardcodeJs _addDirectlyToJsOutputResources() { this.addJavaScript({ ...(this.inputResource.fileOptions || {}), path: this.inputResource.path, data: this.inputResource.data, }); } _getOption(name, options) { if (options && _.has(options, name)) { return options[name]; } const fileOptions = this.inputResource.fileOptions; return fileOptions && fileOptions[name]; } _isLazy(options, isJavaScript) { let lazy = this._getOption("lazy", options); if (typeof lazy === "boolean") { return lazy; } const isApp = ! this.packageSourceBatch.unibuild.pkg.name; if (! isApp) { // Meteor package files must be explicitly added by api.addFiles or // api.mainModule, and are implicitly eager unless specified // otherwise via this.inputResource.fileOptions.lazy, which we // already checked above. return false; } // The rest of this method assumes we're considering a resource in an // application rather than a Meteor package. if (! this.packageSourceBatch.useMeteorInstall) { // If this application is somehow still not using the module system, // then everything is eagerly loaded. return false; } const { isTest = false, isAppTest = false, } = global.testCommandMetadata || {}; const runningTests = isTest || isAppTest; if (isJavaScript) { if (runningTests) { const testModule = this._getOption("testModule", options); // If we set fileOptions.testModule = true in _inferFileOptions, // then consider this module an eager entry point for tests. If we // set it to false (rather than leaving it undefined), that means // a meteor.testModule was configured in package.json, and this // test module was not it. In that case, we fall through to the // mainModule check, ignoring isTestFilePath, because we can // assume this is not an eager test module. If testModule was not // set to a boolean, then isTestFilePath should determine if this // is an eager test module. const isEagerTestModule = typeof testModule === "boolean" ? testModule : isTestFilePath(this.inputResource.path); if (isEagerTestModule) { // If we know it's eager, then it isn't lazy. return false; } if (! isAppTest) { // If running `meteor test` without the --full-app option, then // any JS modules that are not eager test modules must be lazy. return true; } } // PackageSource#_inferFileOptions (in package-source.js) sets the // mainModule option to false to indicate that a meteor.mainModule // was configured for this architecture, but this module was not it. // It's important to wait until this point (ResourceSlot#_isLazy) to // make the final call, because we can finally tell whether the // output resource is JavaScript or not (non-JS resources are not // affected by the meteor.mainModule option). const mainModule = this._getOption("mainModule", options); if (typeof mainModule === "boolean") { return ! mainModule; } } // In other words, the imports directory remains relevant for non-JS // resources, and for JS resources in the absence of an explicit // meteor.mainModule configuration in package.json. const splitPath = this.inputResource.path.split(files.pathSep); const isInImports = splitPath.indexOf("imports") >= 0; return isInImports; } _isBare(options) { return !! this._getOption("bare", options); } hmrAvailable() { return this.packageSourceBatch.hmrAvailable; } addStylesheet(options, lazyFinalizer) { if (! this.sourceProcessor) { throw Error("addStylesheet on non-source ResourceSlot?"); } // In contrast to addJavaScript, CSS resources passed to addStylesheet // default to being eager (non-lazy). options.lazy = this._isLazy(options, false); const cssResource = new CssOutputResource({ resourceSlot: this, options, lazyFinalizer, }); if (this.packageSourceBatch.useMeteorInstall && cssResource.lazy) { // If the current packageSourceBatch supports modules, and this CSS // file is lazy, add it as a lazy JS module instead of adding it // unconditionally as a CSS resource, so that it can be imported // when needed. const jsResource = this.addJavaScript(options, async () => { const result = {}; let css = await this.packageSourceBatch.processor .minifyCssResource(cssResource); if (! css && typeof css !== "string") { // The minifier didn't do anything, so we should use the // original contents of cssResource.data. css = (await cssResource.data).toString("utf8"); if (cssResource.sourceMap) { // Add the source map as an asset, and append a // sourceMappingURL comment to the end of the CSS text that // will be dynamically inserted when/if this JS module is // evaluated at runtime. Note that this only happens when the // minifier did not modify the CSS, and thus does not happen // when we are building for production. const { servePath } = this.addAsset({ path: jsResource.targetPath + ".map.json", data: JSON.stringify(cssResource.sourceMap) }); css += "\n//# sourceMappingURL=" + servePath + "\n"; } } result.data = Buffer.from(cssToCommonJS(css), "utf8"); // The JavaScript module that dynamically loads this CSS should // not inherit the source map of the original CSS output. result.sourceMap = null; return result; }); } else { // Eager CSS is added unconditionally to a combined <style> tag at // the beginning of the <head>. If the corresponding module ever // gets imported, its module.exports object should be an empty stub, // rather than a <style> node added dynamically to the <head>. this.addJavaScript({ ...options, // As above, the JavaScript module that dynamically loads this CSS // should not inherit the source map of the original CSS output. sourceMap: null, data: Buffer.from( "// These styles have already been applied to the document.\n", "utf8"), lazy: true, // If a compiler plugin calls addJavaScript with the same // sourcePath, that code should take precedence over this empty // stub, so setting .implicit marks the resource as disposable. }).implicit = true; // TODO[FIBERS]: Look into this. We probably don't want addStylesheet // to be async, and I'm also not sure the old behavior here is what we wanted // // if (! cssResource.lazy &&s // ! Buffer.isBuffer(cssResource.data)) { // // If there was an error processing this file, cssResource.data // // will not be a Buffer, and accessing cssResource.data here // // should cause the error to be reported via inputFile.error. // return; // } this.outputResources.push(cssResource); } } addJavaScript(options, lazyFinalizer) { // #HardcodeJs this gets called by constructor in the "js" case if (! this.sourceProcessor && this.inputResource.extension !== "js" && this.inputResource.type !== "js") { throw Error("addJavaScript on non-source ResourceSlot?"); } const resource = new JsOutputResource({ resourceSlot: this, options, lazyFinalizer, }); this.jsOutputResources.push(resource); return resource; } // Emit a lazy stub JS resource for a source file whose compilation failed // (errors recorded in this.errors) without producing any output resource. // The stub exists only so the deferred errors are carried into the // ImportScanner: JsOutputResource shares this.errors, so reportPendingErrors // surfaces the real compile error the moment the module is imported. The // stub is always lazy (eager files already report errors synchronously via // InputFile#_reportError) and is filtered out of the final output by // ImportScanner#getOutputFiles once hasErrors is set, so its empty data is // never used. See https://github.com/meteor/meteor/issues/10366. addErrorPlaceholder() { return this.addJavaScript({ path: this.inputResource.path, sourcePath: this.inputResource.path, data: Buffer.from("", "utf8"), lazy: true, }); } addAsset(options, lazyFinalizer) { if (! this.sourceProcessor) { throw Error("addAsset on non-source ResourceSlot?"); } const resource = new AssetOutputResource({ resourceSlot: this, options, lazyFinalizer, }); this.outputResources.push(resource); return resource; } addHtml(options) { const self = this; const unibuild = self.packageSourceBatch.unibuild; if (! archinfo.matches(unibuild.arch, "web")) { throw new Error("Document sections can only be emitted to " + "web targets: " + self.inputResource.path); } if (options.section !== "head" && options.section !== "body") { throw new Error("'section' must be 'head' or 'body': " + self.inputResource.path); } if (typeof options.data !== "string") { throw new Error("'data' option to appendDocument must be a string: " + self.inputResource.path); } self.outputResources.push({ type: options.section, data: Buffer.from(files.convertToStandardLineEndings(options.data), 'utf8'), lazy: self._isLazy(options, false), }); } addError(message, info) { // If this file is ever actually imported, only then will we report // the error. this.errors.push({ message, info }); } } class OutputResource { constructor({ type, resourceSlot, options = Object.create(null), lazyFinalizer = null, }) { this._lazyFinalizer = lazyFinalizer; this._initialOptions = options; this._finalizerPromise = null; // Share the errors array of the resourceSlot. this._errors = resourceSlot.errors; let sourcePath = resourceSlot.inputResource.path; if (_.has(options, "sourcePath") && typeof options.sourcePath === "string") { sourcePath = options.sourcePath; } const targetPath = options.path || sourcePath; const servePath = targetPath ? resourceSlot.packageSourceBatch.unibuild.pkg._getServePath(targetPath) : resourceSlot.inputResource.servePath; Object.assign(this, { type, lazy: resourceSlot._isLazy(options, true), bare: resourceSlot._isBare(options), mainModule: !! resourceSlot._getOption("mainModule", options), sourcePath, targetPath, servePath, sourceRoot: resourceSlot.packageSourceBatch.sourceRoot, // Remember the source hash so that changes to the source that // disappear after compilation can still contribute to the hash. // Bypassing SourceResource.hash getter so if the compiler plugin doesn't // use the resource's content we don't unnecessarily mark it as used. _inputHash: resourceSlot.inputResource._hash, }); } async finalize() { if (this._finalizerPromise) { await this._finalizerPromise; } else if (this._lazyFinalizer) { const finalize = this._lazyFinalizer; this._lazyFinalizer = null; // It's important to initialize this._finalizerPromise to the new // Promise before calling finalize(), so there's no possibility of // finalize() triggering code that reenters this function before we // have the final version of this._finalizerPromise. If this code // used `new Promise(resolve => resolve(finalize()))` instead of // `Promise.resolve().then(finalize)`, the finalize() call would // begin before this._finalizerPromise was fully initialized. (this._finalizerPromise = Promise.resolve().then(finalize).then(result => { if (result) { Object.assign(this._initialOptions, result); } else if (this._errors.length === 0) { // In case the finalize() call failed without reporting any // errors, create at least one generic error that can be // reported when reportPendingErrors is called. const error = new Error("lazyFinalizer failed"); error.info = { resource: this, finalize }; this._errors.push(error); } // The this._finalizerPromise object only survives for the // duration of the initial finalization. this._finalizerPromise = null; })); await this._finalizerPromise; } } async hasPendingErrors() { await this.finalize(); return this._errors.length > 0; } async reportPendingErrors() { if (await this.hasPendingErrors()) { const firstError = this._errors[0]; buildmessage.error( firstError.message, firstError.info ); } return this._errors.length; } get data() { return this._get("data"); } set data(value) { return this._set("data", value); } get hash() { return this._get("hash"); } set hash(value) { return this._set("hash", value); } get sourceMap() { return this._get("sourceMap"); } set sourceMap(value) { return this._set("sourceMap", value); } // Method for getting properties that may be computed lazily, or that // require some one-time post-processing. async _get(name) { if (hasOwn.call(this, name)) { return this[name]; } if (await this.hasPendingErrors()) { // If you're considering using this resource, you should call // hasPendingErrors or reportPendingErrors to find out if it's safe // to access computed properties like .data, .hash, or .sourceMap. // If you get here without checking for errors first, those errors // will be fatal. throw new Error( `_get "${name}" called for file with pending errors | ERROR: ${JSON.stringify( this._errors[0] )}` ); } switch (name) { case "data": let { data = null } = this._initialOptions; if (typeof data === "string") { data = Buffer.from(data, "utf8"); } return this._set("data", data); case "hash": { const hashes = []; if (typeof this._inputHash === "string") { hashes.push(this._inputHash); } hashes.push(sha1(await this._get("data"))); return this._set("hash", sha1(...hashes)); } case "sourceMap": let { sourceMap } = this._initialOptions; if (sourceMap && typeof sourceMap === "string") { sourceMap = JSON.parse(sourceMap); } return this._set("sourceMap", sourceMap); } if (! hasOwn.call(this._initialOptions, name)) { throw new Error(`Unknown JsOutputResource property: ${name}`); } return this[name] = this._initialOptions[name]; } // This method must be used to set any properties that have a getter // defined above (data, hash, sourceMap). _set(name, value) { Object.defineProperty(this, name, { value, enumerable: true, writable: true, configurable: true, }); return value; } } class JsOutputResource extends OutputResource { constructor(params) { super({ ...params, type: "js" }); } } class CssOutputResource extends OutputResource { constructor(params) { super({ ...params, type: "css" }); this.refreshable = true; } } class AssetOutputResource extends OutputResource { constructor(params) { super({ ...params, type: "asset" }); // Asset paths must always be explicitly specified. this.path = this._initialOptions.path; // Eagerness/laziness should never matter for assets. delete this.lazy; } } export class PackageSourceBatch { constructor(unibuild, processor, { sourceRoot, linkerCacheDir, scannerCacheDir, }) { const self = this; buildmessage.assertInJob(); self.unibuild = unibuild; self.processor = processor; self.sourceRoot = sourceRoot; self.linkerCacheDir = linkerCacheDir; self.scannerCacheDir = scannerCacheDir; self.importExtensions = [".js", ".json"]; self._nodeModulesPaths = null; self.resourceSlots = []; // Compute imports by merging the exports of all of the packages we // use. Note that in the case of conflicting symbols, later packages get // precedence. // // We don't get imports from unordered dependencies (since they // may not be defined yet) or from // weak/debugOnly/prodOnly/testOnly dependencies (because the // meaning of a name shouldn't be affected by the non-local // decision of whether or not an unrelated package in the target // depends on something). self.importedSymbolToPackageName = {}; // map from symbol to supplying package name self.deps = []; } async init() { const self = this; for (const resource of this.unibuild.resources) { const slot = await self.makeResourceSlot(resource); if (slot) { self.resourceSlots.push(slot); } } await compiler.eachUsedUnibuild({ dependencies: self.unibuild.uses, arch: self.processor.arch, isopackCache: self.processor.isopackCache, skipUnordered: true, // don't import symbols from debugOnly, prodOnly and testOnly packages, because // if the package is not linked it will cause a runtime error. // the code must access them with `Package["my-package"].MySymbol`. skipDebugOnly: true, skipProdOnly: true, skipTestOnly: true, }, (depUnibuild, { weak, unordered }) => { let packageName = depUnibuild.pkg.name; _.each(depUnibuild.declaredExports, function (symbol) { // Slightly hacky implementation of test-only exports. if (! symbol.testOnly || self.unibuild.pkg.isTest) { self.importedSymbolToPackageName[symbol.name] = packageName; } }); self.deps.push({ package: packageName, weak, unordered }); }); self.useMeteorInstall = _.isString(self.sourceRoot) && self.processor.isopackCache.uses( self.unibuild.pkg, "modules", self.unibuild.arch ); const isDevelopment = self.processor.buildMode === 'development'; const usesHMRPackage = self.unibuild.pkg.name !== "hot-module-replacement" && self.processor.isopackCache.uses( self.unibuild.pkg, "hot-module-replacement", self.unibuild.arch ); const supportedArch = archinfo.matches(self.unibuild.arch, 'web'); self.hmrAvailable = self.useMeteorInstall && isDevelopment && usesHMRPackage && supportedArch; // These are the options that should be passed as the second argument // to meteorInstall when modules in this source batch are installed. self.meteorInstallOptions = self.useMeteorInstall ? { extensions: self.importExtensions, } : null; } async compileOneJsResource(resource) { const slot = await this.makeResourceSlot({ type: "source", extension: "js", // Need { data, path, hash } here, at least. ...resource, fileOptions: { lazy: true, ...resource.fileOptions, } }); if (slot) { // If the resource was not handled by a source processor, it will be // added directly to slot.jsOutputResources by makeResourceSlot, // meaning we do not need to compile it. if (slot.jsOutputResources.length > 0) { return slot.jsOutputResources; } const inputFile = new InputFile(slot); inputFile.supportsLazyCompilation = false; if (slot.sourceProcessor) { const { userPlugin } = slot.sourceProcessor; if (userPlugin) { const markedMethod = buildmessage.markBoundary( userPlugin.processFilesForTarget, userPlugin ); try { await markedMethod([inputFile]); } catch (e) { buildmessage.exception(e); } } } return slot.jsOutputResources; } return []; } async makeResourceSlot(resource) { let sourceProcessor = null; if (resource.type === "source") { var extension = resource.extension; if (extension === null) { const filename = files.pathBasename(resource.path); sourceProcessor = (await this._getSourceProcessorSet()).getByFilename(filename); if (! sourceProcessor) { buildmessage.error( `no plugin found for ${ resource.path } in ` + `${ this.unibuild.pkg.displayName() }; a plugin for ${ filename } ` + `was active when it was published but none is now`); return null; // recover by ignoring } } else { sourceProcessor = (await this._getSourceProcessorSet()).getByExtension(extension); // If resource.extension === 'js', it's ok for there to be no // sourceProcessor, since we #HardcodeJs in ResourceSlot. if (! sourceProcessor && extension !== 'js') { buildmessage.error( `no plugin found for ${ resource.path } in ` + `${ this.unibuild.pkg.displayName() }; a plugin for *.${ extension } ` + `was active when it was published but none is now`); return null; // recover by ignoring } this.addImportExtension(extension); } } return new ResourceSlot(resource, sourceProcessor, this); } addImportExtension(extension) { extension = extension.toLowerCase(); if (! extension.startsWith(".")) { extension = "." + extension; } if (this.importExtensions.indexOf(extension) < 0) { this.importExtensions.push(extension); } } getResolver(options = {}) { return Resolver.getOrCreate({ caller: "PackageSourceBatch#getResolver", sourceRoot: this.sourceRoot, targetArch: this.processor.arch, extensions: this.importExtensions, nodeModulesPaths: this._getNodeModulesPaths(), ...options, }); } _getNodeModulesPaths() { if (! this._nodeModulesPaths) { const nmds = this.unibuild.nodeModulesDirectories; this._nodeModulesPaths = []; _.each(nmds, (nmd, path) => { if (! nmd.local) { this._nodeModulesPaths.push( files.convertToOSPath(path.replace(/\/$/g, ""))); } }); } return this._nodeModulesPaths; } async _getSourceProcessorSet() { if (! this._sourceProcessorSet) { buildmessage.assertInJob(); const isopack = this.unibuild.pkg; const activePluginPackages = await compiler.getActivePluginPackages(isopack, { uses: this.unibuild.uses, isopackCache: this.processor.isopackCache }); this._sourceProcessorSet = new buildPluginModule.SourceProcessorSet( isopack.displayName(), { hardcodeJs: true }); for (const otherPkg of activePluginPackages) { await otherPkg.ensurePluginsInitialized(); this._sourceProcessorSet.merge(otherPkg.sourceProcessors.compiler, { arch: this.processor.arch, }); } } return this._sourceProcessorSet; } // Returns a map from package names to arrays of JS output files. static async computeJsOutputFilesMap(sourceBatches) { const map = new Map; sourceBatches.forEach(batch => { const name = batch.unibuild.pkg.name || null; const inputFiles = []; batch.resourceSlots.forEach(slot => { // If a compiler plugin reported an error for a lazy source file but // produced no output resource (e.g. a Blaze/Stylus template that // fails to compile and aborts before calling addJavaScript), the // error was deferred by InputFile#_reportError (because lazy files // should not break the build unless they are actually imported). // Without an output resource to carry that error, it is never handed // to the ImportScanner, so reportPendingErrors() is never called and // the error is silently swallowed -- the import resolves to "missing" // and the only symptom is a runtime "Cannot find module" error. Emit // a lazy stub JS resource that carries the deferred error so the // ImportScanner can surface it (via reportPendingErrors) if and when // the file is imported, while leaving unimported broken files // harmless. See https://github.com/meteor/meteor/issues/10366. if (slot.sourceProcessor && slot.errors.length > 0 && slot.jsOutputResources.length === 0) { slot.addErrorPlaceholder(); } inputFiles.push(...slot.jsOutputResources); }); map.set(name, { files: inputFiles, mainModule: _.find(inputFiles, file => file.mainModule) || null, batch, importScannerWatchSet: new WatchSet(), }); }); if (! map.has("modules")) { // In the unlikely event that no package is using the modules // package, then the map is already complete, and we don't need to // do any import scanning. return this._watchOutputFiles(map); } // Append install(<name>) calls to the install-packages.js file in the // modules package for every Meteor package name used. for (const file of map.get("modules").files) { if (file.sourcePath !== "install-packages.js") { continue; } const meteorPackageInstalls = []; map.forEach((info, name) => { if (! name) return; const mainModule = info.mainModule && `meteor/${name}/${info.mainModule.targetPath}`; meteorPackageInstalls.push( "install(" + JSON.stringify(name) + (mainModule ? ", " + JSON.stringify(mainModule) : '') + ");\n" ); }); if (meteorPackageInstalls.length === 0) { continue; } const fileData = await file.data; const bufferData = Buffer.from( fileData.toString("utf8") + "\n" + meteorPackageInstalls.join(""), "utf8" ); const fileHash = sha1(bufferData); // The getter's from file (file.data and file.hash) are async, unfortunately. // That's why we need the Object.assign here. Object.assign(file, { data: bufferData, hash: fileHash }); break; } // Map from module identifiers that previously could not be imported // to lists of info objects describing the failed imports. const allMissingModules = Object.create(null); // Records the subset of allMissingModules that were successfully // relocated to a source batch that could handle them. const allRelocatedModules = Object.create(null); const scannerMap = new Map(); for (const batch of sourceBatches) { const name = batch.unibuild.pkg.name || null; const isApp = ! name; if (! batch.useMeteorInstall && ! isApp) { // If this batch represents a package that does not use the module // system, then we don't need to scan its dependencies. continue; } const nodeModulesPaths = []; _.each(batch.unibuild.nodeModulesDirectories, (nmd, sourcePath) => { if (! nmd.local) { // Local node_modules directories will be found by the // ImportScanner, but we need to tell it about any external // node_modules directories (e.g. .npm/package/node_modules). nodeModulesPaths.push(sourcePath); } }); const entry = map.get(name); const scanner = new ImportScanner({ name, bundleArch: batch.processor.arch, extensions: batch.importExtensions, sourceRoot: batch.sourceRoot, nodeModulesPaths, watchSet: entry.importScannerWatchSet, cacheDir: batch.scannerCacheDir, }); await scanner.addInputFiles(entry.files); if (batch.useMeteorInstall) { await scanner.scanImports(); await ImportScanner.mergeMissing( allMissingModules, scanner.allMissingModules ); } scannerMap.set(name, scanner); } async function handleMissing(missingModules) { const missingMap = new Map; for (let [id, importInfoList] of Object.entries(missingModules)) { const parts = id.split("/"); let name = null; if (parts[0] === "meteor") { let found = false; name = parts[1]; if (parts.length > 2) { parts[1] = "."; id = parts.slice(1).join("/"); found = true; } else { const entry = map.get(name); const mainModule = entry && entry.mainModule; if (mainModule) { id = "./" + mainModule.sourcePath; found = true; } } if (! found) { continue; } } if (! scannerMap.has(name)) { continue; } if (! missingMap.has(name)) { missingMap.set(name, Object.create(null)); } await ImportScanner.mergeMissing( missingMap.get(name), { [id]: importInfoList } ); } const nextMissingModules = Object.create(null); for (const [name, missing] of missingMap) { const { newlyAdded, newlyMissing } = await scannerMap.get(name).scanMissingModules(missing); await ImportScanner.mergeMissing(allRelocatedModules, newlyAdded); await ImportScanner.mergeMissing(nextMissingModules, newlyMissing); } if (! _.isEmpty(nextMissingModules)) { await handleMissing(nextMissingModules); } } await handleMissing(allMissingModules); Object.keys(allRelocatedModules).forEach(id => { delete allMissingModules[id]; }); this._warnAboutMissingModules(allMissingModules); scannerMap.forEach((scanner, name) => { const isApp = ! name; const outputFiles = scanner.getOutputFiles(); const entry = map.get(name); if (entry.batch.useMeteorInstall) { outputFiles.forEach(file => { // Give every file the same meteorInstallOptions object, so the // linker can emit one meteorInstall call per options object. file.meteorInstallOptions = entry.batch.meteorInstallOptions; }); } if (isApp) { const appFilesWithoutNodeModules = []; const modulesEntry = map.get("modules"); outputFiles.forEach(file => { const parts = file.absModuleId.split("/"); assert.strictEqual(parts[0], ""); const nodeModulesIndex = parts.indexOf("node_modules"); if (nodeModulesIndex === -1 || (nodeModulesIndex === 1 && parts[2] === "meteor")) { appFilesWithoutNodeModules.push(file); } else { // There's a chance the application does not use the module // system, which means entry.batch.useMeteorInstall will be // false and file.meteorInstallOptions will not have been // defined above. In that case, just use meteorInstallOptions // from the modules source batch, since we're moving this file // into the modules bundle. file.meteorInstallOptions = file.meteorInstallOptions || modulesEntry.batch.meteorInstallOptions; // This file is going to be installed in a node_modules // directory, so we move it to the modules bundle so that it // can be imported by any package that uses the modules // package. Note that this includes all files within any // node_modules directory in the app, even though packages in // client/node_modules will not be importable by Meteor // packages, because it's important for all npm packages in // the app to share the same limited scope (i.e. the scope of // the modules package). However, these relocated files have // their own meteorInstallOptions, and will be installed with // a separate call to meteorInstall in the modules bundle. modulesEntry.files.push(file); } }); entry.files = appFilesWithoutNodeModules; } else { entry.files = outputFiles; } }); return this._watchOutputFiles(map); } static _watchOutputFiles(jsOutputFilesMap) { // Watch all output files produced by computeJsOutputFilesMap. jsOutputFilesMap.forEach(entry => { entry.files.forEach(file => { // Output resources are not directly marked as definitely used. Instead, // its input resource might be if its content was used by a build plugin. // This is checked in Target#_emitResources if (file instanceof OutputResource) { return; } const { sourcePath, absPath = sourcePath && files.pathJoin(entry.batch.sourceRoot, sourcePath), } = file; const { importScannerWatchSet } = entry; if ( typeof absPath === "string" && // Blindly calling importScannerWatchSet.addFile would be // logically correct here, but we can save the cost of calling // optimisticHashOrNull(absPath) if the importScannerWatchSet // already knows about the file and it has not been marked as // potentially unused. ! importScannerWatchSet.isDefinitelyUsed(absPath) ) { // If this file was previously added to the importScannerWatchSet // using the addPotentiallyUnusedFile method (see compileUnibuild), // calling addFile here will update its usage status to reflect that // the ImportScanner did, in fact, end up "using" the file. importScannerWatchSet.addFile(absPath, optimisticHashOrNull(absPath)); } }); }); return jsOutputFilesMap; } static _warnAboutMissingModules(missingModules) { const topLevelMissingIDs = {}; const warnings = []; Object.keys(missingModules).forEach(id => { // Issue at most one warning per module identifier, even if there // are multiple parent modules that failed to import it. missingModules[id].some(info => maybeWarn(id, info)); }); function maybeWarn(id, info) { if (info.packageName) { // Silence warnings generated by Meteor packages, since package // authors can be trusted to test their packages, and may have // different/better approaches to ensuring their dependencies are // available. This blanket check makes some of the checks below // redundant, but I would rather create a bit of dead code than // risk introducing bugs when/if this check is reverted. return; } if (info.possiblySpurious) { // Silence warnings for missing dependencies in Browserify/Webpack // bundles, since we can reasonably conclude at this point that // they are false positives. return; } if (id.startsWith("#")) { // CSS fragment identifiers are not JS modules. return; } if (id in serverLibPackages && archinfo.matches(info.bundleArch, "os")) { // Packages in dev_bundle/server-lib/node_modules can always be // resolved at runtime on the server, so we don't need to warn // about them here. return; } if (id === "meteor-node-stubs" && info.packageName === "modules" && info.parentPath.endsWith("stubs.js")) { // Don't warn about the require("meteor-node-stubs") call in // packages/modules/stubs.js. return; } const parts = id.split("/"); if ("./".indexOf(id.charAt(0)) < 0) { const packageDir = parts[0].startsWith("@") ? parts[0] + "/" + parts[1] : parts[0]; if (packageDir === "meteor") { // Don't print warnings for uninstalled Meteor packages. return; } if (! _.has(topLevelMissingIDs, packageDir)) { // This information will be used to recommend installing npm // packages below. topLevelMissingIDs[packageDir] = id; } if (id.startsWith("meteor-node-stubs/deps/")) { // Instead of printing a warning that meteor-node-stubs/deps/fs // is missing, warn about the "fs" module, but still recommend // installing meteor-node-stubs via npm below. id = parts.slice(2).join("/"); } } else if (info.packageName) { // Disable warnings about relative module resolution failures in // Meteor packages, since there's not much the application // developer can do about those. return; } warnings.push(` ${JSON.stringify(id)} in ${ info.parentPath} (${info.bundleArch})`); return true; } if (warnings.length > 0) { Console.rawWarn("\nUnable to resolve some modules:\n\n"); warnings.forEach(text => Console.warn(text)); Console.warn(); const topLevelKeys = Object.keys(topLevelMissingIDs); if (topLevelKeys.length > 0) { Console.warn("If you notice problems related to these missing modules, consider running:"); Console.warn(); Console.warn(" meteor npm install --save " + topLevelKeys.join(" ")); Console.warn(); } } } // Called by bundler's Target._emitResources. It returns the actual resources // that end up in the program for this package. By this point, it knows what // its dependencies are and what their exports are, so it can set up // linker-style imports and exports. async getResources(jsResources, onCacheKey) { buildmessage.assertInJob(); const resources = []; this.resourceSlots.forEach(slot => { resources.push(...slot.outputResources); }); resources.push(...await this._linkJS(jsResources, onCacheKey)); return resources; } async _linkJS(jsResources, onCacheKey = () => {}) { const self = this; buildmessage.assertInJob(); var bundleArch = self.processor.arch; // Run the linker. const isApp = ! self.unibuild.pkg.name; const isWeb = archinfo.matches(self.unibuild.arch, "web"); const linkerOptions = { isApp, bundleArch, // I was confused about this, so I am leaving a comment -- the // combinedServePath is either [pkgname].js or [pluginName]:plugin.js. // XXX: If we change this, we can get rid of source arch names! combinedServePath: isApp ? "/app.js" : "/packages/" + colonConverter.convert( self.unibuild.pkg.name + (self.unibuild.kind === "main" ? "" : (":" + self.unibuild.kind)) + ".js"), name: self.unibuild.pkg.name || null, declaredExports: _.pluck(self.unibuild.declaredExports, 'name'), imports: self.importedSymbolToPackageName, // XXX report an error if there is a package called global-imports includeSourceMapInstructions: isWeb, deps: self.deps }; const fileHashes = []; const cacheKeyPrefix = sha1(JSON.stringify({ linkerOptions, files: await Promise.all( jsResources.map(async (inputFile) => { fileHashes.push(await inputFile.hash); return { meteorInstallOptions: inputFile.meteorInstallOptions, absModuleId: inputFile.absModuleId, sourceMap: !!(await inputFile.sourceMap), mainModule: inputFile.mainModule, imported: inputFile.imported, alias: inputFile.alias, lazy: inputFile.lazy, bare: inputFile.bare, }; }) ) })); const cacheKeySuffix = sha1(JSON.stringify({ LINKER_CACHE_SALT, fileHashes })); const cacheKey = `${cacheKeyPrefix}_${cacheKeySuffix}`; await onCacheKey(cacheKey, jsResources); if (LINKER_CACHE.has(cacheKey)) { if (CACHE_DEBUG) { console.log('LINKER IN-MEMORY CACHE HIT:', linkerOptions.name, bundleArch); } return LINKER_CACHE.get(cacheKey); } const cacheFilename = self.linkerCacheDir && files.pathJoin(self.linkerCacheDir, cacheKey + '.cache'); const wildcardCacheFilename = cacheFilename && files.pathJoin(self.linkerCacheDir, cacheKeyPrefix + "_*.cache"); // The return value from _linkJS includes Buffers, but we want everything to // be JSON for writing to the disk cache. This function converts the string // version to the Buffer version. function bufferifyJSONReturnValue(resources) { resources.forEach((r) => { r.data = Buffer.from(r.data, 'utf8'); }); } if (cacheFilename) { let diskCached = null; try { diskCached = files.readJSONOrNull(cacheFilename); } catch (e) { // Ignore JSON parse errors; pretend there was no cache. if (!(e instanceof SyntaxError)) { throw e; } } if (diskCached && diskCached instanceof Array) { // Fix the non-JSON part of our return value. bufferifyJSONReturnValue(diskCached); if (CACHE_DEBUG) { console.log('LINKER DISK CACHE HIT:', linkerOptions.name, bundleArch); } // Add the bufferized value of diskCached to the in-memory LRU cache // so we don't have to go to disk next time. LINKER_CACHE.set(cacheKey, diskCached); return diskCached; } } if (CACHE_DEBUG) { console.log('LINKER CACHE MISS:', linkerOptions.name, bundleArch); } // nb: linkedFiles might be aliased to an entry in LINKER_CACHE, so don't // mutate anything from it. let canCache = true; let linkedFiles = null; await buildmessage.enterJob('linking', async () => { linkedFiles = await linker.fullLink(jsResources, linkerOptions); if (buildmessage.jobHasMessages()) { canCache = false; } }); // Add each output as a resource const ret = linkedFiles.map((file) => { const sm = (typeof file.sourceMap === 'string') ? JSON.parse(file.sourceMap) : file.sourceMap; return { type: "js", // This is a string... but we will convert it to a Buffer // before returning from the method (but after writing // to cache). data: file.source, hash: file.hash, servePath: file.servePath, sourceMap: sm }; }); let retAsJSON; if (canCache && cacheFilename) { retAsJSON = JSON.stringify(ret); } // Convert strings to buffers, now that we've serialized it. bufferifyJSONReturnValue(ret); if (canCache) { LINKER_CACHE.set(cacheKey, ret); if (cacheFilename) { // Write asynchronously. try { await files.rm_recursive_deferred(wildcardCacheFilename); } finally { await files.writeFileAtomically(cacheFilename, retAsJSON); } } } return ret; } } _.each([ "getResources", "_linkJS", ], method => { const proto = PackageSourceBatch.prototype; proto[method] = Profile( "PackageSourceBatch#" + method, proto[method] ); }); // static methods to measure in profile _.each([ "computeJsOutputFilesMap", "_watchOutputFiles" ], method => { PackageSourceBatch[method] = Profile( "PackageSourceBatch." + method, PackageSourceBatch[method]); });