UNPKG

15.1 kBJavaScriptView Raw
1/*
2 MIT License http://www.opensource.org/licenses/mit-license.php
3 Author Tobias Koppers @sokra
4*/
5"use strict";
6
7const NativeModule = require("module");
8
9const {
10 CachedSource,
11 LineToLineMappedSource,
12 OriginalSource,
13 RawSource,
14 SourceMapSource
15} = require("webpack-sources");
16const { getContext, runLoaders } = require("loader-runner");
17
18const WebpackError = require("./WebpackError");
19const Module = require("./Module");
20const ModuleParseError = require("./ModuleParseError");
21const ModuleBuildError = require("./ModuleBuildError");
22const ModuleError = require("./ModuleError");
23const ModuleWarning = require("./ModuleWarning");
24const createHash = require("./util/createHash");
25const contextify = require("./util/identifier").contextify;
26
27/** @typedef {import("./util/createHash").Hash} Hash */
28
29const asString = buf => {
30 if (Buffer.isBuffer(buf)) {
31 return buf.toString("utf-8");
32 }
33 return buf;
34};
35
36const asBuffer = str => {
37 if (!Buffer.isBuffer(str)) {
38 return Buffer.from(str, "utf-8");
39 }
40 return str;
41};
42
43class NonErrorEmittedError extends WebpackError {
44 constructor(error) {
45 super();
46
47 this.name = "NonErrorEmittedError";
48 this.message = "(Emitted value instead of an instance of Error) " + error;
49
50 Error.captureStackTrace(this, this.constructor);
51 }
52}
53
54/**
55 * @typedef {Object} CachedSourceEntry
56 * @property {TODO} source the generated source
57 * @property {string} hash the hash value
58 */
59
60class NormalModule extends Module {
61 constructor({
62 type,
63 request,
64 userRequest,
65 rawRequest,
66 loaders,
67 resource,
68 matchResource,
69 parser,
70 generator,
71 resolveOptions
72 }) {
73 super(type, getContext(resource));
74
75 // Info from Factory
76 this.request = request;
77 this.userRequest = userRequest;
78 this.rawRequest = rawRequest;
79 this.binary = type.startsWith("webassembly");
80 this.parser = parser;
81 this.generator = generator;
82 this.resource = resource;
83 this.matchResource = matchResource;
84 this.loaders = loaders;
85 if (resolveOptions !== undefined) this.resolveOptions = resolveOptions;
86
87 // Info from Build
88 this.error = null;
89 this._source = null;
90 this._buildHash = "";
91 this.buildTimestamp = undefined;
92 /** @private @type {Map<string, CachedSourceEntry>} */
93 this._cachedSources = new Map();
94
95 // Options for the NormalModule set by plugins
96 // TODO refactor this -> options object filled from Factory
97 this.useSourceMap = false;
98 this.lineToLine = false;
99
100 // Cache
101 this._lastSuccessfulBuildMeta = {};
102 }
103
104 identifier() {
105 return this.request;
106 }
107
108 readableIdentifier(requestShortener) {
109 return requestShortener.shorten(this.userRequest);
110 }
111
112 libIdent(options) {
113 return contextify(options.context, this.userRequest);
114 }
115
116 nameForCondition() {
117 const resource = this.matchResource || this.resource;
118 const idx = resource.indexOf("?");
119 if (idx >= 0) return resource.substr(0, idx);
120 return resource;
121 }
122
123 updateCacheModule(module) {
124 this.type = module.type;
125 this.request = module.request;
126 this.userRequest = module.userRequest;
127 this.rawRequest = module.rawRequest;
128 this.parser = module.parser;
129 this.generator = module.generator;
130 this.resource = module.resource;
131 this.matchResource = module.matchResource;
132 this.loaders = module.loaders;
133 this.resolveOptions = module.resolveOptions;
134 }
135
136 createSourceForAsset(name, content, sourceMap) {
137 if (!sourceMap) {
138 return new RawSource(content);
139 }
140
141 if (typeof sourceMap === "string") {
142 return new OriginalSource(content, sourceMap);
143 }
144
145 return new SourceMapSource(content, name, sourceMap);
146 }
147
148 createLoaderContext(resolver, options, compilation, fs) {
149 const requestShortener = compilation.runtimeTemplate.requestShortener;
150 const getCurrentLoaderName = () => {
151 const currentLoader = this.getCurrentLoader(loaderContext);
152 if (!currentLoader) return "(not in loader scope)";
153 return requestShortener.shorten(currentLoader.loader);
154 };
155 const loaderContext = {
156 version: 2,
157 emitWarning: warning => {
158 if (!(warning instanceof Error)) {
159 warning = new NonErrorEmittedError(warning);
160 }
161 this.warnings.push(
162 new ModuleWarning(this, warning, {
163 from: getCurrentLoaderName()
164 })
165 );
166 },
167 emitError: error => {
168 if (!(error instanceof Error)) {
169 error = new NonErrorEmittedError(error);
170 }
171 this.errors.push(
172 new ModuleError(this, error, {
173 from: getCurrentLoaderName()
174 })
175 );
176 },
177 getLogger: name => {
178 const currentLoader = this.getCurrentLoader(loaderContext);
179 return compilation.getLogger(() =>
180 [currentLoader && currentLoader.loader, name, this.identifier()]
181 .filter(Boolean)
182 .join("|")
183 );
184 },
185 // TODO remove in webpack 5
186 exec: (code, filename) => {
187 // @ts-ignore Argument of type 'this' is not assignable to parameter of type 'Module'.
188 const module = new NativeModule(filename, this);
189 // @ts-ignore _nodeModulePaths is deprecated and undocumented Node.js API
190 module.paths = NativeModule._nodeModulePaths(this.context);
191 module.filename = filename;
192 module._compile(code, filename);
193 return module.exports;
194 },
195 resolve(context, request, callback) {
196 resolver.resolve({}, context, request, {}, callback);
197 },
198 getResolve(options) {
199 const child = options ? resolver.withOptions(options) : resolver;
200 return (context, request, callback) => {
201 if (callback) {
202 child.resolve({}, context, request, {}, callback);
203 } else {
204 return new Promise((resolve, reject) => {
205 child.resolve({}, context, request, {}, (err, result) => {
206 if (err) reject(err);
207 else resolve(result);
208 });
209 });
210 }
211 };
212 },
213 emitFile: (name, content, sourceMap, assetInfo) => {
214 if (!this.buildInfo.assets) {
215 this.buildInfo.assets = Object.create(null);
216 this.buildInfo.assetsInfo = new Map();
217 }
218 this.buildInfo.assets[name] = this.createSourceForAsset(
219 name,
220 content,
221 sourceMap
222 );
223 this.buildInfo.assetsInfo.set(name, assetInfo);
224 },
225 rootContext: options.context,
226 webpack: true,
227 sourceMap: !!this.useSourceMap,
228 mode: options.mode || "production",
229 _module: this,
230 _compilation: compilation,
231 _compiler: compilation.compiler,
232 fs: fs
233 };
234
235 compilation.hooks.normalModuleLoader.call(loaderContext, this);
236 if (options.loader) {
237 Object.assign(loaderContext, options.loader);
238 }
239
240 return loaderContext;
241 }
242
243 getCurrentLoader(loaderContext, index = loaderContext.loaderIndex) {
244 if (
245 this.loaders &&
246 this.loaders.length &&
247 index < this.loaders.length &&
248 index >= 0 &&
249 this.loaders[index]
250 ) {
251 return this.loaders[index];
252 }
253 return null;
254 }
255
256 createSource(source, resourceBuffer, sourceMap) {
257 // if there is no identifier return raw source
258 if (!this.identifier) {
259 return new RawSource(source);
260 }
261
262 // from here on we assume we have an identifier
263 const identifier = this.identifier();
264
265 if (this.lineToLine && resourceBuffer) {
266 return new LineToLineMappedSource(
267 source,
268 identifier,
269 asString(resourceBuffer)
270 );
271 }
272
273 if (this.useSourceMap && sourceMap) {
274 return new SourceMapSource(source, identifier, sourceMap);
275 }
276
277 if (Buffer.isBuffer(source)) {
278 // @ts-ignore
279 // TODO We need to fix @types/webpack-sources to allow RawSource to take a Buffer | string
280 return new RawSource(source);
281 }
282
283 return new OriginalSource(source, identifier);
284 }
285
286 doBuild(options, compilation, resolver, fs, callback) {
287 const loaderContext = this.createLoaderContext(
288 resolver,
289 options,
290 compilation,
291 fs
292 );
293
294 runLoaders(
295 {
296 resource: this.resource,
297 loaders: this.loaders,
298 context: loaderContext,
299 readResource: fs.readFile.bind(fs)
300 },
301 (err, result) => {
302 if (result) {
303 this.buildInfo.cacheable = result.cacheable;
304 this.buildInfo.fileDependencies = new Set(result.fileDependencies);
305 this.buildInfo.contextDependencies = new Set(
306 result.contextDependencies
307 );
308 }
309
310 if (err) {
311 if (!(err instanceof Error)) {
312 err = new NonErrorEmittedError(err);
313 }
314 const currentLoader = this.getCurrentLoader(loaderContext);
315 const error = new ModuleBuildError(this, err, {
316 from:
317 currentLoader &&
318 compilation.runtimeTemplate.requestShortener.shorten(
319 currentLoader.loader
320 )
321 });
322 return callback(error);
323 }
324
325 const resourceBuffer = result.resourceBuffer;
326 const source = result.result[0];
327 const sourceMap = result.result.length >= 1 ? result.result[1] : null;
328 const extraInfo = result.result.length >= 2 ? result.result[2] : null;
329
330 if (!Buffer.isBuffer(source) && typeof source !== "string") {
331 const currentLoader = this.getCurrentLoader(loaderContext, 0);
332 const err = new Error(
333 `Final loader (${
334 currentLoader
335 ? compilation.runtimeTemplate.requestShortener.shorten(
336 currentLoader.loader
337 )
338 : "unknown"
339 }) didn't return a Buffer or String`
340 );
341 const error = new ModuleBuildError(this, err);
342 return callback(error);
343 }
344
345 this._source = this.createSource(
346 this.binary ? asBuffer(source) : asString(source),
347 resourceBuffer,
348 sourceMap
349 );
350 this._ast =
351 typeof extraInfo === "object" &&
352 extraInfo !== null &&
353 extraInfo.webpackAST !== undefined
354 ? extraInfo.webpackAST
355 : null;
356 return callback();
357 }
358 );
359 }
360
361 markModuleAsErrored(error) {
362 // Restore build meta from successful build to keep importing state
363 this.buildMeta = Object.assign({}, this._lastSuccessfulBuildMeta);
364 this.error = error;
365 this.errors.push(this.error);
366 this._source = new RawSource(
367 "throw new Error(" + JSON.stringify(this.error.message) + ");"
368 );
369 this._ast = null;
370 }
371
372 applyNoParseRule(rule, content) {
373 // must start with "rule" if rule is a string
374 if (typeof rule === "string") {
375 return content.indexOf(rule) === 0;
376 }
377
378 if (typeof rule === "function") {
379 return rule(content);
380 }
381 // we assume rule is a regexp
382 return rule.test(content);
383 }
384
385 // check if module should not be parsed
386 // returns "true" if the module should !not! be parsed
387 // returns "false" if the module !must! be parsed
388 shouldPreventParsing(noParseRule, request) {
389 // if no noParseRule exists, return false
390 // the module !must! be parsed.
391 if (!noParseRule) {
392 return false;
393 }
394
395 // we only have one rule to check
396 if (!Array.isArray(noParseRule)) {
397 // returns "true" if the module is !not! to be parsed
398 return this.applyNoParseRule(noParseRule, request);
399 }
400
401 for (let i = 0; i < noParseRule.length; i++) {
402 const rule = noParseRule[i];
403 // early exit on first truthy match
404 // this module is !not! to be parsed
405 if (this.applyNoParseRule(rule, request)) {
406 return true;
407 }
408 }
409 // no match found, so this module !should! be parsed
410 return false;
411 }
412
413 _initBuildHash(compilation) {
414 const hash = createHash(compilation.outputOptions.hashFunction);
415 if (this._source) {
416 hash.update("source");
417 this._source.updateHash(hash);
418 }
419 hash.update("meta");
420 hash.update(JSON.stringify(this.buildMeta));
421 this._buildHash = /** @type {string} */ (hash.digest("hex"));
422 }
423
424 build(options, compilation, resolver, fs, callback) {
425 this.buildTimestamp = Date.now();
426 this.built = true;
427 this._source = null;
428 this._ast = null;
429 this._buildHash = "";
430 this.error = null;
431 this.errors.length = 0;
432 this.warnings.length = 0;
433 this.buildMeta = {};
434 this.buildInfo = {
435 cacheable: false,
436 fileDependencies: new Set(),
437 contextDependencies: new Set(),
438 assets: undefined,
439 assetsInfo: undefined
440 };
441
442 return this.doBuild(options, compilation, resolver, fs, err => {
443 this._cachedSources.clear();
444
445 // if we have an error mark module as failed and exit
446 if (err) {
447 this.markModuleAsErrored(err);
448 this._initBuildHash(compilation);
449 return callback();
450 }
451
452 // check if this module should !not! be parsed.
453 // if so, exit here;
454 const noParseRule = options.module && options.module.noParse;
455 if (this.shouldPreventParsing(noParseRule, this.request)) {
456 this._initBuildHash(compilation);
457 return callback();
458 }
459
460 const handleParseError = e => {
461 const source = this._source.source();
462 const loaders = this.loaders.map(item =>
463 contextify(options.context, item.loader)
464 );
465 const error = new ModuleParseError(this, source, e, loaders);
466 this.markModuleAsErrored(error);
467 this._initBuildHash(compilation);
468 return callback();
469 };
470
471 const handleParseResult = result => {
472 this._lastSuccessfulBuildMeta = this.buildMeta;
473 this._initBuildHash(compilation);
474 return callback();
475 };
476
477 try {
478 const result = this.parser.parse(
479 this._ast || this._source.source(),
480 {
481 current: this,
482 module: this,
483 compilation: compilation,
484 options: options
485 },
486 (err, result) => {
487 if (err) {
488 handleParseError(err);
489 } else {
490 handleParseResult(result);
491 }
492 }
493 );
494 if (result !== undefined) {
495 // parse is sync
496 handleParseResult(result);
497 }
498 } catch (e) {
499 handleParseError(e);
500 }
501 });
502 }
503
504 getHashDigest(dependencyTemplates) {
505 // TODO webpack 5 refactor
506 let dtHash = dependencyTemplates.get("hash");
507 return `${this.hash}-${dtHash}`;
508 }
509
510 source(dependencyTemplates, runtimeTemplate, type = "javascript") {
511 const hashDigest = this.getHashDigest(dependencyTemplates);
512 const cacheEntry = this._cachedSources.get(type);
513 if (cacheEntry !== undefined && cacheEntry.hash === hashDigest) {
514 // We can reuse the cached source
515 return cacheEntry.source;
516 }
517
518 const source = this.generator.generate(
519 this,
520 dependencyTemplates,
521 runtimeTemplate,
522 type
523 );
524
525 const cachedSource = new CachedSource(source);
526 this._cachedSources.set(type, {
527 source: cachedSource,
528 hash: hashDigest
529 });
530 return cachedSource;
531 }
532
533 originalSource() {
534 return this._source;
535 }
536
537 needRebuild(fileTimestamps, contextTimestamps) {
538 // always try to rebuild in case of an error
539 if (this.error) return true;
540
541 // always rebuild when module is not cacheable
542 if (!this.buildInfo.cacheable) return true;
543
544 // Check timestamps of all dependencies
545 // Missing timestamp -> need rebuild
546 // Timestamp bigger than buildTimestamp -> need rebuild
547 for (const file of this.buildInfo.fileDependencies) {
548 const timestamp = fileTimestamps.get(file);
549 if (!timestamp) return true;
550 if (timestamp >= this.buildTimestamp) return true;
551 }
552 for (const file of this.buildInfo.contextDependencies) {
553 const timestamp = contextTimestamps.get(file);
554 if (!timestamp) return true;
555 if (timestamp >= this.buildTimestamp) return true;
556 }
557 // elsewise -> no rebuild needed
558 return false;
559 }
560
561 size() {
562 return this._source ? this._source.size() : -1;
563 }
564
565 /**
566 * @param {Hash} hash the hash used to track dependencies
567 * @returns {void}
568 */
569 updateHash(hash) {
570 hash.update(this._buildHash);
571 super.updateHash(hash);
572 }
573}
574
575module.exports = NormalModule;