Socket
Socket
Sign inDemoInstall

editions

Package Overview
Dependencies
Maintainers
2
Versions
130
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

editions - npm Package Compare versions

Comparing version 2.0.2 to 2.1.0

edition-browsers/util.js

471

edition-browsers/index.js
/* eslint no-console:0 */
'use strict';
'use strict'; // Imports
/**
* @typedef {Object} Edition
* @property {string} description
* @property {string} directory
* @property {string} entry
* @property {Array<string>} syntaxes
* @property {false|Object.<string, string|boolean>} engines
* @public
*/
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
/**
* @typedef {Object} Options
* @property {Function} require - the require method of the calling module, used to ensure require paths remain correct
* @property {string} [packagePath] - if provided, this is used for debugging
* @property {boolean} [verbose] - if provided, any error loading an edition will be logged. By default, errors are only logged if all editions failed. If not provided, process.env.EDITIONS_VERBOSE is used.
* @property {string} [cwd] - if provided, this will be the cwd for entries
* @property {string} [entry] - if provided, should be a relative path to the entry point of the edition
* @property {string} [package] - if provided, should be the name of the package that we are loading the editions for
* @property {stream.Writable} [stderr] - if not provided, will use process.stderr instead. It is the stream that verbose errors are logged to.
* @public
*/
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// Imports
var pathUtil = require('path');
var pathUtil = require('path');
var semver = require('semver');
var Errlop = require('errlop');
// Helpers
function stringify(value) {
return typeof value === 'string' ? value : JSON.stringify(value);
}
var _require = require('./util.js'),
errtion = _require.errtion,
stringify = _require.stringify,
simplifyRange = _require.simplifyRange;
/**
* The current node version that we are operating within.
* It is compared in {@link requireEdition} against {@link Edition.engines}.
* @type {string}
* @private
*/
// Environment fetching
var BLACKLIST = process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(/[, ]+/g);
var NODE_VERSION = process.versions.node;
/**
* Set the environment variable `EDITIONS_VERBOSE` to output debugging information to stderr on how editions selected the edition it did.
* Values of `yes` and `true` are supported.
* @example env EDITIONS_VERBOSE=true node mypackage/index.js
* @typedef {String} EDITIONS_VERBOSE
*/
/**
* Whether or not {@link EDITIONS_VERBOSE} is enabled.
* @type {bollean}
* @private
*/
var VERBOSE = process.env.EDITIONS_VERBOSE === true || process.env.EDITIONS_VERBOSE === 'yes' || process.env.EDITIONS_VERBOSE === 'true' || false;
/**
* Set the environment variable `EDITIONS_TAG_BLACKLIST` to the tags you wish to blacklist, and editions will skip editions that contain them.
* For backwards compatibility `EDITIONS_SYNTAX_BLACKLIST` is also supported.
* It is compared in {@link requireEdition} against {@link Edition.tags}.
* The value of this is stored locally in the {@link BLACKLIST} cache.
* @example env EDITIONS_TAG_BLACKLIST=esnext,typescript,coffeescript node mypackage/index.js
* @typedef {String} EDITIONS_TAG_BLACKLIST
*/
// Cache of which syntax combinations are supported or unsupported
// Object.<string, Error>
/**
* A list of the blacklisted tags.
* Data imported from {@link EDITIONS_TAG_BLACKLIST}.
* @type {Array<string>}
* @private
*/
var BLACKLIST = process.env.EDITIONS_TAG_BLACKLIST && process.env.EDITIONS_TAG_BLACKLIST.split(/[, ]+/g) || process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(/[, ]+/g);
/**
* A mapping of blacklisted tags to their reasons.
* Keys are the tags.
* Values are the error instances that contain the reasoning for why/how that tag is/became blacklisted.
* Data imported from {@link EDITIONS_TAG_BLACKLIST}.
* @type {Object.<string, Error>}
* @private
*/
var blacklist = {};
/**
* Edition entries must conform to the following specification.
* @typedef {Object} Edition
* @property {string} description Use this property to decribe the edition in human readable terms. Such as what it does and who it is for. It is used to reference the edition in user facing reporting, such as error messages. E.g. `esnext source code with require for modules`.
* @property {string} directory The location to where this directory is located. It should be a relative path from the `package.json` file. E.g. `source`.
* @property {string} entry The default entry location for this edition, relative to the edition's directory. E.g. `index.js`.
* @property {Array<string>} [tags] Any keywords you wish to associate to the edition. Useful for various ecosystem tooling, such as automatic ESNext lint configuration if the `esnext` tag is present in the source edition tags. Consumers also make use of this via {@link EDITIONS_TAG_BLACKLIST} for preventing loading editions that contain a blacklisted tag. Previously this field was named `syntaxes`. E.g. `["javascript", "esnext", "require"]`.
* @property {false|Object.<string, string|boolean>} engines This field is used to specific which Node.js and Browser environments this edition supports. If `false` this edition does not support either. If `node` is a string, it should be a semver range of node.js versions that the edition targets. If `browsers` is a string, it should be a [browserlist](https://github.com/browserslist/browserslist) value of the specific browser values the edition targets. If `node` or `browsers` is true, it indicates that this edition is compatible with those environments.
* @example
* {
* "description": "esnext source code with require for modules",
* "directory": "source",
* "entry": "index.js",
* "tags": [
* "javascript",
* "esnext",
* "require"
* ],
* "engines": {
* "node": ">=6",
* "browsers": "defaults"
* }
* }
*/
// Check the environment configuration for a syntax blacklist
/**
* These are the various options that you can use to customise the behaviour of certain methods.
* @typedef {Object} Options
* @property {Function} require The require method of the calling module, used to ensure require paths remain correct.
* @property {string} [packagePath] If provided, this is used for debugging.
* @property {boolean} [verbose] If provided, any error loading an edition will be logged. By default, errors are only logged if all editions failed. If not provided, process.env.EDITIONS_VERBOSE is used.
* @property {boolean} [strict] If `true`, then only exact version matches will be loaded. If `false`, then likely matches using {@link simplifyRange} will be evaluated, with a fallback to the last. If missing, then `true` is attempted first and if no result, then `false` is attempted.
* @property {string} [cwd] If provided, this will be the cwd for entries.
* @property {string} [entry] If provided, should be a relative path to the entry point of the edition.
* @property {string} [package] If provided, should be the name of the package that we are loading the editions for.
* @property {stream.Writable} [stderr] If not provided, will use process.stderr instead. It is the stream that verbose errors are logged to.
*/
// Create the mapping of blacklisted tags and their reasonings
if (BLACKLIST) {
for (var i = 0; i < BLACKLIST.length; ++i) {
var syntax = BLACKLIST[i].trim().toLowerCase();
blacklist[syntax] = new Errlop('The EDITIONS_SYNTAX_BLACKLIST environment variable blacklisted the syntax [' + syntax + ']');
}
}
for (var i = 0; i < BLACKLIST.length; ++i) {
var tag = BLACKLIST[i].trim().toLowerCase();
blacklist[tag] = errtion({
message: "The EDITIONS_TAG_BLACKLIST (aka EDITIONS_SYNTAX_BLACKLIST) environment variable blacklisted the tag [".concat(tag, "]"),
code: 'blacklisted-tag'
});
}
} // Blacklist the tag 'esnext' if our node version is below 0.12
// Blacklist the syntax 'esnext' if our node version is below 0.12
if (semver.satisfies(NODE_VERSION, '<0.12')) {
blacklist.esnext = new Error('The esnext syntax is skipped on early node versions as attempting to use esnext features will output debugging information on these node versions');
blacklist.esnext = new Error('The esnext tag is skipped on early node versions as attempting to use esnext features will output debugging information on these node versions');
}
/**
* Attempt to load a specific {@link Edition}.
* @param {Edition} edition
* @param {Options} opts
* @returns {*} The result of the loaded edition.
* @throws {Error} An error if the edition failed to load.
* @public
*/
function loadEdition(edition, opts) {
var entry = pathUtil.resolve(opts.cwd || '', edition.directory, opts.entry || edition.entry);
if (opts.require == null) {
throw errtion({
message: "Skipped edition [".concat(edition.description, "] as opts.require was not provided, this is probably due to a testing misconfiguration."),
code: 'unsupported-edition-require'
});
}
try {
return opts.require(entry);
} catch (loadError) {
// Note the error with more details
throw errtion({
message: "Skipped edition [".concat(edition.description, "] at entry [").concat(entry, "] because it failed to load"),
code: 'unsupported-edition-tried'
}, loadError);
}
}
/**
* Attempt to load a specific edition
* Attempt to require an {@link Edition}, based on its compatibility with the current environment, such as {@link NODE_VERSION} and {@link EDITIONS_TAG_BLACKLIST} compatibility.
* If compatibility is established with the environment, it will load the edition using {@link loadEdition}.
* @param {Edition} edition

@@ -67,123 +160,225 @@ * @param {Options} opts

*/
function requireEdition(edition, opts) {
// Verify the edition is valid
if (!edition.description || !edition.directory || !edition.entry || edition.engines == null) {
var editionInvalidError = new Errlop('Each edition must have its [description, directory, entry, engines] fields defined, yet all it had was [' + Object.keys(edition).join(', ') + ']');
editionInvalidError.level = 'fatal';
throw editionInvalidError;
}
// Verify the edition is valid
if (!edition.description || !edition.directory || !edition.entry || edition.engines == null) {
throw errtion({
message: "Each edition must have its [description, directory, entry, engines] fields defined, yet all it had was [".concat(Object.keys(edition).join(', '), "]"),
code: 'unsupported-edition-malformed',
level: 'fatal'
});
} // Handle strict omission
// Verify engine support
if (edition.engines === false) {
throw new Errlop('Skipping edition [' + edition.description + '] because its engines field was false');
}
if (!edition.engines.node) {
throw new Errlop('Skipping edition [' + edition.description + '] because its .engines.node field was falsey');
}
if (edition.engines.node !== true && semver.satisfies(NODE_VERSION, edition.engines.node) === false) {
throw new Errlop('Skipping edition [' + edition.description + '] because our current node version [' + NODE_VERSION + '] is not supported by its [' + stringify(edition.engines.node) + ']');
}
// Verify syntax support
// Convert syntaxes into a sorted lowercase string
var syntaxes = edition.syntaxes && edition.syntaxes.map(function (i) {
return i.toLowerCase();
}).sort() || [];
for (var index = 0; index < syntaxes.length; index++) {
var _syntax = syntaxes[index];
var blacklisted = blacklist[_syntax];
if (blacklisted) {
throw new Errlop('Skipping edition [' + edition.description + '] because it contained a blacklisted syntax [' + _syntax + ']', blacklisted);
}
}
if (opts.strict == null) {
try {
return requireEdition(edition, _objectSpread({}, opts, {
strict: true
}));
} catch (err) {
return requireEdition(edition, _objectSpread({}, opts, {
strict: false
}));
}
} // Verify tag support
// Convert tags into a sorted lowercase string
// Load the edition
var entry = pathUtil.resolve(opts.cwd || '', edition.directory, opts.entry || edition.entry);
try {
return opts.require(entry);
} catch (loadError) {
// Note the error with more details
throw new Errlop('Skipped edition [' + edition.description + '] at entry [' + entry + '] because it failed to load', loadError);
}
var tags = (edition.tags || edition.syntaxes || []).map(function (i) {
return i.toLowerCase();
}).sort();
for (var index = 0; index < tags.length; index++) {
var _tag = tags[index];
var blacklisted = blacklist[_tag];
if (blacklisted) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because it contained a blacklisted tag [").concat(_tag, "]"),
code: 'unsupported-edition-backlisted-tag'
}, blacklisted);
}
} // Verify engine support
if (edition.engines === false) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because its engines field was false"),
code: 'unsupported-edition-engine'
});
}
if (!edition.engines.node) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because its .engines.node field was falsey"),
code: 'unsupported-edition-engines-node'
});
}
if (opts.strict) {
if (edition.engines.node === true) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because its .engines.node field was true yet we are in strict mode"),
code: 'unsupported-edition-engines-node-version-true'
});
} else if (semver.satisfies(NODE_VERSION, edition.engines.node) === false) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because our current node version [").concat(NODE_VERSION, "] is not supported by its specific range [").concat(stringify(edition.engines.node), "]"),
code: 'unsupported-edition-engines-node-version-specific'
});
}
} else if (edition.engines.node !== true) {
var simplifiedRange = simplifyRange(edition.engines.node);
if (semver.satisfies(NODE_VERSION, simplifiedRange) === false) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because our current node version [").concat(NODE_VERSION, "] is not supported by its simplified range [").concat(stringify(simplifiedRange), "]"),
code: 'unsupported-edition-engines-node-version-simplified'
});
}
} // Load the edition
return loadEdition(edition, opts);
}
/**
* Cycles through a list of editions, returning the first suitable edition that it was able to load
* Cycles through a list of editions, returning the require result of the first suitable {@link Edition} that it was able to load.
* Editions should be ordered from most preferable first, to least desirable last.
* Providing the editions configuration is valid, individual edition handling is forwarded to {@link requireEdition}.
* @param {Array<Edition>} editions
* @param {Options} opts
* @returns {*} the result of the loaded edition
* @throws {Error} an error if a suitable edition was unable to be resolved
* @returns {*} The result of the loaded edition.
* @throws {Error} An error if a suitable edition was unable to be resolved.
* @public
*/
function requireEditions(editions, opts) {
// Check
if (!editions || editions.length === 0) {
if (opts.packagePath) {
throw new Errlop('There were no editions specified for package [' + opts.packagePath + ']');
} else {
throw new Errlop('There were no editions specified');
}
}
// Check
if (!editions || editions.length === 0) {
if (opts.packagePath) {
throw errtion({
message: "There were no editions specified for package [".concat(opts.packagePath, "]"),
code: 'unsupported-editions-missing'
});
} else {
throw errtion({
message: 'There were no editions specified',
code: 'unsupported-editions-missing'
});
}
} // Handle strict omission
// Note the last error message
var result = void 0,
editionsError = null,
loaded = false;
// Cycle through the editions
for (var _i = 0; _i < editions.length; ++_i) {
var edition = editions[_i];
try {
result = requireEdition(edition, opts);
loaded = true;
break;
} catch (editionError) {
if (editionError.level === 'fatal') {
editionsError = editionError;
break;
} else if (editionsError) {
editionsError = new Errlop(editionsError, editionError);
} else {
editionsError = editionError;
}
}
}
if (opts.strict == null) {
try {
return requireEditions(editions, _objectSpread({}, opts, {
strict: true
}));
} catch (err) {
return requireEditions(editions, _objectSpread({}, opts, {
strict: false
}));
}
} // Whether or not we should be verbose
if (loaded) {
var verbose = opts.verbose == null ? VERBOSE : opts.verbose;
if (editionsError && verbose) {
var stderr = opts.stderr || process.stderr;
stderr.write(editionsError.stack + '\n');
}
return result;
} else if (editionsError) {
if (opts.packagePath) {
throw new Errlop('There were no suitable editions for package [' + opts.packagePath + ']', editionsError);
} else {
throw new Errlop('There were no suitable editions', editionsError);
}
}
var verbose = opts.verbose == null ? VERBOSE : opts.verbose; // Capture the load result, the last error, and the fallback option
var result,
loaded = false,
editionsError = null,
fallbackEdition = null; // Cycle through the editions determing the above
for (var _i = 0; _i < editions.length; ++_i) {
var edition = editions[_i];
try {
result = requireEdition(edition, opts);
loaded = true;
break;
} catch (editionError) {
if (editionError.level === 'fatal') {
editionsError = editionError;
break;
} else if (editionsError) {
editionsError = errtion(editionsError, editionError);
} else {
editionsError = editionError;
}
if (editionError.code.indexOf('unsupported-edition-engines-node-version') === 0) {
fallbackEdition = edition;
}
}
} // if no edition was suitable for our environment, then try the fallback if it exists
// that is to say, ignore its engines.node
if (opts.strict === false && loaded === false && fallbackEdition) {
try {
result = loadEdition(fallbackEdition, opts);
loaded = true;
} catch (editionError) {
editionsError = new Errlop(editionError, editionsError);
}
} // if we were able to load something, then provide it
if (loaded) {
// make note of any errors if desired
if (editionsError && verbose) {
var stderr = opts.stderr || process.stderr;
stderr.write(editionsError.stack + '\n');
}
return result;
} // otherwise, provide the error
else if (editionsError) {
if (opts.packagePath) {
throw errtion({
message: "There were no suitable editions for package [".concat(opts.packagePath, "]"),
code: 'unsupported-editions-tried'
}, editionsError);
} else {
throw errtion({
message: 'There were no suitable editions',
code: 'unsupported-editions-tried'
}, editionsError);
}
}
}
/**
* Cycle through the editions for a package and require the correct one
* Cycle through the editions for a package and require the correct one.
* Providing the package configuration is valid, editions handling is forwarded to {@link requireEditions}.
* @param {Options.cwd} cwd
* @param {Options.require} require
* @param {Options.entry} [entry]
* @returns {*} the result of the loaded edition
* @throws {Error} an error if a suitable edition was unable to be resolved
* @public
* @returns {any} The result of the loaded edition.
* @throws {Error} An error if a suitable edition was unable to be resolved.
*/
function requirePackage(cwd, require, entry) {
// Load the package.json file to fetch `name` for debugging and `editions` for loading
var packagePath = pathUtil.resolve(cwd, 'package.json');
// Load the package.json file to fetch `name` for debugging and `editions` for loading
var packagePath = pathUtil.resolve(cwd, 'package.json');
var _require = require(packagePath),
editions = _require.editions;
var _require2 = require(packagePath),
editions = _require2.editions;
var opts = { packagePath: packagePath, cwd: cwd, require: require, entry: entry };
return requireEditions(editions, opts);
}
var opts = {
packagePath: packagePath,
cwd: cwd,
require: require,
entry: entry
};
return requireEditions(editions, opts);
} // Exports
// Exports
module.exports = { requireEdition: requireEdition, requireEditions: requireEditions, requirePackage: requirePackage };
module.exports = {
requireEdition: requireEdition,
requireEditions: requireEditions,
requirePackage: requirePackage
};
/* eslint no-console:0 */
'use strict';
'use strict'; // Imports
/**
* @typedef {Object} Edition
* @property {string} description
* @property {string} directory
* @property {string} entry
* @property {Array<string>} syntaxes
* @property {false|Object.<string, string|boolean>} engines
* @public
*/
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
/**
* @typedef {Object} Options
* @property {Function} require - the require method of the calling module, used to ensure require paths remain correct
* @property {string} [packagePath] - if provided, this is used for debugging
* @property {boolean} [verbose] - if provided, any error loading an edition will be logged. By default, errors are only logged if all editions failed. If not provided, process.env.EDITIONS_VERBOSE is used.
* @property {string} [cwd] - if provided, this will be the cwd for entries
* @property {string} [entry] - if provided, should be a relative path to the entry point of the edition
* @property {string} [package] - if provided, should be the name of the package that we are loading the editions for
* @property {stream.Writable} [stderr] - if not provided, will use process.stderr instead. It is the stream that verbose errors are logged to.
* @public
*/
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
// Imports
var pathUtil = require('path');
var pathUtil = require('path');
var semver = require('semver');
var Errlop = require('errlop');
// Helpers
function stringify(value) {
return typeof value === 'string' ? value : JSON.stringify(value);
}
var _require = require('./util.js'),
errtion = _require.errtion,
stringify = _require.stringify,
simplifyRange = _require.simplifyRange;
/**
* The current node version that we are operating within.
* It is compared in {@link requireEdition} against {@link Edition.engines}.
* @type {string}
* @private
*/
// Environment fetching
var BLACKLIST = process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(/[, ]+/g);
var NODE_VERSION = process.versions.node;
/**
* Set the environment variable `EDITIONS_VERBOSE` to output debugging information to stderr on how editions selected the edition it did.
* Values of `yes` and `true` are supported.
* @example env EDITIONS_VERBOSE=true node mypackage/index.js
* @typedef {String} EDITIONS_VERBOSE
*/
/**
* Whether or not {@link EDITIONS_VERBOSE} is enabled.
* @type {bollean}
* @private
*/
var VERBOSE = process.env.EDITIONS_VERBOSE === true || process.env.EDITIONS_VERBOSE === 'yes' || process.env.EDITIONS_VERBOSE === 'true' || false;
/**
* Set the environment variable `EDITIONS_TAG_BLACKLIST` to the tags you wish to blacklist, and editions will skip editions that contain them.
* For backwards compatibility `EDITIONS_SYNTAX_BLACKLIST` is also supported.
* It is compared in {@link requireEdition} against {@link Edition.tags}.
* The value of this is stored locally in the {@link BLACKLIST} cache.
* @example env EDITIONS_TAG_BLACKLIST=esnext,typescript,coffeescript node mypackage/index.js
* @typedef {String} EDITIONS_TAG_BLACKLIST
*/
// Cache of which syntax combinations are supported or unsupported
// Object.<string, Error>
/**
* A list of the blacklisted tags.
* Data imported from {@link EDITIONS_TAG_BLACKLIST}.
* @type {Array<string>}
* @private
*/
var BLACKLIST = process.env.EDITIONS_TAG_BLACKLIST && process.env.EDITIONS_TAG_BLACKLIST.split(/[, ]+/g) || process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(/[, ]+/g);
/**
* A mapping of blacklisted tags to their reasons.
* Keys are the tags.
* Values are the error instances that contain the reasoning for why/how that tag is/became blacklisted.
* Data imported from {@link EDITIONS_TAG_BLACKLIST}.
* @type {Object.<string, Error>}
* @private
*/
var blacklist = {};
/**
* Edition entries must conform to the following specification.
* @typedef {Object} Edition
* @property {string} description Use this property to decribe the edition in human readable terms. Such as what it does and who it is for. It is used to reference the edition in user facing reporting, such as error messages. E.g. `esnext source code with require for modules`.
* @property {string} directory The location to where this directory is located. It should be a relative path from the `package.json` file. E.g. `source`.
* @property {string} entry The default entry location for this edition, relative to the edition's directory. E.g. `index.js`.
* @property {Array<string>} [tags] Any keywords you wish to associate to the edition. Useful for various ecosystem tooling, such as automatic ESNext lint configuration if the `esnext` tag is present in the source edition tags. Consumers also make use of this via {@link EDITIONS_TAG_BLACKLIST} for preventing loading editions that contain a blacklisted tag. Previously this field was named `syntaxes`. E.g. `["javascript", "esnext", "require"]`.
* @property {false|Object.<string, string|boolean>} engines This field is used to specific which Node.js and Browser environments this edition supports. If `false` this edition does not support either. If `node` is a string, it should be a semver range of node.js versions that the edition targets. If `browsers` is a string, it should be a [browserlist](https://github.com/browserslist/browserslist) value of the specific browser values the edition targets. If `node` or `browsers` is true, it indicates that this edition is compatible with those environments.
* @example
* {
* "description": "esnext source code with require for modules",
* "directory": "source",
* "entry": "index.js",
* "tags": [
* "javascript",
* "esnext",
* "require"
* ],
* "engines": {
* "node": ">=6",
* "browsers": "defaults"
* }
* }
*/
// Check the environment configuration for a syntax blacklist
/**
* These are the various options that you can use to customise the behaviour of certain methods.
* @typedef {Object} Options
* @property {Function} require The require method of the calling module, used to ensure require paths remain correct.
* @property {string} [packagePath] If provided, this is used for debugging.
* @property {boolean} [verbose] If provided, any error loading an edition will be logged. By default, errors are only logged if all editions failed. If not provided, process.env.EDITIONS_VERBOSE is used.
* @property {boolean} [strict] If `true`, then only exact version matches will be loaded. If `false`, then likely matches using {@link simplifyRange} will be evaluated, with a fallback to the last. If missing, then `true` is attempted first and if no result, then `false` is attempted.
* @property {string} [cwd] If provided, this will be the cwd for entries.
* @property {string} [entry] If provided, should be a relative path to the entry point of the edition.
* @property {string} [package] If provided, should be the name of the package that we are loading the editions for.
* @property {stream.Writable} [stderr] If not provided, will use process.stderr instead. It is the stream that verbose errors are logged to.
*/
// Create the mapping of blacklisted tags and their reasonings
if (BLACKLIST) {
for (var i = 0; i < BLACKLIST.length; ++i) {
var syntax = BLACKLIST[i].trim().toLowerCase();
blacklist[syntax] = new Errlop('The EDITIONS_SYNTAX_BLACKLIST environment variable blacklisted the syntax [' + syntax + ']');
}
}
for (var i = 0; i < BLACKLIST.length; ++i) {
var tag = BLACKLIST[i].trim().toLowerCase();
blacklist[tag] = errtion({
message: "The EDITIONS_TAG_BLACKLIST (aka EDITIONS_SYNTAX_BLACKLIST) environment variable blacklisted the tag [".concat(tag, "]"),
code: 'blacklisted-tag'
});
}
} // Blacklist the tag 'esnext' if our node version is below 0.12
// Blacklist the syntax 'esnext' if our node version is below 0.12
if (semver.satisfies(NODE_VERSION, '<0.12')) {
blacklist.esnext = new Error('The esnext syntax is skipped on early node versions as attempting to use esnext features will output debugging information on these node versions');
blacklist.esnext = new Error('The esnext tag is skipped on early node versions as attempting to use esnext features will output debugging information on these node versions');
}
/**
* Attempt to load a specific {@link Edition}.
* @param {Edition} edition
* @param {Options} opts
* @returns {*} The result of the loaded edition.
* @throws {Error} An error if the edition failed to load.
* @public
*/
function loadEdition(edition, opts) {
var entry = pathUtil.resolve(opts.cwd || '', edition.directory, opts.entry || edition.entry);
if (opts.require == null) {
throw errtion({
message: "Skipped edition [".concat(edition.description, "] as opts.require was not provided, this is probably due to a testing misconfiguration."),
code: 'unsupported-edition-require'
});
}
try {
return opts.require(entry);
} catch (loadError) {
// Note the error with more details
throw errtion({
message: "Skipped edition [".concat(edition.description, "] at entry [").concat(entry, "] because it failed to load"),
code: 'unsupported-edition-tried'
}, loadError);
}
}
/**
* Attempt to load a specific edition
* Attempt to require an {@link Edition}, based on its compatibility with the current environment, such as {@link NODE_VERSION} and {@link EDITIONS_TAG_BLACKLIST} compatibility.
* If compatibility is established with the environment, it will load the edition using {@link loadEdition}.
* @param {Edition} edition

@@ -67,123 +160,225 @@ * @param {Options} opts

*/
function requireEdition(edition, opts) {
// Verify the edition is valid
if (!edition.description || !edition.directory || !edition.entry || edition.engines == null) {
var editionInvalidError = new Errlop('Each edition must have its [description, directory, entry, engines] fields defined, yet all it had was [' + Object.keys(edition).join(', ') + ']');
editionInvalidError.level = 'fatal';
throw editionInvalidError;
}
// Verify the edition is valid
if (!edition.description || !edition.directory || !edition.entry || edition.engines == null) {
throw errtion({
message: "Each edition must have its [description, directory, entry, engines] fields defined, yet all it had was [".concat(Object.keys(edition).join(', '), "]"),
code: 'unsupported-edition-malformed',
level: 'fatal'
});
} // Handle strict omission
// Verify engine support
if (edition.engines === false) {
throw new Errlop('Skipping edition [' + edition.description + '] because its engines field was false');
}
if (!edition.engines.node) {
throw new Errlop('Skipping edition [' + edition.description + '] because its .engines.node field was falsey');
}
if (edition.engines.node !== true && semver.satisfies(NODE_VERSION, edition.engines.node) === false) {
throw new Errlop('Skipping edition [' + edition.description + '] because our current node version [' + NODE_VERSION + '] is not supported by its [' + stringify(edition.engines.node) + ']');
}
// Verify syntax support
// Convert syntaxes into a sorted lowercase string
var syntaxes = edition.syntaxes && edition.syntaxes.map(function (i) {
return i.toLowerCase();
}).sort() || [];
for (var index = 0; index < syntaxes.length; index++) {
var _syntax = syntaxes[index];
var blacklisted = blacklist[_syntax];
if (blacklisted) {
throw new Errlop('Skipping edition [' + edition.description + '] because it contained a blacklisted syntax [' + _syntax + ']', blacklisted);
}
}
if (opts.strict == null) {
try {
return requireEdition(edition, _objectSpread({}, opts, {
strict: true
}));
} catch (err) {
return requireEdition(edition, _objectSpread({}, opts, {
strict: false
}));
}
} // Verify tag support
// Convert tags into a sorted lowercase string
// Load the edition
var entry = pathUtil.resolve(opts.cwd || '', edition.directory, opts.entry || edition.entry);
try {
return opts.require(entry);
} catch (loadError) {
// Note the error with more details
throw new Errlop('Skipped edition [' + edition.description + '] at entry [' + entry + '] because it failed to load', loadError);
}
var tags = (edition.tags || edition.syntaxes || []).map(function (i) {
return i.toLowerCase();
}).sort();
for (var index = 0; index < tags.length; index++) {
var _tag = tags[index];
var blacklisted = blacklist[_tag];
if (blacklisted) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because it contained a blacklisted tag [").concat(_tag, "]"),
code: 'unsupported-edition-backlisted-tag'
}, blacklisted);
}
} // Verify engine support
if (edition.engines === false) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because its engines field was false"),
code: 'unsupported-edition-engine'
});
}
if (!edition.engines.node) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because its .engines.node field was falsey"),
code: 'unsupported-edition-engines-node'
});
}
if (opts.strict) {
if (edition.engines.node === true) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because its .engines.node field was true yet we are in strict mode"),
code: 'unsupported-edition-engines-node-version-true'
});
} else if (semver.satisfies(NODE_VERSION, edition.engines.node) === false) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because our current node version [").concat(NODE_VERSION, "] is not supported by its specific range [").concat(stringify(edition.engines.node), "]"),
code: 'unsupported-edition-engines-node-version-specific'
});
}
} else if (edition.engines.node !== true) {
var simplifiedRange = simplifyRange(edition.engines.node);
if (semver.satisfies(NODE_VERSION, simplifiedRange) === false) {
throw errtion({
message: "Skipping edition [".concat(edition.description, "] because our current node version [").concat(NODE_VERSION, "] is not supported by its simplified range [").concat(stringify(simplifiedRange), "]"),
code: 'unsupported-edition-engines-node-version-simplified'
});
}
} // Load the edition
return loadEdition(edition, opts);
}
/**
* Cycles through a list of editions, returning the first suitable edition that it was able to load
* Cycles through a list of editions, returning the require result of the first suitable {@link Edition} that it was able to load.
* Editions should be ordered from most preferable first, to least desirable last.
* Providing the editions configuration is valid, individual edition handling is forwarded to {@link requireEdition}.
* @param {Array<Edition>} editions
* @param {Options} opts
* @returns {*} the result of the loaded edition
* @throws {Error} an error if a suitable edition was unable to be resolved
* @returns {*} The result of the loaded edition.
* @throws {Error} An error if a suitable edition was unable to be resolved.
* @public
*/
function requireEditions(editions, opts) {
// Check
if (!editions || editions.length === 0) {
if (opts.packagePath) {
throw new Errlop('There were no editions specified for package [' + opts.packagePath + ']');
} else {
throw new Errlop('There were no editions specified');
}
}
// Check
if (!editions || editions.length === 0) {
if (opts.packagePath) {
throw errtion({
message: "There were no editions specified for package [".concat(opts.packagePath, "]"),
code: 'unsupported-editions-missing'
});
} else {
throw errtion({
message: 'There were no editions specified',
code: 'unsupported-editions-missing'
});
}
} // Handle strict omission
// Note the last error message
var result = void 0,
editionsError = null,
loaded = false;
// Cycle through the editions
for (var _i = 0; _i < editions.length; ++_i) {
var edition = editions[_i];
try {
result = requireEdition(edition, opts);
loaded = true;
break;
} catch (editionError) {
if (editionError.level === 'fatal') {
editionsError = editionError;
break;
} else if (editionsError) {
editionsError = new Errlop(editionsError, editionError);
} else {
editionsError = editionError;
}
}
}
if (opts.strict == null) {
try {
return requireEditions(editions, _objectSpread({}, opts, {
strict: true
}));
} catch (err) {
return requireEditions(editions, _objectSpread({}, opts, {
strict: false
}));
}
} // Whether or not we should be verbose
if (loaded) {
var verbose = opts.verbose == null ? VERBOSE : opts.verbose;
if (editionsError && verbose) {
var stderr = opts.stderr || process.stderr;
stderr.write(editionsError.stack + '\n');
}
return result;
} else if (editionsError) {
if (opts.packagePath) {
throw new Errlop('There were no suitable editions for package [' + opts.packagePath + ']', editionsError);
} else {
throw new Errlop('There were no suitable editions', editionsError);
}
}
var verbose = opts.verbose == null ? VERBOSE : opts.verbose; // Capture the load result, the last error, and the fallback option
var result,
loaded = false,
editionsError = null,
fallbackEdition = null; // Cycle through the editions determing the above
for (var _i = 0; _i < editions.length; ++_i) {
var edition = editions[_i];
try {
result = requireEdition(edition, opts);
loaded = true;
break;
} catch (editionError) {
if (editionError.level === 'fatal') {
editionsError = editionError;
break;
} else if (editionsError) {
editionsError = errtion(editionsError, editionError);
} else {
editionsError = editionError;
}
if (editionError.code.indexOf('unsupported-edition-engines-node-version') === 0) {
fallbackEdition = edition;
}
}
} // if no edition was suitable for our environment, then try the fallback if it exists
// that is to say, ignore its engines.node
if (opts.strict === false && loaded === false && fallbackEdition) {
try {
result = loadEdition(fallbackEdition, opts);
loaded = true;
} catch (editionError) {
editionsError = new Errlop(editionError, editionsError);
}
} // if we were able to load something, then provide it
if (loaded) {
// make note of any errors if desired
if (editionsError && verbose) {
var stderr = opts.stderr || process.stderr;
stderr.write(editionsError.stack + '\n');
}
return result;
} // otherwise, provide the error
else if (editionsError) {
if (opts.packagePath) {
throw errtion({
message: "There were no suitable editions for package [".concat(opts.packagePath, "]"),
code: 'unsupported-editions-tried'
}, editionsError);
} else {
throw errtion({
message: 'There were no suitable editions',
code: 'unsupported-editions-tried'
}, editionsError);
}
}
}
/**
* Cycle through the editions for a package and require the correct one
* Cycle through the editions for a package and require the correct one.
* Providing the package configuration is valid, editions handling is forwarded to {@link requireEditions}.
* @param {Options.cwd} cwd
* @param {Options.require} require
* @param {Options.entry} [entry]
* @returns {*} the result of the loaded edition
* @throws {Error} an error if a suitable edition was unable to be resolved
* @public
* @returns {any} The result of the loaded edition.
* @throws {Error} An error if a suitable edition was unable to be resolved.
*/
function requirePackage(cwd, require, entry) {
// Load the package.json file to fetch `name` for debugging and `editions` for loading
var packagePath = pathUtil.resolve(cwd, 'package.json');
// Load the package.json file to fetch `name` for debugging and `editions` for loading
var packagePath = pathUtil.resolve(cwd, 'package.json');
var _require = require(packagePath),
editions = _require.editions;
var _require2 = require(packagePath),
editions = _require2.editions;
var opts = { packagePath: packagePath, cwd: cwd, require: require, entry: entry };
return requireEditions(editions, opts);
}
var opts = {
packagePath: packagePath,
cwd: cwd,
require: require,
entry: entry
};
return requireEditions(editions, opts);
} // Exports
// Exports
module.exports = { requireEdition: requireEdition, requireEditions: requireEditions, requirePackage: requirePackage };
module.exports = {
requireEdition: requireEdition,
requireEditions: requireEditions,
requirePackage: requirePackage
};
# History
## v2.1.0 2018 November 15
- If none of the editions for a package match the current node version, editions will try to find a compatible package by converting strict version ranges likes `4 || 6 || 8 || 10` to looser ones like `>=4`, and if that fails, then it will attempt to load the last edition for the environment.
- This brings editions handling of engines closer in line with how node handles it, which is as a warning/recomendation, rather than a requirement/enforcement.
- This has the benefit that edition authors can specify ranges as the specific versions that they have tested the edition against that pass, rather than having to omit that information for runtime compatibility.
- As such editions will now automatically select the edition with guaranteed support for the environment, and if there are none with guaranteed support, then editions will select the one is most likely supported, and if there are none that are likely supported, then it will try the last edition, which should be the most compatible edition.
- This is timely, as node v11 is now the version most developers use, yet if edition authors specified only LTS releases, then the editions autoloader would reject loading on v11, despite compatibility being likely with the most upper edition.
- This behaviour is dictated by the new `strict` option, which omission of a value enables the above behaviour.
- Change `syntaxes` to `tags`, with backwards compatibility. This applies to edition specifications, as well as for the blacklist environment variable which is now named `EDITIONS_TAG_BLACKLIST`.
- Added codes to the different types of errors we may produce.
- Upgraded babel from v6 to v7
- Documentation has swapped from Documentation.js to JSDoc with the Minami theme.
## v2.0.2 2018 September 3

