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.

417 lines
12 KiB

/*
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;
}
}
const PATH_QUERY_FRAGMENT_REGEXP = /^((?:\0.|[^?#\0])*)(\?(?:\0.|[^#\0])*)?(#.*)?$/;
/**
* @param {string} str the path with query and fragment
* @returns {{ path: string, query: string, fragment: string }} parsed parts
*/
function parsePathQueryFragment(str) {
var match = PATH_QUERY_FRAGMENT_REGEXP.exec(str);
return {
path: match[1].replace(/\0(.)/g, "$1"),
query: match[2] ? match[2].replace(/\0(.)/g, "$1") : "",
fragment: match[3] || ""
};
}
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,
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: function() {
return obj.path.replace(/#/g, "\0#") + obj.query.replace(/#/g, "\0#") + obj.fragment;
},
set: function(value) {
if(typeof value === "string") {
var splittedRequest = parsePathQueryFragment(value);
obj.path = splittedRequest.path;
obj.query = splittedRequest.query;
obj.fragment = splittedRequest.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) {
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] = Buffer.from(args[0], "utf-8");
}
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);
// Determine whether to continue the pitching process based on
// argument values (as opposed to argument presence) in order
// to support synchronous and asynchronous usages.
var hasArg = args.some(function(value) {
return value !== undefined;
});
if(hasArg) {
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) {
options.processResource(loaderContext, resourcePath, function(err) {
if(err) return callback(err);
var args = Array.prototype.slice.call(arguments, 1);
options.resourceBuffer = args[0];
iterateNormalLoaders(options, loaderContext, args, 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 path = parsePathQueryFragment(resource).path;
return dirname(path);
};
exports.runLoaders = function runLoaders(options, callback) {
// read options
var resource = options.resource || "";
var loaders = options.loaders || [];
var loaderContext = options.context || {};
var processResource = options.processResource || ((readResource, context, resource, callback) => {
context.addDependency(resource);
readResource(resource, callback);
}).bind(null, options.readResource || readFile);
//
var splittedResource = resource && parsePathQueryFragment(resource);
var resourcePath = splittedResource ? splittedResource.path : undefined;
var resourceQuery = splittedResource ? splittedResource.query : undefined;
var resourceFragment = splittedResource ? splittedResource.fragment : undefined;
var contextDirectory = resourcePath ? dirname(resourcePath) : null;
// execution state
var requestCacheable = true;
var fileDependencies = [];
var contextDependencies = [];
var 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.slice();
};
loaderContext.getContextDependencies = function getContextDependencies() {
return contextDependencies.slice();
};
loaderContext.getMissingDependencies = function getMissingDependencies() {
return missingDependencies.slice();
};
loaderContext.clearDependencies = function clearDependencies() {
fileDependencies.length = 0;
contextDependencies.length = 0;
missingDependencies.length = 0;
requestCacheable = true;
};
Object.defineProperty(loaderContext, "resource", {
enumerable: true,
get: function() {
if(loaderContext.resourcePath === undefined)
return undefined;
return loaderContext.resourcePath.replace(/#/g, "\0#") + loaderContext.resourceQuery.replace(/#/g, "\0#") + loaderContext.resourceFragment;
},
set: function(value) {
var splittedResource = value && parsePathQueryFragment(value);
loaderContext.resourcePath = splittedResource ? splittedResource.path : undefined;
loaderContext.resourceQuery = splittedResource ? splittedResource.query : undefined;
loaderContext.resourceFragment = splittedResource ? splittedResource.fragment : 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,
processResource: processResource
};
iteratePitchingLoaders(processOptions, loaderContext, function(err, result) {
if(err) {
return callback(err, {
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies,
missingDependencies: missingDependencies
});
}
callback(null, {
result: result,
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies,
missingDependencies: missingDependencies
});
});
};