UNPKG

loader-runner

Version:
516 lines (465 loc) 13.6 kB
/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const fs = require("fs"); const readFile = fs.readFile.bind(fs); const loadLoader = require("./loadLoader"); function utf8BufferToString(buf) { const str = buf.toString("utf8"); if (str.charCodeAt(0) === 0xfeff) { return str.slice(1); } return str; } const PATH_QUERY_FRAGMENT_REGEXP = /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/; const ZERO_ESCAPE_REGEXP = /\0(.)/g; /** * @param {string} identifier identifier * @returns {[string, string, string]} parsed identifier */ function parseIdentifier(identifier) { // Fast path for inputs that don't use \0 escaping. const firstEscape = identifier.indexOf("\0"); if (firstEscape < 0) { const queryStart = identifier.indexOf("?"); const fragmentStart = identifier.indexOf("#"); if (fragmentStart < 0) { if (queryStart < 0) { // No fragment, no query return [identifier, "", ""]; } // Query, no fragment return [ identifier.slice(0, queryStart), identifier.slice(queryStart), "", ]; } if (queryStart < 0 || fragmentStart < queryStart) { // Fragment, no query return [ identifier.slice(0, fragmentStart), "", identifier.slice(fragmentStart), ]; } // Query and fragment return [ identifier.slice(0, queryStart), identifier.slice(queryStart, fragmentStart), identifier.slice(fragmentStart), ]; } const match = PATH_QUERY_FRAGMENT_REGEXP.exec(identifier); return [ match[1].replace(ZERO_ESCAPE_REGEXP, "$1"), match[2] ? match[2].replace(ZERO_ESCAPE_REGEXP, "$1") : "", match[3] || "", ]; } function dirname(path) { if (path === "/") return "/"; const i = path.lastIndexOf("/"); const j = path.lastIndexOf("\\"); const i2 = path.indexOf("/"); const j2 = path.indexOf("\\"); const idx = i > j ? i : j; const idx2 = i > j ? i2 : j2; if (idx < 0) return path; if (idx === idx2) return path.slice(0, idx + 1); return path.slice(0, idx); } function createLoaderObject(loader) { const obj = { path: null, query: null, fragment: null, options: null, ident: null, normal: null, pitch: null, raw: null, data: null, pitchExecuted: false, normalExecuted: false, }; Object.defineProperty(obj, "request", { enumerable: true, get() { return ( obj.path.replace(/#/g, "\0#") + obj.query.replace(/#/g, "\0#") + obj.fragment ); }, set(value) { if (typeof value === "string") { const [path, query, fragment] = parseIdentifier(value); obj.path = path; obj.query = query; obj.fragment = fragment; obj.options = undefined; obj.ident = undefined; } else { if (!value.loader) { throw new Error( `request should be a string or object with loader and options (${JSON.stringify( value )})` ); } obj.path = value.loader; obj.fragment = value.fragment || ""; obj.type = value.type; obj.options = value.options; obj.ident = value.ident; if (obj.options === null) { obj.query = ""; } else if (obj.options === undefined) { obj.query = ""; } else if (typeof obj.options === "string") { obj.query = `?${obj.options}`; } else if (obj.ident) { obj.query = `??${obj.ident}`; } else if (typeof obj.options === "object" && obj.options.ident) { obj.query = `??${obj.options.ident}`; } else { obj.query = `?${JSON.stringify(obj.options)}`; } } }, }); obj.request = loader; if (Object.preventExtensions) { Object.preventExtensions(obj); } return obj; } function runSyncOrAsync(fn, context, args, callback) { let isSync = true; let isDone = false; let isError = false; // internal error let reportedError = false; // eslint-disable-next-line func-name-matching const innerCallback = (context.callback = function innerCallback() { if (isDone) { if (reportedError) return; // ignore throw new Error("callback(): The callback was already called."); } isDone = true; isSync = false; try { callback.apply(null, arguments); } catch (err) { isError = true; throw err; } }); context.async = function async() { if (isDone) { if (reportedError) return; // ignore throw new Error("async(): The callback was already called."); } isSync = false; return innerCallback; }; try { const result = (function LOADER_EXECUTION() { return fn.apply(context, args); })(); if (isSync) { isDone = true; if (result === undefined) return callback(); if ( result && typeof result === "object" && typeof result.then === "function" ) { return result.then((r) => { callback(null, r); }, callback); } return callback(null, result); } } catch (err) { if (isError) throw err; if (isDone) { // loader is already "done", so we cannot use the callback function // for better debugging we print the error on the console if (typeof err === "object" && err.stack) { // eslint-disable-next-line no-console console.error(err.stack); } else { // eslint-disable-next-line no-console console.error(err); } return; } isDone = true; reportedError = true; callback(err); } } function convertArgs(args, raw) { if (!raw && Buffer.isBuffer(args[0])) { args[0] = utf8BufferToString(args[0]); } else if (raw && typeof args[0] === "string") { args[0] = Buffer.from(args[0], "utf8"); } } function iterateNormalLoaders(options, loaderContext, args, callback) { if (loaderContext.loaderIndex < 0) return callback(null, args); const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate if (currentLoaderObject.normalExecuted) { loaderContext.loaderIndex--; return iterateNormalLoaders(options, loaderContext, args, callback); } const fn = currentLoaderObject.normal; currentLoaderObject.normalExecuted = true; if (!fn) { return iterateNormalLoaders(options, loaderContext, args, callback); } convertArgs(args, currentLoaderObject.raw); runSyncOrAsync(fn, loaderContext, args, function runSyncOrAsyncCallback(err) { if (err) return callback(err); const args = Array.prototype.slice.call(arguments, 1); iterateNormalLoaders(options, loaderContext, args, callback); }); } function processResource(options, loaderContext, callback) { // set loader index to last loader loaderContext.loaderIndex = loaderContext.loaders.length - 1; const { resourcePath } = loaderContext; if (resourcePath) { options.processResource( loaderContext, resourcePath, function processResourceCallback(err) { if (err) return callback(err); const args = Array.prototype.slice.call(arguments, 1); [options.resourceBuffer] = args; iterateNormalLoaders(options, loaderContext, args, callback); } ); } else { iterateNormalLoaders(options, loaderContext, [null], callback); } } function iteratePitchingLoaders(options, loaderContext, callback) { // abort after last loader if (loaderContext.loaderIndex >= loaderContext.loaders.length) { return processResource(options, loaderContext, callback); } const currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate if (currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // load loader module loadLoader(currentLoaderObject, (err) => { if (err) { loaderContext.cacheable(false); return callback(err); } const fn = currentLoaderObject.pitch; currentLoaderObject.pitchExecuted = true; if (!fn) return iteratePitchingLoaders(options, loaderContext, callback); runSyncOrAsync( fn, loaderContext, [ loaderContext.remainingRequest, loaderContext.previousRequest, (currentLoaderObject.data = {}), ], function runSyncOrAsyncCallback(err) { if (err) return callback(err); const args = Array.prototype.slice.call(arguments, 1); // Determine whether to continue the pitching process based on // argument values (as opposed to argument presence) in order // to support synchronous and asynchronous usages. const hasArg = args.some((value) => value !== undefined); if (hasArg) { loaderContext.loaderIndex--; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } ); }); } module.exports.getContext = function getContext(resource) { const [path] = parseIdentifier(resource); return dirname(path); }; module.exports.runLoaders = function runLoaders(options, callback) { // read options const resource = options.resource || ""; let loaders = options.loaders || []; const loaderContext = options.context || {}; const processResource = options.processResource || ((readResource, context, resource, callback) => { context.addDependency(resource); readResource(resource, callback); }).bind(null, options.readResource || readFile); const splittedResource = resource && parseIdentifier(resource); const resourcePath = splittedResource ? splittedResource[0] : ""; const resourceQuery = splittedResource ? splittedResource[1] : ""; const resourceFragment = splittedResource ? splittedResource[2] : ""; const contextDirectory = resourcePath ? dirname(resourcePath) : null; // execution state let requestCacheable = true; const fileDependencies = []; const contextDependencies = []; const missingDependencies = []; // prepare loader objects loaders = loaders.map(createLoaderObject); loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; loaderContext.loaders = loaders; loaderContext.resourcePath = resourcePath; loaderContext.resourceQuery = resourceQuery; loaderContext.resourceFragment = resourceFragment; loaderContext.async = null; loaderContext.callback = null; loaderContext.cacheable = function cacheable(flag) { if (flag === false) { requestCacheable = false; } }; loaderContext.dependency = loaderContext.addDependency = function addDependency(file) { fileDependencies.push(file); }; loaderContext.addContextDependency = function addContextDependency(context) { contextDependencies.push(context); }; loaderContext.addMissingDependency = function addMissingDependency(context) { missingDependencies.push(context); }; loaderContext.getDependencies = function getDependencies() { return [...fileDependencies]; }; loaderContext.getContextDependencies = function getContextDependencies() { return [...contextDependencies]; }; loaderContext.getMissingDependencies = function getMissingDependencies() { return [...missingDependencies]; }; loaderContext.clearDependencies = function clearDependencies() { fileDependencies.length = 0; contextDependencies.length = 0; missingDependencies.length = 0; requestCacheable = true; }; Object.defineProperty(loaderContext, "resource", { enumerable: true, get() { return ( loaderContext.resourcePath.replace(/#/g, "\0#") + loaderContext.resourceQuery.replace(/#/g, "\0#") + loaderContext.resourceFragment ); }, set(value) { const splittedResource = value && parseIdentifier(value); loaderContext.resourcePath = splittedResource ? splittedResource[0] : ""; loaderContext.resourceQuery = splittedResource ? splittedResource[1] : ""; loaderContext.resourceFragment = splittedResource ? splittedResource[2] : ""; }, }); Object.defineProperty(loaderContext, "request", { enumerable: true, get() { return loaderContext.loaders .map((loader) => loader.request) .concat(loaderContext.resource || "") .join("!"); }, }); Object.defineProperty(loaderContext, "remainingRequest", { enumerable: true, get() { if ( loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource ) { return ""; } return loaderContext.loaders .slice(loaderContext.loaderIndex + 1) .map((loader) => loader.request) .concat(loaderContext.resource || "") .join("!"); }, }); Object.defineProperty(loaderContext, "currentRequest", { enumerable: true, get() { return loaderContext.loaders .slice(loaderContext.loaderIndex) .map((loader) => loader.request) .concat(loaderContext.resource || "") .join("!"); }, }); Object.defineProperty(loaderContext, "previousRequest", { enumerable: true, get() { return loaderContext.loaders .slice(0, loaderContext.loaderIndex) .map((loader) => loader.request) .join("!"); }, }); Object.defineProperty(loaderContext, "query", { enumerable: true, get() { const entry = loaderContext.loaders[loaderContext.loaderIndex]; return entry.options && typeof entry.options === "object" ? entry.options : entry.query; }, }); Object.defineProperty(loaderContext, "data", { enumerable: true, get() { return loaderContext.loaders[loaderContext.loaderIndex].data; }, }); // finish loader context if (Object.preventExtensions) { Object.preventExtensions(loaderContext); } const processOptions = { resourceBuffer: null, processResource, }; iteratePitchingLoaders(processOptions, loaderContext, (err, result) => { if (err) { return callback(err, { cacheable: requestCacheable, fileDependencies, contextDependencies, missingDependencies, }); } callback(null, { result, resourceBuffer: processOptions.resourceBuffer, cacheable: requestCacheable, fileDependencies, contextDependencies, missingDependencies, }); }); };