build 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. #! /usr/bin/env node
  2. /*************************************************************
  3. *
  4. * Copyright (c) 2018 The MathJax Consortium
  5. *
  6. * Licensed under the Apache License, Version 2.0 (the "License");
  7. * you may not use this file except in compliance with the License.
  8. * You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. */
  18. /**
  19. * @fileoverview Builds the lib directory for a component
  20. *
  21. * @author dpvc@mathjax.org (Davide Cervone)
  22. */
  23. const fs = require('fs');
  24. const path = require('path');
  25. /**
  26. * The amount of space for each level of indentation
  27. */
  28. const INDENT = ' ';
  29. /**
  30. * The pattern to use when looking for explort commands to process
  31. */
  32. const EXPORTPATTERN = /(^export(?:\s+default)?(?:\s+abstract)?\s+[^ {*}]+\s+[a-zA-Z0-9_.$]+)/m;
  33. const EXPORT_IGNORE = ['type', 'interface'];
  34. const EXPORT_PROCESS = ['let', 'const', 'var', 'function', 'class', 'namespace'];
  35. /**
  36. * The relative path to the MathJax directory
  37. */
  38. const mjPath = path.relative(process.cwd(), path.resolve(__dirname, '..', '..', 'js'));
  39. const mjGlobal = path.join('..', mjPath, 'components', 'global.js');
  40. /**
  41. * Read the configuration for the component
  42. */
  43. const config = JSON.parse(fs.readFileSync(process.argv[2] || 'build.json'));
  44. function getType() {
  45. const component = config.component || 'part';
  46. if (component.match(/\/(svg|chtml|common)\/fonts\//)) return RegExp.$1 + '-font';
  47. if (component.match(/\/(mathml|tex)\/.+\//)) return RegExp.$1 + '-extension';
  48. if (component.match(/^(.+)\//)) return RegExp.$1;
  49. return component;
  50. }
  51. /**
  52. * Extract the configuration values
  53. */
  54. const COMPONENT = path.basename(config.component || 'part'); // name of the component
  55. const ID = config.id || config.component || 'part'; // the ID of the component
  56. const TARGETS = config.targets || []; // the files to include in the component
  57. const EXCLUDE = new Map((config.exclude || []).map(name => [name, true])); // files to exclude from the component
  58. const EXCLUDESUBDIRS = config.excludeSubdirs === 'true'; // exclude subdirectories or not
  59. const JS = config.js || config.mathjax || mjPath; // path to the compiled .js files
  60. const LIB = config.lib || './lib'; // path to the lib directory to create
  61. const TS = config.ts || JS.replace(/js$/, 'ts'); // path to the .ts files
  62. const GLOBAL = config.global || mjGlobal; // path to the global.js file
  63. const VERSION = config.version || mjGlobal.replace(/global/, 'version'); // path to the version.js file
  64. const TYPE = config.type || getType(); // the module type
  65. /**
  66. * The list of files that need to be added to the lib directory
  67. */
  68. let PACKAGE = [];
  69. /**
  70. * Process an array of files/directories to be included in the component
  71. *
  72. * @param {string} base The root directory for the .ts files
  73. * @param {string} dir The relative path within the root for the files to process
  74. * @param {string[]} list An array of file/directory names to process
  75. * @param {boolean} top True if this is the initial list of files
  76. */
  77. function processList(base, dir, list, top = true) {
  78. for (const item of list) {
  79. const file = path.join(dir, item);
  80. if (!EXCLUDE.has(file)) {
  81. const stat = fs.statSync(path.resolve(base, file));
  82. if (stat.isDirectory()) {
  83. if (top || !EXCLUDESUBDIRS) {
  84. processDir(base, file);
  85. }
  86. } else if (file.match(/\.ts$/)) {
  87. processFile(base, file);
  88. }
  89. }
  90. }
  91. }
  92. /**
  93. * Process the contents of the directory
  94. *
  95. * @param {string} base The root directory for the .ts files
  96. * @param {string} dir The relative path within the root for the directory to process
  97. */
  98. function processDir(base, dir) {
  99. const root = path.resolve(base, dir);
  100. processList(base, dir, fs.readdirSync(root), false);
  101. }
  102. /**
  103. * Process the contents of a .ts file
  104. *
  105. * Look for export commands within the file, and determine the objects
  106. * that they reference, then produce the library file that defines
  107. * them, and add the file to the package list.
  108. *
  109. * @param {string} base The root directory for the .ts files
  110. * @param {string} name The path of the file relative to the base
  111. */
  112. function processFile(base, name) {
  113. console.info(' ' + name);
  114. const file = fs.readFileSync(path.resolve(base, name)).toString();
  115. const parts = file.split(EXPORTPATTERN);
  116. const objects = processParts(parts);
  117. const lines = processLines(name, objects);
  118. makeFile(name, lines);
  119. if (objects.length) {
  120. PACKAGE.push(name);
  121. }
  122. }
  123. /**
  124. * Process the export lines to record the ones that need to be added to the
  125. * library file.
  126. *
  127. * @param {string[]} parts The file broken into export lines and the text between them
  128. * @return {string[]} An array of names of exported objects
  129. */
  130. function processParts(parts) {
  131. const objects = [];
  132. for (let i = 1; i < parts.length; i += 2) {
  133. const words = parts[i].split(/\s+/);
  134. const type = words[words.length - 2];
  135. const name = words[words.length - 1];
  136. if (words[1] === 'default' || type === 'default') {
  137. objects.push('default');
  138. } else if (EXPORT_PROCESS.indexOf(type) >= 0) {
  139. objects.push(name);
  140. } else if (EXPORT_IGNORE.indexOf(type) < 0) {
  141. console.info(" Can't process '" + parts[i] + "'");
  142. }
  143. }
  144. return objects;
  145. }
  146. /**
  147. * Create the lines of the lib file using the object names from the file.
  148. *
  149. * @param {string} name The path of the file relative to the root .ts directory
  150. * @param {string[]} objects Array of names of the object exported by the file
  151. * @return {string[]} Array of lines for the contents of the library file
  152. */
  153. function processLines(file, objects) {
  154. if (objects.length === 0) return [];
  155. const dir = path.dirname(file).replace(/^\.$/, '');
  156. const dots = dir.replace(/[^\/]+/g, '..') || '.';
  157. const relative = path.join(dots, '..', JS, dir, path.basename(file)).replace(/\.ts$/, '.js');
  158. const name = path.parse(file).name;
  159. const lines = [
  160. '"use strict";',
  161. `Object.defineProperty(exports, '__esModule', {value: true});`
  162. ];
  163. let source = ((dir.replace(/\//g, '.') + '.' + name).replace(/^\./, '')
  164. + (exists(path.resolve(JS, file.replace(/\.ts$/, ''))) ? '_ts' : ''))
  165. .replace(/\.[^.]*/g, (x) => (x.substr(1).match(/[^a-zA-Z_]/) ? '[\'' + x.substr(1) + '\']' : x));
  166. for (const id of objects) {
  167. lines.push(`exports.${id} = MathJax._.${source}.${id};`);
  168. }
  169. return lines;
  170. }
  171. /**
  172. * @param {string} file Path to a file
  173. * @return {boolean} True if the file exists, false if not
  174. */
  175. function exists(file) {
  176. if (!fs.existsSync(file)) return false;
  177. //
  178. // For case-insensitive file systems (like Mac OS),
  179. // check that the file names match exactly
  180. //
  181. const [dir, name] = [path.dirname(file), path.basename(file)];
  182. return fs.readdirSync(dir).indexOf(name) >= 0;
  183. }
  184. /**
  185. * Recursively make any needed directories
  186. *
  187. * @param {string} dir The directory relative to the library directory
  188. */
  189. function makeDir(dir) {
  190. const fulldir = path.resolve(LIB, dir);
  191. if (fs.existsSync(fulldir)) return;
  192. makeDir(path.dirname(fulldir));
  193. fs.mkdirSync(fulldir);
  194. }
  195. /**
  196. * Make a file in the library directory from the given lines of javascript
  197. *
  198. * @param {string} file The name of the file relative to the root .ts directory
  199. * @param {string[]} lines The contents of the file to create
  200. */
  201. function makeFile(file, lines) {
  202. if (!lines.length) return;
  203. const [dir, name] = [path.dirname(file), path.basename(file)];
  204. makeDir(dir);
  205. fs.writeFileSync(path.resolve(LIB, dir, name.replace(/\.ts$/, '.js')), lines.join('\n') + '\n');
  206. }
  207. /**
  208. * Make the library file that adds all the exported objects into the MathJax global object
  209. */
  210. function processGlobal() {
  211. console.info(' ' + COMPONENT + '.ts');
  212. const lines = [
  213. `import {combineWithMathJax} from '${GLOBAL}';`,
  214. `import {VERSION} from '${VERSION}';`,
  215. '',
  216. ];
  217. const packages = [];
  218. PACKAGE = PACKAGE.sort(sortDir);
  219. while (PACKAGE.length) {
  220. const dir = path.dirname(PACKAGE[0]).split(path.sep)[0];
  221. packages.push(processPackage(lines, INDENT, dir));
  222. }
  223. const name = (ID.match(/[^a-zA-Z0-9_]/) ? `"${ID}"` : ID);
  224. lines.push(
  225. '',
  226. 'if (MathJax.loader) {',
  227. INDENT + `MathJax.loader.checkVersion('${ID}', VERSION, '${TYPE}');`,
  228. '}',
  229. '',
  230. `combineWithMathJax({_: {`,
  231. INDENT + packages.join(',\n' + INDENT),
  232. '}});'
  233. );
  234. fs.writeFileSync(path.join(LIB, COMPONENT + '.js'), lines.join('\n') + '\n');
  235. }
  236. /**
  237. * Sort file paths alphabetically
  238. */
  239. function sortDir(a, b) {
  240. const A = a.replace(/\//g, '|'); // Replace directory separator by one that is after the alphnumerics
  241. const B = b.replace(/\//g, '|');
  242. return (A === B ? 0 : A < B ? -1 : 1);
  243. }
  244. let importCount = 0;
  245. /**
  246. * Recursively process packages, collecting them into groups by subdirectory, properly indented
  247. *
  248. * @param {string[]} lines The array where lines of code defining the packages are to be stored
  249. * @param {string} space The current level of indentation
  250. * @param {string} dir The subdirectory currently being processed
  251. * @return {string} The string to use to load all the objects from the given directory
  252. */
  253. function processPackage(lines, space, dir) {
  254. const packages = [];
  255. //
  256. // Loop through the lines that are in the current directory
  257. //
  258. while (PACKAGE.length && (PACKAGE[0].substr(0, dir.length) === dir || dir === '.')) {
  259. //
  260. // If the current package is in this directory (not a subdirectory)
  261. // Get the location of transpiled mathjax file
  262. // Get the name to use for the data from that file
  263. // Create an entry for the file in the MathJax global that loads the reuired component
  264. // Otherwise (its in a subdirectory)
  265. // Get the subdirectory name
  266. // Process the subdirectory using an additional indentation
  267. //
  268. if (path.dirname(PACKAGE[0]) === dir) {
  269. const file = PACKAGE.shift();
  270. const name = path.basename(file);
  271. const relativefile = path.join('..', JS, dir, name).replace(/\.ts$/, '.js');
  272. const component = 'module' + (++importCount);
  273. lines.push(`import * as ${component} from '${relativefile}';`);
  274. let property = name.replace(/\.ts$/, '');
  275. if (property !== name && exists(path.resolve(JS, file.replace(/\.ts$/, '')))) {
  276. property += '_ts';
  277. }
  278. if (property.match(/[^a-zA-Z0-9_]/)) {
  279. property = `"${property}"`;
  280. }
  281. packages.push(`${property}: ${component}`);
  282. } else {
  283. let subdir = path.dirname(PACKAGE[0]);
  284. while (path.dirname(subdir) !== dir && subdir !== '.') {
  285. subdir = path.dirname(subdir);
  286. }
  287. packages.push(processPackage(lines, space + (dir === '.' ? '' : INDENT), subdir));
  288. }
  289. }
  290. //
  291. // Create the string defining the object that loads all the needed files into the proper places
  292. //
  293. if (dir === '.') return packages.join(',\n ');
  294. return path.basename(dir) + ': {\n' + INDENT + space + packages.join(',\n' + INDENT + space) + '\n' + space + '}';
  295. }
  296. /**
  297. * @param {string} dir The path to the directory tree to be removed (recursively)
  298. */
  299. function rmDir(dir) {
  300. if (!dir) return;
  301. if (fs.existsSync(dir)) {
  302. for (const name of fs.readdirSync(dir)) {
  303. const file = path.join(dir, name);
  304. if (fs.lstatSync(file).isDirectory()) {
  305. rmDir(file);
  306. } else {
  307. fs.unlinkSync(file);
  308. }
  309. }
  310. fs.rmdirSync(dir);
  311. }
  312. }
  313. //
  314. // Remove the existing lib directory contents, if any.
  315. // Load all the target files from the MathJax .ts directory.
  316. // Create the file that loads all the target objects into the MathJax global object.
  317. //
  318. rmDir(LIB);
  319. console.info("Processing:");
  320. processList(TS, '', TARGETS);
  321. processGlobal();