var archinfo = require('../utils/archinfo.js'); var buildmessage = require('../utils/buildmessage.js'); var buildPluginModule = require('./build-plugin.js'); var colonConverter = require('../utils/colon-converter.js'); var files = require('../fs/files.js'); var compiler = require('./compiler.js'); var linker = require('./linker.js'); var util = require('util'); var _ = require('underscore'); var Profile = require('../tool-env/profile.js').Profile; import {sha1, readAndWatchFileWithHash} from '../fs/watch.js'; import LRU from 'lru-cache'; import Fiber from 'fibers'; import {sourceMapLength} from '../utils/utils.js'; import {Console} from '../console/console.js'; import ImportScanner from './import-scanner.js'; import {cssToCommonJS} from "./css-modules.js"; import Resolver from "./resolver.js"; import { optimisticStatOrNull, } from "../fs/optimistic.js"; import { isTestFilePath } from './test-files.js'; // 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 = 13; // Increment this number to force relinking. const LINKER_CACHE = new LRU({ max: CACHE_SIZE, // Cache is measured in bytes. We don't care about servePath. // Key is JSONification of all options plus all hashes. length: function (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, isopackCache, linkerCacheDir, }) { const self = this; self.unibuilds = unibuilds; self.arch = arch; self.sourceRoot = sourceRoot; self.isopackCache = isopackCache; self.linkerCacheDir = linkerCacheDir; if (self.linkerCacheDir) { files.mkdir_p(self.linkerCacheDir); } } runCompilerPlugins() { const self = this; buildmessage.assertInJob(); // plugin id -> {sourceProcessor, resourceSlots} var sourceProcessorsWithSlots = {}; var sourceBatches = _.map(self.unibuilds, function (unibuild) { const { pkg: { name }, arch } = unibuild; const sourceRoot = name && self.isopackCache.getSourceRoot(name, arch) || self.sourceRoot; return new PackageSourceBatch(unibuild, self, { sourceRoot, linkerCacheDir: self.linkerCacheDir }); }); // 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. _.each(sourceProcessorsWithSlots, function (data, id) { var sourceProcessor = data.sourceProcessor; var resourceSlots = data.resourceSlots; var jobTitle = [ "processing files with ", sourceProcessor.isopack.name, " (for target ", self.arch, ")" ].join(''); Profile.time("plugin "+sourceProcessor.isopack.name, () => { buildmessage.enterJob({ title: jobTitle }, function () { var inputFiles = _.map(resourceSlots, function (resourceSlot) { return new InputFile(resourceSlot); }); var markedMethod = buildmessage.markBoundary( sourceProcessor.userPlugin.processFilesForTarget.bind( sourceProcessor.userPlugin)); try { 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); } 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() { var self = this; // XXX fileOptions only exists on some resources (of type "source"). The JS // resources might not have this property. return self._resourceSlot.inputResource.fileOptions || {}; } readAndWatchFileWithHash(path) { const osPath = files.convertToOSPath(path); const sourceRoot = this.getSourceRoot(); const relPath = files.pathRelative(sourceRoot, osPath); if (relPath.startsWith("..")) { throw new Error( `Attempting to read file outside ${ this.getPackageName() || "the app"}: ${osPath}` ); } const sourceBatch = this._resourceSlot.packageSourceBatch; return readAndWatchFileWithHash( sourceBatch.unibuild.watchSet, osPath ); } 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(); 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. * @memberOf InputFile * @instance */ addStylesheet(options) { var self = this; if (options.sourceMap && typeof options.sourceMap === 'string') { // XXX remove an anti-XSSI header? ")]}'\n" options.sourceMap = JSON.parse(options.sourceMap); } self._resourceSlot.addStylesheet(options); } /** * @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. * @memberOf InputFile * @instance */ addJavaScript(options) { var self = this; if (options.sourceMap && typeof options.sourceMap === 'string') { // XXX remove an anti-XSSI header? ")]}'\n" options.sourceMap = JSON.parse(options.sourceMap); } self._resourceSlot.addJavaScript(options); } /** * @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. * @memberOf InputFile * @instance */ addAsset(options) { var self = this; self._resourceSlot.addAsset(options); } /** * @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. * @memberOf InputFile * @instance */ addHtml(options) { var self = this; self._resourceSlot.addHtml(options); } _reportError(message, info) { if (this.getFileOptions().lazy === true) { // Files with fileOptions.lazy === true were not explicitly added to // the source batch via api.addFiles or api.mainModule, so any // compilation errors should not be fatal until the files are // actually imported by the ImportScanner. Attempting compilation is // still important for lazy files that might end up being imported // later, which is why we defang the error here, instead of avoiding // compilation preemptively. Note also that exceptions thrown by the // compiler will still cause build errors. this._resourceSlot.addError(message, info); } else { 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 = []; self.sourceProcessor = sourceProcessor; self.packageSourceBatch = packageSourceBatch; if (self.inputResource.type === "source") { if (sourceProcessor) { // If we have a sourceProcessor, it will handle the adding of the // final processed JavaScript. } else if (self.inputResource.extension === "js") { // If there is no sourceProcessor for a .js file, add the source // directly to the output. #HardcodeJs self.addJavaScript({ // XXX it's a shame to keep converting between Buffer and string, but // files.convertToStandardLineEndings only works on strings for now data: self.inputResource.data.toString('utf8'), path: self.inputResource.path, hash: self.inputResource.hash, bare: self.inputResource.fileOptions && (self.inputResource.fileOptions.bare || // XXX eventually get rid of backward-compatibility "raw" name // XXX COMPAT WITH 0.6.4 self.inputResource.fileOptions.raw) }); } } else { if (sourceProcessor) { throw Error("sourceProcessor for non-source? " + JSON.stringify(unibuildResourceInfo)); } // Any resource that isn't handled by compiler plugins just gets passed // through. if (self.inputResource.type === "js") { let resource = self.inputResource; if (! _.isString(resource.sourcePath)) { resource.sourcePath = self.inputResource.path; } if (! _.isString(resource.targetPath)) { resource.targetPath = resource.sourcePath; } self.jsOutputResources.push(resource); } else { self.outputResources.push(self.inputResource); } } } _getOption(name, options) { if (options && _.has(options, name)) { return options[name]; } const fileOptions = this.inputResource.fileOptions; return fileOptions && fileOptions[name]; } _isLazy(options) { let lazy = this._getOption("lazy", options); if (typeof lazy === "boolean") { return lazy; } // If file.lazy was not previously defined, mark the file lazy if // it is contained by an imports directory. Note that any files // contained by a node_modules directory will already have been // marked lazy in PackageSource#_inferFileOptions. Same for // non-test files if running (non-full-app) tests (`meteor test`) if (!this.packageSourceBatch.useMeteorInstall) { return false; } const splitPath = this.inputResource.path.split(files.pathSep); const isInImports = splitPath.indexOf("imports") >= 0; if (global.testCommandMetadata && (global.testCommandMetadata.isTest || global.testCommandMetadata.isAppTest)) { // test files should always be included, if we're running app // tests. return isInImports && !isTestFilePath(this.inputResource.path); } else { return isInImports; } } addStylesheet(options) { const self = this; if (! self.sourceProcessor) { throw Error("addStylesheet on non-source ResourceSlot?"); } const data = files.convertToStandardLineEndings(options.data); const useMeteorInstall = self.packageSourceBatch.useMeteorInstall; const sourcePath = this.inputResource.path; const targetPath = options.path || sourcePath; const resource = { refreshable: true, sourcePath, targetPath, servePath: self.packageSourceBatch.unibuild.pkg._getServePath(targetPath), hash: sha1(data), lazy: this._isLazy(options), }; if (useMeteorInstall && resource.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. resource.type = "js"; resource.data = new Buffer(cssToCommonJS(data, resource.hash), "utf8"); self.jsOutputResources.push(resource); } else { // Eager CSS is added unconditionally to a combined