@@ -4,0 +16,0 @@ - Fixed `Error: Cannot find module 'editions'` on Windows (caused by edition directories containing `:` which is unsupported on Windows)

{
"name": "editions",
"version": "2.0.2",
"version": "2.1.0",
"description": "Publish multiple editions for your JavaScript packages consistently and easily (e.g. source edition, esnext edition, es2015 edition)",

@@ -27,14 +27,20 @@ "homepage": "https://github.com/bevry/editions",

"patreon",
"flattr",
"liberapay",
"thanksapp",
"boostlab",
"buymeacoffee",
"opencollective",
"flattr",
"crypto",
"paypal",
"bitcoin",
"wishlist"
],
"config": {
"buymeacoffeeUsername": "balupton",
"cryptoURL": "https://bevry.me/crypto",
"flattrUsername": "balupton",
"liberapayUsername": "bevry",
"opencollectiveUsername": "bevry",
"patreonUsername": "bevry",
"opencollectiveUsername": "bevry",
"flattrUsername": "balupton",
"paypalURL": "https://bevry.me/paypal",
"bitcoinURL": "https://bevry.me/bitcoin",
"wishlistURL": "https://bevry.me/wishlist"

@@ -66,3 +72,3 @@ }

"entry": "index.js",
"syntaxes": [
"tags": [
"javascript",

@@ -81,3 +87,3 @@ "esnext",

"entry": "index.js",
"syntaxes": [
"tags": [
"javascript",

@@ -95,3 +101,3 @@ "require"

"entry": "index.js",
"syntaxes": [
"tags": [
"javascript",

@@ -101,3 +107,3 @@ "require"

"engines": {
"node": "0.8 || 0.10 || 0.12 || 4 || 6 || 8 || 10",
"node": true,
"browsers": false

@@ -110,13 +116,15 @@ }

"dependencies": {
"errlop": "^1.0.2",
"semver": "^5.5.0"
"errlop": "^1.0.3",
"semver": "^5.6.0"
},
"devDependencies": {
"@babel/cli": "^7.1.5",
"@babel/core": "^7.1.6",
"@babel/preset-env": "^7.1.6",
"assert-helpers": "^4.5.1",
"babel-cli": "^6.26.0",
"babel-preset-env": "^1.7.0",
"documentation": "^8.1.2",
"eslint": "^5.5.0",
"eslint": "^5.9.0",
"joe": "^2.0.2",
"joe-reporter-console": "^2.0.2",
"jsdoc": "^3.5.5",
"minami": "^1.2.3",
"projectz": "^1.4.0",

@@ -126,3 +134,3 @@ "surge": "^0.20.1"

"scripts": {
"our:clean": "rm -Rf ./docs ./edition:* ./es2015 ./es5 ./out",
"our:clean": "rm -Rf ./docs ./edition-* ./es2015 ./es5 ./out",
"our:compile": "npm run our:compile:edition-browsers && npm run our:compile:edition-node-0.8",

@@ -133,3 +141,3 @@ "our:compile:edition-browsers": "env BABEL_ENV=edition-browsers babel --out-dir ./edition-browsers ./source",

"our:meta": "npm run our:meta:docs && npm run our:meta:projectz",
"our:meta:docs": "documentation build -f html -o ./docs -g --shallow ./source/**.js",
"our:meta:docs": "rm -Rf ./docs && jsdoc --recurse --pedantic --access all --destination ./docs --package ./package.json --readme ./README.md --template ./node_modules/minami ./source && mv ./docs/$npm_package_name/$npm_package_version/* ./docs/ && rm -Rf ./docs/$npm_package_name/$npm_package_version",
"our:meta:projectz": "projectz compile",

@@ -154,3 +162,3 @@ "our:release": "npm run our:release:prepare && npm run our:release:check-changelog && npm run our:release:check-dirty && npm run our:release:tag && npm run our:release:push",

[
"env",
"@babel/preset-env",
{

@@ -167,3 +175,3 @@ "targets": {

[
"env",
"@babel/preset-env",
{

@@ -170,0 +178,0 @@ "targets": {

@@ -17,6 +17,10 @@ <!-- TITLE/ -->

<span class="badge-patreon"><a href="https://patreon.com/bevry" title="Donate to this project using Patreon"><img src="https://img.shields.io/badge/patreon-donate-yellow.svg" alt="Patreon donate button" /></a></span>
<span class="badge-flattr"><a href="https://flattr.com/profile/balupton" title="Donate to this project using Flattr"><img src="https://img.shields.io/badge/flattr-donate-yellow.svg" alt="Flattr donate button" /></a></span>
<span class="badge-liberapay"><a href="https://liberapay.com/bevry" title="Donate to this project using Liberapay"><img src="https://img.shields.io/badge/liberapay-donate-yellow.svg" alt="Liberapay donate button" /></a></span>
<span class="badge-thanksapp"><a href="https://givethanks.app/donate/npm/editions" title="Donate to this project using Thanks App"><img src="https://img.shields.io/badge/thanksapp-donate-yellow.svg" alt="Thanks App donate button" /></a></span>
<span class="badge-boostlab"><a href="https://boost-lab.app/bevry/editions" title="Donate to this project using Boost Lab"><img src="https://img.shields.io/badge/boostlab-donate-yellow.svg" alt="Boost Lab donate button" /></a></span>
<span class="badge-buymeacoffee"><a href="https://buymeacoffee.com/balupton" title="Donate to this project using Buy Me A Coffee"><img src="https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg" alt="Buy Me A Coffee donate button" /></a></span>
<span class="badge-opencollective"><a href="https://opencollective.com/bevry" title="Donate to this project using Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-yellow.svg" alt="Open Collective donate button" /></a></span>
<span class="badge-flattr"><a href="https://flattr.com/profile/balupton" title="Donate to this project using Flattr"><img src="https://img.shields.io/badge/flattr-donate-yellow.svg" alt="Flattr donate button" /></a></span>
<span class="badge-crypto"><a href="https://bevry.me/crypto" title="Donate to this project using Cryptocurrency"><img src="https://img.shields.io/badge/crypto-donate-yellow.svg" alt="crypto donate button" /></a></span>
<span class="badge-paypal"><a href="https://bevry.me/paypal" title="Donate to this project using Paypal"><img src="https://img.shields.io/badge/paypal-donate-yellow.svg" alt="PayPal donate button" /></a></span>
<span class="badge-bitcoin"><a href="https://bevry.me/bitcoin" title="Donate once-off to this project using Bitcoin"><img src="https://img.shields.io/badge/bitcoin-donate-yellow.svg" alt="Bitcoin donate button" /></a></span>
<span class="badge-wishlist"><a href="https://bevry.me/wishlist" title="Buy an item on our wishlist for us"><img src="https://img.shields.io/badge/wishlist-donate-yellow.svg" alt="Wishlist browse button" /></a></span>

@@ -76,6 +80,10 @@

<span class="badge-patreon"><a href="https://patreon.com/bevry" title="Donate to this project using Patreon"><img src="https://img.shields.io/badge/patreon-donate-yellow.svg" alt="Patreon donate button" /></a></span>
<span class="badge-flattr"><a href="https://flattr.com/profile/balupton" title="Donate to this project using Flattr"><img src="https://img.shields.io/badge/flattr-donate-yellow.svg" alt="Flattr donate button" /></a></span>
<span class="badge-liberapay"><a href="https://liberapay.com/bevry" title="Donate to this project using Liberapay"><img src="https://img.shields.io/badge/liberapay-donate-yellow.svg" alt="Liberapay donate button" /></a></span>
<span class="badge-thanksapp"><a href="https://givethanks.app/donate/npm/editions" title="Donate to this project using Thanks App"><img src="https://img.shields.io/badge/thanksapp-donate-yellow.svg" alt="Thanks App donate button" /></a></span>
<span class="badge-boostlab"><a href="https://boost-lab.app/bevry/editions" title="Donate to this project using Boost Lab"><img src="https://img.shields.io/badge/boostlab-donate-yellow.svg" alt="Boost Lab donate button" /></a></span>
<span class="badge-buymeacoffee"><a href="https://buymeacoffee.com/balupton" title="Donate to this project using Buy Me A Coffee"><img src="https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg" alt="Buy Me A Coffee donate button" /></a></span>
<span class="badge-opencollective"><a href="https://opencollective.com/bevry" title="Donate to this project using Open Collective"><img src="https://img.shields.io/badge/open%20collective-donate-yellow.svg" alt="Open Collective donate button" /></a></span>
<span class="badge-flattr"><a href="https://flattr.com/profile/balupton" title="Donate to this project using Flattr"><img src="https://img.shields.io/badge/flattr-donate-yellow.svg" alt="Flattr donate button" /></a></span>
<span class="badge-crypto"><a href="https://bevry.me/crypto" title="Donate to this project using Cryptocurrency"><img src="https://img.shields.io/badge/crypto-donate-yellow.svg" alt="crypto donate button" /></a></span>
<span class="badge-paypal"><a href="https://bevry.me/paypal" title="Donate to this project using Paypal"><img src="https://img.shields.io/badge/paypal-donate-yellow.svg" alt="PayPal donate button" /></a></span>
<span class="badge-bitcoin"><a href="https://bevry.me/bitcoin" title="Donate once-off to this project using Bitcoin"><img src="https://img.shields.io/badge/bitcoin-donate-yellow.svg" alt="Bitcoin donate button" /></a></span>
<span class="badge-wishlist"><a href="https://bevry.me/wishlist" title="Buy an item on our wishlist for us"><img src="https://img.shields.io/badge/wishlist-donate-yellow.svg" alt="Wishlist browse button" /></a></span>

@@ -82,0 +90,0 @@

/* eslint no-console:0 */
'use strict'
/**
* @typedef {Object} Edition
* @property {string} description
* @property {string} directory
* @property {string} entry
* @property {Array<string>} syntaxes
* @property {false|Object.<string, string|boolean>} engines
* @public
*/
/**
* @typedef {Object} Options
* @property {Function} require - the require method of the calling module, used to ensure require paths remain correct
* @property {string} [packagePath] - if provided, this is used for debugging
* @property {boolean} [verbose] - if provided, any error loading an edition will be logged. By default, errors are only logged if all editions failed. If not provided, process.env.EDITIONS_VERBOSE is used.
* @property {string} [cwd] - if provided, this will be the cwd for entries
* @property {string} [entry] - if provided, should be a relative path to the entry point of the edition
* @property {string} [package] - if provided, should be the name of the package that we are loading the editions for
* @property {stream.Writable} [stderr] - if not provided, will use process.stderr instead. It is the stream that verbose errors are logged to.
* @public
*/
// Imports

@@ -30,34 +8,146 @@ const pathUtil = require('path')

const Errlop = require('errlop')
const { errtion, stringify, simplifyRange } = require('./util.js')
// Helpers
function stringify (value) {
return typeof value === 'string' ? value : JSON.stringify(value)
}
// Environment fetching
const BLACKLIST = process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(/[, ]+/g)
/**
* The current node version that we are operating within.
* It is compared in {@link requireEdition} against {@link Edition.engines}.
* @type {string}
* @private
*/
const NODE_VERSION = process.versions.node
/**
* Set the environment variable `EDITIONS_VERBOSE` to output debugging information to stderr on how editions selected the edition it did.
* Values of `yes` and `true` are supported.
* @example env EDITIONS_VERBOSE=true node mypackage/index.js
* @typedef {String} EDITIONS_VERBOSE
*/
/**
* Whether or not {@link EDITIONS_VERBOSE} is enabled.
* @type {bollean}
* @private
*/
const VERBOSE = process.env.EDITIONS_VERBOSE === true || process.env.EDITIONS_VERBOSE === 'yes' || process.env.EDITIONS_VERBOSE === 'true' || false
// Cache of which syntax combinations are supported or unsupported
// Object.<string, Error>
/**
* Set the environment variable `EDITIONS_TAG_BLACKLIST` to the tags you wish to blacklist, and editions will skip editions that contain them.
* For backwards compatibility `EDITIONS_SYNTAX_BLACKLIST` is also supported.
* It is compared in {@link requireEdition} against {@link Edition.tags}.
* The value of this is stored locally in the {@link BLACKLIST} cache.
* @example env EDITIONS_TAG_BLACKLIST=esnext,typescript,coffeescript node mypackage/index.js
* @typedef {String} EDITIONS_TAG_BLACKLIST
*/
/**
* A list of the blacklisted tags.
* Data imported from {@link EDITIONS_TAG_BLACKLIST}.
* @type {Array<string>}
* @private
*/
const BLACKLIST =
(process.env.EDITIONS_TAG_BLACKLIST && process.env.EDITIONS_TAG_BLACKLIST.split(/[, ]+/g))
|| (process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(/[, ]+/g))
/**
* A mapping of blacklisted tags to their reasons.
* Keys are the tags.
* Values are the error instances that contain the reasoning for why/how that tag is/became blacklisted.
* Data imported from {@link EDITIONS_TAG_BLACKLIST}.
* @type {Object.<string, Error>}
* @private
*/
const blacklist = {}
// Check the environment configuration for a syntax blacklist
/**
* Edition entries must conform to the following specification.
* @typedef {Object} Edition
* @property {string} description Use this property to decribe the edition in human readable terms. Such as what it does and who it is for. It is used to reference the edition in user facing reporting, such as error messages. E.g. `esnext source code with require for modules`.
* @property {string} directory The location to where this directory is located. It should be a relative path from the `package.json` file. E.g. `source`.
* @property {string} entry The default entry location for this edition, relative to the edition's directory. E.g. `index.js`.
* @property {Array<string>} [tags] Any keywords you wish to associate to the edition. Useful for various ecosystem tooling, such as automatic ESNext lint configuration if the `esnext` tag is present in the source edition tags. Consumers also make use of this via {@link EDITIONS_TAG_BLACKLIST} for preventing loading editions that contain a blacklisted tag. Previously this field was named `syntaxes`. E.g. `["javascript", "esnext", "require"]`.
* @property {false|Object.<string, string|boolean>} engines This field is used to specific which Node.js and Browser environments this edition supports. If `false` this edition does not support either. If `node` is a string, it should be a semver range of node.js versions that the edition targets. If `browsers` is a string, it should be a [browserlist](https://github.com/browserslist/browserslist) value of the specific browser values the edition targets. If `node` or `browsers` is true, it indicates that this edition is compatible with those environments.
* @example
* {
* "description": "esnext source code with require for modules",
* "directory": "source",
* "entry": "index.js",
* "tags": [
* "javascript",
* "esnext",
* "require"
* ],
* "engines": {
* "node": ">=6",
* "browsers": "defaults"
* }
* }
*/
/**
* These are the various options that you can use to customise the behaviour of certain methods.
* @typedef {Object} Options
* @property {Function} require The require method of the calling module, used to ensure require paths remain correct.
* @property {string} [packagePath] If provided, this is used for debugging.
* @property {boolean} [verbose] If provided, any error loading an edition will be logged. By default, errors are only logged if all editions failed. If not provided, process.env.EDITIONS_VERBOSE is used.
* @property {boolean} [strict] If `true`, then only exact version matches will be loaded. If `false`, then likely matches using {@link simplifyRange} will be evaluated, with a fallback to the last. If missing, then `true` is attempted first and if no result, then `false` is attempted.
* @property {string} [cwd] If provided, this will be the cwd for entries.
* @property {string} [entry] If provided, should be a relative path to the entry point of the edition.
* @property {string} [package] If provided, should be the name of the package that we are loading the editions for.
* @property {stream.Writable} [stderr] If not provided, will use process.stderr instead. It is the stream that verbose errors are logged to.
*/
// Create the mapping of blacklisted tags and their reasonings
if (BLACKLIST) {
for (let i = 0; i < BLACKLIST.length; ++i) {
const syntax = BLACKLIST[i].trim().toLowerCase()
blacklist[syntax] = new Errlop(`The EDITIONS_SYNTAX_BLACKLIST environment variable blacklisted the syntax [${syntax}]`)
const tag = BLACKLIST[i].trim().toLowerCase()
blacklist[tag] = errtion({
message: `The EDITIONS_TAG_BLACKLIST (aka EDITIONS_SYNTAX_BLACKLIST) environment variable blacklisted the tag [${tag}]`,
code: 'blacklisted-tag'
})
}
}
// Blacklist the syntax 'esnext' if our node version is below 0.12
// Blacklist the tag 'esnext' if our node version is below 0.12
if (semver.satisfies(NODE_VERSION, '<0.12')) {
blacklist.esnext = new Error('The esnext syntax is skipped on early node versions as attempting to use esnext features will output debugging information on these node versions')
blacklist.esnext = new Error('The esnext tag is skipped on early node versions as attempting to use esnext features will output debugging information on these node versions')
}
/**
* Attempt to load a specific edition
* Attempt to load a specific {@link Edition}.
* @param {Edition} edition
* @param {Options} opts
* @returns {*} The result of the loaded edition.
* @throws {Error} An error if the edition failed to load.
* @public
*/
function loadEdition (edition, opts) {
const entry = pathUtil.resolve(opts.cwd || '', edition.directory, opts.entry || edition.entry)
if (opts.require == null) {
throw errtion({
message: `Skipped edition [${edition.description}] as opts.require was not provided, this is probably due to a testing misconfiguration.`,
code: 'unsupported-edition-require'
})
}
try {
return opts.require(entry)
}
catch (loadError) {
// Note the error with more details
throw errtion({
message: `Skipped edition [${edition.description}] at entry [${entry}] because it failed to load`,
code: 'unsupported-edition-tried'
}, loadError)
}
}
/**
* Attempt to require an {@link Edition}, based on its compatibility with the current environment, such as {@link NODE_VERSION} and {@link EDITIONS_TAG_BLACKLIST} compatibility.
* If compatibility is established with the environment, it will load the edition using {@link loadEdition}.
* @param {Edition} edition
* @param {Options} opts
* @returns {*} the result of the loaded edition

@@ -70,26 +160,69 @@ * @throws {Error} an error if the edition failed to load

if (!edition.description || !edition.directory || !edition.entry || edition.engines == null) {
const editionInvalidError = new Errlop(`Each edition must have its [description, directory, entry, engines] fields defined, yet all it had was [${Object.keys(edition).join(', ')}]`)
editionInvalidError.level = 'fatal'
throw editionInvalidError
throw errtion({
message: `Each edition must have its [description, directory, entry, engines] fields defined, yet all it had was [${Object.keys(edition).join(', ')}]`,
code: 'unsupported-edition-malformed',
level: 'fatal'
})
}
// Handle strict omission
if (opts.strict == null) {
try {
return requireEdition(edition, { ...opts, strict: true })
}
catch (err) {
return requireEdition(edition, { ...opts, strict: false })
}
}
// Verify tag support
// Convert tags into a sorted lowercase string
const tags = (edition.tags || edition.syntaxes || []).map(
(i) => i.toLowerCase()
).sort()
for (let index = 0; index < tags.length; index++) {
const tag = tags[index]
const blacklisted = blacklist[tag]
if (blacklisted) {
throw errtion({
message: `Skipping edition [${edition.description}] because it contained a blacklisted tag [${tag}]`,
code: 'unsupported-edition-backlisted-tag'
}, blacklisted)
}
}
// Verify engine support
if (edition.engines === false) {
throw new Errlop(`Skipping edition [${edition.description}] because its engines field was false`)
throw errtion({
message: `Skipping edition [${edition.description}] because its engines field was false`,
code: 'unsupported-edition-engine'
})
}
if (!edition.engines.node) {
throw new Errlop(`Skipping edition [${edition.description}] because its .engines.node field was falsey`)
throw errtion({
message: `Skipping edition [${edition.description}] because its .engines.node field was falsey`,
code: 'unsupported-edition-engines-node'
})
}
if (edition.engines.node !== true && semver.satisfies(NODE_VERSION, edition.engines.node) === false) {
throw new Errlop(`Skipping edition [${edition.description}] because our current node version [${NODE_VERSION}] is not supported by its [${stringify(edition.engines.node)}]`)
if (opts.strict) {
if (edition.engines.node === true) {
throw errtion({
message: `Skipping edition [${edition.description}] because its .engines.node field was true yet we are in strict mode`,
code: 'unsupported-edition-engines-node-version-true'
})
}
else if (semver.satisfies(NODE_VERSION, edition.engines.node) === false) {
throw errtion({
message: `Skipping edition [${edition.description}] because our current node version [${NODE_VERSION}] is not supported by its specific range [${stringify(edition.engines.node)}]`,
code: 'unsupported-edition-engines-node-version-specific'
})
}
}
// Verify syntax support
// Convert syntaxes into a sorted lowercase string
const syntaxes = (edition.syntaxes && edition.syntaxes.map((i) => i.toLowerCase()).sort()) || []
for (let index = 0; index < syntaxes.length; index++) {
const syntax = syntaxes[index]
const blacklisted = blacklist[syntax]
if (blacklisted) {
throw new Errlop(`Skipping edition [${edition.description}] because it contained a blacklisted syntax [${syntax}]`, blacklisted)
else if (edition.engines.node !== true) {
const simplifiedRange = simplifyRange(edition.engines.node)
if (semver.satisfies(NODE_VERSION, simplifiedRange) === false) {
throw errtion({
message: `Skipping edition [${edition.description}] because our current node version [${NODE_VERSION}] is not supported by its simplified range [${stringify(simplifiedRange)}]`,
code: 'unsupported-edition-engines-node-version-simplified'
})
}

@@ -99,18 +232,13 @@ }

// Load the edition
const entry = pathUtil.resolve(opts.cwd || '', edition.directory, opts.entry || edition.entry)
try {
return opts.require(entry)
}
catch (loadError) {
// Note the error with more details
throw new Errlop(`Skipped edition [${edition.description}] at entry [${entry}] because it failed to load`, loadError)
}
return loadEdition(edition, opts)
}
/**
* Cycles through a list of editions, returning the first suitable edition that it was able to load
* Cycles through a list of editions, returning the require result of the first suitable {@link Edition} that it was able to load.
* Editions should be ordered from most preferable first, to least desirable last.
* Providing the editions configuration is valid, individual edition handling is forwarded to {@link requireEdition}.
* @param {Array<Edition>} editions
* @param {Options} opts
* @returns {*} the result of the loaded edition
* @throws {Error} an error if a suitable edition was unable to be resolved
* @returns {*} The result of the loaded edition.
* @throws {Error} An error if a suitable edition was unable to be resolved.
* @public

@@ -122,13 +250,32 @@ */

if (opts.packagePath) {
throw new Errlop(`There were no editions specified for package [${opts.packagePath}]`)
throw errtion({
message: `There were no editions specified for package [${opts.packagePath}]`,
code: 'unsupported-editions-missing'
})
}
else {
throw new Errlop('There were no editions specified')
throw errtion({
message: 'There were no editions specified',
code: 'unsupported-editions-missing'
})
}
}
// Note the last error message
let result, editionsError = null, loaded = false
// Handle strict omission
if (opts.strict == null) {
try {
return requireEditions(editions, { ...opts, strict: true })
}
catch (err) {
return requireEditions(editions, { ...opts, strict: false })
}
}
// Cycle through the editions
// Whether or not we should be verbose
const verbose = opts.verbose == null ? VERBOSE : opts.verbose
// Capture the load result, the last error, and the fallback option
let result, loaded = false, editionsError = null, fallbackEdition = null
// Cycle through the editions determing the above
for (let i = 0; i < editions.length; ++i) {

@@ -147,3 +294,3 @@ const edition = editions[i]

else if (editionsError) {
editionsError = new Errlop(editionsError, editionError)
editionsError = errtion(editionsError, editionError)
}

@@ -153,7 +300,23 @@ else {

}
if (editionError.code.indexOf('unsupported-edition-engines-node-version') === 0) {
fallbackEdition = edition
}
}
}
// if no edition was suitable for our environment, then try the fallback if it exists
// that is to say, ignore its engines.node
if (opts.strict === false && loaded === false && fallbackEdition) {
try {
result = loadEdition(fallbackEdition, opts)
loaded = true
}
catch (editionError) {
editionsError = new Errlop(editionError, editionsError)
}
}
// if we were able to load something, then provide it
if (loaded) {
const verbose = opts.verbose == null ? VERBOSE : opts.verbose
// make note of any errors if desired
if (editionsError && verbose) {

@@ -165,8 +328,15 @@ const stderr = opts.stderr || process.stderr

}
// otherwise, provide the error
else if (editionsError) {
if (opts.packagePath) {
throw new Errlop(`There were no suitable editions for package [${opts.packagePath}]`, editionsError)
throw errtion({
message: `There were no suitable editions for package [${opts.packagePath}]`,
code: 'unsupported-editions-tried'
}, editionsError)
}
else {
throw new Errlop('There were no suitable editions', editionsError)
throw errtion({
message: 'There were no suitable editions',
code: 'unsupported-editions-tried'
}, editionsError)
}

@@ -177,9 +347,9 @@ }

/**
* Cycle through the editions for a package and require the correct one
* Cycle through the editions for a package and require the correct one.
* Providing the package configuration is valid, editions handling is forwarded to {@link requireEditions}.
* @param {Options.cwd} cwd
* @param {Options.require} require
* @param {Options.entry} [entry]
* @returns {*} the result of the loaded edition
* @throws {Error} an error if a suitable edition was unable to be resolved
* @public
* @returns {any} The result of the loaded edition.
* @throws {Error} An error if a suitable edition was unable to be resolved.
*/

@@ -186,0 +356,0 @@ function requirePackage (cwd, require, entry) {

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc