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