index.js 8.3 KB

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