|
|
/* 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;
|