webpack/lib/NormalModule.js

467 lines
14 KiB
JavaScript

/*
MIT License http://www.opensource.org/licenses/mit-license.php
Author Tobias Koppers @sokra
*/
"use strict";
const path = require("path");
const NativeModule = require("module");
const crypto = require("crypto");
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)) {
return buf.toString("utf-8");
}
return buf;
}
function contextify(options, request) {
return request.split("!").map(function(r) {
let rp = path.relative(options.context, r);
if(path.sep === "\\")
rp = rp.replace(/\\/g, "/");
if(rp.indexOf("../") !== 0)
rp = "./" + rp;
return rp;
}).join("!");
}
class NormalModule extends Module {
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, 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);
}
if(typeof sourceMap === "string") {
return new OriginalSource(content, sourceMap);
}
return new SourceMapSource(content, name, sourceMap);
}
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);
}
// from here on we assume we have an identifier
const identifier = this.identifier();
if(this.lineToLine && resourceBuffer) {
return new LineToLineMappedSource(
source, identifier, asString(resourceBuffer));
}
if(this.useSourceMap && sourceMap) {
return new SourceMapSource(source, identifier, sourceMap);
}
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;
}
if(err) {
const error = new ModuleBuildError(this, err);
return callback(error);
}
const resourceBuffer = result.resourceBuffer;
const source = result.result[0];
const sourceMap = result.result[1];
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);
}
this._source = this.createSource(asString(source), resourceBuffer, sourceMap);
return callback();
});
}
disconnect() {
this.built = false;
super.disconnect();
}
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) + ");");
}
applyNoParseRule(rule, request) {
// must start with "rule" if rule is a string
if(typeof rule === "string") {
return request.indexOf(rule) === 0;
}
// we assume rule is a regexp
return rule.test(request);
}
// check if module should not be parsed
// returns "true" if the module should !not! be parsed
// returns "false" if the module !must! be parsed
preventParsing(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.preventParsing(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
};
}
variableInjectionFunctionWrapperStartCode(varNames) {
const openingIIFEParanthesis = "(";
const args = varNames.join(", ");
return `/* WEBPACK VAR INJECTION */${openingIIFEParanthesis}function(${args}) {`;
}
contextArgument(block) {
if(this === block) {
return this.exportsArgument || "exports";
}
return "this";
}
variableInjectionFunctionWrapperEndCode(varExpressions, block) {
const firstParam = this.contextArgument(block);
const furtherParams = varExpressions.map(e => e.source()).join(", ");
const closingIIFEParanthesis = ")";
return `}.call(${firstParam}, ${furtherParams})${closingIIFEParanthesis}`;
}
sourceBlock(block, availableVars, dependencyTemplates, source, outputOptions, requestShortener) {
block.dependencies.forEach((dependency) => this.sourceDependency(
dependency, dependencyTemplates, source, outputOptions, requestShortener));
const vars = block.variables.map((variable) => this.sourceVariables(
variable, availableVars, dependencyTemplates, outputOptions, requestShortener))
.filter(Boolean);
if(vars.length > 0) {
const injectionResult = vars.reduce((injections, variable) => {
if(injections.varNames.indexOf(variable.name) >= 0) {
const functionWrapperStart = this.variableInjectionFunctionWrapperStartCode(
injections.varNames);
const functionWrapperEnd = this.variableInjectionFunctionWrapperEndCode(
injections.varExpressions, block);
// add IIFE to existing injection wrapper IIFEs
injections.varStartCode = injections.varStartCode + functionWrapperStart;
injections.varEndCode = functionWrapperEnd + injections.varEndCode;
// reset varNames and expressions
injections.varNames = [];
injections.varExpressions = [];
}
injections.varNames.push(variable.name);
injections.varExpressions.push(variable.expression);
return injections;
}, {
varNames: [],
varExpressions: [],
varStartCode: "",
varEndCode: "",
});
if(injectionResult.varNames.length !== 0) {
const functionWrapperStart = this.variableInjectionFunctionWrapperStartCode(
injectionResult.varNames);
const functionWrapperEnd = this.variableInjectionFunctionWrapperEndCode(
injectionResult.varExpressions, block);
// add IIFE to existing injection wrapper IIFEs
injectionResult.varStartCode = injectionResult.varStartCode + functionWrapperStart;
injectionResult.varEndCode = functionWrapperEnd + injectionResult.varEndCode;
}
const varStartCode = injectionResult.varStartCode;
const varEndCode = injectionResult.varEndCode;
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
};
const topLevelBlock = this;
this.sourceBlock(this, [], dependencyTemplates, source, outputOptions, requestShortener, topLevelBlock);
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);
}
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;