var _ = require("underscore"); var buildmessage = require('../utils/buildmessage.js'); var utils = require('../utils/utils.js'); var compiler = require('./compiler.js'); var archinfo = require('../utils/archinfo'); var catalog = require('../packaging/catalog/catalog.js'); // It's important that we import these functions individually instead of // importing the whole files.* namespace, because now it's easier to tell // that this module doesn't actually touch the file system. import { pathRelative, convertToPosixPath, } from "../fs/files"; function toArray (x) { if (_.isArray(x)) { return x; } return x ? [x] : []; } function toArchArray(arch) { if (! Array.isArray(arch)) { arch = arch ? [arch] : compiler.ALL_ARCHES; } const seen = Object.create(null); arch.splice(0).forEach(where => { if (seen[where]) return; seen[where] = true; arch.push(...archinfo.mapWhereToArches(where)); }); // avoid using _.each so as to not add more frames to skip for (var i = 0; i < arch.length; ++i) { var inputArch = arch[i]; var isMatch = _.any(_.map(compiler.ALL_ARCHES, function (actualArch) { return archinfo.matches(actualArch, inputArch); })); if (! isMatch) { buildmessage.error( "Invalid 'where' argument: '" + inputArch + "'", // skip toArchArray in addition to the actual API function {useMyCaller: 1}); } } return arch; } // Iterates over the list of target archs and calls f(arch) for all archs // that match an element of self.allarchs. function forAllMatchingArchs (archs, f) { compiler.ALL_ARCHES.forEach(matchArch => { archs.some(arch => { if (archinfo.matches(matchArch, arch)) { f(matchArch); return true; } }); }); } /** * @name PackageAPI * @class PackageAPI * @instanceName api * @showInstanceName true * @global * @summary Type of the API object passed into the `Package.onUse` function. */ export class PackageAPI { constructor(options) { options = options || {}; this.buildingIsopackets = !!options.buildingIsopackets; // source files used. // It's a multi-level map structured as: // arch -> sources|assets -> relPath -> {relPath, fileOptions} this.files = {}; // symbols exported this.exports = {}; // packages used and implied (keys are 'package', 'unordered', and // 'weak'). an "implied" package is a package that will be used by a unibuild // which uses us. this.uses = {}; this.implies = {}; _.each(compiler.ALL_ARCHES, arch => { this.files[arch] = { assets: [], sources: [], main: null, }; this.exports[arch] = []; this.uses[arch] = []; this.implies[arch] = []; }); this.releaseRecords = []; this.pendingPromises = []; } // Called when this package wants to make another package be // used. Can also take literal package objects, if you have // anonymous packages you want to use (eg, app packages) // // @param arch 'web', 'web.browser', 'web.cordova', 'server', // or an array of those. // The default is ['web', 'server']. // // options can include: // // - unordered: if true, don't require this package to load // before us -- just require it to be loaded anytime. If // false, override a true value specified in // a previous call to use for this package name. (A // limitation of the current implementation is that this // flag is not tracked per-environment or per-role.) This // option can be used to resolve circular dependencies in // exceptional circumstances, eg, the 'meteor' package // depends on 'handlebars', but all packages (including // 'handlebars') have an implicit dependency on // 'meteor'. Internal use only -- future support of this // is not guaranteed. #UnorderedPackageReferences // // - weak: if true, don't require this package to load at all, but if // it's going to load, load it before us. Don't bring this // package's imports into our namespace and don't allow us to use // its plugins. (Has the same limitation as "unordered" that this // flag is not tracked per-environment or per-role; this may // change.) /** * @memberOf PackageAPI * @instance * @summary Depend on package `packagename`. * @locus package.js * @param {String|String[]} packageNames Packages being depended on. * Package names may be suffixed with an @version tag. * * In general, you must specify a package's version (e.g., * `'accounts@1.0.0'` to use version 1.0.0 or a higher * compatible version (ex: 1.0.1, 1.5.0, etc.) of the * `accounts` package). If you are sourcing core * packages from a Meteor release with `versionsFrom`, you may leave * off version names for core packages. You may also specify constraints, * such as `my:forms@=1.0.0` (this package demands `my:forms` at `1.0.0` exactly), * or `my:forms@1.0.0 || =2.0.1` (`my:forms` at `1.x.y`, or exactly `2.0.1`). * @param {String|String[]} [architecture] If you only use the package on the * server (or the client), you can pass in the second argument (e.g., * `'server'`, `'client'`, `'web.browser'`, `'web.cordova'`) to specify * what architecture the package is used with. You can specify multiple * architectures by passing in an array, for example `['web.cordova', 'os.linux']`. * @param {Object} [options] * @param {Boolean} options.weak Establish a weak dependency on a * package. If package A has a weak dependency on package B, it means * that including A in an app does not force B to be included too — but, * if B is included or by another package, then B will load before A. * You can use this to make packages that optionally integrate with or * enhance other packages if those packages are present. * When you weakly depend on a package you don't see its exports. * You can detect if the possibly-present weakly-depended-on package * is there by seeing if `Package.foo` exists, and get its exports * from the same place. * @param {Boolean} options.unordered It's okay to load this dependency * after your package. (In general, dependencies specified by `api.use` * are loaded before your package.) You can use this option to break * circular dependencies. */ use(names, arch, options) { var self = this; // Support `api.use(package, {weak: true})` without arch. if (_.isObject(arch) && !_.isArray(arch) && !options) { options = arch; arch = null; } options = options || {}; names = toArray(names); arch = toArchArray(arch); // A normal dependency creates an ordering constraint and a "if I'm // used, use that" constraint. Unordered dependencies lack the // former; weak dependencies lack the latter. There's no point to a // dependency that lacks both! if (options.unordered && options.weak) { buildmessage.error( "A dependency may not be both unordered and weak.", { useMyCaller: true }); // recover by ignoring return; } // using for loop rather than underscore to help with useMyCaller for (var i = 0; i < names.length; ++i) { var name = names[i]; try { var parsed = utils.parsePackageConstraint(name); } catch (e) { if (!e.versionParserError) { throw e; } buildmessage.error(e.message, {useMyCaller: true}); // recover by ignoring continue; } forAllMatchingArchs(arch, function (a) { self.uses[a].push({ package: parsed.package, constraint: parsed.constraintString, unordered: options.unordered || false, weak: options.weak || false }); }); } } // Called when this package wants packages using it to also use // another package. eg, for umbrella packages which want packages // using them to also get symbols or plugins from their components. /** * * @memberOf PackageAPI * @summary Give users of this package access to another package (by passing * in the string `packagename`) or a collection of packages (by passing in * an array of strings [`packagename1`, `packagename2`] * @locus package.js * @instance * @param {String|String[]} packageNames Name of a package, or array of * package names, with an optional @version component for each. * @param {String|String[]} [architecture] If you only use the package on * the server (or the client), you can pass in the second argument (e.g., * `'server'`, `'client'`, `'web.browser'`, `'web.cordova'`) to specify what * architecture the package is used with. You can specify multiple * architectures by passing in an array, for example `['web.cordova', * 'os.linux']`. */ imply(names, arch) { var self = this; // We currently disallow build plugins in // debugOnly/prodOnly/testOnly packages; but if you could use // imply in a debugOnly package, you could pull in the build // plugin from an implied package, which would have the same // problem as allowing build plugins directly in the package. So // no imply either! if (self.debugOnly || self.prodOnly || self.testOnly) { buildmessage.error("can't use imply in packages that are debugOnly, prodOnly or testOnly"); // recover by ignoring return; } names = toArray(names); arch = toArchArray(arch); // using for loop rather than underscore to help with useMyCaller for (var i = 0; i < names.length; ++i) { var name = names[i]; try { var parsed = utils.parsePackageConstraint(name); } catch (e) { if (!e.versionParserError) { throw e; } buildmessage.error(e.message, {useMyCaller: true}); // recover by ignoring continue; } // api.imply('isobuild:compiler-plugin') doesn't really make any sense. If // we change our mind and think it makes sense, we can always implement it // later... if (compiler.isIsobuildFeaturePackage(parsed.package)) { buildmessage.error( `to declare that your package requires the build tool feature ` + `'{parsed.package}', use 'api.use', not 'api.imply'`); // recover by ignoring continue; } forAllMatchingArchs(arch, function (a) { // We don't allow weak or unordered implies, since the main // purpose of imply is to provide imports and plugins. self.implies[a].push({ package: parsed.package, constraint: parsed.constraintString }); }); } } // Top-level call to add a source file to a package. It will // be processed according to its extension (eg, *.coffee // files will be compiled to JavaScript). /** * @memberOf PackageAPI * @instance * @summary Specify source code files for your package. * @locus package.js * @param {String|String[]} filenames Paths to the source files. * @param {String|String[]} [architecture] If you only want to use the file * on the server (or the client), you can pass this argument * (e.g., 'server', 'legacy', 'client', 'web.browser', 'web.cordova') to specify * what architecture the file is used with. You can call api.addFiles(files, "legacy") * in your package.js configuration file to add extra files to the legacy bundle, * or api.addFiles(files, "client") to add files to all client bundles, * or api.addFiles(files, "web.browser") to add files only to the modern bundle. * You can specify multiple * architectures by passing in an array, for example * `['web.cordova', 'os.linux']`. By default, the file will be loaded on both * server and client. * @param {Object} [options] Options that will be passed to build * plugins. * @param {Boolean} [options.bare] If this file is JavaScript code or will * be compiled into JavaScript code by a build plugin, don't wrap the * resulting file in a closure. Has the same effect as putting a file into the * `client/compatibility` directory in an app. */ addFiles(paths, arch, fileOptions) { if (fileOptions && fileOptions.isAsset) { // XXX it would be great to print a warning here, see the issue: // https://github.com/meteor/meteor/issues/5495 this._addFiles("assets", paths, arch); return; } // Watch out - we rely on the levels of stack traces inside this // function so don't wrap it in another function without changing that logic this._addFiles("sources", paths, arch, fileOptions); } mainModule(path, arch, fileOptions = {}) { arch = toArchArray(arch); const errors = []; forAllMatchingArchs(arch, a => { const filesForArch = this.files[a]; const source = { relPath: pathRelative(".", path), fileOptions: { ...fileOptions, mainModule: true } }; const oldMain = filesForArch.main; if (oldMain) { // It's not an error to call api.mainModule multiple times, but // the last call takes precedence over the earlier calls. oldMain.fileOptions.mainModule = false; if (! _.has(oldMain.fileOptions, "lazy")) { // If the laziness of the old main module was not explicitly // specified, then it would have been implicitly eager just // because it was the main module. Since we are revoking its // status as main module now, we should also explicitly revoke // the eagerness that came with that status. oldMain.fileOptions.lazy = true; } } if (filesForArch.sources.some(old => source.relPath === old.relPath)) { errors.push(`Duplicate api.mainModule: ${path}`); } filesForArch.main = source; filesForArch.sources.push(source); this._forbidExportWithLazyMain(a); }); errors.forEach(error => { buildmessage.error(error, { useMyCaller: 1 }); }); } _forbidExportWithLazyMain(arch) { const filesForArch = this.files[arch]; if (filesForArch.main && filesForArch.main.fileOptions.lazy && this.exports[arch].length > 0) { buildmessage.error( "Architecture " + JSON.stringify(arch) + " cannot both " + "export symbols and have a lazy main module" ); } } /** * @memberOf PackageAPI * @instance * @summary Specify asset files for your package. They can be accessed via * the [Assets API](#assets) from the server, or at the URL * `/packages/username_package-name/file-name` from the client, depending on the * architecture passed. * @locus package.js * @param {String|String[]} filenames Paths to the asset files. * @param {String|String[]} architecture Specify where this asset should be * available (e.g., 'server', 'client', 'web.browser', 'web.cordova'). You can * specify multiple architectures by passing in an array, for example * `['web.cordova', 'os.linux']`. */ addAssets(paths, arch) { if(!arch) { buildmessage.error('addAssets requires a second argument specifying ' + 'where the asset should be available. For example: "client", ' + '"server", or ["client", "server"].', { useMyCaller: true }); return; } // Watch out - we rely on the levels of stack traces inside this // function so don't wrap it in another function without changing that logic this._addFiles("assets", paths, arch); } /** * Internal method used by addFiles and addAssets. */ _addFiles(type, paths, arch, fileOptions) { if (type !== "sources" && type !== "assets") { throw new Error(`Can only handle sources and assets, not '${type}'.`); } var self = this; paths = toArray(paths); arch = toArchArray(arch); // Convert Dos-style paths to Unix-style paths. // XXX it is possible to convert an already Unix-style path by mistake // and break it. e.g.: 'some\folder/anotherFolder' is a valid path // consisting of two components. #WindowsPathApi paths = _.map(paths, function (p) { // Normalize ./foo.js to foo.js. p = pathRelative(".", p); if (p.indexOf('/') !== -1) { // it is already a Unix-style path most likely return p; } return convertToPosixPath(p, true); }); var errors = []; _.each(paths, function (path) { forAllMatchingArchs(arch, function (a) { const filesOfType = self.files[a][type]; // Check if we have already added a file at this path if (filesOfType.some(source => source.relPath === path)) { // We want the singular form of the file type const typeName = { sources: 'source', assets: 'asset' }[type]; errors.push(`Duplicate ${typeName} file: ${path}`); return; } const source = { relPath: path }; if (fileOptions) { source.fileOptions = fileOptions; } filesOfType.push(source); }); }); // Spit out all the errors at the end, where the number of stack frames to // skip is just 2 (this function and its callers) instead of something like // 7 from forAllMatchingArchs and _.each. Avoid using _.each here to keep // stack predictable. for (var i = 0; i < errors.length; ++i) { buildmessage.error(errors[i], { useMyCaller: 1 }); } } // Use this release to resolve unclear dependencies for this package. If // you don't fill in dependencies for some of your implies/uses, we will // look at the packages listed in the release to figure that out. /** * @memberOf PackageAPI * @instance * @summary Use versions of core packages from a release. Unless provided, * all packages will default to the versions released along with * `meteorRelease`. This will save you from having to figure out the exact * versions of the core packages you want to use. For example, if the newest * release of meteor is `METEOR@0.9.0` and it includes `jquery@1.0.0`, you * can write `api.versionsFrom('METEOR@0.9.0')` in your package, and when you * later write `api.use('jquery')`, it will be equivalent to * `api.use('jquery@1.0.0')`. You may specify an array of multiple releases, * in which case the default value for constraints will be the "or" of the * versions from each release: `api.versionsFrom(['METEOR@0.9.0', * 'METEOR@0.9.5'])` may cause `api.use('jquery')` to be interpreted as * `api.use('jquery@1.0.0 || 2.0.0')`. * @locus package.js * @param {String | String[]} meteorRelease Specification of a release: * track@version. Just 'version' (e.g. `"0.9.0"`) is sufficient if using the * default release track `METEOR`. Can be an array of specifications. */ versionsFrom(releases) { var self = this; // Packages in isopackets really ought to be in the core release, by // definition, so saying that they should use versions from another // release doesn't make sense. Moreover, if we're running from a // checkout, we build isopackets before we initialize catalog.official // (since we may need the ddp isopacket to refresh catalog.official), // so we wouldn't actually be able to interpret the release name // anyway. if (self.buildingIsopackets) { buildmessage.error( "packages in isopackets may not use versionsFrom"); // recover by ignoring return; } releases = toArray(releases); // using for loop rather than underscore to help with useMyCaller for (var i = 0; i < releases.length; ++i) { var release = releases[i]; // If you don't specify a track, use our default. if (release.indexOf('@') === -1) { release = catalog.DEFAULT_TRACK + "@" + release; } var relInf = release.split('@'); if (relInf.length !== 2) { buildmessage.error("Release names in versionsFrom may not contain '@'.", { useMyCaller: true }); return; } let promise = catalog.official.getReleaseVersion(relInf[0], relInf[1]) .then(releaseRecord => { if (!releaseRecord) { buildmessage.error("Unknown release "+ release, { tags: { refreshCouldHelp: true } }); } else { self.releaseRecords.push(releaseRecord); } }); this.pendingPromises.push(promise); } } // Internal method used by the meteor-tool _waitForAsyncWork() { let promises = this.pendingPromises; this.pendingPromises = []; return Promise.all(promises); } // Export symbols from this package. // // @param symbols String (eg "Foo") or array of String // @param arch 'web', 'server', 'web.browser', 'web.cordova' // or an array of those. // The default is ['web', 'server']. // @param options 'testOnly', boolean. /** * * @memberOf PackageAPI * @instance * @summary Export package-level variables in your package. The specified * variables (declared without `var` in the source code) will be available * to packages that use your package. If your package sets the `debugOnly`, * `prodOnly` or `testOnly` options to `true` when it calls * `Package.describe()`, then packages that use your package will need to use * `Package["package-name"].ExportedVariableName` to access the value of an * exported variable. * @locus package.js * @param {String|String[]} exportedObjects Name of the object to export, or * an array of object names. * @param {String|String[]} [architecture] If you only want to export the * object on the server (or the client), you can pass in the second argument * (e.g., 'server', 'client', 'web.browser', 'web.cordova') to specify what * architecture the export is used with. You can specify multiple * architectures by passing in an array, for example `['web.cordova', * 'os.linux']`. * @param {Object} [exportOptions] * @param {Boolean} exportOptions.testOnly If true, this symbol will only be * exported when running tests for this package. */ "export"(symbols, arch, options) { var self = this; // Support `api.export("FooTest", {testOnly: true})` without // arch. if (_.isObject(arch) && !_.isArray(arch) && !options) { options = arch; arch = null; } options = options || {}; symbols = toArray(symbols); arch = toArchArray(arch); _.each(symbols, function (symbol) { // XXX be unicode-friendlier if (!symbol.match(/^([_$a-zA-Z][_$a-zA-Z0-9]*)$/)) { buildmessage.error("Bad exported symbol: " + symbol, { useMyCaller: true }); // recover by ignoring return; } forAllMatchingArchs(arch, function (w) { self.exports[w].push({ name: symbol, testOnly: !! options.testOnly, }); self._forbidExportWithLazyMain(w); }); }); } }