var _ = require('underscore'); var archinfo = require('../utils/archinfo.js'); var buildmessage = require('../utils/buildmessage.js'); var bundler = require('./bundler.js'); var isopack = require('./isopack.js'); var meteorNpm = require('./meteor-npm.js'); var watch = require('../fs/watch.js'); var Console = require('../console/console.js').Console; var files = require('../fs/files.js'); var colonConverter = require('../utils/colon-converter.js'); var linterPluginModule = require('./linter-plugin.js'); var compileStepModule = require('./compiler-deprecated-compile-step.js'); var Profile = require('../tool-env/profile.js').Profile; import { SourceProcessorSet } from './build-plugin.js'; var compiler = exports; // Whenever you change anything about the code that generates isopacks, bump // this version number. The idea is that the "format" field of the isopack // JSON file only changes when the actual specified structure of the // isopack/unibuild changes, but this version (which is build-tool-specific) // can change when the the contents (not structure) of the built output // changes. So eg, if we improve the linker's static analysis, this should be // bumped. // // You should also update this whenever you update any of the packages used // directly by the isopack creation process since they do not end up as watched // dependencies. (At least for now, packages only used in target creation (eg // minifiers) don't require you to update BUILT_BY, though you will need to quit // and rerun "meteor run".) compiler.BUILT_BY = 'meteor/17'; // This is a list of all possible architectures that a build can target. (Client // is expanded into 'web.browser' and 'web.cordova') compiler.ALL_ARCHES = [ "os", "web.browser", "web.cordova" ]; compiler.compile = Profile(function (packageSource, options) { return `compiler.compile(${ packageSource.name || 'the app' })`; }, function (packageSource, options) { buildmessage.assertInCapture(); var packageMap = options.packageMap; var isopackCache = options.isopackCache; var includeCordovaUnibuild = options.includeCordovaUnibuild; var pluginWatchSet = packageSource.pluginWatchSet.clone(); var plugins = {}; var pluginProviderPackageNames = {}; // Build plugins _.each(packageSource.pluginInfo, function (info) { buildmessage.enterJob({ title: "building plugin `" + info.name + "` in package `" + packageSource.name + "`", rootPath: packageSource.sourceRoot }, function () { // XXX we should probably also pass options.noLineNumbers into // buildJsImage so it can pass it back to its call to // compiler.compile var buildResult = bundler.buildJsImage({ name: info.name, packageMap: packageMap, isopackCache: isopackCache, use: info.use, sourceRoot: packageSource.sourceRoot, sources: info.sources, // While we're not actually "serving" the file, the serveRoot is used to // calculate file names in source maps. serveRoot: 'packages/' + packageSource.name, npmDependencies: info.npmDependencies, // Plugins have their own npm dependencies separate from the // rest of the package, so they need their own separate npm // shrinkwrap and cache state. npmDir: files.pathResolve( files.pathJoin(packageSource.sourceRoot, '.npm', 'plugin', info.name)) }); // Add this plugin's dependencies to our "plugin dependency" // WatchSet. buildResult.watchSet will end up being the merged // watchSets of all of the unibuilds of the plugin -- plugins have // only one unibuild and this should end up essentially being just // the source files of the plugin. // // Note that we do this even on error, so that you can fix the error // and have the runner restart. pluginWatchSet.merge(buildResult.watchSet); if (buildmessage.jobHasMessages()) return; _.each(buildResult.usedPackageNames, function (packageName) { pluginProviderPackageNames[packageName] = true; }); // Register the built plugin's code. if (!_.has(plugins, info.name)) plugins[info.name] = {}; plugins[info.name][buildResult.image.arch] = buildResult.image; }); }); // Grab any npm dependencies. Keep them in a cache in the package // source directory so we don't have to do this from scratch on // every build. // // Go through a specialized npm dependencies update process, // ensuring we don't get new versions of any (sub)dependencies. This // process also runs mostly safely multiple times in parallel (which // could happen if you have two apps running locally using the same // package). // // We run this even if we have no dependencies, because we might // need to delete dependencies we used to have. var isPortable = true; var nodeModulesPath = null; if (packageSource.npmCacheDirectory) { if (meteorNpm.updateDependencies(packageSource.name, packageSource.npmCacheDirectory, packageSource.npmDependencies)) { nodeModulesPath = files.pathJoin(packageSource.npmCacheDirectory, 'node_modules'); if (! meteorNpm.dependenciesArePortable(packageSource.npmCacheDirectory)) isPortable = false; } } // Find all the isobuild:* pseudo-packages that this package depends on. Why // do we need to do this? Well, we actually load the plugins in this package // before we've fully compiled the package --- plugins are loaded before the // compiler builds the unibuilds in this package (because plugins are allowed // to act on the package itself). But when we load plugins, we need to know if // the package depends on (eg) isobuild:compiler-plugin, to know if the plugin // is allowed to call Plugin.registerCompiler. At this point, the Isopack // object doesn't yet have any unibuilds... but isopack.js doesn't have access // to the PackageSource either (because it needs to work with both // compiled-from-source and loaded-from-disk packages). So we need to make // sure here that the Isopack has *some* reference to the isobuild features // which the unibuilds depend on, so we do it here (and also in // Isopack#initFromPath). var isobuildFeatures = []; packageSource.architectures.forEach((sourceArch) => { sourceArch.uses.forEach((use) => { if (!use.weak && isIsobuildFeaturePackage(use.package) && isobuildFeatures.indexOf(use.package) === -1) { isobuildFeatures.push(use.package); } }); }); isobuildFeatures = _.uniq(isobuildFeatures); var isopk = new isopack.Isopack; isopk.initFromOptions({ name: packageSource.name, metadata: packageSource.metadata, version: packageSource.version, isTest: packageSource.isTest, plugins: plugins, pluginWatchSet: pluginWatchSet, cordovaDependencies: packageSource.cordovaDependencies, npmDiscards: packageSource.npmDiscards, includeTool: packageSource.includeTool, debugOnly: packageSource.debugOnly, prodOnly: packageSource.prodOnly, pluginCacheDir: options.pluginCacheDir, isobuildFeatures }); _.each(packageSource.architectures, function (architecture) { if (architecture.arch === 'web.cordova' && ! includeCordovaUnibuild) return; var unibuildResult = compileUnibuild({ isopack: isopk, sourceArch: architecture, isopackCache: isopackCache, nodeModulesPath: nodeModulesPath, isPortable: isPortable, noLineNumbers: options.noLineNumbers }); _.extend(pluginProviderPackageNames, unibuildResult.pluginProviderPackageNames); }); if (options.includePluginProviderPackageMap) { isopk.setPluginProviderPackageMap( packageMap.makeSubsetMap(_.keys(pluginProviderPackageNames))); } return isopk; }); // options: // - isopack // - isopackCache // - includeCordovaUnibuild compiler.lint = function (packageSource, options) { // Note: the buildmessage context of compiler.lint and lintUnibuild is a // normal error message context (eg, there might be errors from initializing // plugins in getLinterSourceProcessorSet). We return the linter warnings as // our return value. buildmessage.assertInJob(); const warnings = new buildmessage._MessageSet; let linted = false; _.each(packageSource.architectures, function (architecture) { // skip Cordova if not required if (! options.includeCordovaUnibuild && architecture.arch === 'web.cordova') { return; } const unibuildWarnings = lintUnibuild({ isopack: options.isopack, isopackCache: options.isopackCache, sourceArch: architecture }); if (unibuildWarnings) { linted = true; warnings.merge(unibuildWarnings); } }); return {warnings, linted}; }; compiler.getMinifiers = function (packageSource, options) { buildmessage.assertInJob(); var minifiers = []; _.each(packageSource.architectures, function (architecture) { var activePluginPackages = getActivePluginPackages(options.isopack, { isopackCache: options.isopackCache, uses: architecture.uses }); _.each(activePluginPackages, function (otherPkg) { otherPkg.ensurePluginsInitialized(); _.each(otherPkg.sourceProcessors.minifier.allSourceProcessors, (sp) => { minifiers.push(sp); }); }); }); minifiers = _.uniq(minifiers); // check for extension-wise uniqness _.each(['js', 'css'], function (ext) { var plugins = _.filter(minifiers, function (plugin) { return _.contains(plugin.extensions, ext); }); if (plugins.length > 1) { var packages = _.map(plugins, function (p) { return p.isopack.name; }); buildmessage.error(packages.join(', ') + ': multiple packages registered minifiers for extension "' + ext + '".'); } }); return minifiers; }; function getLinterSourceProcessorSet({isopack, activePluginPackages}) { buildmessage.assertInJob(); const sourceProcessorSet = new SourceProcessorSet( isopack.displayName, { allowConflicts: true }); _.each(activePluginPackages, function (otherPkg) { otherPkg.ensurePluginsInitialized(); sourceProcessorSet.merge(otherPkg.sourceProcessors.linter); }); return sourceProcessorSet; } var lintUnibuild = function ({isopack, isopackCache, sourceArch}) { // Note: the buildmessage context of compiler.lint and lintUnibuild is a // normal error message context (eg, there might be errors from initializing // plugins in getLinterSourceProcessorSet). We return the linter warnings as // our return value. buildmessage.assertInJob(); var activePluginPackages = getActivePluginPackages( isopack, { isopackCache, uses: sourceArch.uses }); const sourceProcessorSet = getLinterSourceProcessorSet({isopack, activePluginPackages}); // bail out early if we had trouble loading plugins or if we're not // going to lint anything if (buildmessage.jobHasMessages() || sourceProcessorSet.isEmpty()) { return null; } const unibuild = _.findWhere(isopack.unibuilds, {arch: sourceArch.arch}); if (! unibuild) { throw Error(`No ${ sourceArch.arch } unibuild for ${ isopack.name }!`); } var sourceItems = sourceArch.getSourcesFunc( sourceProcessorSet, unibuild.watchSet); const linterMessages = buildmessage.capture(() => { runLinters({ isopackCache, sourceItems, sourceProcessorSet, inputSourceArch: sourceArch, watchSet: unibuild.watchSet }); }); return linterMessages; }; // options.sourceArch is a SourceArch to compile. Process all source files // through the appropriate legacy handlers. Create a new Unibuild and add it to // options.isopack. // // Returns a list of source files that were used in the compilation. var compileUnibuild = function (options) { buildmessage.assertInCapture(); var isopk = options.isopack; var inputSourceArch = options.sourceArch; var isopackCache = options.isopackCache; var nodeModulesPath = options.nodeModulesPath; var isPortable = options.isPortable; var noLineNumbers = options.noLineNumbers; var isApp = ! inputSourceArch.pkg.name; var resources = []; var pluginProviderPackageNames = {}; var watchSet = inputSourceArch.watchSet.clone(); // *** Determine and load active plugins var activePluginPackages = getActivePluginPackages(isopk, { uses: inputSourceArch.uses, isopackCache: isopackCache, // If other package is built from source, then we need to rebuild this // package if any file in the other package that could define a plugin // changes. getActivePluginPackages will add entries to this WatchSet. pluginProviderWatchSet: watchSet, pluginProviderPackageNames }); // *** Assemble the SourceProcessorSet from the plugins. This data // structure lets us decide what to do with each file: which plugin // should process it in what method. // // We also build a SourceProcessorSet for this package's linters even // though we're not linting right now. This is so we can tell the // difference between an file added to a package as a linter config // file (not handled by any compiler), and a file that's truly not // handled by anything (which is an error unless explicitly declared // as a static asset). let sourceProcessorSet, linterSourceProcessorSet; buildmessage.enterJob("determining active plugins", () => { sourceProcessorSet = new SourceProcessorSet( isopk.displayName(), { hardcodeJs: true}); activePluginPackages.forEach((otherPkg) => { otherPkg.ensurePluginsInitialized(); // Note that this may log a buildmessage if there are conflicts. sourceProcessorSet.merge(otherPkg.sourceProcessors.compiler); }); // Used to excuse functions from the "undeclared static asset" check. linterSourceProcessorSet = getLinterSourceProcessorSet({ activePluginPackages, isopack: isopk }); if (buildmessage.jobHasMessages()) { // Recover by not calling getSourcesFunc and pretending there are no // items. sourceProcessorSet = null; } }); // *** Determine source files // Note: the getSourcesFunc function isn't expected to add its // source files to watchSet; rather, the watchSet is for other // things that the getSourcesFunc consulted (such as directory // listings or, in some hypothetical universe, control files) to // determine its source files. const sourceItems = sourceProcessorSet ? inputSourceArch.getSourcesFunc(sourceProcessorSet, watchSet) : []; if (nodeModulesPath) { // If this slice has node modules, we should consider the shrinkwrap file // to be part of its inputs. (This is a little racy because there's no // guarantee that what we read here is precisely the version that's used, // but it's better than nothing at all.) // // Note that this also means that npm modules used by plugins will get // this npm-shrinkwrap.json in their pluginDependencies (including for all // packages that depend on us)! This is good: this means that a tweak to // an indirect dependency of the coffee-script npm module used by the // coffeescript package will correctly cause packages with *.coffee files // to be rebuilt. var shrinkwrapPath = nodeModulesPath.replace( /node_modules$/, 'npm-shrinkwrap.json'); watch.readAndWatchFile(watchSet, shrinkwrapPath); } // *** Process each source file var addAsset = function (contents, relPath, hash) { // XXX hack if (! inputSourceArch.pkg.name) relPath = relPath.replace(/^(private|public)\//, ''); resources.push({ type: "asset", data: contents, path: relPath, servePath: colonConverter.convert( files.pathJoin(inputSourceArch.pkg.serveRoot, relPath)), hash: hash }); }; _.each(sourceItems, function (source) { var relPath = source.relPath; var fileOptions = _.clone(source.fileOptions) || {}; var absPath = files.pathResolve(inputSourceArch.pkg.sourceRoot, relPath); var filename = files.pathBasename(relPath); // Find the handler for source files with this extension (unless explicitly // tagged as a static asset). let classification = null; if (! fileOptions.isAsset) { classification = sourceProcessorSet.classifyFilename( filename, inputSourceArch.arch); if (classification.type === 'wrong-arch') { // This file is for a compiler plugin but not for this arch. Skip it, // and don't even watch it. (eg, skip CSS preprocessor files on the // server.) This `return` goes on to the next `_.each(sourceItems` // iteration. return; } if (classification.type === 'unmatched') { // This is not matched by any compiler plugin or legacy source handler, // and it wasn't explicitly tagged as a static asset (with {isAsset: // true} in packages, or by being in private/public in apps). // // Prior to the batch-plugins project, these would be implicitly treated // as static assets. Now we consider this to be an error; you need to // explicitly tell that you want something to be a static asset by // including the isAsset flag in packages, or by putting it in // public/private in an app. // // This is a backwards-incompatible change, but it doesn't affect // previously-published packages (because the check is occuring in the // compiler), and it doesn't affect apps (where random files outside of // private/public never end up in the source list anyway). // // As one special case, if a file is unmatched by the compiler // SourceProcessorSet but is matched by the linker SourceProcessorSet // (ie, a linter config file), we don't report an error; this is so that // you can run `api.addFiles('.jshintrc')` and have it work and not also // add the file as a static asset. (This is only relevant for // packages.) We don't put these files in the WatchSet, though; that // happens via compiler.lint. if (isApp) { // This shouldn't happen, because initFromAppDir's getSourcesFunc // should only return sources that have isAsset set or which match // sourceProcessorSet. throw Error("app contains non-asset files without plugin? " + relPath + " - " + filename); } const linterClassification = linterSourceProcessorSet.classifyFilename( filename, inputSourceArch.arch); if (linterClassification.type !== 'unmatched') { // The linter knows about this, so we'll just ignore it instead of // throwing an error. return; } buildmessage.error( `No plugin known to handle file '${ relPath }'. If you want this ` + `file to be a static asset, pass the {isAsset: true} option ` + `to api.addFiles; eg, api.addFiles('${relPath}', 'client', ` + `{isAsset: true}).`); // recover by ignoring return; } } // readAndWatchFileWithHash returns an object carrying a buffer with the // file-contents. The buffer contains the original data of the file (no EOL // transforms from the tools/files.js part). var file = watch.readAndWatchFileWithHash(watchSet, absPath); var hash = file.hash; var contents = file.contents; Console.nudge(true); if (contents === null) { // It really sucks to put this check here, since this isn't publish // code... // XXX We think this code can probably be deleted at this point because // people probably aren't trying to use files with colons in them any // more. if (source.relPath.match(/:/)) { buildmessage.error( "Couldn't build this package on Windows due to the following file " + "with a colon -- " + source.relPath + ". Please rename and " + "and re-publish the package."); } else { buildmessage.error("File not found: " + source.relPath); } // recover by ignoring (but still watching the file) return; } // Add static assets. if (fileOptions.isAsset) { addAsset(contents, relPath, hash); return; } if (classification.isNonLegacySource()) { // This is source used by a new-style compiler plugin; it will be fully // processed later in the bundler. resources.push({ type: "source", extension: classification.extension || null, usesDefaultSourceProcessor: !! classification.usesDefaultSourceProcessor, data: contents, path: relPath, hash: hash, fileOptions: fileOptions }); return; } if (classification.type !== 'legacy-handler') { throw Error("unhandled type: " + classification.type); } // OK, time to handle legacy handlers. var compileStep = compileStepModule.makeCompileStep( source, file, inputSourceArch, { resources: resources, addAsset: addAsset }); try { (buildmessage.markBoundary(classification.legacyHandler))(compileStep); } catch (e) { e.message = e.message + " (compiling " + relPath + ")"; buildmessage.exception(e); // Recover by ignoring this source file (as best we can -- the // handler might already have emitted resources) } }); // *** Determine captured variables var declaredExports = _.map(inputSourceArch.declaredExports, function (symbol) { return _.pick(symbol, ['name', 'testOnly']); }); // *** Consider npm dependencies and portability var arch = inputSourceArch.arch; if (arch === "os" && ! isPortable) { // Contains non-portable compiled npm modules, so set arch correctly arch = archinfo.host(); } if (! archinfo.matches(arch, "os")) { // npm modules only work on server architectures nodeModulesPath = undefined; } // *** Output unibuild object isopk.addUnibuild({ kind: inputSourceArch.kind, arch: arch, uses: inputSourceArch.uses, implies: inputSourceArch.implies, watchSet: watchSet, nodeModulesPath: nodeModulesPath, declaredExports: declaredExports, resources: resources }); return { pluginProviderPackageNames: pluginProviderPackageNames }; }; function runLinters({inputSourceArch, isopackCache, sourceItems, sourceProcessorSet, watchSet}) { // The buildmessage context here is for linter warnings only! runLinters // should not do anything that can have a real build failure. buildmessage.assertInCapture(); if (sourceProcessorSet.isEmpty()) { return; } // First we calculate the symbols imported into the current package by // packages we depend on. This is because most JS linters are going to want to // warn about the use of unknown global variables, and the linker import // system works by doing something that looks a whole lot like using // undeclared globals! That said, we don't actually know the imports that // will be active when an app is built if the versions of the imported // packages differ from those available at package lint time. But it's a good // heuristic, at least. (If we transition from linker to ES2015 modules, we // won't have the issue any more.) // We want to look at the arch of the used packages that matches the arch // we're compiling. Normally when we call compiler.eachUsedUnibuild, we're // either specifically looking at archinfo.host() because we're doing // something related to plugins (which always run in the host environment), or // we're in the process of building a bundler Target (a program), which has a // specific arch which is never 'os'. In this odd case, though, we're trying // to run eachUsedUnibuild at package-compile time (not bundle time), so the // only 'arch' we've heard of might be 'os', if we're building a portable // unibuild. In that case, we should look for imports in the host arch if it // exists instead of failing because a dependency does not have an 'os' // unibuild. const whichArch = inputSourceArch.arch === 'os' ? archinfo.host() : inputSourceArch.arch; // For linters, figure out what are the global imports from other packages // that we use directly, or are implied. const globalImports = ['Package']; if (archinfo.matches(inputSourceArch.arch, "os")) { globalImports.push('Npm', 'Assets'); } compiler.eachUsedUnibuild({ dependencies: inputSourceArch.uses, arch: whichArch, isopackCache: isopackCache, skipUnordered: true, // don't import symbols from debugOnly and prodOnly 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, }, (unibuild) => { if (unibuild.pkg.name === inputSourceArch.pkg.name) return; _.each(unibuild.declaredExports, (symbol) => { if (! symbol.testOnly || inputSourceArch.isTest) { globalImports.push(symbol.name); } }); }); // sourceProcessor.id -> {sourceProcessor, sources: [WrappedSourceItem]} const sourceItemsForLinter = {}; sourceItems.forEach((sourceItem) => { const { relPath } = sourceItem; const classification = sourceProcessorSet.classifyFilename( files.pathBasename(relPath), inputSourceArch.arch); // If we don't have a linter for this file (or we do but it's only on // another arch), skip without even reading the file into a WatchSet. if (classification.type === 'wrong-arch' || classification.type === 'unmatched') return; // We shouldn't ever add a legacy handler and we're not hardcoding JS for // linters, so we should always have SourceProcessor if anything matches. if (! classification.sourceProcessors) { throw Error( `Unexpected classification for ${ relPath }: ${ classification.type }`); } // Read the file and add it to the WatchSet. const {hash, contents} = watch.readAndWatchFileWithHash( watchSet, files.pathResolve(inputSourceArch.pkg.sourceRoot, relPath)); const wrappedSource = { relPath, contents, hash, arch: inputSourceArch.arch, 'package': inputSourceArch.pkg.name }; // There can be multiple linters on a file. classification.sourceProcessors.forEach((sourceProcessor) => { if (! sourceItemsForLinter.hasOwnProperty(sourceProcessor.id)) { sourceItemsForLinter[sourceProcessor.id] = { sourceProcessor, sources: [] }; } sourceItemsForLinter[sourceProcessor.id].sources.push(wrappedSource); }); }); // Run linters on files. This skips linters that don't have any files. _.each(sourceItemsForLinter, ({sourceProcessor, sources}) => { const sourcesToLint = sources.map( wrappedSource => new linterPluginModule.LintingFile(wrappedSource) ); const linter = sourceProcessor.userPlugin.processFilesForPackage; function archToString(arch) { if (arch.match(/web\.cordova/)) return "Cordova"; if (arch.match(/web\..*/)) return "Client"; if (arch.match(/os.*/)) return "Server"; throw new Error("Don't know how to display the arch: " + arch); } buildmessage.enterJob({ title: "linting files with " + sourceProcessor.isopack.name + " for " + inputSourceArch.pkg.displayName() + " (" + archToString(inputSourceArch.arch) + ")" }, () => { try { var markedLinter = buildmessage.markBoundary(linter.bind( sourceProcessor.userPlugin)); markedLinter(sourcesToLint, { globals: globalImports }); } catch (e) { buildmessage.exception(e); } }); }); }; // takes an isopack and returns a list of packages isopack depends on, // containing at least one plugin export function getActivePluginPackages(isopk, { uses, isopackCache, pluginProviderPackageNames, pluginProviderWatchSet }) { // XXX we used to include our own plugins only if we were the // "use" role. now we include them everywhere because we don't have // a special "use" role anymore. it's not totally clear to me what // the correct behavior should be -- we need to resolve whether we // think about plugins as being global to a package or particular // to a unibuild. // (there's also some weirdness here with handling implies, because // the implies field is on the target unibuild, but we really only care // about packages.) var activePluginPackages = [isopk]; if (pluginProviderPackageNames) pluginProviderPackageNames[isopk.name] = true; // We don't use plugins from weak dependencies, because the ability // to compile a certain type of file shouldn't depend on whether or // not some unrelated package in the target has a dependency. And we // skip unordered dependencies, because it's not going to work to // have circular build-time dependencies. // // eachUsedUnibuild takes care of pulling in implied dependencies for us (eg, // templating from standard-app-packages). // // We pass archinfo.host here, not self.arch, because it may be more specific, // and because plugins always have to run on the host architecture. compiler.eachUsedUnibuild({ dependencies: uses, arch: archinfo.host(), isopackCache: isopackCache, skipUnordered: true // implicitly skip weak deps by not specifying acceptableWeakPackages option }, function (unibuild) { if (unibuild.pkg.name === isopk.name) return; if (pluginProviderPackageNames) { pluginProviderPackageNames[unibuild.pkg.name] = true; } if (pluginProviderWatchSet) { pluginProviderWatchSet.merge(unibuild.pkg.pluginWatchSet); } if (_.isEmpty(unibuild.pkg.plugins)) return; activePluginPackages.push(unibuild.pkg); }); activePluginPackages = _.uniq(activePluginPackages); return activePluginPackages; } // Iterates over each in options.dependencies as well as unibuilds implied by // them. The packages in question need to already be built and in // options.isopackCache. // // Skips isobuild:* pseudo-packages. compiler.eachUsedUnibuild = function ( options, callback) { buildmessage.assertInCapture(); var dependencies = options.dependencies; var arch = options.arch; var isopackCache = options.isopackCache; var acceptableWeakPackages = options.acceptableWeakPackages || {}; var processedUnibuildId = {}; var usesToProcess = []; _.each(dependencies, function (use) { if (options.skipUnordered && use.unordered) return; if (use.weak && !_.has(acceptableWeakPackages, use.package)) return; usesToProcess.push(use); }); while (! _.isEmpty(usesToProcess)) { var use = usesToProcess.shift(); // We only care about real packages, not isobuild:* psuedo-packages. if (isIsobuildFeaturePackage(use.package)) continue; var usedPackage = isopackCache.getIsopack(use.package); // Ignore this package if we were told to skip debug-only packages and it is // debug-only. if (usedPackage.debugOnly && options.skipDebugOnly) continue; // Ditto prodOnly. if (usedPackage.prodOnly && options.skipProdOnly) continue; var unibuild = usedPackage.getUnibuildAtArch(arch); if (!unibuild) { // The package exists but there's no unibuild for us. A buildmessage has // already been issued. Recover by skipping. continue; } if (_.has(processedUnibuildId, unibuild.id)) continue; processedUnibuildId[unibuild.id] = true; callback(unibuild, { unordered: !!use.unordered, weak: !!use.weak }); _.each(unibuild.implies, function (implied) { usesToProcess.push(implied); }); } }; // Note: this code is duplicated in packages/constraint-solver/solver.js export function isIsobuildFeaturePackage(packageName) { return packageName.startsWith('isobuild:'); } export const KNOWN_ISOBUILD_FEATURE_PACKAGES = { // This package directly calls Plugin.registerCompiler. Package authors // must explicitly depend on this feature package to use the API. 'isobuild:compiler-plugin': ['1.0.0'], // This package directly calls Plugin.registerMinifier. Package authors // must explicitly depend on this feature package to use the API. 'isobuild:minifier-plugin': ['1.0.0'], // This package directly calls Plugin.registerLinter. Package authors // must explicitly depend on this feature package to use the API. 'isobuild:linter-plugin': ['1.0.0'], // This package is only published in the isopack-2 format, not isopack-1 or // older. ie, it contains "source" files for compiler plugins, not just // JS/CSS/static assets/head/body. // This is implicitly added at publish time to any such package; package // authors don't have to add it explicitly. It isn't relevant for local // packages, which can be rebuilt if possible by the older tool. // // Specifically, this is to avoid the case where a package is published with a // dependency like `api.use('less@1.0.0 || 2.0.0')` and the publication // selects the newer compiler plugin version to generate the isopack. The // published package (if this feature package wasn't implicitly included) // could still be selected by the Version Solver to be used with an old // Isobuild... just because less@2.0.0 depends on isobuild:compiler-plugin // doesn't mean it couldn't choose less@1.0.0, which is not actually // compatible with this published package. (Constraints of the form described // above are not very helpful, but at least we can prevent old Isobuilds from // choking on confusing packages.) // // (Why not isobuild:isopack@2.0.0? Well, that would imply that Version Solver // would have to choose only one isobuild:isopack feature version, which // doesn't make sense here.) 'isobuild:isopack-2': ['1.0.0'], // This package uses the `prodOnly` metadata flag, which causes it to // automatically depend on the `isobuild:prod-only` feature package. 'isobuild:prod-only': ['1.0.0'], };