builder.js 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. "use strict";
  2. const fs = require("fs"),
  3. path = require("path"),
  4. vm = require("vm");
  5. const AllWhitespaceRegexp = /^\s+$/g;
  6. /**
  7. * A simple preprocessor that is based on the Firefox preprocessor
  8. * (https://dxr.mozilla.org/mozilla-central/source/build/docs/preprocessor.rst).
  9. * The main difference is that this supports a subset of the commands and it
  10. * supports preprocessor commands in HTML-style comments.
  11. *
  12. * Currently supported commands:
  13. * - if
  14. * - elif
  15. * - else
  16. * - endif
  17. * - include
  18. * - expand
  19. * - error
  20. *
  21. * Every #if must be closed with an #endif. Nested conditions are supported.
  22. *
  23. * Within an #if or #else block, one level of comment tokens is stripped. This
  24. * allows us to write code that can run even without preprocessing. For example:
  25. *
  26. * //#if SOME_RARE_CONDITION
  27. * // // Decrement by one
  28. * // --i;
  29. * //#else
  30. * // // Increment by one.
  31. * ++i;
  32. * //#endif
  33. */
  34. function preprocess(inFilename, outFilename, defines) {
  35. let lineNumber = 0;
  36. function loc() {
  37. return fs.realpathSync(inFilename) + ":" + lineNumber;
  38. }
  39. function expandCssImports(content, baseUrl) {
  40. return content.replace(
  41. /^\s*@import\s+url\(([^)]+)\);\s*$/gm,
  42. function (all, url) {
  43. const file = path.join(path.dirname(baseUrl), url);
  44. const imported = fs.readFileSync(file, "utf8").toString();
  45. return expandCssImports(imported, file);
  46. }
  47. );
  48. }
  49. // TODO make this really read line by line.
  50. let content = fs.readFileSync(inFilename, "utf8").toString();
  51. // Handle CSS-imports first, when necessary.
  52. if (/\.css$/i.test(inFilename)) {
  53. content = expandCssImports(content, inFilename);
  54. }
  55. const lines = content.split("\n"),
  56. totalLines = lines.length;
  57. const out = [];
  58. let i = 0;
  59. function readLine() {
  60. if (i < totalLines) {
  61. return lines[i++];
  62. }
  63. return null;
  64. }
  65. const writeLine =
  66. typeof outFilename === "function"
  67. ? outFilename
  68. : function (line) {
  69. if (!line || AllWhitespaceRegexp.test(line)) {
  70. const prevLine = out[out.length - 1];
  71. if (!prevLine || AllWhitespaceRegexp.test(prevLine)) {
  72. return; // Avoid adding consecutive blank lines.
  73. }
  74. }
  75. out.push(line);
  76. };
  77. function evaluateCondition(code) {
  78. if (!code || !code.trim()) {
  79. throw new Error("No JavaScript expression given at " + loc());
  80. }
  81. try {
  82. return vm.runInNewContext(code, defines, { displayErrors: false });
  83. } catch (e) {
  84. throw new Error(
  85. 'Could not evaluate "' +
  86. code +
  87. '" at ' +
  88. loc() +
  89. "\n" +
  90. e.name +
  91. ": " +
  92. e.message
  93. );
  94. }
  95. }
  96. function include(file) {
  97. const realPath = fs.realpathSync(inFilename);
  98. const dir = path.dirname(realPath);
  99. try {
  100. let fullpath;
  101. if (file.indexOf("$ROOT/") === 0) {
  102. fullpath = path.join(
  103. __dirname,
  104. "../..",
  105. file.substring("$ROOT/".length)
  106. );
  107. } else {
  108. fullpath = path.join(dir, file);
  109. }
  110. preprocess(fullpath, writeLine, defines);
  111. } catch (e) {
  112. if (e.code === "ENOENT") {
  113. throw new Error('Failed to include "' + file + '" at ' + loc());
  114. }
  115. throw e; // Some other error
  116. }
  117. }
  118. function expand(line) {
  119. line = line.replace(/__[\w]+__/g, function (variable) {
  120. variable = variable.substring(2, variable.length - 2);
  121. if (variable in defines) {
  122. return defines[variable];
  123. }
  124. return "";
  125. });
  126. writeLine(line);
  127. }
  128. // not inside if or else (process lines)
  129. const STATE_NONE = 0;
  130. // inside if, condition false (ignore until #else or #endif)
  131. const STATE_IF_FALSE = 1;
  132. // inside else, #if was false, so #else is true (process lines until #endif)
  133. const STATE_ELSE_TRUE = 2;
  134. // inside if, condition true (process lines until #else or #endif)
  135. const STATE_IF_TRUE = 3;
  136. // inside else or elif, #if/#elif was true, so following #else or #elif is
  137. // false (ignore lines until #endif)
  138. const STATE_ELSE_FALSE = 4;
  139. let line;
  140. let state = STATE_NONE;
  141. const stack = [];
  142. const control =
  143. /^(?:\/\/|\s*\/\*|<!--)\s*#(if|elif|else|endif|expand|include|error)\b(?:\s+(.*?)(?:\*\/|-->)?$)?/;
  144. while ((line = readLine()) !== null) {
  145. ++lineNumber;
  146. const m = control.exec(line);
  147. if (m) {
  148. switch (m[1]) {
  149. case "if":
  150. stack.push(state);
  151. state = evaluateCondition(m[2]) ? STATE_IF_TRUE : STATE_IF_FALSE;
  152. break;
  153. case "elif":
  154. if (state === STATE_IF_TRUE || state === STATE_ELSE_FALSE) {
  155. state = STATE_ELSE_FALSE;
  156. } else if (state === STATE_IF_FALSE) {
  157. state = evaluateCondition(m[2]) ? STATE_IF_TRUE : STATE_IF_FALSE;
  158. } else if (state === STATE_ELSE_TRUE) {
  159. throw new Error("Found #elif after #else at " + loc());
  160. } else {
  161. throw new Error("Found #elif without matching #if at " + loc());
  162. }
  163. break;
  164. case "else":
  165. if (state === STATE_IF_TRUE || state === STATE_ELSE_FALSE) {
  166. state = STATE_ELSE_FALSE;
  167. } else if (state === STATE_IF_FALSE) {
  168. state = STATE_ELSE_TRUE;
  169. } else {
  170. throw new Error("Found #else without matching #if at " + loc());
  171. }
  172. break;
  173. case "endif":
  174. if (state === STATE_NONE) {
  175. throw new Error("Found #endif without #if at " + loc());
  176. }
  177. state = stack.pop();
  178. break;
  179. case "expand":
  180. if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) {
  181. expand(m[2]);
  182. }
  183. break;
  184. case "include":
  185. if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) {
  186. include(m[2]);
  187. }
  188. break;
  189. case "error":
  190. if (state !== STATE_IF_FALSE && state !== STATE_ELSE_FALSE) {
  191. throw new Error("Found #error " + m[2] + " at " + loc());
  192. }
  193. break;
  194. }
  195. } else {
  196. if (state === STATE_NONE) {
  197. writeLine(line);
  198. } else if (
  199. (state === STATE_IF_TRUE || state === STATE_ELSE_TRUE) &&
  200. !stack.includes(STATE_IF_FALSE) &&
  201. !stack.includes(STATE_ELSE_FALSE)
  202. ) {
  203. writeLine(
  204. line
  205. .replace(/^\/\/|^<!--/g, " ")
  206. .replace(/(^\s*)\/\*/g, "$1 ")
  207. .replace(/\*\/$|-->$/g, "")
  208. );
  209. }
  210. }
  211. }
  212. if (state !== STATE_NONE || stack.length !== 0) {
  213. throw new Error(
  214. "Missing #endif in preprocessor for " + fs.realpathSync(inFilename)
  215. );
  216. }
  217. if (typeof outFilename !== "function") {
  218. fs.writeFileSync(outFilename, out.join("\n"));
  219. }
  220. }
  221. exports.preprocess = preprocess;
  222. /**
  223. * Merge two defines arrays. Values in the second param will override values in
  224. * the first.
  225. */
  226. function merge(defaults, defines) {
  227. const ret = Object.create(null);
  228. for (const key in defaults) {
  229. ret[key] = defaults[key];
  230. }
  231. for (const key in defines) {
  232. ret[key] = defines[key];
  233. }
  234. return ret;
  235. }
  236. exports.merge = merge;