Files
meteor/tools/isobuild/package-namespace.js
Jan Dvorak 21c5f6a092 Update history & finish removal of deprecated package API
And correct versions for release
2021-05-20 21:40:00 +02:00

295 lines
11 KiB
JavaScript

import { each, size, compact } from "underscore";
import { inCheckout } from "../fs/files";
import buildmessage from "../utils/buildmessage.js";
import packageVersionParser from "../packaging/package-version-parser.js";
export class PackageNamespace {
/**
* @summary Class of the 'Package' object visible in package.js
* @locus package.js
* @instanceName Package
* @showInstanceName true
*/
constructor(packageSource) {
this._packageSource = packageSource;
this._fileAndDepLoader = null;
this._hasTests = false;
}
// Set package metadata. Options:
// - summary: for 'meteor list' & package server
// - version: package version string
// There used to be a third option documented here,
// 'environments', but it was never implemented and no package
// ever used it.
/**
* @summary Provide basic package information.
* @locus package.js
* @memberOf PackageNamespace
* @param {Object} options
* @param {String} options.summary A concise 1-2 sentence description of
* the package, required for publication.
* @param {String} options.version The (extended)
* [semver](http://www.semver.org) version for your package. Additionally,
* Meteor allows a wrap number: a positive integer that follows the
* version number. If you are porting another package that uses semver
* versioning, you may want to use the original version, postfixed with
* `_wrapnumber`. For example, `1.2.3_1` or `2.4.5-rc1_4`. Wrap numbers
* sort after the original numbers: `1.2.3` < `1.2.3_1` < `1.2.3_2` <
* `1.2.4-rc.0`. If no version is specified, this field defaults to
* `0.0.0`. If you want to publish your package to the package server, you
* must specify a version.
* @param {String} options.name Optional name override. By default, the
* package name comes from the name of its directory.
* @param {String} options.git Optional Git URL to the source repository.
* @param {String} options.documentation Optional Filepath to
* documentation. Set to 'README.md' by default. Set this to null to submit
* no documentation.
* @param {Boolean} options.debugOnly A package with this flag set to true
* will not be bundled into production builds. This is useful for packages
* meant to be used in development only.
* @param {Boolean} options.prodOnly A package with this flag set to true
* will ONLY be bundled into production builds.
* @param {Boolean} options.testOnly A package with this flag set to true
* will ONLY be bundled as part of `meteor test`.
* @param {Boolean|String} options.deprecated A flag that will mark the
* package as deprecated. Provide string to override the default message.
*/
describe(options) {
const source = this._packageSource;
each(options, function (value, key) {
if (key === "summary" ||
key === "git") {
source.metadata[key] = value;
} else if (key === "documentation") {
source.metadata[key] = value;
source.docsExplicitlyProvided = true;
} else if (key === "version") {
if (typeof(value) !== "string") {
buildmessage.error("The package version (specified with "
+ "Package.describe) must be a string.");
// Recover by pretending that version was not set.
} else {
var goodVersion = true;
try {
var parsedVersion =
packageVersionParser.getValidServerVersion(value);
} catch (e) {
if (!e.versionParserError) {
throw e;
}
buildmessage.error(
"The package version " + value + " (specified with Package.describe) "
+ "is not a valid Meteor package version.\n"
+ "Valid package versions are semver (see http://semver.org/), "
+ "optionally followed by '_' and an integer greater or equal to 1.");
goodVersion = false;
}
// Recover by pretending that the version was not set.
}
if (goodVersion && parsedVersion !== value) {
buildmessage.error(
"The package version (specified with Package.describe) may not "
+ "contain a plus-separated build ID.");
// Recover by pretending that the version was not set.
goodVersion = false;
}
if (goodVersion) {
source.version = value;
source.versionExplicitlyProvided = true;
}
} else if (key === "name" && ! source.isTest) {
if (! source.name) {
source.name = value;
} else if (source.name !== value) {
// Woah, so we requested a non-test package by name, and it is not
// the name that we find inside. That's super weird.
buildmessage.error(
"trying to initialize a nonexistent base package " + value);
}
// `debugOnly`, `prodOnly` and `testOnly` are boolean
// flags you can put on a package, currently undocumented.
// when set to true, they cause a package's code to be
// only included (i.e. linked into the bundle) in dev
// mode, prod mode (`meteor --production`) or app tests
// (`meteor test`), and excluded otherwise.
//
// Notes:
//
// * These flags do not affect which packages or which versions are
// are selected by the version solver.
//
// * When you use a debugOnly, prodOnly or testOnly
// package, its exports are not imported for you. You
// have to access them using
// `Package["my-package"].MySymbol`.
//
// * These flags CAN cause different package load orders in
// development and production! We should probably fix this.
// Basically, packages that are excluded from the build using
// these flags are also excluded from the build order calculation,
// and that's the problem
//
// * We should consider publicly documenting these flags, since they
// are effectively part of the public API.
} else if (key === "debugOnly") {
source.debugOnly = !!value;
} else if (key === "prodOnly") {
source.prodOnly = !!value;
} else if (key === "testOnly") {
source.testOnly = !!value;
} else if (key === "deprecated") {
if (typeof(value) === "string") {
source.deprecatedMessage = value;
}
source.deprecated = !!value;
} else {
// Do nothing. We might want to add some keys later, and we should err
// on the side of backwards compatibility.
}
if (size(compact([source.debugOnly, source.prodOnly, source.testOnly])) > 1) {
buildmessage.error(
"Package can't have more than one of: debugOnly, prodOnly, testOnly.");
}
});
}
/**
* @summary Define package dependencies and expose package methods.
* @locus package.js
* @param {Function} func A function that takes in the package control `api` object, which keeps track of dependencies and exports.
*/
onUse(f) {
if (! this._packageSource.isTest) {
if (this._fileAndDepLoader) {
buildmessage.error("duplicate onUse handler; a package may have " +
"only one", { useMyCaller: true });
// Recover by ignoring the duplicate
return;
}
this._fileAndDepLoader = f;
}
}
/**
* @summary Define dependencies and expose package methods for unit tests.
* @locus package.js
* @param {Function} func A function that takes in the package control 'api' object, which keeps track of dependencies and exports.
*/
onTest(f) {
const isTest = this._packageSource.isTest;
// If we are not initializing the test package, then we are initializing
// the normal package and have now noticed that it has tests. So, let's
// register the test. This is a medium-length hack until we have new
// control files.
if (! isTest) {
this._hasTests = true;
return;
}
// We are initializing the test, so proceed as normal.
if (isTest) {
if (this._fileAndDepLoader) {
buildmessage.error("duplicate onTest handler; a package may have " +
"only one", { useMyCaller: true });
// Recover by ignoring the duplicate
return;
}
this._fileAndDepLoader = f;
}
}
// Define a plugin. A plugin extends the build process for
// targets that use this package. For example, a Coffeescript
// compiler would be a plugin. A plugin is its own little
// program, with its own set of source files, used packages, and
// npm dependencies.
//
// This is an experimental API and for now you should assume
// that it will change frequently and radically (thus the
// '_transitional_'). For maximum R&D velocity and for the good
// of the platform, we will push changes that break your
// packages that use this API. You've been warned.
//
// Options:
// - name: a name for this plugin. required (cosmetic -- string)
// - use: package to use for the plugin (names, as strings)
// - sources: sources for the plugin (array of string)
// - npmDependencies: map from npm package name to required
// version (string)
/**
* @summary Define a build plugin. A build plugin extends the build
* process for apps and packages that use this package. For example,
* the `coffeescript` package uses a build plugin to compile CoffeeScript
* source files into JavaScript.
* @param {Object} [options]
* @param {String} options.name A cosmetic name, must be unique in the
* package.
* @param {String|String[]} options.use Meteor packages that this
* plugin uses, independent of the packages specified in
* [api.onUse](#pack_onUse).
* @param {String[]} options.sources The source files that make up the
* build plugin, independent from [api.addFiles](#pack_addFiles).
* @param {Object} options.npmDependencies An object where the keys
* are NPM package names, and the values are the version numbers of
* required NPM packages, just like in [Npm.depends](#Npm-depends).
* @locus package.js
*/
registerBuildPlugin(options) {
const isTest = this._packageSource.isTest;
// Tests don't have plugins; plugins initialized in the control file
// belong to the package and not to the test. (This will be less
// confusing in the new control file format).
if (isTest) {
return;
}
if (! ('name' in options)) {
buildmessage.error("build plugins require a name",
{ useMyCaller: true });
// recover by ignoring plugin
return;
}
const pluginInfo = this._packageSource.pluginInfo;
if (options.name in pluginInfo) {
buildmessage.error("this package already has a plugin named '" +
options.name + "'",
{ useMyCaller: true });
// recover by ignoring plugin
return;
}
if (options.name.match(/\.\./) ||
options.name.match(/[\\\/]/)) {
buildmessage.error("bad plugin name", { useMyCaller: true });
// recover by ignoring plugin
return;
}
// XXX probably want further type checking
pluginInfo[options.name] = options;
}
includeTool() {
const source = this._packageSource;
if (! inCheckout()) {
buildmessage.error("Package.includeTool() can only be used with a " +
"checkout of meteor");
} else if (source.includeTool) {
buildmessage.error("Duplicate includeTool call");
} else {
source.includeTool = true;
}
}
}