365 lines
11 KiB
JavaScript
365 lines
11 KiB
JavaScript
var HappyFakeLoaderContext = require('./HappyFakeLoaderContext');
|
|
var fnOncePedantic = require('./fnOncePedantic');
|
|
var assert = require('assert');
|
|
|
|
/**
|
|
* If this ain't a monster, I don't know what is.
|
|
*
|
|
*
|
|
* .'''''-, ,-`````.
|
|
* `-.._ | | _..-'
|
|
* \ `, ,' /
|
|
* '= ,/ \, =`
|
|
* '= ( ) =`
|
|
* .\ / \ /.
|
|
* / `,.' `.,' \
|
|
* \ `. ,' /
|
|
* \ \ / /
|
|
* \ .`. __.---. ,`. /
|
|
* \.' .'`` `. `./
|
|
* \.' -'''-.. `./
|
|
* / / '. \
|
|
* / / .-- .-'''` '.
|
|
* ' | ,---. _ \
|
|
* /``-----._.-. \ / ,-. '-' '. .-._.-----``\
|
|
* \__ . | : `.' ((O)) ,-. \ : | . __/
|
|
* `. '-...\_` | '-' ((O)) | '_/...-` .'
|
|
* .----..) ` \ \ / '-' / / ' (..----.
|
|
* (o `. / \ \ /\ .' / \ .' o)
|
|
* ```---.. `. /`. '--' '---' .'\ .' ..---```
|
|
* `-. `. /`. `. .' .'\ .' .-'
|
|
* `..` / `.' ` - - - ' `.' \ '..'
|
|
* / / \ \
|
|
* / ,' `. \
|
|
* \ ,'`. .'`. /
|
|
* `/ \ / \'
|
|
* ,= ( ) =,
|
|
* ,= '\ /` =,
|
|
* LGB / .' `. \
|
|
* .-''' | | ```-.
|
|
* `......' `......'
|
|
*
|
|
* @param {Object} runContext
|
|
* @param {HappyFakeCompiler} runContext.compiler
|
|
* @param {Array.<Object>} runContext.loaders
|
|
* @param {Object} runContext.loaderContext
|
|
*
|
|
* @param {String} runContext.loaderContext.remoteLoaderId
|
|
* The ID of the HappyLoader instance that initiated this compilation
|
|
* request.
|
|
*
|
|
* @param {String} runContext.loaderContext.request
|
|
* The full request for the resource including the loader chain string.
|
|
* Something like:
|
|
* '/path/to/style-loader.js!/path/to/css-loader.js!/path/to/resource.css'
|
|
*
|
|
* @param {String} runContext.loaderContext.context
|
|
* The directory of the resource, useful for resolving siblings.
|
|
*
|
|
* @param {String} runContext.loaderContext.resource
|
|
* The resource path without the loader chain. This includes the file
|
|
* and the query fragment.
|
|
*
|
|
* @param {String} runContext.loaderContext.resourcePath
|
|
* The resource file.
|
|
*
|
|
* @param {String} runContext.loaderContext.resourceQuery
|
|
* The resource query.
|
|
*
|
|
* @param {String} sourceCode
|
|
* @param {Object} sourceMap
|
|
*
|
|
* @param {Function} done
|
|
* @param {String|Error} done.err
|
|
* @param {String} done.sourceCode
|
|
* @param {Object} done.sourceMap
|
|
*/
|
|
module.exports = function applyLoaders(runContext, sourceCode, sourceMap, done) {
|
|
assert(runContext.compiler, "Missing webpack compiler instance!");
|
|
|
|
// we start out by creating fake loader contexts for every loader
|
|
var loaders = runContext.loaders.map(function(loader) {
|
|
return {
|
|
request: loader.request,
|
|
path: loader.path,
|
|
query: loader.query,
|
|
module: require(loader.path),
|
|
|
|
// TODO: this is probably not the best place to create the context, and it
|
|
// also should be cached at some layer
|
|
context: HappyFakeLoaderContext({
|
|
_compiler: runContext.compiler, // for compiler RPCs
|
|
_remoteLoaderId: runContext.loaderContext.remoteLoaderId, // for loader RPCs
|
|
fs: runContext.compiler.inputFileSystem,
|
|
|
|
query: loader.query,
|
|
options: runContext.compiler.options,
|
|
|
|
request: runContext.loaderContext.request,
|
|
resource: runContext.loaderContext.resource,
|
|
resourcePath: runContext.loaderContext.resourcePath,
|
|
resourceQuery: runContext.loaderContext.resourceQuery,
|
|
context: runContext.loaderContext.context,
|
|
|
|
_dependencies: runContext.loaderContext._dependencies,
|
|
_contextDependencies: runContext.loaderContext._contextDependencies,
|
|
|
|
sourceMap: Boolean(runContext.loaderContext.useSourceMap),
|
|
target: runContext.loaderContext.target,
|
|
minimize: runContext.loaderContext.minimize,
|
|
})
|
|
};
|
|
});
|
|
|
|
loaders.forEach(function exposeLoaderSetToEachLoader(loader, index) {
|
|
loader.context.loaders = loaders;
|
|
loader.context.loaderIndex = index;
|
|
});
|
|
|
|
// Go through the pitching phase. This might affect the loader contexts.
|
|
applyPitchLoaders(runContext.loaderContext.request, loaders, function(err, pitchResult) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
var apply = NormalLoaderApplier(loaders, pitchResult.dataMap, done);
|
|
|
|
// pitching phase did yield something so we skip the loaders to the right
|
|
// and use the yielded code as the starting sourceCode
|
|
if (pitchResult.sourceCode) {
|
|
apply(pitchResult.cursor - 1, pitchResult.sourceCode, pitchResult.sourceMap);
|
|
}
|
|
// otherwise, we just start from the right-most loader using the input
|
|
// source code and map:
|
|
else {
|
|
apply(loaders.length - 1, sourceCode, sourceMap);
|
|
}
|
|
});
|
|
}
|
|
|
|
// Perform the loader pitching phase.
|
|
//
|
|
// Pitching loaders are applied from left to right. Each loader is presented
|
|
// with the request string disected into two bits; from the start until its
|
|
// place in the string, then the remainder of the request.
|
|
//
|
|
// For example, for a request like the following:
|
|
//
|
|
// "c!b!a!./foo.js"
|
|
//
|
|
// The run order will be like the following with the inputs outlined at each
|
|
// application step:
|
|
//
|
|
// 1. Loader "c"
|
|
//
|
|
// [remainingRequest] => "b!a!./foo.js"
|
|
// [precedingRequest] => ""
|
|
//
|
|
// 2. Loader "b"
|
|
//
|
|
// [remainingRequest] => "a!./foo.js"
|
|
// [precedingRequest] => "c"
|
|
//
|
|
// 2. Loader "a"
|
|
//
|
|
// [remainingRequest] => "./foo.js"
|
|
// [precedingRequest] => "c!b"
|
|
//
|
|
// Finally, it is also presented with a special "data" state variable that will
|
|
// be shared between the pitch and normal phases for **that specific loader**.
|
|
//
|
|
// For example:
|
|
//
|
|
// module.exports = function() {
|
|
// console.log(this.data.value); // => 42
|
|
// };
|
|
//
|
|
// module.exports.pitch = function(_, _, data) {
|
|
// data.value = 42;
|
|
// };
|
|
//
|
|
function applyPitchLoaders(request, loaders, done) {
|
|
var requestFragments = request.split(/\!/g);
|
|
|
|
(function applyPitchLoader(cursor, dataMap) {
|
|
var loader = loaders[cursor];
|
|
|
|
if (!loader) {
|
|
return done(null, {
|
|
cursor: null,
|
|
sourceCode: null,
|
|
sourceMap: null,
|
|
dataMap: dataMap,
|
|
});
|
|
}
|
|
else if (!loader.module.pitch) {
|
|
return applyPitchLoader(cursor + 1, dataMap);
|
|
}
|
|
else {
|
|
// pitch loader, when applied, may modify any of these context variables:
|
|
//
|
|
// - this.resourcePath
|
|
// - this.resourceQuery
|
|
// - this.resource
|
|
// - this.loaderIndex (TODO: why would this be modified? does it affect the run order?!)
|
|
applySyncOrAsync(loader.module.pitch, loader.context, [
|
|
requestFragments.slice(cursor+1).join('!'),
|
|
requestFragments.slice(0, cursor).join('!'),
|
|
dataMap[cursor]
|
|
], function(err, sourceCode, sourceMap) {
|
|
if (err) {
|
|
done(err);
|
|
}
|
|
else if (sourceCode) {
|
|
done(null, {
|
|
cursor: cursor,
|
|
sourceCode: sourceCode,
|
|
sourceMap: sourceMap,
|
|
dataMap: dataMap,
|
|
});
|
|
}
|
|
else {
|
|
applyPitchLoader(cursor + 1, dataMap);
|
|
}
|
|
});
|
|
}
|
|
}(0, loaders.map(function() { return {}; })));
|
|
}
|
|
|
|
// Generates a function that will perform the "normal" loader phase (ie
|
|
// non-pitching.)
|
|
//
|
|
// Normal loaders are applied from right-to-left and may yield one or both of
|
|
// "sourceCode" and "sourceMap" values.
|
|
//
|
|
// Each application will forward a state variable called "inputValues" to the
|
|
// succeeding loader, which gets populated as "values" in the preceding one.
|
|
//
|
|
// For example, for a request like this "b!a!./foo.js"
|
|
//
|
|
// // a-loader.js
|
|
// module.exports = function(code) {
|
|
// this.values = { foo: 'bar' };
|
|
//
|
|
// return code;
|
|
// };
|
|
//
|
|
// // b-loader.js
|
|
// module.exports = function(code) {
|
|
// console.log(this.inputValues); // { foo: "bar" };
|
|
//
|
|
// return code;
|
|
// };
|
|
//
|
|
// @param {Array.<Object>} dataMap
|
|
// Data generated during the pitching phase. This set is expected to be
|
|
// fully populated (even if each item is an empty object) and be ordered
|
|
// after the loaders in the @loaders array.
|
|
//
|
|
function NormalLoaderApplier(loaders, dataMap, done) {
|
|
return function apply(cursor, sourceCode, sourceMap, inputValues) {
|
|
var loader = loaders[cursor];
|
|
var context;
|
|
|
|
if (!loader) {
|
|
return done(null, sourceCode, sourceMap);
|
|
}
|
|
|
|
context = loader.context;
|
|
context.data = dataMap[cursor];
|
|
context.inputValues = inputValues || {};
|
|
|
|
var fn = loader.module.__esModule === true ?
|
|
loader.module.default :
|
|
loader.module
|
|
;
|
|
|
|
applySyncOrAsync(fn, context, [ sourceCode, sourceMap ],
|
|
function(err, nextSource, nextSourceMap) {
|
|
if (err) {
|
|
return done(err);
|
|
}
|
|
|
|
apply(cursor - 1, nextSource, nextSourceMap, context.values);
|
|
}
|
|
);
|
|
};
|
|
}
|
|
|
|
// Utility function for applying a function and accepting a result in one of
|
|
// three ways:
|
|
//
|
|
// - a synchronous return, where only one value may be yielded
|
|
// - a multi-value yield using a generated `this.callback(...)` callback
|
|
// - a multi-value yield that is also asynchronous using `callback = this.async();`
|
|
//
|
|
// @done will always be called with {String|Error, ...} for an error and the
|
|
// yielded values.
|
|
//
|
|
// Example victims:
|
|
//
|
|
// 1. Synchronous, single-yield:
|
|
//
|
|
// function() {
|
|
// return 'hello!';
|
|
// }
|
|
//
|
|
// 2. Synchronous, multi-yield:
|
|
//
|
|
// function() {
|
|
// this.callback(null, 'hello', 'world!');
|
|
// }
|
|
//
|
|
// 3. Asynchronous, single-yield:
|
|
//
|
|
// function() {
|
|
// var callback = this.async();
|
|
//
|
|
// setTimeout(function() {
|
|
// callback(null, 'hello');
|
|
// }, 1000);
|
|
// }
|
|
//
|
|
// 4. Asynchronous, multi-yield:
|
|
//
|
|
// function() {
|
|
// var callback = this.async();
|
|
//
|
|
// setTimeout(function() {
|
|
// callback(null, 'hello', 'world!');
|
|
// }, 1000);
|
|
// }
|
|
function applySyncOrAsync(fn, context, args, done) {
|
|
var expectSynchronousResponse = true;
|
|
|
|
// sync/async this.callback() style
|
|
context.callback = fnOncePedantic(function() {
|
|
expectSynchronousResponse = false;
|
|
|
|
return done.apply(null, arguments);
|
|
}, "this.callback(): The callback was already called.");
|
|
|
|
context.async = fnOncePedantic(function() {
|
|
expectSynchronousResponse = false;
|
|
|
|
return done;
|
|
}, "this.async(): The callback was already called.");
|
|
|
|
try {
|
|
// synchronus return style
|
|
var result = fn.apply(context, args);
|
|
|
|
if (expectSynchronousResponse) {
|
|
if (result) {
|
|
done(null, result);
|
|
}
|
|
else {
|
|
done();
|
|
}
|
|
}
|
|
}
|
|
catch(e) { // abort the chain
|
|
done(e);
|
|
}
|
|
}
|