/* MIT License http://www.opensource.org/licenses/mit-license.php Author Tobias Koppers @sokra */ "use strict"; const NativeModule = require("module"); const { CachedSource, LineToLineMappedSource, OriginalSource, RawSource, SourceMapSource } = require("webpack-sources"); const { getContext, runLoaders } = require("loader-runner"); 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 createHash = require("./util/createHash"); const contextify = require("./util/identifier").contextify; /** @typedef {import("./util/createHash").Hash} Hash */ const asString = buf => { if (Buffer.isBuffer(buf)) { return buf.toString("utf-8"); } return buf; }; const asBuffer = str => { if (!Buffer.isBuffer(str)) { return Buffer.from(str, "utf-8"); } return str; }; 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); } } /** * @typedef {Object} CachedSourceEntry * @property {TODO} source the generated source * @property {string} hash the hash value */ class NormalModule extends Module { constructor({ type, request, userRequest, rawRequest, loaders, resource, matchResource, parser, generator, resolveOptions }) { super(type, getContext(resource)); // Info from Factory this.request = request; this.userRequest = userRequest; this.rawRequest = rawRequest; this.binary = type.startsWith("webassembly"); this.parser = parser; this.generator = generator; this.resource = resource; this.matchResource = matchResource; this.loaders = loaders; if (resolveOptions !== undefined) this.resolveOptions = resolveOptions; // Info from Build this.error = null; this._source = null; this._sourceSize = null; this._buildHash = ""; this.buildTimestamp = undefined; /** @private @type {Map<string, CachedSourceEntry>} */ this._cachedSources = new Map(); // Options for the NormalModule set by plugins // TODO refactor this -> options object filled from Factory this.useSourceMap = false; this.lineToLine = false; // Cache this._lastSuccessfulBuildMeta = {}; } identifier() { return this.request; } readableIdentifier(requestShortener) { return requestShortener.shorten(this.userRequest); } libIdent(options) { return contextify(options.context, this.userRequest); } nameForCondition() { const resource = this.matchResource || this.resource; const idx = resource.indexOf("?"); if (idx >= 0) return resource.substr(0, idx); return resource; } updateCacheModule(module) { this.type = module.type; this.request = module.request; this.userRequest = module.userRequest; this.rawRequest = module.rawRequest; this.parser = module.parser; this.generator = module.generator; this.resource = module.resource; this.matchResource = module.matchResource; this.loaders = module.loaders; this.resolveOptions = module.resolveOptions; } 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 requestShortener = compilation.runtimeTemplate.requestShortener; const getCurrentLoaderName = () => { const currentLoader = this.getCurrentLoader(loaderContext); if (!currentLoader) return "(not in loader scope)"; return requestShortener.shorten(currentLoader.loader); }; const loaderContext = { version: 2, emitWarning: warning => { if (!(warning instanceof Error)) { warning = new NonErrorEmittedError(warning); } this.warnings.push( new ModuleWarning(this, warning, { from: getCurrentLoaderName() }) ); }, emitError: error => { if (!(error instanceof Error)) { error = new NonErrorEmittedError(error); } this.errors.push( new ModuleError(this, error, { from: getCurrentLoaderName() }) ); }, getLogger: name => { const currentLoader = this.getCurrentLoader(loaderContext); return compilation.getLogger(() => [currentLoader && currentLoader.loader, name, this.identifier()] .filter(Boolean) .join("|") ); }, // TODO remove in webpack 5 exec: (code, filename) => { // @ts-ignore Argument of type 'this' is not assignable to parameter of type 'Module'. const module = new NativeModule(filename, this); // @ts-ignore _nodeModulePaths is deprecated and undocumented Node.js API 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); }, getResolve(options) { const child = options ? resolver.withOptions(options) : resolver; return (context, request, callback) => { if (callback) { child.resolve({}, context, request, {}, callback); } else { return new Promise((resolve, reject) => { child.resolve({}, context, request, {}, (err, result) => { if (err) reject(err); else resolve(result); }); }); } }; }, emitFile: (name, content, sourceMap, assetInfo) => { if (!this.buildInfo.assets) { this.buildInfo.assets = Object.create(null); this.buildInfo.assetsInfo = new Map(); } this.buildInfo.assets[name] = this.createSourceForAsset( name, content, sourceMap ); this.buildInfo.assetsInfo.set(name, assetInfo); }, rootContext: options.context, webpack: true, sourceMap: !!this.useSourceMap, mode: options.mode || "production", _module: this, _compilation: compilation, _compiler: compilation.compiler, fs: fs }; compilation.hooks.normalModuleLoader.call(loaderContext, this); if (options.loader) { Object.assign(loaderContext, options.loader); } return loaderContext; } getCurrentLoader(loaderContext, index = loaderContext.loaderIndex) { if ( this.loaders && this.loaders.length && index < this.loaders.length && index >= 0 && this.loaders[index] ) { return this.loaders[index]; } return null; } 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); } if (Buffer.isBuffer(source)) { // @ts-ignore // TODO We need to fix @types/webpack-sources to allow RawSource to take a Buffer | string return new RawSource(source); } return new OriginalSource(source, identifier); } doBuild(options, compilation, resolver, fs, callback) { 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.buildInfo.cacheable = result.cacheable; this.buildInfo.fileDependencies = new Set(result.fileDependencies); this.buildInfo.contextDependencies = new Set( result.contextDependencies ); } if (err) { if (!(err instanceof Error)) { err = new NonErrorEmittedError(err); } const currentLoader = this.getCurrentLoader(loaderContext); const error = new ModuleBuildError(this, err, { from: currentLoader && compilation.runtimeTemplate.requestShortener.shorten( currentLoader.loader ) }); return callback(error); } const resourceBuffer = result.resourceBuffer; const source = result.result[0]; const sourceMap = result.result.length >= 1 ? result.result[1] : null; const extraInfo = result.result.length >= 2 ? result.result[2] : null; if (!Buffer.isBuffer(source) && typeof source !== "string") { const currentLoader = this.getCurrentLoader(loaderContext, 0); const err = new Error( `Final loader (${ currentLoader ? compilation.runtimeTemplate.requestShortener.shorten( currentLoader.loader ) : "unknown" }) didn't return a Buffer or String` ); const error = new ModuleBuildError(this, err); return callback(error); } this._source = this.createSource( this.binary ? asBuffer(source) : asString(source), resourceBuffer, sourceMap ); this._sourceSize = null; this._ast = typeof extraInfo === "object" && extraInfo !== null && extraInfo.webpackAST !== undefined ? extraInfo.webpackAST : null; return callback(); } ); } markModuleAsErrored(error) { // Restore build meta from successful build to keep importing state this.buildMeta = Object.assign({}, this._lastSuccessfulBuildMeta); this.error = error; this.errors.push(this.error); this._source = new RawSource( "throw new Error(" + JSON.stringify(this.error.message) + ");" ); this._sourceSize = null; this._ast = null; } 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; } _initBuildHash(compilation) { const hash = createHash(compilation.outputOptions.hashFunction); if (this._source) { hash.update("source"); this._source.updateHash(hash); } hash.update("meta"); hash.update(JSON.stringify(this.buildMeta)); this._buildHash = /** @type {string} */ (hash.digest("hex")); } build(options, compilation, resolver, fs, callback) { this.buildTimestamp = Date.now(); this.built = true; this._source = null; this._sourceSize = null; this._ast = null; this._buildHash = ""; this.error = null; this.errors.length = 0; this.warnings.length = 0; this.buildMeta = {}; this.buildInfo = { cacheable: false, fileDependencies: new Set(), contextDependencies: new Set(), assets: undefined, assetsInfo: undefined }; return this.doBuild(options, compilation, resolver, fs, err => { this._cachedSources.clear(); // if we have an error mark module as failed and exit if (err) { this.markModuleAsErrored(err); this._initBuildHash(compilation); 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)) { this._initBuildHash(compilation); return callback(); } const handleParseError = e => { const source = this._source.source(); const loaders = this.loaders.map(item => contextify(options.context, item.loader) ); const error = new ModuleParseError(this, source, e, loaders); this.markModuleAsErrored(error); this._initBuildHash(compilation); return callback(); }; const handleParseResult = result => { this._lastSuccessfulBuildMeta = this.buildMeta; this._initBuildHash(compilation); return callback(); }; try { const result = this.parser.parse( this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) { handleParseError(err); } else { handleParseResult(result); } } ); if (result !== undefined) { // parse is sync handleParseResult(result); } } catch (e) { handleParseError(e); } }); } getHashDigest(dependencyTemplates) { // TODO webpack 5 refactor let dtHash = dependencyTemplates.get("hash"); return `${this.hash}-${dtHash}`; } source(dependencyTemplates, runtimeTemplate, type = "javascript") { const hashDigest = this.getHashDigest(dependencyTemplates); const cacheEntry = this._cachedSources.get(type); if (cacheEntry !== undefined && cacheEntry.hash === hashDigest) { // We can reuse the cached source return cacheEntry.source; } const source = this.generator.generate( this, dependencyTemplates, runtimeTemplate, type ); const cachedSource = new CachedSource(source); this._cachedSources.set(type, { source: cachedSource, hash: hashDigest }); return cachedSource; } originalSource() { return this._source; } needRebuild(fileTimestamps, contextTimestamps) { // always try to rebuild in case of an error if (this.error) return true; // always rebuild when module is not cacheable if (!this.buildInfo.cacheable) return true; // Check timestamps of all dependencies // Missing timestamp -> need rebuild // Timestamp bigger than buildTimestamp -> need rebuild for (const file of this.buildInfo.fileDependencies) { const timestamp = fileTimestamps.get(file); if (!timestamp) return true; if (timestamp >= this.buildTimestamp) return true; } for (const file of this.buildInfo.contextDependencies) { const timestamp = contextTimestamps.get(file); if (!timestamp) return true; if (timestamp >= this.buildTimestamp) return true; } // elsewise -> no rebuild needed return false; } size() { if (this._sourceSize === null) { this._sourceSize = this._source ? this._source.size() : -1; } return this._sourceSize; } /** * @param {Hash} hash the hash used to track dependencies * @returns {void} */ updateHash(hash) { hash.update(this._buildHash); super.updateHash(hash); } } module.exports = NormalModule;