source: frontend/node_modules/loader-runner/lib/LoaderRunner.js

Last change on this file was 9af201e, checked in by MBK <marija.karapandzova@…>, 11 days ago

Fix frontend appearance

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