提交学习笔记专用
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

515 lines
13 KiB

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