parser.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. // External libraries are lazy-loaded only if these file types exist.
  2. var Yaml = null,
  3. VisionmediaYaml = null,
  4. Coffee = null,
  5. Iced = null,
  6. CSON = null,
  7. PPARSER = null,
  8. JSON5 = null,
  9. TOML = null,
  10. HJSON = null,
  11. XML = null;
  12. // Define soft dependencies so transpilers don't include everything
  13. var COFFEE_2_DEP = 'coffeescript',
  14. COFFEE_DEP = 'coffee-script',
  15. ICED_DEP = 'iced-coffee-script',
  16. JS_YAML_DEP = 'js-yaml',
  17. YAML_DEP = 'yaml',
  18. JSON5_DEP = 'json5',
  19. HJSON_DEP = 'hjson',
  20. TOML_DEP = 'toml',
  21. CSON_DEP = 'cson',
  22. PPARSER_DEP = 'properties',
  23. XML_DEP = 'x2js',
  24. TS_DEP = 'ts-node';
  25. var Parser = module.exports;
  26. Parser.parse = function(filename, content) {
  27. var parserName = filename.substr(filename.lastIndexOf('.') +1); // file extension
  28. if (typeof definitions[parserName] === 'function') {
  29. return definitions[parserName](filename, content);
  30. }
  31. // TODO: decide what to do in case of a missing parser
  32. };
  33. Parser.xmlParser = function(filename, content) {
  34. if (!XML) {
  35. XML = require(XML_DEP);
  36. }
  37. var x2js = new XML();
  38. var configObject = x2js.xml2js(content);
  39. var rootKeys = Object.keys(configObject);
  40. if(rootKeys.length === 1) {
  41. return configObject[rootKeys[0]];
  42. }
  43. return configObject;
  44. };
  45. Parser.jsParser = function(filename, content) {
  46. return require(filename);
  47. };
  48. Parser.tsParser = function(filename, content) {
  49. if (!require.extensions['.ts']) {
  50. require(TS_DEP).register({
  51. lazy: true,
  52. compilerOptions: {
  53. allowJs: true,
  54. }
  55. });
  56. }
  57. // Imports config if it is exported via module.exports = ...
  58. // See https://github.com/lorenwest/node-config/issues/524
  59. var configObject = require(filename);
  60. // Because of ES6 modules usage, `default` is treated as named export (like any other)
  61. // Therefore config is a value of `default` key.
  62. if (configObject.default) {
  63. return configObject.default
  64. }
  65. return configObject;
  66. };
  67. Parser.coffeeParser = function(filename, content) {
  68. // .coffee files can be loaded with either coffee-script or iced-coffee-script.
  69. // Prefer iced-coffee-script, if it exists.
  70. // Lazy load the appropriate extension
  71. if (!Coffee) {
  72. Coffee = {};
  73. // The following enables iced-coffee-script on .coffee files, if iced-coffee-script is available.
  74. // This is commented as per a decision on a pull request.
  75. //try {
  76. // Coffee = require('iced-coffee-script');
  77. //}
  78. //catch (e) {
  79. // Coffee = require('coffee-script');
  80. //}
  81. try {
  82. // Try to load coffeescript
  83. Coffee = require(COFFEE_2_DEP);
  84. }
  85. catch (e) {
  86. // If it doesn't exist, try to load it using the deprecated module name
  87. Coffee = require(COFFEE_DEP);
  88. }
  89. // coffee-script >= 1.7.0 requires explicit registration for require() to work
  90. if (Coffee.register) {
  91. Coffee.register();
  92. }
  93. }
  94. // Use the built-in parser for .coffee files with coffee-script
  95. return require(filename);
  96. };
  97. Parser.icedParser = function(filename, content) {
  98. Iced = require(ICED_DEP);
  99. // coffee-script >= 1.7.0 requires explicit registration for require() to work
  100. if (Iced.register) {
  101. Iced.register();
  102. }
  103. };
  104. Parser.yamlParser = function(filename, content) {
  105. if (!Yaml && !VisionmediaYaml) {
  106. // Lazy loading
  107. try {
  108. // Try to load the better js-yaml module
  109. Yaml = require(JS_YAML_DEP);
  110. }
  111. catch (e) {
  112. try {
  113. // If it doesn't exist, load the fallback visionmedia yaml module.
  114. VisionmediaYaml = require(YAML_DEP);
  115. }
  116. catch (e) { }
  117. }
  118. }
  119. if (Yaml) {
  120. return Yaml.load(content);
  121. }
  122. else if (VisionmediaYaml) {
  123. // The yaml library doesn't like strings that have newlines but don't
  124. // end in a newline: https://github.com/visionmedia/js-yaml/issues/issue/13
  125. content += '\n';
  126. return VisionmediaYaml.eval(Parser.stripYamlComments(content));
  127. }
  128. else {
  129. console.error('No YAML parser loaded. Suggest adding js-yaml dependency to your package.json file.')
  130. }
  131. };
  132. Parser.jsonParser = function(filename, content) {
  133. try {
  134. return JSON.parse(content);
  135. }
  136. catch (e) {
  137. // All JS Style comments will begin with /, so all JSON parse errors that
  138. // encountered a syntax error will complain about this character.
  139. if (e.name !== 'SyntaxError' || e.message.indexOf('Unexpected token /') !== 0) {
  140. throw e;
  141. }
  142. if (!JSON5) {
  143. JSON5 = require(JSON5_DEP);
  144. }
  145. return JSON5.parse(content);
  146. }
  147. };
  148. Parser.json5Parser = function(filename, content) {
  149. if (!JSON5) {
  150. JSON5 = require(JSON5_DEP);
  151. }
  152. return JSON5.parse(content);
  153. };
  154. Parser.hjsonParser = function(filename, content) {
  155. if (!HJSON) {
  156. HJSON = require(HJSON_DEP);
  157. }
  158. return HJSON.parse(content);
  159. };
  160. Parser.tomlParser = function(filename, content) {
  161. if(!TOML) {
  162. TOML = require(TOML_DEP);
  163. }
  164. return TOML.parse(content);
  165. };
  166. Parser.csonParser = function(filename, content) {
  167. if (!CSON) {
  168. CSON = require(CSON_DEP);
  169. }
  170. // Allow comments in CSON files
  171. if (typeof CSON.parseSync === 'function') {
  172. return CSON.parseSync(Parser.stripComments(content));
  173. }
  174. return CSON.parse(Parser.stripComments(content));
  175. };
  176. Parser.propertiesParser = function(filename, content) {
  177. if (!PPARSER) {
  178. PPARSER = require(PPARSER_DEP);
  179. }
  180. return PPARSER.parse(content, { namespaces: true, variables: true, sections: true });
  181. };
  182. /**
  183. * Strip all Javascript type comments from the string.
  184. *
  185. * The string is usually a file loaded from the O/S, containing
  186. * newlines and javascript type comments.
  187. *
  188. * Thanks to James Padolsey, and all who contributed to this implementation.
  189. * http://james.padolsey.com/javascript/javascript-comment-removal-revisted/
  190. *
  191. * @protected
  192. * @method stripComments
  193. * @param fileStr {string} The string to strip comments from
  194. * @param stringRegex {RegExp} Optional regular expression to match strings that
  195. * make up the config file
  196. * @return {string} The string with comments stripped.
  197. */
  198. Parser.stripComments = function(fileStr, stringRegex) {
  199. stringRegex = stringRegex || /(['"])(\\\1|.)+?\1/g;
  200. var uid = '_' + +new Date(),
  201. primitives = [],
  202. primIndex = 0;
  203. return (
  204. fileStr
  205. /* Remove strings */
  206. .replace(stringRegex, function(match){
  207. primitives[primIndex] = match;
  208. return (uid + '') + primIndex++;
  209. })
  210. /* Remove Regexes */
  211. .replace(/([^\/])(\/(?!\*|\/)(\\\/|.)+?\/[gim]{0,3})/g, function(match, $1, $2){
  212. primitives[primIndex] = $2;
  213. return $1 + (uid + '') + primIndex++;
  214. })
  215. /*
  216. - Remove single-line comments that contain would-be multi-line delimiters
  217. E.g. // Comment /* <--
  218. - Remove multi-line comments that contain would be single-line delimiters
  219. E.g. /* // <--
  220. */
  221. .replace(/\/\/.*?\/?\*.+?(?=\n|\r|$)|\/\*[\s\S]*?\/\/[\s\S]*?\*\//g, '')
  222. /*
  223. Remove single and multi-line comments,
  224. no consideration of inner-contents
  225. */
  226. .replace(/\/\/.+?(?=\n|\r|$)|\/\*[\s\S]+?\*\//g, '')
  227. /*
  228. Remove multi-line comments that have a replaced ending (string/regex)
  229. Greedy, so no inner strings/regexes will stop it.
  230. */
  231. .replace(RegExp('\\/\\*[\\s\\S]+' + uid + '\\d+', 'g'), '')
  232. /* Bring back strings & regexes */
  233. .replace(RegExp(uid + '(\\d+)', 'g'), function(match, n){
  234. return primitives[n];
  235. })
  236. );
  237. };
  238. /**
  239. * Strip YAML comments from the string
  240. *
  241. * The 2.0 yaml parser doesn't allow comment-only or blank lines. Strip them.
  242. *
  243. * @protected
  244. * @method stripYamlComments
  245. * @param fileStr {string} The string to strip comments from
  246. * @return {string} The string with comments stripped.
  247. */
  248. Parser.stripYamlComments = function(fileStr) {
  249. // First replace removes comment-only lines
  250. // Second replace removes blank lines
  251. return fileStr.replace(/^\s*#.*/mg,'').replace(/^\s*[\n|\r]+/mg,'');
  252. };
  253. /**
  254. * Parses the environment variable to the boolean equivalent.
  255. * Defaults to false
  256. *
  257. * @param {String} content - Environment variable value
  258. * @return {boolean} - Boolean value fo the passed variable value
  259. */
  260. Parser.booleanParser = function(filename, content) {
  261. return content === 'true';
  262. };
  263. /**
  264. * Parses the environment variable to the number equivalent.
  265. * Defaults to undefined
  266. *
  267. * @param {String} content - Environment variable value
  268. * @return {Number} - Number value fo the passed variable value
  269. */
  270. Parser.numberParser = function(filename, content) {
  271. const numberValue = Number(content);
  272. return Number.isNaN(numberValue) ? undefined : numberValue;
  273. };
  274. var order = ['js', 'cjs', 'ts', 'json', 'json5', 'hjson', 'toml', 'coffee', 'iced', 'yaml', 'yml', 'cson', 'properties', 'xml',
  275. 'boolean', 'number'];
  276. var definitions = {
  277. cjs: Parser.jsParser,
  278. coffee: Parser.coffeeParser,
  279. cson: Parser.csonParser,
  280. hjson: Parser.hjsonParser,
  281. iced: Parser.icedParser,
  282. js: Parser.jsParser,
  283. json: Parser.jsonParser,
  284. json5: Parser.json5Parser,
  285. properties: Parser.propertiesParser,
  286. toml: Parser.tomlParser,
  287. ts: Parser.tsParser,
  288. xml: Parser.xmlParser,
  289. yaml: Parser.yamlParser,
  290. yml: Parser.yamlParser,
  291. boolean: Parser.booleanParser,
  292. number: Parser.numberParser
  293. };
  294. Parser.getParser = function(name) {
  295. return definitions[name];
  296. };
  297. Parser.setParser = function(name, parser) {
  298. definitions[name] = parser;
  299. if (order.indexOf(name) === -1) {
  300. order.push(name);
  301. }
  302. };
  303. Parser.getFilesOrder = function(name) {
  304. if (name) {
  305. return order.indexOf(name);
  306. }
  307. return order;
  308. };
  309. Parser.setFilesOrder = function(name, newIndex) {
  310. if (Array.isArray(name)) {
  311. return order = name;
  312. }
  313. if (typeof newIndex === 'number') {
  314. var index = order.indexOf(name);
  315. order.splice(newIndex, 0, name);
  316. if (index > -1) {
  317. order.splice(index >= newIndex ? index +1 : index, 1);
  318. }
  319. }
  320. return order;
  321. };