index.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211
  1. /* @flow */
  2. /* eslint no-console:0 */
  3. 'use strict';
  4. // Imports
  5. function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
  6. function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
  7. function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
  8. var pathUtil = require('path');
  9. // Helper class to display nested error in a sensible way
  10. var DetailedError = function (_Error) {
  11. _inherits(DetailedError, _Error);
  12. function DetailedError(message /* :string */, details /* :Object */) {
  13. _classCallCheck(this, DetailedError);
  14. Object.keys(details).forEach(function (key) {
  15. var data = details[key];
  16. var value = require('util').inspect(data.stack || data.message || data);
  17. message += '\n' + key + ': ' + value;
  18. });
  19. return _possibleConstructorReturn(this, (DetailedError.__proto__ || Object.getPrototypeOf(DetailedError)).call(this, message));
  20. }
  21. return DetailedError;
  22. }(Error);
  23. // Environment fetching
  24. var blacklist = process && process.env && process.env.EDITIONS_SYNTAX_BLACKLIST && process.env.EDITIONS_SYNTAX_BLACKLIST.split(',');
  25. // Cache of which syntax combinations are supported or unsupported, hash of booleans
  26. var syntaxFailedCombitions = {}; // sorted lowercase syntax combination => Error instance of failure
  27. var syntaxBlacklist = {};
  28. syntaxBlacklist.import = new Error('The import syntax is skipped as the module package.json field eliminates the need for autoloader support');
  29. syntaxBlacklist.coffeescript = new Error('The coffeescript syntax is skipped as we want to use a precompiled edition rather than compiling at runtime');
  30. syntaxBlacklist.typescript = new Error('The typescript syntax is skipped as we want to use a precompiled edition rather than compiling at runtime');
  31. // Blacklist non-esnext node versions from esnext
  32. if (process && process.versions && process.versions.node) {
  33. var EARLIEST_ESNEXT_NODE_VERSION = [0, 12];
  34. var NODE_VERSION = process.versions.node.split('.').map(function (n) {
  35. return parseInt(n, 10);
  36. });
  37. var ESNEXT_UNSUPPORTED = NODE_VERSION[0] < EARLIEST_ESNEXT_NODE_VERSION[0] || NODE_VERSION[0] === EARLIEST_ESNEXT_NODE_VERSION[0] && NODE_VERSION[1] < EARLIEST_ESNEXT_NODE_VERSION[1];
  38. if (ESNEXT_UNSUPPORTED) syntaxBlacklist.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');
  39. }
  40. // Check the environment configuration for a syntax blacklist
  41. if (blacklist) {
  42. for (var i = 0; i < blacklist.length; ++i) {
  43. var syntax = blacklist[i].trim().toLowerCase();
  44. syntaxBlacklist[syntax] = new DetailedError('The EDITIONS_SYNTAX_BLACKLIST environment variable has blacklisted an edition syntax:', { syntax: syntax, blacklist: blacklist });
  45. }
  46. }
  47. /* ::
  48. type edition = {
  49. name:number,
  50. description?:string,
  51. directory?:string,
  52. entry?:string,
  53. syntaxes?:Array<string>
  54. };
  55. type options = {
  56. cwd?:string,
  57. package?:string,
  58. entry?:string,
  59. require:function
  60. };
  61. */
  62. /**
  63. * Cycle through the editions and require the correct one
  64. * @protected internal function that is untested for public consumption
  65. * @param {edition} edition - the edition entry
  66. * @param {Object} opts - the following options
  67. * @param {string} opts.require - the require method of the calling module, used to ensure require paths remain correct
  68. * @param {string} [opts.cwd] - if provided, this will be the cwd for entries
  69. * @param {string} [opts.entry] - if provided, should be a relative or absolute path to the entry point of the edition
  70. * @param {string} [opts.package] - if provided, should be the name of the package that we are loading the editions for
  71. * @returns {*}
  72. */
  73. function requireEdition(edition /* :edition */, opts /* :options */) /* :any */{
  74. // Prevent require from being included in debug logs
  75. Object.defineProperty(opts, 'require', { value: opts.require, enumerable: false });
  76. // Get the correct entry path
  77. // As older versions o
  78. var cwd = opts.cwd || '';
  79. var dir = edition.directory || '';
  80. var entry = opts.entry || edition.entry || '';
  81. if (dir && entry && entry.indexOf(dir + '/') === 0) entry = entry.substring(dir.length + 1);
  82. // ^ this should not be needed, but as previous versions of editions included the directory inside the entry
  83. // it unfortunately is, as such this is a stepping stone for the new format, the new format being
  84. // if entry is specified by itself, it is cwd => entry
  85. // if entry is specified with a directory, it is cwd => dir => entry
  86. // if entry is not specified but dir is, it is cwd => dir
  87. // if neither entry nor dir are specified, we have a problem
  88. if (!dir && !entry) {
  89. var editionFailure = new DetailedError('Skipped edition due to no entry or directory being specified:', { edition: edition, cwd: cwd, dir: dir, entry: entry });
  90. throw editionFailure;
  91. }
  92. var entryPath = pathUtil.resolve(cwd, dir, entry);
  93. // Check syntax support
  94. // Convert syntaxes into a sorted lowercase string
  95. var syntaxes = edition.syntaxes && edition.syntaxes.map(function (i) {
  96. return i.toLowerCase();
  97. }).sort();
  98. var syntaxCombination = syntaxes && syntaxes.join(', ');
  99. if (syntaxes && syntaxCombination) {
  100. // Check if any of the syntaxes are unsupported
  101. var unsupportedSyntaxes = syntaxes.filter(function (i) {
  102. return syntaxBlacklist[i.toLowerCase()];
  103. });
  104. if (unsupportedSyntaxes.length) {
  105. var _editionFailure = new DetailedError('Skipped edition due to it containing an unsupported syntax:', { edition: edition, unsupportedSyntaxes: unsupportedSyntaxes });
  106. throw _editionFailure;
  107. }
  108. // Is this syntax combination unsupported? If so skip it with a soft failure to try the next edition
  109. else if (syntaxFailedCombitions[syntaxCombination]) {
  110. var previousCombinationFailure = syntaxFailedCombitions[syntaxCombination];
  111. var _editionFailure2 = new DetailedError('Skipped edition due to its syntax combinatiom failing previously:', { edition: edition, previousCombinationFailure: previousCombinationFailure });
  112. throw _editionFailure2;
  113. }
  114. }
  115. // Try and load this syntax combination
  116. try {
  117. return opts.require(entryPath);
  118. } catch (error) {
  119. // Note the error with more details
  120. var _editionFailure3 = new DetailedError('Failed to load the edition due to a load error:', { edition: edition, error: error.stack });
  121. // Blacklist the combination, even if it may have worked before
  122. // Perhaps in the future note if that if it did work previously, then we should instruct module owners to be more specific with their syntaxes
  123. if (syntaxCombination) syntaxFailedCombitions[syntaxCombination] = _editionFailure3;
  124. // Continue to the next edition
  125. throw _editionFailure3;
  126. }
  127. }
  128. /**
  129. * Cycle through the editions and require the correct one
  130. * @protected internal function that is untested for public consumption
  131. * @param {Array<edition>} editions - an array of edition entries
  132. * @param {Object} opts - the following options
  133. * @param {string} opts.require - the require method of the calling module, used to ensure require paths remain correct
  134. * @param {string} [opts.cwd] - if provided, this will be the cwd for entries
  135. * @param {string} [opts.entry] - if provided, should be a relative path to the entry point of the edition
  136. * @param {string} [opts.package] - if provided, should be the name of the package that we are loading the editions for
  137. * @returns {*}
  138. */
  139. function requireEditions(editions /* :Array<edition> */, opts /* :options */) /* :any */{
  140. // Extract
  141. if (opts.package == null) opts.package = 'custom runtime package';
  142. // Check
  143. if (!editions || editions.length === 0) {
  144. throw new DetailedError('No editions were specified:', { opts: opts });
  145. }
  146. // Note the last error message
  147. var editionFailures = [];
  148. // Cycle through the editions
  149. for (var _i = 0; _i < editions.length; ++_i) {
  150. var edition = editions[_i];
  151. try {
  152. return requireEdition(edition, opts);
  153. } catch (err) {
  154. editionFailures.push(err);
  155. }
  156. }
  157. // Through the error as no edition loaded
  158. throw new DetailedError('There are no suitable editions for this environment:', { opts: opts, editions: editions, failures: editionFailures });
  159. }
  160. /**
  161. * Cycle through the editions for a package and require the correct one
  162. * @param {string} cwd - the path of the package, used to load package.json:editions and handle relative edition entry points
  163. * @param {function} require - the require method of the calling module, used to ensure require paths remain correct
  164. * @param {string} [entry] - an optional override for the entry of an edition, requires the edition to specify a `directory` property
  165. * @returns {*}
  166. */
  167. function requirePackage(cwd /* :string */, require /* :function */, entry /* :: ?:string */) /* :any */{
  168. // Load the package.json file to fetch `name` for debugging and `editions` for loading
  169. var packagePath = pathUtil.resolve(cwd, 'package.json');
  170. var _require = require(packagePath),
  171. name = _require.name,
  172. editions = _require.editions;
  173. var opts /* :options */ = { cwd: cwd, require: require };
  174. if (name) opts.package = name;
  175. if (entry) opts.entry = entry;
  176. return requireEditions(editions, opts);
  177. }
  178. // Exports
  179. module.exports = { requireEdition: requireEdition, requireEditions: requireEditions, requirePackage: requirePackage };