Files
meteor/tools/package-api.js
David Glasser 0629678414 Use registerCompiler via isobuild:compiler-plugin
Previously, registerCompiler was enabled by the *real* package
`compiler-plugin`, which arranged to only be available in versions of
the tool that support registerCompiler via... well, mostly via wishful
thinking.

Now this is implemented via a fake package called
isobuild:compiler-plugin. "isobuild" is a real Atmosphere organization
that will never publish any packages. The tool pretends that packages
isobuild:compiler-plugin@1.0.0, isobuild:linter-plugin@1.0.0, and
isobuild:minifier-plugin@1.0.0 exist, and carefully arranges for them to
be avoided in the actual process of building and app; they just are
referenced in Version Solver.

When we add future features like this, users of this version of Meteor
who try to depend on packages that need the feature will get a nice
error message pointing to
https://github.com/meteor/meteor/wiki/Isobuild-Feature-Packages

Users of current versions of Meteor who try to depend on packages that
require isobuild:compiler-plugin will get a slightly confusing message
about isobuild:compiler-plugin not existing.  Users of current versions
of Meteor who try to depend on packages only some of whose versions
require isobuild:compiler-plugin will get a version that doesn't require
it.
2015-07-14 10:30:42 -07:00

486 lines
18 KiB
JavaScript

var assert = require("assert");
var _ = require("underscore");
var buildmessage = require('./buildmessage.js');
var utils = require('./utils.js');
var compiler = require('./compiler.js');
var archinfo = require('./archinfo.js');
var files = require('./files.js');
var catalog = require('./catalog.js');
function toArray (x) {
if (_.isArray(x))
return x;
return x ? [x] : [];
}
function toArchArray (arch) {
if (! _.isArray(arch)) {
arch = arch ? [arch] : compiler.ALL_ARCHES;
}
arch = _.uniq(arch);
arch = _.map(arch, mapWhereToArch);
// 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;
}
// We currently have a 1 to 1 mapping between 'where' and 'arch'.
// 'client' -> 'web'
// 'server' -> 'os'
// '*' -> '*'
function mapWhereToArch (where) {
if (where === 'server') {
return 'os';
} else if (where === 'client') {
return 'web';
} else {
return where;
}
}
// 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) {
_.each(archs, function (arch) {
_.each(compiler.ALL_ARCHES, function (matchArch) {
if (archinfo.matches(matchArch, arch)) {
f(matchArch);
}
});
});
}
/**
* @name PackageAPI
* @class PackageAPI
* @instanceName api
* @global
* @summary Type of the API object passed into the `Package.onUse` function.
*/
function PackageAPI (options) {
var self = this;
assert.ok(self instanceof PackageAPI);
options = options || {};
self.buildingIsopackets = !!options.buildingIsopackets;
// source files used. Map arch -> relPath -> {relPath, fileOptions}
self.sources = {};
// symbols exported
self.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.
self.uses = {};
self.implies = {};
_.each(compiler.ALL_ARCHES, function (arch) {
self.sources[arch] = {};
self.exports[arch] = [];
self.uses[arch] = [];
self.implies[arch] = [];
});
self.releaseRecords = [];
}
_.extend(PackageAPI.prototype, {
// 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. Also
// don't bring this package's imports into our
// namespace. 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: function (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: function (names, arch) {
var self = this;
// We currently disallow build plugins in debugOnly 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) {
buildmessage.error("can't use imply in debugOnly packages");
// 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 the source code for your package.
* @locus package.js
* @param {String|String[]} filename Name of the source file, or array of
* strings of source file names.
* @param {String|String[]} [architecture] If you only want to export the file
* 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 file is used with. You can specify multiple
* architectures by passing in an array, for example `['web.cordova', 'os.linux']`.
* @param {Object} [fileOptions] Options that will be passed to build
* plugins. For example, for JavaScript files, you can pass `{bare: true}`
* to not wrap the individual file in its own closure. To add a static asset,
* pass `{isAsset: true}`; use the `architecture` parameter to determine
* if this is a client-side asset served by the HTTP server or a server-side
* asset accessible to the `Assets` APIs.
*/
addFiles: function (paths, arch, fileOptions) {
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) {
if (p.indexOf('/') !== -1) {
// it is already a Unix-style path most likely
return p;
}
return files.convertToPosixPath(p, true);
});
var errors = [];
_.each(paths, function (path) {
forAllMatchingArchs(arch, function (a) {
if (_.has(self.sources[a], path)) {
errors.push("Duplicate source file: " + path);
return;
}
var source = {relPath: path};
if (fileOptions)
source.fileOptions = fileOptions;
self.sources[a][path] = source;
});
});
// Spit out all the errors at the end, where the number of stack frames to
// skip is just 1 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: true });
}
},
// 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: function (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;
}
var releaseRecord = catalog.official.getReleaseVersion(
relInf[0], relInf[1]);
if (!releaseRecord) {
buildmessage.error("Unknown release "+ release,
{ tags: { refreshCouldHelp: true } });
} else {
self.releaseRecords.push(releaseRecord);
}
}
},
// 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 this package.
* @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: function (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});
});
});
}
});
// XXX COMPAT WITH 0.8.x
PackageAPI.prototype.add_files = PackageAPI.prototype.addFiles;
exports.PackageAPI = PackageAPI;