LoaderRunner.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. Author Tobias Koppers @sokra
  4. */
  5. "use strict";
  6. const { readFile } = require("fs");
  7. const loadLoader = require("./loadLoader");
  8. const HASH_ESCAPE_REGEXP = /#/g;
  9. // UTF-8 encoding of the BOM: EF BB BF
  10. const UTF8_BOM_0 = 0xef;
  11. const UTF8_BOM_1 = 0xbb;
  12. const UTF8_BOM_2 = 0xbf;
  13. function utf8BufferToString(buf) {
  14. // Detect and skip the BOM at the buffer level to avoid materializing the
  15. // prefix as JS string and then re-slicing it.
  16. if (
  17. buf.length >= 3 &&
  18. buf[0] === UTF8_BOM_0 &&
  19. buf[1] === UTF8_BOM_1 &&
  20. buf[2] === UTF8_BOM_2
  21. ) {
  22. return buf.toString("utf8", 3);
  23. }
  24. return buf.toString("utf8");
  25. }
  26. /**
  27. * Escape `#` characters with a preceding `\0` byte. Short-circuits when the input contains no `#`, avoiding the regex scan for the common case.
  28. * @param {string} str input string
  29. * @returns {string} escaped string
  30. */
  31. function escapeHash(str) {
  32. return str.includes("#") ? str.replace(HASH_ESCAPE_REGEXP, "\0#") : str;
  33. }
  34. const PATH_QUERY_FRAGMENT_REGEXP =
  35. /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
  36. const ZERO_ESCAPE_REGEXP = /\0(.)/g;
  37. /**
  38. * @param {string} identifier identifier
  39. * @returns {[string, string, string]} parsed identifier
  40. */
  41. function parseIdentifier(identifier) {
  42. // Fast path for inputs that don't use \0 escaping.
  43. const firstEscape = identifier.indexOf("\0");
  44. if (firstEscape < 0) {
  45. const queryStart = identifier.indexOf("?");
  46. const fragmentStart = identifier.indexOf("#");
  47. if (fragmentStart < 0) {
  48. if (queryStart < 0) {
  49. // No fragment, no query
  50. return [identifier, "", ""];
  51. }
  52. // Query, no fragment
  53. return [
  54. identifier.slice(0, queryStart),
  55. identifier.slice(queryStart),
  56. "",
  57. ];
  58. }
  59. if (queryStart < 0 || fragmentStart < queryStart) {
  60. // Fragment, no query
  61. return [
  62. identifier.slice(0, fragmentStart),
  63. "",
  64. identifier.slice(fragmentStart),
  65. ];
  66. }
  67. // Query and fragment
  68. return [
  69. identifier.slice(0, queryStart),
  70. identifier.slice(queryStart, fragmentStart),
  71. identifier.slice(fragmentStart),
  72. ];
  73. }
  74. const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier);
  75. return [
  76. match[1].replace(ZERO_ESCAPE_REGEXP, "$1"),
  77. match[2] ? match[2].replace(ZERO_ESCAPE_REGEXP, "$1") : "",
  78. match[3] || "",
  79. ];
  80. }
  81. function dirname(path) {
  82. if (path === "/") return "/";
  83. const i = path.lastIndexOf("/");
  84. const j = path.lastIndexOf("\\");
  85. const i2 = path.indexOf("/");
  86. const j2 = path.indexOf("\\");
  87. const idx = i > j ? i : j;
  88. const idx2 = i > j ? i2 : j2;
  89. if (idx < 0) return path;
  90. if (idx === idx2) return path.slice(0, idx + 1);
  91. return path.slice(0, idx);
  92. }
  93. function createLoaderObject(loader) {
  94. const obj = {
  95. path: null,
  96. query: null,
  97. fragment: null,
  98. options: null,
  99. ident: null,
  100. normal: null,
  101. pitch: null,
  102. raw: null,
  103. data: null,
  104. pitchExecuted: false,
  105. normalExecuted: false,
  106. };
  107. Object.defineProperty(obj, "request", {
  108. enumerable: true,
  109. get() {
  110. return escapeHash(obj.path) + escapeHash(obj.query) + obj.fragment;
  111. },
  112. set(value) {
  113. if (typeof value === "string") {
  114. const [path, query, fragment] = parseIdentifier(value);
  115. obj.path = path;
  116. obj.query = query;
  117. obj.fragment = fragment;
  118. obj.options = undefined;
  119. obj.ident = undefined;
  120. return;
  121. }
  122. if (!value.loader) {
  123. throw new Error(
  124. `request should be a string or object with loader and options (${JSON.stringify(
  125. value
  126. )})`
  127. );
  128. }
  129. const { loader: path, fragment, type, options, ident } = value;
  130. obj.path = path;
  131. obj.fragment = fragment || "";
  132. obj.type = type;
  133. obj.options = options;
  134. obj.ident = ident;
  135. if (options === null || options === undefined) {
  136. obj.query = "";
  137. } else if (typeof options === "string") {
  138. obj.query = `?${options}`;
  139. } else if (ident) {
  140. obj.query = `??${ident}`;
  141. } else if (typeof options === "object" && options.ident) {
  142. obj.query = `??${options.ident}`;
  143. } else {
  144. obj.query = `?${JSON.stringify(options)}`;
  145. }
  146. },
  147. });
  148. obj.request = loader;
  149. if (Object.preventExtensions) {
  150. Object.preventExtensions(obj);
  151. }
  152. return obj;
  153. }
  154. function runSyncOrAsync(fn, context, args, callback) {
  155. let isSync = true;
  156. let isDone = false;
  157. let isError = false; // internal error
  158. let reportedError = false;
  159. // eslint-disable-next-line func-name-matching
  160. const innerCallback = (context.callback = function innerCallback(
  161. ...callbackArgs
  162. ) {
  163. if (isDone) {
  164. if (reportedError) return; // ignore
  165. throw new Error("callback(): The callback was already called.");
  166. }
  167. isDone = true;
  168. isSync = false;
  169. try {
  170. callback(...callbackArgs);
  171. } catch (err) {
  172. isError = true;
  173. throw err;
  174. }
  175. });
  176. context.async = function async() {
  177. if (isDone) {
  178. if (reportedError) return; // ignore
  179. throw new Error("async(): The callback was already called.");
  180. }
  181. isSync = false;
  182. return innerCallback;
  183. };
  184. try {
  185. const result = (function LOADER_EXECUTION() {
  186. return fn.apply(context, args);
  187. })();
  188. if (isSync) {
  189. isDone = true;
  190. if (result === undefined) return callback();
  191. if (
  192. result &&
  193. typeof result === "object" &&
  194. typeof result.then === "function"
  195. ) {
  196. return result.then((r) => {
  197. callback(null, r);
  198. }, callback);
  199. }
  200. return callback(null, result);
  201. }
  202. } catch (err) {
  203. if (isError) throw err;
  204. if (isDone) {
  205. // loader is already "done", so we cannot use the callback function
  206. // for better debugging we print the error on the console
  207. if (typeof err === "object" && err.stack) {
  208. // eslint-disable-next-line no-console
  209. console.error(err.stack);
  210. } else {
  211. // eslint-disable-next-line no-console
  212. console.error(err);
  213. }
  214. return;
  215. }
  216. isDone = true;
  217. reportedError = true;
  218. callback(err);
  219. }
  220. }
  221. function convertArgs(args, raw) {
  222. if (!raw && Buffer.isBuffer(args[0])) {
  223. args[0] = utf8BufferToString(args[0]);
  224. } else if (raw && typeof args[0] === "string") {
  225. args[0] = Buffer.from(args[0], "utf8");
  226. }
  227. }
  228. function iterateNormalLoaders(options, loaderContext, args, callback) {
  229. while (loaderContext.loaderIndex >= 0) {
  230. const currentLoaderObject =
  231. loaderContext.loaders[loaderContext.loaderIndex];
  232. if (currentLoaderObject.normalExecuted) {
  233. loaderContext.loaderIndex--;
  234. continue;
  235. }
  236. const fn = currentLoaderObject.normal;
  237. currentLoaderObject.normalExecuted = true;
  238. if (!fn) continue;
  239. convertArgs(args, currentLoaderObject.raw);
  240. return runSyncOrAsync(fn, loaderContext, args, (err, ...nextArgs) => {
  241. if (err) return callback(err);
  242. iterateNormalLoaders(options, loaderContext, nextArgs, callback);
  243. });
  244. }
  245. return callback(null, args);
  246. }
  247. function processResource(options, loaderContext, callback) {
  248. // set loader index to last loader
  249. loaderContext.loaderIndex = loaderContext.loaders.length - 1;
  250. const { resourcePath } = loaderContext;
  251. if (!resourcePath) {
  252. return iterateNormalLoaders(options, loaderContext, [null], callback);
  253. }
  254. options.processResource(loaderContext, resourcePath, (err, ...args) => {
  255. if (err) return callback(err);
  256. // eslint-disable-next-line prefer-destructuring
  257. options.resourceBuffer = args[0];
  258. iterateNormalLoaders(options, loaderContext, args, callback);
  259. });
  260. }
  261. function iteratePitchingLoaders(options, loaderContext, callback) {
  262. // Iterative walk over already-pitched loaders without recursion.
  263. while (loaderContext.loaderIndex < loaderContext.loaders.length) {
  264. const currentLoaderObject =
  265. loaderContext.loaders[loaderContext.loaderIndex];
  266. if (currentLoaderObject.pitchExecuted) {
  267. loaderContext.loaderIndex++;
  268. continue;
  269. }
  270. return loadLoader(currentLoaderObject, (err) => {
  271. if (err) {
  272. loaderContext.cacheable(false);
  273. return callback(err);
  274. }
  275. const fn = currentLoaderObject.pitch;
  276. currentLoaderObject.pitchExecuted = true;
  277. if (!fn) return iteratePitchingLoaders(options, loaderContext, callback);
  278. runSyncOrAsync(
  279. fn,
  280. loaderContext,
  281. [
  282. loaderContext.remainingRequest,
  283. loaderContext.previousRequest,
  284. (currentLoaderObject.data = {}),
  285. ],
  286. (pitchErr, ...args) => {
  287. if (pitchErr) return callback(pitchErr);
  288. // Determine whether to continue the pitching process based on
  289. // argument values (as opposed to argument presence) in order
  290. // to support synchronous and asynchronous usages. Inline loop
  291. // avoids allocating a predicate closure per pitched loader.
  292. let hasArg = false;
  293. for (let i = 0; i < args.length; i++) {
  294. if (args[i] !== undefined) {
  295. hasArg = true;
  296. break;
  297. }
  298. }
  299. if (hasArg) {
  300. loaderContext.loaderIndex--;
  301. iterateNormalLoaders(options, loaderContext, args, callback);
  302. } else {
  303. iteratePitchingLoaders(options, loaderContext, callback);
  304. }
  305. }
  306. );
  307. });
  308. }
  309. // Reached the end: move on to processing the resource itself.
  310. return processResource(options, loaderContext, callback);
  311. }
  312. /**
  313. * Join loader requests into a single `!`-separated string for a range of loader indices.
  314. * @param {object[]} loaders loader objects
  315. * @param {number} start inclusive start index
  316. * @param {number} end exclusive end index
  317. * @param {string} resource resource string
  318. * @returns {string} joined request
  319. */
  320. function joinRequests(loaders, start, end, resource) {
  321. let result = "";
  322. for (let i = start; i < end; i++) {
  323. result += `${loaders[i].request}!`;
  324. }
  325. return result + resource;
  326. }
  327. module.exports.getContext = function getContext(resource) {
  328. const [path] = parseIdentifier(resource);
  329. return dirname(path);
  330. };
  331. module.exports.runLoaders = function runLoaders(options, callback) {
  332. // read options
  333. const resource = options.resource || "";
  334. const loaderContext = options.context || {};
  335. const processResourceFn =
  336. options.processResource ||
  337. ((readResource, context, res, cb) => {
  338. context.addDependency(res);
  339. readResource(res, cb);
  340. }).bind(null, options.readResource || readFile);
  341. const splittedResource = resource && parseIdentifier(resource);
  342. const resourcePath = splittedResource ? splittedResource[0] : "";
  343. const resourceQuery = splittedResource ? splittedResource[1] : "";
  344. const resourceFragment = splittedResource ? splittedResource[2] : "";
  345. const contextDirectory = resourcePath ? dirname(resourcePath) : null;
  346. // execution state
  347. let requestCacheable = true;
  348. const fileDependencies = [];
  349. const contextDependencies = [];
  350. const missingDependencies = [];
  351. // prepare loader objects
  352. const loaders = (options.loaders || []).map(createLoaderObject);
  353. loaderContext.context = contextDirectory;
  354. loaderContext.loaderIndex = 0;
  355. loaderContext.loaders = loaders;
  356. loaderContext.resourcePath = resourcePath;
  357. loaderContext.resourceQuery = resourceQuery;
  358. loaderContext.resourceFragment = resourceFragment;
  359. loaderContext.async = null;
  360. loaderContext.callback = null;
  361. loaderContext.cacheable = (flag) => {
  362. if (flag === false) {
  363. requestCacheable = false;
  364. }
  365. };
  366. loaderContext.dependency = loaderContext.addDependency = (file) => {
  367. fileDependencies.push(file);
  368. };
  369. loaderContext.addContextDependency = (context) => {
  370. contextDependencies.push(context);
  371. };
  372. loaderContext.addMissingDependency = (context) => {
  373. missingDependencies.push(context);
  374. };
  375. loaderContext.getDependencies = () => fileDependencies.slice();
  376. loaderContext.getContextDependencies = () => contextDependencies.slice();
  377. loaderContext.getMissingDependencies = () => missingDependencies.slice();
  378. loaderContext.clearDependencies = () => {
  379. fileDependencies.length = 0;
  380. contextDependencies.length = 0;
  381. missingDependencies.length = 0;
  382. requestCacheable = true;
  383. };
  384. Object.defineProperty(loaderContext, "resource", {
  385. enumerable: true,
  386. get() {
  387. return (
  388. escapeHash(loaderContext.resourcePath) +
  389. escapeHash(loaderContext.resourceQuery) +
  390. loaderContext.resourceFragment
  391. );
  392. },
  393. set(value) {
  394. const splitted = value && parseIdentifier(value);
  395. loaderContext.resourcePath = splitted ? splitted[0] : "";
  396. loaderContext.resourceQuery = splitted ? splitted[1] : "";
  397. loaderContext.resourceFragment = splitted ? splitted[2] : "";
  398. },
  399. });
  400. Object.defineProperty(loaderContext, "request", {
  401. enumerable: true,
  402. get() {
  403. return joinRequests(
  404. loaders,
  405. 0,
  406. loaders.length,
  407. loaderContext.resource || ""
  408. );
  409. },
  410. });
  411. Object.defineProperty(loaderContext, "remainingRequest", {
  412. enumerable: true,
  413. get() {
  414. return joinRequests(
  415. loaders,
  416. loaderContext.loaderIndex + 1,
  417. loaders.length,
  418. loaderContext.resource
  419. );
  420. },
  421. });
  422. Object.defineProperty(loaderContext, "currentRequest", {
  423. enumerable: true,
  424. get() {
  425. return joinRequests(
  426. loaders,
  427. loaderContext.loaderIndex,
  428. loaders.length,
  429. loaderContext.resource
  430. );
  431. },
  432. });
  433. Object.defineProperty(loaderContext, "previousRequest", {
  434. enumerable: true,
  435. get() {
  436. const end = loaderContext.loaderIndex;
  437. if (end === 0) return "";
  438. let result = loaders[0].request;
  439. for (let i = 1; i < end; i++) {
  440. result += `!${loaders[i].request}`;
  441. }
  442. return result;
  443. },
  444. });
  445. Object.defineProperty(loaderContext, "query", {
  446. enumerable: true,
  447. get() {
  448. const entry = loaders[loaderContext.loaderIndex];
  449. return entry.options && typeof entry.options === "object"
  450. ? entry.options
  451. : entry.query;
  452. },
  453. });
  454. Object.defineProperty(loaderContext, "data", {
  455. enumerable: true,
  456. get() {
  457. return loaders[loaderContext.loaderIndex].data;
  458. },
  459. });
  460. // finish loader context
  461. if (Object.preventExtensions) {
  462. Object.preventExtensions(loaderContext);
  463. }
  464. const processOptions = {
  465. resourceBuffer: null,
  466. processResource: processResourceFn,
  467. };
  468. iteratePitchingLoaders(processOptions, loaderContext, (err, result) => {
  469. if (err) {
  470. return callback(err, {
  471. cacheable: requestCacheable,
  472. fileDependencies,
  473. contextDependencies,
  474. missingDependencies,
  475. });
  476. }
  477. callback(null, {
  478. result,
  479. resourceBuffer: processOptions.resourceBuffer,
  480. cacheable: requestCacheable,
  481. fileDependencies,
  482. contextDependencies,
  483. missingDependencies,
  484. });
  485. });
  486. };