| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| var fs = require("fs"); |
| var readFile = fs.readFile.bind(fs); |
| var loadLoader = require("./loadLoader"); |
| |
| function utf8BufferToString(buf) { |
| var str = buf.toString("utf-8"); |
| if(str.charCodeAt(0) === 0xFEFF) { |
| return str.substr(1); |
| } else { |
| return str; |
| } |
| } |
| |
| function splitQuery(req) { |
| var i = req.indexOf("?"); |
| if(i < 0) return [req, ""]; |
| return [req.substr(0, i), req.substr(i)]; |
| } |
| |
| function dirname(path) { |
| if(path === "/") return "/"; |
| var i = path.lastIndexOf("/"); |
| var j = path.lastIndexOf("\\"); |
| var i2 = path.indexOf("/"); |
| var j2 = path.indexOf("\\"); |
| var idx = i > j ? i : j; |
| var idx2 = i > j ? i2 : j2; |
| if(idx < 0) return path; |
| if(idx === idx2) return path.substr(0, idx + 1); |
| return path.substr(0, idx); |
| } |
| |
| function createLoaderObject(loader) { |
| var obj = { |
| path: null, |
| query: null, |
| options: null, |
| ident: null, |
| normal: null, |
| pitch: null, |
| raw: null, |
| data: null, |
| pitchExecuted: false, |
| normalExecuted: false |
| }; |
| Object.defineProperty(obj, "request", { |
| enumerable: true, |
| get: function() { |
| return obj.path + obj.query; |
| }, |
| set: function(value) { |
| if(typeof value === "string") { |
| var splittedRequest = splitQuery(value); |
| obj.path = splittedRequest[0]; |
| obj.query = splittedRequest[1]; |
| obj.options = undefined; |
| obj.ident = undefined; |
| } else { |
| if(!value.loader) |
| throw new Error("request should be a string or object with loader and object (" + JSON.stringify(value) + ")"); |
| obj.path = value.loader; |
| 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) { |
| var isSync = true; |
| var isDone = false; |
| var isError = false; // internal error |
| var reportedError = false; |
| context.async = function async() { |
| if(isDone) { |
| if(reportedError) return; // ignore |
| throw new Error("async(): The callback was already called."); |
| } |
| isSync = false; |
| return innerCallback; |
| }; |
| var innerCallback = context.callback = function() { |
| 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(e) { |
| isError = true; |
| throw e; |
| } |
| }; |
| try { |
| var 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(function(r) { |
| callback(null, r); |
| }, callback); |
| } |
| return callback(null, result); |
| } |
| } catch(e) { |
| if(isError) throw e; |
| 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 e === "object" && e.stack) console.error(e.stack); |
| else console.error(e); |
| return; |
| } |
| isDone = true; |
| reportedError = true; |
| callback(e); |
| } |
| |
| } |
| |
| function convertArgs(args, raw) { |
| if(!raw && Buffer.isBuffer(args[0])) |
| args[0] = utf8BufferToString(args[0]); |
| else if(raw && typeof args[0] === "string") |
| args[0] = new Buffer(args[0], "utf-8"); // eslint-disable-line |
| } |
| |
| function iteratePitchingLoaders(options, loaderContext, callback) { |
| // abort after last loader |
| if(loaderContext.loaderIndex >= loaderContext.loaders.length) |
| return processResource(options, loaderContext, callback); |
| |
| var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; |
| |
| // iterate |
| if(currentLoaderObject.pitchExecuted) { |
| loaderContext.loaderIndex++; |
| return iteratePitchingLoaders(options, loaderContext, callback); |
| } |
| |
| // load loader module |
| loadLoader(currentLoaderObject, function(err) { |
| if(err) { |
| loaderContext.cacheable(false); |
| return callback(err); |
| } |
| var fn = currentLoaderObject.pitch; |
| currentLoaderObject.pitchExecuted = true; |
| if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); |
| |
| runSyncOrAsync( |
| fn, |
| loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], |
| function(err) { |
| if(err) return callback(err); |
| var args = Array.prototype.slice.call(arguments, 1); |
| if(args.length > 0) { |
| loaderContext.loaderIndex--; |
| iterateNormalLoaders(options, loaderContext, args, callback); |
| } else { |
| iteratePitchingLoaders(options, loaderContext, callback); |
| } |
| } |
| ); |
| }); |
| } |
| |
| function processResource(options, loaderContext, callback) { |
| // set loader index to last loader |
| loaderContext.loaderIndex = loaderContext.loaders.length - 1; |
| |
| var resourcePath = loaderContext.resourcePath; |
| if(resourcePath) { |
| loaderContext.addDependency(resourcePath); |
| options.readResource(resourcePath, function(err, buffer) { |
| if(err) return callback(err); |
| options.resourceBuffer = buffer; |
| iterateNormalLoaders(options, loaderContext, [buffer], callback); |
| }); |
| } else { |
| iterateNormalLoaders(options, loaderContext, [null], callback); |
| } |
| } |
| |
| function iterateNormalLoaders(options, loaderContext, args, callback) { |
| if(loaderContext.loaderIndex < 0) |
| return callback(null, args); |
| |
| var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; |
| |
| // iterate |
| if(currentLoaderObject.normalExecuted) { |
| loaderContext.loaderIndex--; |
| return iterateNormalLoaders(options, loaderContext, args, callback); |
| } |
| |
| var fn = currentLoaderObject.normal; |
| currentLoaderObject.normalExecuted = true; |
| if(!fn) { |
| return iterateNormalLoaders(options, loaderContext, args, callback); |
| } |
| |
| convertArgs(args, currentLoaderObject.raw); |
| |
| runSyncOrAsync(fn, loaderContext, args, function(err) { |
| if(err) return callback(err); |
| |
| var args = Array.prototype.slice.call(arguments, 1); |
| iterateNormalLoaders(options, loaderContext, args, callback); |
| }); |
| } |
| |
| exports.getContext = function getContext(resource) { |
| var splitted = splitQuery(resource); |
| return dirname(splitted[0]); |
| }; |
| |
| exports.runLoaders = function runLoaders(options, callback) { |
| // read options |
| var resource = options.resource || ""; |
| var loaders = options.loaders || []; |
| var loaderContext = options.context || {}; |
| var readResource = options.readResource || readFile; |
| |
| // |
| var splittedResource = resource && splitQuery(resource); |
| var resourcePath = splittedResource ? splittedResource[0] : undefined; |
| var resourceQuery = splittedResource ? splittedResource[1] : undefined; |
| var contextDirectory = resourcePath ? dirname(resourcePath) : null; |
| |
| // execution state |
| var requestCacheable = true; |
| var fileDependencies = []; |
| var contextDependencies = []; |
| |
| // prepare loader objects |
| loaders = loaders.map(createLoaderObject); |
| |
| loaderContext.context = contextDirectory; |
| loaderContext.loaderIndex = 0; |
| loaderContext.loaders = loaders; |
| loaderContext.resourcePath = resourcePath; |
| loaderContext.resourceQuery = resourceQuery; |
| 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.getDependencies = function getDependencies() { |
| return fileDependencies.slice(); |
| }; |
| loaderContext.getContextDependencies = function getContextDependencies() { |
| return contextDependencies.slice(); |
| }; |
| loaderContext.clearDependencies = function clearDependencies() { |
| fileDependencies.length = 0; |
| contextDependencies.length = 0; |
| requestCacheable = true; |
| }; |
| Object.defineProperty(loaderContext, "resource", { |
| enumerable: true, |
| get: function() { |
| if(loaderContext.resourcePath === undefined) |
| return undefined; |
| return loaderContext.resourcePath + loaderContext.resourceQuery; |
| }, |
| set: function(value) { |
| var splittedResource = value && splitQuery(value); |
| loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined; |
| loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined; |
| } |
| }); |
| Object.defineProperty(loaderContext, "request", { |
| enumerable: true, |
| get: function() { |
| return loaderContext.loaders.map(function(o) { |
| return o.request; |
| }).concat(loaderContext.resource || "").join("!"); |
| } |
| }); |
| Object.defineProperty(loaderContext, "remainingRequest", { |
| enumerable: true, |
| get: function() { |
| if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource) |
| return ""; |
| return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) { |
| return o.request; |
| }).concat(loaderContext.resource || "").join("!"); |
| } |
| }); |
| Object.defineProperty(loaderContext, "currentRequest", { |
| enumerable: true, |
| get: function() { |
| return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) { |
| return o.request; |
| }).concat(loaderContext.resource || "").join("!"); |
| } |
| }); |
| Object.defineProperty(loaderContext, "previousRequest", { |
| enumerable: true, |
| get: function() { |
| return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) { |
| return o.request; |
| }).join("!"); |
| } |
| }); |
| Object.defineProperty(loaderContext, "query", { |
| enumerable: true, |
| get: function() { |
| var entry = loaderContext.loaders[loaderContext.loaderIndex]; |
| return entry.options && typeof entry.options === "object" ? entry.options : entry.query; |
| } |
| }); |
| Object.defineProperty(loaderContext, "data", { |
| enumerable: true, |
| get: function() { |
| return loaderContext.loaders[loaderContext.loaderIndex].data; |
| } |
| }); |
| |
| // finish loader context |
| if(Object.preventExtensions) { |
| Object.preventExtensions(loaderContext); |
| } |
| |
| var processOptions = { |
| resourceBuffer: null, |
| readResource: readResource |
| }; |
| iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { |
| if(err) { |
| return callback(err, { |
| cacheable: requestCacheable, |
| fileDependencies: fileDependencies, |
| contextDependencies: contextDependencies |
| }); |
| } |
| callback(null, { |
| result: result, |
| resourceBuffer: processOptions.resourceBuffer, |
| cacheable: requestCacheable, |
| fileDependencies: fileDependencies, |
| contextDependencies: contextDependencies |
| }); |
| }); |
| }; |