package.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /*************************************************************
  2. *
  3. * Copyright (c) 2018-2022 The MathJax Consortium
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. /**
  18. * @fileoverview Implements component Package object for handling
  19. * dynamic loading of components.
  20. *
  21. * @author dpvc@mathjax.org (Davide Cervone)
  22. */
  23. import {CONFIG, Loader} from './loader.js';
  24. /*
  25. * The browser document (for creating scripts to load components)
  26. */
  27. declare var document: Document;
  28. /**
  29. * A map of package names to Package instances
  30. */
  31. export type PackageMap = Map<string, Package>;
  32. /**
  33. * An error class that includes the package name
  34. */
  35. export class PackageError extends Error {
  36. /* tslint:disable:jsdoc-require */
  37. public package: string;
  38. constructor(message: string, name: string) {
  39. super(message);
  40. this.package = name;
  41. }
  42. /* tslint:enable */
  43. }
  44. /**
  45. * Types for ready() and failed() functions and for promises
  46. */
  47. export type PackageReady = (name: string) => string | void;
  48. export type PackageFailed = (message: PackageError) => void;
  49. export type PackagePromise = (resolve: PackageReady, reject: PackageFailed) => void;
  50. /**
  51. * The configuration data for a package
  52. */
  53. export interface PackageConfig {
  54. ready?: PackageReady; // Function to call when package is loaded successfully
  55. failed?: PackageFailed; // Function to call when package fails to load
  56. checkReady?: () => Promise<void>; // Function called to see if package is fully loaded
  57. // (may cause additional packages to load, for example)
  58. }
  59. /**
  60. * The Package class for handling individual components
  61. */
  62. export class Package {
  63. /**
  64. * The set of packages being used
  65. */
  66. public static packages: PackageMap = new Map();
  67. /**
  68. * The package name
  69. */
  70. public name: string;
  71. /**
  72. * True when the package has been loaded successfully
  73. */
  74. public isLoaded: boolean = false;
  75. /**
  76. * A promise that resolves when the package is loaded successfully and rejects when it fails to load
  77. */
  78. public promise: Promise<string>;
  79. /**
  80. * True when the package is being loaded but hasn't yet finished loading
  81. */
  82. protected isLoading: boolean = false;
  83. /**
  84. * True if the package has failed to load
  85. */
  86. protected hasFailed: boolean = false;
  87. /**
  88. * True if this package should be loaded automatically (e.g., it was created in reference
  89. * to a MathJax.loader.ready() call when the package hasn't been requested to load)
  90. */
  91. protected noLoad: boolean;
  92. /**
  93. * The function that resolves the package's promise
  94. */
  95. protected resolve: PackageReady;
  96. /**
  97. * The function that rejects the package's promise
  98. */
  99. protected reject: PackageFailed;
  100. /**
  101. * The packages that require this one
  102. */
  103. protected dependents: Package[] = [];
  104. /**
  105. * The packages that this one depends on
  106. */
  107. protected dependencies: Package[] = [];
  108. /**
  109. * The number of dependencies that haven't yet been loaded
  110. */
  111. protected dependencyCount: number = 0;
  112. /**
  113. * The sub-packages that this one provides
  114. */
  115. protected provided: Package[] = [];
  116. /**
  117. * @return {boolean} True when the package can be loaded (i.e., its dependencies are all loaded,
  118. * it is allowed to be loaded, isn't already loading, and hasn't failed to load
  119. * in the past)
  120. */
  121. get canLoad(): boolean {
  122. return this.dependencyCount === 0 && !this.noLoad && !this.isLoading && !this.hasFailed;
  123. }
  124. /**
  125. * Compute the path for a package using the loader's path filters
  126. *
  127. * @param {string} name The name of the package to resolve
  128. * @param {boolean} addExtension True if .js should be added automatically
  129. * @return {string} The path (file or URL) for this package
  130. */
  131. public static resolvePath(name: string, addExtension: boolean = true): string {
  132. const data = {name, original: name, addExtension};
  133. Loader.pathFilters.execute(data);
  134. return data.name;
  135. }
  136. /**
  137. * Attempt to load all packages that are ready to be loaded
  138. * (i.e., that have no unloaded dependencies, and that haven't
  139. * already been loaded, and that aren't in process of being
  140. * loaded, and that aren't marked as noLoad).
  141. */
  142. public static loadAll() {
  143. for (const extension of this.packages.values()) {
  144. if (extension.canLoad) {
  145. extension.load();
  146. }
  147. }
  148. }
  149. /**
  150. * @param {string} name The name of the package
  151. * @param {boolean} noLoad True when the package is just for reference, not loading
  152. */
  153. constructor(name: string, noLoad: boolean = false) {
  154. this.name = name;
  155. this.noLoad = noLoad;
  156. Package.packages.set(name, this);
  157. this.promise = this.makePromise(this.makeDependencies());
  158. }
  159. /**
  160. * @return {Promise<string>[]} The array of promises that must be resolved before this package
  161. * can be loaded
  162. */
  163. protected makeDependencies(): Promise<string>[] {
  164. const promises = [] as Promise<string>[];
  165. const map = Package.packages;
  166. const noLoad = this.noLoad;
  167. const name = this.name;
  168. //
  169. // Get the dependencies for this package
  170. //
  171. const dependencies = [] as string[];
  172. if (CONFIG.dependencies.hasOwnProperty(name)) {
  173. dependencies.push(...CONFIG.dependencies[name]);
  174. } else if (name !== 'core') {
  175. dependencies.push('core'); // Add 'core' dependency by default
  176. }
  177. //
  178. // Add all the dependencies (creating them, if needed)
  179. // and record the promises of unloaded ones
  180. //
  181. for (const dependent of dependencies) {
  182. const extension = map.get(dependent) || new Package(dependent, noLoad);
  183. if (this.dependencies.indexOf(extension) < 0) {
  184. extension.addDependent(this, noLoad);
  185. this.dependencies.push(extension);
  186. if (!extension.isLoaded) {
  187. this.dependencyCount++;
  188. promises.push(extension.promise);
  189. }
  190. }
  191. }
  192. //
  193. // Return the collected promises
  194. //
  195. return promises;
  196. }
  197. /**
  198. * @param {Promise<string>[]} promises The array or promises that must be resolved before
  199. * this package can load
  200. */
  201. protected makePromise(promises: Promise<string>[]) {
  202. //
  203. // Make a promise and save its resolve/reject functions
  204. //
  205. let promise = new Promise<string>(((resolve, reject) => {
  206. this.resolve = resolve;
  207. this.reject = reject;
  208. }) as PackagePromise);
  209. //
  210. // If there is a ready() function in the configuration for this package,
  211. // Add running that to the promise
  212. //
  213. const config = (CONFIG[this.name] || {}) as PackageConfig;
  214. if (config.ready) {
  215. promise = promise.then((_name: string) => config.ready(this.name)) as Promise<string>;
  216. }
  217. //
  218. // If there are promises for dependencies,
  219. // Add the one for loading this package and create a promise for all of them
  220. // (That way, if any of them fail to load, our promise will reject automatically)
  221. //
  222. if (promises.length) {
  223. promises.push(promise);
  224. promise = Promise.all(promises).then((names: string[]) => names.join(', '));
  225. }
  226. //
  227. // If there is a failed() function in the configuration for this package,
  228. // Add a catch to handle the error
  229. //
  230. if (config.failed) {
  231. promise.catch((message: string) => config.failed(new PackageError(message, this.name)));
  232. }
  233. //
  234. // Return the promise that represents when this file is loaded
  235. //
  236. return promise;
  237. }
  238. /**
  239. * Attempt to load this package
  240. */
  241. public load() {
  242. if (!this.isLoaded && !this.isLoading && !this.noLoad) {
  243. this.isLoading = true;
  244. const url = Package.resolvePath(this.name);
  245. if (CONFIG.require) {
  246. this.loadCustom(url);
  247. } else {
  248. this.loadScript(url);
  249. }
  250. }
  251. }
  252. /**
  253. * Load using a custom require method (usually the one from node.js)
  254. */
  255. protected loadCustom(url: string) {
  256. try {
  257. const result = CONFIG.require(url);
  258. if (result instanceof Promise) {
  259. result.then(() => this.checkLoad())
  260. .catch((err) => this.failed('Can\'t load "' + url + '"\n' + err.message.trim()));
  261. } else {
  262. this.checkLoad();
  263. }
  264. } catch (err) {
  265. this.failed(err.message);
  266. }
  267. }
  268. /**
  269. * Load in a browser by inserting a script to load the proper URL
  270. */
  271. protected loadScript(url: string) {
  272. const script = document.createElement('script');
  273. script.src = url;
  274. script.charset = 'UTF-8';
  275. script.onload = (_event) => this.checkLoad();
  276. script.onerror = (_event) => this.failed('Can\'t load "' + url + '"');
  277. // FIXME: Should there be a timeout failure as well?
  278. document.head.appendChild(script);
  279. }
  280. /**
  281. * Called when the package is loaded.
  282. *
  283. * Mark it as loaded, and tell its dependents that this package
  284. * has been loaded (may cause dependents to load themselves).
  285. * Mark any provided packages as loaded.
  286. * Resolve the promise that says this package is loaded.
  287. */
  288. public loaded() {
  289. this.isLoaded = true;
  290. this.isLoading = false;
  291. for (const dependent of this.dependents) {
  292. dependent.requirementSatisfied();
  293. }
  294. for (const provided of this.provided) {
  295. provided.loaded();
  296. }
  297. this.resolve(this.name);
  298. }
  299. /**
  300. * Called when the package fails to load for some reason
  301. *
  302. * Mark it as failed to load
  303. * Reject the promise for this package with an error
  304. *
  305. * @param {string} message The error message for a load failure
  306. */
  307. protected failed(message: string) {
  308. this.hasFailed = true;
  309. this.isLoading = false;
  310. this.reject(new PackageError(message, this.name));
  311. }
  312. /**
  313. * Check if a package is really ready to be marked as loaded
  314. * (When it is loaded, it may set its own checkReady() function
  315. * as a means of loading additional packages. E.g., an output
  316. * jax may load a font package, dependent on its configuration.)
  317. *
  318. * The configuration's checkReady() function returns a promise
  319. * that allows the loader to wait for addition actions to finish
  320. * before marking the file as loaded (or failing to load).
  321. */
  322. protected checkLoad() {
  323. const config = (CONFIG[this.name] || {}) as PackageConfig;
  324. const checkReady = config.checkReady || (() => Promise.resolve());
  325. checkReady().then(() => this.loaded())
  326. .catch((message) => this.failed(message));
  327. }
  328. /**
  329. * This is called when a dependency loads.
  330. *
  331. * Decrease the dependency count, and try to load this package
  332. * when the dependencies are all loaded.
  333. */
  334. public requirementSatisfied() {
  335. if (this.dependencyCount) {
  336. this.dependencyCount--;
  337. if (this.canLoad) {
  338. this.load();
  339. }
  340. }
  341. }
  342. /**
  343. * @param {string[]} names The names of the packages that this package provides
  344. */
  345. public provides(names: string[] = []) {
  346. for (const name of names) {
  347. let provided = Package.packages.get(name);
  348. if (!provided) {
  349. if (!CONFIG.dependencies[name]) {
  350. CONFIG.dependencies[name] = [];
  351. }
  352. CONFIG.dependencies[name].push(name);
  353. provided = new Package(name, true);
  354. provided.isLoading = true;
  355. }
  356. this.provided.push(provided);
  357. }
  358. }
  359. /**
  360. * Add a package as a dependent, and if it is not just for reference,
  361. * check if we need to change our noLoad status.
  362. *
  363. * @param {Package} extension The package to add as a dependent
  364. * @param {boolean} noLoad The noLoad status of the dependent
  365. */
  366. public addDependent(extension: Package, noLoad: boolean) {
  367. this.dependents.push(extension);
  368. if (!noLoad) {
  369. this.checkNoLoad();
  370. }
  371. }
  372. /**
  373. * If this package is marked as noLoad, change that and check all
  374. * our dependencies to see if they need to change their noLoad
  375. * status as well.
  376. *
  377. * I.e., if there are dependencies that were set up for reference
  378. * and a leaf node needs to be loaded, make sure all parent nodes
  379. * are marked as needing to be loaded as well.
  380. */
  381. public checkNoLoad() {
  382. if (this.noLoad) {
  383. this.noLoad = false;
  384. for (const dependency of this.dependencies) {
  385. dependency.checkNoLoad();
  386. }
  387. }
  388. }
  389. }