/* 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 WebpackError = require("./WebpackError"); 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(context, request) { return request.split("!").map(function(r) { const splitPath = r.split("?"); splitPath[0] = path.relative(context, splitPath[0]); if(path.sep === "\\") splitPath[0] = splitPath[0].replace(/\\/g, "/"); if(splitPath[0].indexOf("../") !== 0) splitPath[0] = "./" + splitPath[0]; return splitPath.join("?"); }).join("!"); } class NonErrorEmittedError extends WebpackError { constructor(error) { super(); this.name = "NonErrorEmittedError"; this.message = "(Emitted value instead of an instance of Error) " + error; Error.captureStackTrace(this, this.constructor); } } const dependencyTemplatesHashMap = new WeakMap(); 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.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); } 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) => { if(!(warning instanceof Error)) warning = new NonErrorEmittedError(warning); this.warnings.push(new ModuleWarning(this, warning)); }, emitError: (error) => { if(!(error instanceof Error)) error = new NonErrorEmittedError(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, content) { // must start with "rule" if rule is a string if(typeof rule === "string") { return content.indexOf(rule) === 0; } if(typeof rule === "function") { return rule(content); } // we assume rule is a regexp return rule.test(content); } // 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 = Date.now(); 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(dependencyTemplates) { let dtHash = dependencyTemplatesHashMap.get("hash"); const hash = crypto.createHash("md5"); this.updateHash(hash); hash.update(`${dtHash}`); 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.reduce((result, value) => { const variable = this.sourceVariables( value, availableVars, dependencyTemplates, outputOptions, requestShortener); if(variable) { result.push(variable); } return result; }, []); /** * 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) => { return this.variableInjectionFunctionWrapperStartCode( variableChunk.map(variable => variable.name) ); }); // and all the ends const functionWrapperEnds = injectionVariableChunks.map((variableChunk) => { return this.variableInjectionFunctionWrapperEndCode( variableChunk.map(variable => variable.expression), 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(dependencyTemplates); 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); } originalSource() { return this._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;