Merge pull request #4259 from timse/refactor-normalmodule-to-es6

Refactor NormalModule to es6
This commit is contained in:
Tobias Koppers 2017-02-16 11:03:29 +01:00 committed by GitHub
commit 27deabcefe
2 changed files with 864 additions and 281 deletions

View File

@ -2,22 +2,27 @@
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
var path = require("path");
var Module = require("./Module");
var SourceMapSource = require("webpack-sources").SourceMapSource;
var OriginalSource = require("webpack-sources").OriginalSource;
var RawSource = require("webpack-sources").RawSource;
var ReplaceSource = require("webpack-sources").ReplaceSource;
var CachedSource = require("webpack-sources").CachedSource;
var LineToLineMappedSource = require("webpack-sources").LineToLineMappedSource;
var ModuleParseError = require("./ModuleParseError");
"use strict";
var ModuleBuildError = require("./ModuleBuildError");
var ModuleError = require("./ModuleError");
var ModuleWarning = require("./ModuleWarning");
const path = require("path");
const NativeModule = require("module");
const crypto = require("crypto");
var runLoaders = require("loader-runner").runLoaders;
var getContext = require("loader-runner").getContext;
const SourceMapSource = require("webpack-sources").SourceMapSource;
const OriginalSource = require("webpack-sources").OriginalSource;
const RawSource = require("webpack-sources").RawSource;
const ReplaceSource = require("webpack-sources").ReplaceSource;
const CachedSource = require("webpack-sources").CachedSource;
const LineToLineMappedSource = require("webpack-sources").LineToLineMappedSource;
const Module = require("./Module");
const ModuleParseError = require("./ModuleParseError");
const ModuleBuildError = require("./ModuleBuildError");
const ModuleError = require("./ModuleError");
const ModuleWarning = require("./ModuleWarning");
const runLoaders = require("loader-runner").runLoaders;
const getContext = require("loader-runner").getContext;
function asString(buf) {
if(Buffer.isBuffer(buf)) {
@ -26,41 +31,9 @@ function asString(buf) {
return buf;
}
function NormalModule(request, userRequest, rawRequest, loaders, resource, parser) {
Module.call(this);
this.request = request;
this.userRequest = userRequest;
this.rawRequest = rawRequest;
this.parser = parser;
this.resource = resource;
this.context = getContext(resource);
this.loaders = loaders;
this.fileDependencies = [];
this.contextDependencies = [];
this.warnings = [];
this.errors = [];
this.error = null;
this._source = null;
this.assets = {};
this.built = false;
this._cachedSource = null;
}
module.exports = NormalModule;
NormalModule.prototype = Object.create(Module.prototype);
NormalModule.prototype.constructor = NormalModule;
NormalModule.prototype.identifier = function() {
return this.request;
};
NormalModule.prototype.readableIdentifier = function(requestShortener) {
return requestShortener.shorten(this.userRequest);
};
function contextify(options, request) {
function contextify(context, request) {
return request.split("!").map(function(r) {
var rp = path.relative(options.context, r);
let rp = path.relative(context, r);
if(path.sep === "\\")
rp = rp.replace(/\\/g, "/");
if(rp.indexOf("../") !== 0)
@ -69,260 +42,463 @@ function contextify(options, request) {
}).join("!");
}
NormalModule.prototype.libIdent = function(options) {
return contextify(options, this.userRequest);
};
class NormalModule extends Module {
NormalModule.prototype.nameForCondition = function() {
var idx = this.resource.indexOf("?");
if(idx >= 0) return this.resource.substr(0, idx);
return this.resource;
};
constructor(request, userRequest, rawRequest, loaders, resource, parser) {
super();
this.request = request;
this.userRequest = userRequest;
this.rawRequest = rawRequest;
this.parser = parser;
this.resource = resource;
this.context = getContext(resource);
this.loaders = loaders;
this.fileDependencies = [];
this.contextDependencies = [];
this.warnings = [];
this.errors = [];
this.error = null;
this._source = null;
this.assets = {};
this.built = false;
this._cachedSource = null;
}
identifier() {
return this.request;
}
readableIdentifier(requestShortener) {
return requestShortener.shorten(this.userRequest);
}
libIdent(options) {
return contextify(options.context, this.userRequest);
}
nameForCondition() {
const idx = this.resource.indexOf("?");
if(idx >= 0) return this.resource.substr(0, idx);
return this.resource;
}
createSourceForAsset(name, content, sourceMap) {
if(!sourceMap) {
return new RawSource(content);
}
NormalModule.prototype.doBuild = function doBuild(options, compilation, resolver, fs, callback) {
this.cacheable = false;
var module = this;
var loaderContext = {
version: 2,
emitWarning: function(warning) {
module.warnings.push(new ModuleWarning(module, warning));
},
emitError: function(error) {
module.errors.push(new ModuleError(module, error));
},
exec: function(code, filename) {
var Module = require("module");
var m = new Module(filename, module);
m.paths = Module._nodeModulePaths(module.context);
m.filename = filename;
m._compile(code, filename);
return m.exports;
},
resolve: function(context, request, callback) {
resolver.resolve({}, context, request, callback);
},
resolveSync: function(context, request) {
return resolver.resolveSync({}, context, request);
},
options: options
};
loaderContext.webpack = true;
loaderContext.sourceMap = !!this.useSourceMap;
loaderContext.emitFile = function(name, content, sourceMap) {
if(typeof sourceMap === "string") {
this.assets[name] = new OriginalSource(content, sourceMap);
} else if(sourceMap) {
this.assets[name] = new SourceMapSource(content, name, sourceMap);
} else {
this.assets[name] = new RawSource(content);
}
}.bind(this);
loaderContext._module = this;
loaderContext._compilation = compilation;
loaderContext._compiler = compilation.compiler;
loaderContext.fs = fs;
compilation.applyPlugins("normal-module-loader", loaderContext, this);
if(options.loader)
for(var key in options.loader)
loaderContext[key] = options.loader[key];
runLoaders({
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
}, function(err, result) {
if(result) {
module.cacheable = result.cacheable;
module.fileDependencies = result.fileDependencies;
module.contextDependencies = result.contextDependencies;
}
if(err) {
return callback(module.error = new ModuleBuildError(module, err));
return new OriginalSource(content, sourceMap);
}
var resourceBuffer = result.resourceBuffer;
var source = result.result[0];
var sourceMap = result.result[1];
return new SourceMapSource(content, name, sourceMap);
}
if(!Buffer.isBuffer(source) && typeof source !== "string") {
return callback(module.error = new ModuleBuildError(module, new Error("Final loader didn't return a Buffer or String")));
createLoaderContext(resolver, options, compilation, fs) {
const loaderContext = {
version: 2,
emitWarning: (warning) => {
this.warnings.push(new ModuleWarning(this, warning));
},
emitError: (error) => {
this.errors.push(new ModuleError(this, error));
},
exec: (code, filename) => {
const module = new NativeModule(filename, this);
module.paths = NativeModule._nodeModulePaths(this.context);
module.filename = filename;
module._compile(code, filename);
return module.exports;
},
resolve(context, request, callback) {
resolver.resolve({}, context, request, callback);
},
resolveSync(context, request) {
return resolver.resolveSync({}, context, request);
},
emitFile: (name, content, sourceMap) => {
this.assets[name] = this.createSourceForAsset(name, content, sourceMap);
},
options: options,
webpack: true,
sourceMap: !!this.useSourceMap,
_module: this,
_compilation: compilation,
_compiler: compilation.compiler,
fs: fs,
};
compilation.applyPlugins("normal-module-loader", loaderContext, this);
if(options.loader)
Object.assign(loaderContext, options.loader);
return loaderContext;
}
createSource(source, resourceBuffer, sourceMap) {
// if there is no identifier return raw source
if(!this.identifier) {
return new RawSource(source);
}
source = asString(source);
if(module.identifier && module.lineToLine && resourceBuffer) {
module._source = new LineToLineMappedSource(source, module.identifier(),
asString(resourceBuffer));
} else if(module.identifier && module.useSourceMap && sourceMap) {
module._source = new SourceMapSource(source, module.identifier(), sourceMap);
} else if(module.identifier) {
module._source = new OriginalSource(source, module.identifier());
} else {
module._source = new RawSource(source);
// from here on we assume we have an identifier
const identifier = this.identifier();
if(this.lineToLine && resourceBuffer) {
return new LineToLineMappedSource(
source, identifier, asString(resourceBuffer));
}
return callback();
});
};
NormalModule.prototype.disconnect = function disconnect() {
this.built = false;
Module.prototype.disconnect.call(this);
};
if(this.useSourceMap && sourceMap) {
return new SourceMapSource(source, identifier, sourceMap);
}
NormalModule.prototype.build = function build(options, compilation, resolver, fs, callback) {
var _this = this;
_this.buildTimestamp = new Date().getTime();
_this.built = true;
_this._source = null;
_this.error = null;
_this.errors.length = 0;
_this.warnings.length = 0;
_this.meta = {};
return _this.doBuild(options, compilation, resolver, fs, function(err) {
_this.dependencies.length = 0;
_this.variables.length = 0;
_this.blocks.length = 0;
_this._cachedSource = null;
if(err) return setError(err);
if(options.module && options.module.noParse) {
var testRegExp = function testRegExp(regExp) {
return typeof regExp === "string" ?
_this.request.indexOf(regExp) === 0 :
regExp.test(_this.request);
};
if(Array.isArray(options.module.noParse)) {
if(options.module.noParse.some(testRegExp, _this))
return callback();
} else if(testRegExp.call(_this, options.module.noParse)) {
return callback();
return new OriginalSource(source, identifier);
}
doBuild(options, compilation, resolver, fs, callback) {
this.cacheable = false;
const loaderContext = this.createLoaderContext(resolver, options, compilation, fs);
runLoaders({
resource: this.resource,
loaders: this.loaders,
context: loaderContext,
readResource: fs.readFile.bind(fs)
}, (err, result) => {
if(result) {
this.cacheable = result.cacheable;
this.fileDependencies = result.fileDependencies;
this.contextDependencies = result.contextDependencies;
}
}
try {
_this.parser.parse(_this._source.source(), {
current: _this,
module: _this,
compilation: compilation,
options: options
});
} catch(e) {
var source = _this._source.source();
return setError(_this.error = new ModuleParseError(_this, source, e));
}
return callback();
});
function setError(err) {
_this.meta = null;
if(_this.error) {
_this.errors.push(_this.error);
_this._source = new RawSource("throw new Error(" + JSON.stringify(_this.error.message) + ");");
} else {
_this._source = new RawSource("throw new Error('Module build failed');");
}
callback();
}
};
if(err) {
const error = new ModuleBuildError(this, err);
return callback(error);
}
NormalModule.prototype.source = function(dependencyTemplates, outputOptions, requestShortener) {
var hash = require("crypto").createHash("md5");
this.updateHash(hash);
hash = hash.digest("hex");
if(this._cachedSource && this._cachedSource.hash === hash) {
return this._cachedSource.source;
}
var _source = this._source;
if(!_source) return new RawSource("throw new Error('No source available');");
var source = new ReplaceSource(_source);
this._cachedSource = {
source: source,
hash: hash
};
var topLevelBlock = this;
const resourceBuffer = result.resourceBuffer;
const source = result.result[0];
const sourceMap = result.result[1];
function doDep(dep) {
var template = dependencyTemplates.get(dep.constructor);
if(!template) throw new Error("No template for dependency: " + dep.constructor.name);
template.apply(dep, source, outputOptions, requestShortener, dependencyTemplates);
}
if(!Buffer.isBuffer(source) && typeof source !== "string") {
const error = new ModuleBuildError(this, new Error("Final loader didn't return a Buffer or String"));
return callback(error);
}
function doVariable(availableVars, vars, variable) {
var name = variable.name;
var expr = variable.expressionSource(dependencyTemplates, outputOptions, requestShortener);
function isEqual(v) {
return v.name === name && v.expression.source() === expr.source();
}
if(availableVars.some(isEqual)) return;
vars.push({
name: name,
expression: expr
this._source = this.createSource(asString(source), resourceBuffer, sourceMap);
return callback();
});
}
function doBlock(availableVars, block) {
block.dependencies.forEach(doDep);
var vars = [];
if(block.variables.length > 0) {
block.variables.forEach(doVariable.bind(null, availableVars, vars));
var varNames = [];
var varExpressions = [];
var varStartCode = "";
var varEndCode = "";
var emitFunction = function emitFunction() {
if(varNames.length === 0) return;
varStartCode += "/* WEBPACK VAR INJECTION */(function(" + varNames.join(", ") + ") {";
// exports === this in the topLevelBlock, but exports do compress better...
varEndCode = (topLevelBlock === block ? "}.call(" + (topLevelBlock.exportsArgument || "exports") + ", " : "}.call(this, ") +
varExpressions.map(function(e) {
return e.source();
}).join(", ") + "))" + varEndCode;
varNames.length = 0;
varExpressions.length = 0;
};
vars.forEach(function(v) {
if(varNames.indexOf(v.name) >= 0) emitFunction();
varNames.push(v.name);
varExpressions.push(v.expression);
});
emitFunction();
var start = block.range ? block.range[0] : -10;
var end = block.range ? block.range[1] : (_source.size() + 1);
if(varStartCode) source.insert(start + 0.5, varStartCode);
if(varEndCode) source.insert(end + 0.5, "\n/* WEBPACK VAR INJECTION */" + varEndCode);
}
block.blocks.forEach(doBlock.bind(null, availableVars.concat(vars)));
disconnect() {
this.built = false;
super.disconnect();
}
doBlock([], this);
return new CachedSource(source);
};
NormalModule.prototype.needRebuild = function needRebuild(fileTimestamps, contextTimestamps) {
var timestamp = 0;
this.fileDependencies.forEach(function(file) {
var ts = fileTimestamps[file];
if(!ts) timestamp = Infinity;
if(ts > timestamp) timestamp = ts;
});
this.contextDependencies.forEach(function(context) {
var ts = contextTimestamps[context];
if(!ts) timestamp = Infinity;
if(ts > timestamp) timestamp = ts;
});
return timestamp >= this.buildTimestamp;
};
markModuleAsErrored(error) {
this.meta = null;
this.error = error;
this.errors.push(this.error);
this._source = new RawSource("throw new Error(" + JSON.stringify(this.error.message) + ");");
}
NormalModule.prototype.size = function() {
return this._source ? this._source.size() : -1;
};
applyNoParseRule(rule, content) {
// must start with "rule" if rule is a string
if(typeof rule === "string") {
return content.indexOf(rule) === 0;
}
// we assume rule is a regexp
return rule.test(content);
}
NormalModule.prototype.updateHash = function(hash) {
if(this._source) {
// check if module should not be parsed
// returns "true" if the module should !not! be parsed
// returns "false" if the module !must! be parsed
shouldPreventParsing(noParseRule, request) {
// if no noParseRule exists, return false
// the module !must! be parsed.
if(!noParseRule) {
return false;
}
// we only have one rule to check
if(!Array.isArray(noParseRule)) {
// returns "true" if the module is !not! to be parsed
return this.applyNoParseRule(noParseRule, request);
}
for(let i = 0; i < noParseRule.length; i++) {
const rule = noParseRule[i];
// early exit on first truthy match
// this module is !not! to be parsed
if(this.applyNoParseRule(rule, request)) {
return true;
}
}
// no match found, so this module !should! be parsed
return false;
}
build(options, compilation, resolver, fs, callback) {
this.buildTimestamp = new Date().getTime();
this.built = true;
this._source = null;
this.error = null;
this.errors.length = 0;
this.warnings.length = 0;
this.meta = {};
return this.doBuild(options, compilation, resolver, fs, (err) => {
this.dependencies.length = 0;
this.variables.length = 0;
this.blocks.length = 0;
this._cachedSource = null;
// if we have an error mark module as failed and exit
if(err) {
this.markModuleAsErrored(err);
return callback();
}
// check if this module should !not! be parsed.
// if so, exit here;
const noParseRule = options.module && options.module.noParse;
if(this.shouldPreventParsing(noParseRule, this.request)) {
return callback();
}
try {
this.parser.parse(this._source.source(), {
current: this,
module: this,
compilation: compilation,
options: options
});
} catch(e) {
const source = this._source.source();
const error = new ModuleParseError(this, source, e);
this.markModuleAsErrored(error);
return callback();
}
return callback();
});
}
getHashDigest() {
const hash = crypto.createHash("md5");
this.updateHash(hash);
return hash.digest("hex");
}
sourceDependency(dependency, dependencyTemplates, source, outputOptions, requestShortener) {
const template = dependencyTemplates.get(dependency.constructor);
if(!template) throw new Error("No template for dependency: " + dependency.constructor.name);
template.apply(dependency, source, outputOptions, requestShortener, dependencyTemplates);
}
sourceVariables(variable, availableVars, dependencyTemplates, outputOptions, requestShortener) {
const name = variable.name;
const expr = variable.expressionSource(dependencyTemplates, outputOptions, requestShortener);
if(availableVars.some(v => v.name === name && v.expression.source() === expr.source())) {
return;
}
return {
name: name,
expression: expr
};
}
/*
* creates the start part of a IIFE around the module to inject a variable name
* (function(...){ <- this part
* }.call(...))
*/
variableInjectionFunctionWrapperStartCode(varNames) {
const args = varNames.join(", ");
return `/* WEBPACK VAR INJECTION */(function(${args}) {`;
}
contextArgument(block) {
if(this === block) {
return this.exportsArgument || "exports";
}
return "this";
}
/*
* creates the end part of a IIFE around the module to inject a variable name
* (function(...){
* }.call(...)) <- this part
*/
variableInjectionFunctionWrapperEndCode(varExpressions, block) {
const firstParam = this.contextArgument(block);
const furtherParams = varExpressions.map(e => e.source()).join(", ");
return `}.call(${firstParam}, ${furtherParams}))`;
}
splitVariablesInUniqueNamedChunks(vars) {
const startState = [
[]
];
return vars.reduce((chunks, variable) => {
const current = chunks[chunks.length - 1];
// check if variable with same name exists already
// if so create a new chunk of variables.
const variableNameAlreadyExists = current.some(v => v.name === variable.name);
if(variableNameAlreadyExists) {
// start new chunk with current variable
chunks.push([variable]);
} else {
// else add it to current chunk
current.push(variable);
}
return chunks;
}, startState);
}
sourceBlock(block, availableVars, dependencyTemplates, source, outputOptions, requestShortener) {
block.dependencies.forEach((dependency) => this.sourceDependency(
dependency, dependencyTemplates, source, outputOptions, requestShortener));
/**
* Get the variables of all blocks that we need to inject.
* These will contain the variable name and its expression.
* The name will be added as a paramter in a IIFE the expression as its value.
*/
const vars = block.variables.map((variable) => this.sourceVariables(
variable, availableVars, dependencyTemplates, outputOptions, requestShortener))
.filter(Boolean);
/**
* if we actually have variables
* this is important as how #splitVariablesInUniqueNamedChunks works
* it will always return an array in an array which would lead to a IIFE wrapper around
* a module if we do this with an empty vars array.
*/
if(vars.length > 0) {
/**
* Split all variables up into chunks of unique names.
* e.g. imagine you have the following variable names that need to be injected:
* [foo, bar, baz, foo, some, more]
* we can not inject "foo" twice, therefore we just make two IIFEs like so:
* (function(foo, bar, baz){
* (function(foo, some, more){
* ...
* }(...));
* }(...));
*
* "splitVariablesInUniqueNamedChunks" splits the variables shown above up to this:
* [[foo, bar, baz], [foo, some, more]]
*/
const injectionVariableChunks = this.splitVariablesInUniqueNamedChunks(vars);
// create all the beginnings of IIFEs
const functionWrapperStarts = injectionVariableChunks.map((variableChunk) => variableChunk.map(variable => variable.name))
.map(names => this.variableInjectionFunctionWrapperStartCode(names));
// and all the ends
const functionWrapperEnds = injectionVariableChunks.map((variableChunk) => variableChunk.map(variable => variable.expression))
.map(expressions => this.variableInjectionFunctionWrapperEndCode(expressions, block));
// join them to one big string
const varStartCode = functionWrapperStarts.join("");
// reverse the ends first before joining them, as the last added must be the inner most
const varEndCode = functionWrapperEnds.reverse().join("");
// if we have anything, add it to the source
if(varStartCode && varEndCode) {
const start = block.range ? block.range[0] : -10;
const end = block.range ? block.range[1] : (this._source.size() + 1);
source.insert(start + 0.5, varStartCode);
source.insert(end + 0.5, "\n/* WEBPACK VAR INJECTION */" + varEndCode);
}
}
block.blocks.forEach((block) => this.sourceBlock(
block, availableVars.concat(vars), dependencyTemplates, source, outputOptions, requestShortener));
}
source(dependencyTemplates, outputOptions, requestShortener) {
const hashDigest = this.getHashDigest();
if(this._cachedSource && this._cachedSource.hash === hashDigest) {
return this._cachedSource.source;
}
if(!this._source) {
return new RawSource("throw new Error('No source available');");
}
const source = new ReplaceSource(this._source);
this._cachedSource = {
source: source,
hash: hashDigest
};
this.sourceBlock(this, [], dependencyTemplates, source, outputOptions, requestShortener);
return new CachedSource(source);
}
getHighestTimestamp(keys, timestampsByKey) {
let highestTimestamp = 0;
for(let i = 0; i < keys.length; i++) {
const key = keys[i];
const timestamp = timestampsByKey[key];
// if there is no timestamp yet, early return with Infinity
if(!timestamp) return Infinity;
highestTimestamp = Math.max(highestTimestamp, timestamp);
}
return highestTimestamp;
}
needRebuild(fileTimestamps, contextTimestamps) {
const highestFileDepTimestamp = this.getHighestTimestamp(
this.fileDependencies, fileTimestamps);
// if the hightest is Infinity, we need a rebuild
// exit early here.
if(highestFileDepTimestamp === Infinity) {
return true;
}
const highestContextDepTimestamp = this.getHighestTimestamp(
this.contextDependencies, contextTimestamps);
// Again if the hightest is Infinity, we need a rebuild
// exit early here.
if(highestContextDepTimestamp === Infinity) {
return true;
}
// else take the highest of file and context timestamps and compare
// to last build timestamp
return Math.max(highestContextDepTimestamp, highestFileDepTimestamp) >= this.buildTimestamp;
}
size() {
return this._source ? this._source.size() : -1;
}
updateHashWithSource(hash) {
if(!this._source) {
hash.update("null");
return;
}
hash.update("source");
this._source.updateHash(hash);
} else
hash.update("null");
hash.update("meta");
hash.update(JSON.stringify(this.meta));
Module.prototype.updateHash.call(this, hash);
};
}
updateHashWithMeta(hash) {
hash.update("meta");
hash.update(JSON.stringify(this.meta));
}
updateHash(hash) {
this.updateHashWithSource(hash);
this.updateHashWithMeta(hash);
super.updateHash(hash);
}
}
module.exports = NormalModule;

407
test/NormalModule.test.js Normal file
View File

@ -0,0 +1,407 @@
/* globals describe, it, beforeEach, afterEach */
"use strict";
require("should");
const sinon = require("sinon");
const NormalModule = require("../lib/NormalModule");
const path = require("path");
const SourceMapSource = require("webpack-sources").SourceMapSource;
const OriginalSource = require("webpack-sources").OriginalSource;
const RawSource = require("webpack-sources").RawSource;
describe("NormalModule", function() {
let normalModule;
let request;
let userRequest;
let rawRequest;
let loaders;
let resource;
let parser;
beforeEach(function() {
request = "some/request";
userRequest = "some/userRequest";
rawRequest = "some/rawRequest";
loaders = [];
resource = "some/resource";
parser = {
parse() {}
};
normalModule = new NormalModule(
request,
userRequest,
rawRequest,
loaders,
resource,
parser
);
});
describe("#identifier", function() {
it("returns an identifier for this module", function() {
normalModule.identifier().should.eql(request);
});
});
describe("#readableIdentifier", function() {
it("calls the given requestShortener with the user request", function() {
const spy = sinon.spy();
normalModule.readableIdentifier({
shorten: spy
});
spy.callCount.should.eql(1);
spy.args[0][0].should.eql(userRequest);
});
});
describe("#libIdent", function() {
it("contextifies the userRequest of the module", function() {
normalModule.libIdent({
context: "some/context"
}).should.eql("../userRequest");
});
describe("given a userRequest containing loaders", function() {
beforeEach(function() {
userRequest = "some/userRequest!some/other/userRequest!some/thing/is/off/here";
normalModule = new NormalModule(
request,
userRequest,
rawRequest,
loaders,
resource,
parser
);
});
it("contextifies every path in the userRequest", function() {
normalModule.libIdent({
context: "some/context"
}).should.eql("../userRequest!../other/userRequest!../thing/is/off/here");
});
});
});
describe("#nameForCondition", function() {
it("return the resource", function() {
normalModule.nameForCondition().should.eql(resource);
});
describe("given a resource containing a ?-sign", function() {
const baseResource = "some/resource";
beforeEach(function() {
resource = baseResource + "?some=query";
normalModule = new NormalModule(
request,
userRequest,
rawRequest,
loaders,
resource,
parser
);
});
it("return only the part before the ?-sign", function() {
normalModule.nameForCondition().should.eql(baseResource);
});
});
});
describe("#createSourceForAsset", function() {
let name;
let content;
let sourceMap;
beforeEach(function() {
name = "some name";
content = "some content";
sourceMap = "some sourcemap";
});
describe("given no sourcemap", function() {
it("returns a RawSource", function() {
normalModule.createSourceForAsset(name, content).should.be.instanceOf(RawSource);
});
});
describe("given a string as the sourcemap", function() {
it("returns a OriginalSource", function() {
normalModule.createSourceForAsset(name, content, sourceMap).should.be.instanceOf(OriginalSource);
});
});
describe("given a some other kind of sourcemap", function() {
beforeEach(function() {
sourceMap = () => {};
});
it("returns a SourceMapSource", function() {
normalModule.createSourceForAsset(name, content, sourceMap).should.be.instanceOf(SourceMapSource);
});
});
});
describe("#source", function() {
describe("without the module having any source", function() {
beforeEach(function() {
normalModule._source = null;
});
it("returns a Source containing an Error", function() {
normalModule.source().should.be.instanceOf(RawSource);
normalModule.source().source().should.eql("throw new Error('No source available');");
});
});
});
describe("#updateHashWithSource", function() {
let hashSpy;
let hash;
beforeEach(function() {
hashSpy = sinon.spy();
hash = {
update: hashSpy
};
});
describe("without the module having any source", function() {
beforeEach(function() {
normalModule._source = null;
});
it("calls hash function with \"null\"", function() {
normalModule.updateHashWithSource(hash);
hashSpy.callCount.should.eql(1);
hashSpy.args[0][0].should.eql("null");
});
});
describe("without the module having source", function() {
let expectedSource = "some source";
beforeEach(function() {
normalModule._source = new RawSource(expectedSource);
});
it("calls hash function with \"source\" and then the actual source of the module", function() {
normalModule.updateHashWithSource(hash);
hashSpy.callCount.should.eql(2);
hashSpy.args[0][0].should.eql("source");
hashSpy.args[1][0].should.eql(expectedSource);
});
});
});
describe("#needRebuild", function() {
let fileTimestamps;
let contextTimestamps;
let fileDependencies;
let contextDependencies;
let fileA;
let fileB;
function setDeps(
fileDependencies,
contextDependencies) {
normalModule.fileDependencies = fileDependencies;
normalModule.contextDependencies = contextDependencies;
}
beforeEach(function() {
fileA = "fileA";
fileB = "fileB";
fileDependencies = [fileA, fileB];
contextDependencies = [fileA, fileB];
fileTimestamps = {
[fileA]: 1,
[fileB]: 1,
};
contextTimestamps = {
[fileA]: 1,
[fileB]: 1,
};
normalModule.buildTimestamp = 2;
setDeps(fileDependencies, contextDependencies);
});
describe("given all timestamps are older than the buildTimestamp", function() {
it("returns false", function() {
normalModule.needRebuild(fileTimestamps, contextTimestamps).should.eql(false);
});
});
describe("given a file timestamp is newer than the buildTimestamp", function() {
beforeEach(function() {
fileTimestamps[fileA] = 3;
});
it("returns true", function() {
normalModule.needRebuild(fileTimestamps, contextTimestamps).should.eql(true);
});
});
describe("given a no file timestamp exists", function() {
beforeEach(function() {
fileTimestamps = {};
});
it("returns true", function() {
normalModule.needRebuild(fileTimestamps, contextTimestamps).should.eql(true);
});
});
describe("given a context timestamp is newer than the buildTimestamp", function() {
beforeEach(function() {
contextTimestamps[fileA] = 3;
});
it("returns true", function() {
normalModule.needRebuild(fileTimestamps, contextTimestamps).should.eql(true);
});
});
describe("given a no context timestamp exists", function() {
beforeEach(function() {
contextTimestamps = {};
});
it("returns true", function() {
normalModule.needRebuild(fileTimestamps, contextTimestamps).should.eql(true);
});
});
});
describe("#splitVariablesInUniqueNamedChunks", function() {
let variables;
beforeEach(function() {
variables = [{
name: "foo"
}, {
name: "bar"
}, {
name: "baz"
}, {
name: "some"
}, {
name: "more"
}];
});
describe("given an empty array of vars", function() {
it("returns an empty array", function() {
normalModule.splitVariablesInUniqueNamedChunks([]).should.eql([
[]
]);
});
});
describe("given an array of distrinct variables", function() {
it("returns an array containing an array containing the variables", function() {
normalModule.splitVariablesInUniqueNamedChunks(variables).should.eql([variables]);
});
});
describe("given an array with duplicate variables", function() {
it("returns several arrays each containing only distinct variable names", function() {
normalModule.splitVariablesInUniqueNamedChunks(variables.concat(variables)).should.eql([variables, variables]);
});
describe("and a duplicate as the last variable", function() {
it("returns correctly split distinct arrays", function() {
normalModule.splitVariablesInUniqueNamedChunks(variables.concat(variables).concat(variables[0])).should.eql([variables, variables, [variables[0]]]);
});
});
});
});
describe("#applyNoParseRule", function() {
let rule;
let content;
describe("given a string as rule", function() {
beforeEach(function() {
rule = "some-rule";
});
describe("and the content starting with the string specified in rule", function() {
beforeEach(function() {
content = rule + "some-content";
});
it("returns true", function() {
normalModule.shouldPreventParsing(rule, content).should.eql(true);
});
});
describe("and the content does not start with the string specified in rule", function() {
beforeEach(function() {
content = "some-content";
});
it("returns false", function() {
normalModule.shouldPreventParsing(rule, content).should.eql(false);
});
});
});
describe("given a regex as rule", function() {
beforeEach(function() {
rule = /some-rule/;
});
describe("and the content matches the rule", function() {
beforeEach(function() {
content = rule + "some-content";
});
it("returns true", function() {
normalModule.shouldPreventParsing(rule, content).should.eql(true);
});
});
describe("and the content does not match the rule", function() {
beforeEach(function() {
content = "some-content";
});
it("returns false", function() {
normalModule.shouldPreventParsing(rule, content).should.eql(false);
});
});
});
});
describe("#shouldPreventParsing", function() {
let applyNoParseRuleSpy;
beforeEach(function() {
applyNoParseRuleSpy = sinon.stub();
normalModule.applyNoParseRule = applyNoParseRuleSpy;
});
describe("given no noParseRule", function() {
it("returns false", function() {
normalModule.shouldPreventParsing().should.eql(false);
applyNoParseRuleSpy.callCount.should.eql(0);
});
});
describe("given a noParseRule", function() {
let returnValOfSpy;
beforeEach(function() {
returnValOfSpy = true;
applyNoParseRuleSpy.returns(returnValOfSpy);
});
describe("that is a string", function() {
it("calls and returns whatever applyNoParseRule returns", function() {
normalModule.shouldPreventParsing("some rule").should.eql(returnValOfSpy);
applyNoParseRuleSpy.callCount.should.eql(1);
});
});
describe("that is a regex", function() {
it("calls and returns whatever applyNoParseRule returns", function() {
normalModule.shouldPreventParsing("some rule").should.eql(returnValOfSpy);
applyNoParseRuleSpy.callCount.should.eql(1);
});
});
describe("that is an array", function() {
describe("of strings and or regexs", function() {
let someRules;
beforeEach(function() {
someRules = [
"some rule",
/some rule1/,
"some rule2",
];
});
describe("and none of them match", function() {
beforeEach(function() {
returnValOfSpy = false;
applyNoParseRuleSpy.returns(returnValOfSpy);
});
it("returns false", function() {
normalModule.shouldPreventParsing(someRules).should.eql(returnValOfSpy);
applyNoParseRuleSpy.callCount.should.eql(3);
});
});
describe("and the first of them matches", function() {
beforeEach(function() {
returnValOfSpy = true;
applyNoParseRuleSpy.returns(returnValOfSpy);
});
it("returns true", function() {
normalModule.shouldPreventParsing(someRules).should.eql(returnValOfSpy);
applyNoParseRuleSpy.callCount.should.eql(1);
});
});
describe("and the last of them matches", function() {
beforeEach(function() {
returnValOfSpy = true;
applyNoParseRuleSpy.onCall(0).returns(false);
applyNoParseRuleSpy.onCall(1).returns(false);
applyNoParseRuleSpy.onCall(2).returns(true);
});
it("returns true", function() {
normalModule.shouldPreventParsing(someRules).should.eql(returnValOfSpy);
applyNoParseRuleSpy.callCount.should.eql(3);
});
});
});
});
});
});
});