UNPKG

14 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 loaderContext = {
151 version: 2,
152 emitWarning: warning => {
153 if (!(warning instanceof Error)) {
154 warning = new NonErrorEmittedError(warning);
155 }
156 const currentLoader = this.getCurrentLoader(loaderContext);
157 this.warnings.push(
158 new ModuleWarning(this, warning, {
159 from: requestShortener.shorten(currentLoader.loader)
160 })
161 );
162 },
163 emitError: error => {
164 if (!(error instanceof Error)) {
165 error = new NonErrorEmittedError(error);
166 }
167 const currentLoader = this.getCurrentLoader(loaderContext);
168 this.errors.push(
169 new ModuleError(this, error, {
170 from: requestShortener.shorten(currentLoader.loader)
171 })
172 );
173 },
174 // TODO remove in webpack 5
175 exec: (code, filename) => {
176 // @ts-ignore Argument of type 'this' is not assignable to parameter of type 'Module'.
177 const module = new NativeModule(filename, this);
178 // @ts-ignore _nodeModulePaths is deprecated and undocumented Node.js API
179 module.paths = NativeModule._nodeModulePaths(this.context);
180 module.filename = filename;
181 module._compile(code, filename);
182 return module.exports;
183 },
184 resolve(context, request, callback) {
185 resolver.resolve({}, context, request, {}, callback);
186 },
187 emitFile: (name, content, sourceMap) => {
188 if (!this.buildInfo.assets) {
189 this.buildInfo.assets = Object.create(null);
190 }
191 this.buildInfo.assets[name] = this.createSourceForAsset(
192 name,
193 content,
194 sourceMap
195 );
196 },
197 rootContext: options.context,
198 webpack: true,
199 sourceMap: !!this.useSourceMap,
200 _module: this,
201 _compilation: compilation,
202 _compiler: compilation.compiler,
203 fs: fs
204 };
205
206 compilation.hooks.normalModuleLoader.call(loaderContext, this);
207 if (options.loader) {
208 Object.assign(loaderContext, options.loader);
209 }
210
211 return loaderContext;
212 }
213
214 getCurrentLoader(loaderContext, index = loaderContext.loaderIndex) {
215 if (
216 this.loaders &&
217 this.loaders.length &&
218 index < this.loaders.length &&
219 index >= 0 &&
220 this.loaders[index]
221 ) {
222 return this.loaders[index];
223 }
224 return null;
225 }
226
227 createSource(source, resourceBuffer, sourceMap) {
228 // if there is no identifier return raw source
229 if (!this.identifier) {
230 return new RawSource(source);
231 }
232
233 // from here on we assume we have an identifier
234 const identifier = this.identifier();
235
236 if (this.lineToLine && resourceBuffer) {
237 return new LineToLineMappedSource(
238 source,
239 identifier,
240 asString(resourceBuffer)
241 );
242 }
243
244 if (this.useSourceMap && sourceMap) {
245 return new SourceMapSource(source, identifier, sourceMap);
246 }
247
248 if (Buffer.isBuffer(source)) {
249 // @ts-ignore
250 // TODO We need to fix @types/webpack-sources to allow RawSource to take a Buffer | string
251 return new RawSource(source);
252 }
253
254 return new OriginalSource(source, identifier);
255 }
256
257 doBuild(options, compilation, resolver, fs, callback) {
258 const loaderContext = this.createLoaderContext(
259 resolver,
260 options,
261 compilation,
262 fs
263 );
264
265 runLoaders(
266 {
267 resource: this.resource,
268 loaders: this.loaders,
269 context: loaderContext,
270 readResource: fs.readFile.bind(fs)
271 },
272 (err, result) => {
273 if (result) {
274 this.buildInfo.cacheable = result.cacheable;
275 this.buildInfo.fileDependencies = new Set(result.fileDependencies);
276 this.buildInfo.contextDependencies = new Set(
277 result.contextDependencies
278 );
279 }
280
281 if (err) {
282 if (!(err instanceof Error)) {
283 err = new NonErrorEmittedError(err);
284 }
285 const currentLoader = this.getCurrentLoader(loaderContext);
286 const error = new ModuleBuildError(this, err, {
287 from:
288 currentLoader &&
289 compilation.runtimeTemplate.requestShortener.shorten(
290 currentLoader.loader
291 )
292 });
293 return callback(error);
294 }
295
296 const resourceBuffer = result.resourceBuffer;
297 const source = result.result[0];
298 const sourceMap = result.result.length >= 1 ? result.result[1] : null;
299 const extraInfo = result.result.length >= 2 ? result.result[2] : null;
300
301 if (!Buffer.isBuffer(source) && typeof source !== "string") {
302 const currentLoader = this.getCurrentLoader(loaderContext, 0);
303 const err = new Error(
304 `Final loader (${
305 currentLoader
306 ? compilation.runtimeTemplate.requestShortener.shorten(
307 currentLoader.loader
308 )
309 : "unknown"
310 }) didn't return a Buffer or String`
311 );
312 const error = new ModuleBuildError(this, err);
313 return callback(error);
314 }
315
316 this._source = this.createSource(
317 this.binary ? asBuffer(source) : asString(source),
318 resourceBuffer,
319 sourceMap
320 );
321 this._ast =
322 typeof extraInfo === "object" &&
323 extraInfo !== null &&
324 extraInfo.webpackAST !== undefined
325 ? extraInfo.webpackAST
326 : null;
327 return callback();
328 }
329 );
330 }
331
332 markModuleAsErrored(error) {
333 // Restore build meta from successful build to keep importing state
334 this.buildMeta = Object.assign({}, this._lastSuccessfulBuildMeta);
335
336 this.error = error;
337 this.errors.push(this.error);
338 this._source = new RawSource(
339 "throw new Error(" + JSON.stringify(this.error.message) + ");"
340 );
341 this._ast = null;
342 }
343
344 applyNoParseRule(rule, content) {
345 // must start with "rule" if rule is a string
346 if (typeof rule === "string") {
347 return content.indexOf(rule) === 0;
348 }
349
350 if (typeof rule === "function") {
351 return rule(content);
352 }
353 // we assume rule is a regexp
354 return rule.test(content);
355 }
356
357 // check if module should not be parsed
358 // returns "true" if the module should !not! be parsed
359 // returns "false" if the module !must! be parsed
360 shouldPreventParsing(noParseRule, request) {
361 // if no noParseRule exists, return false
362 // the module !must! be parsed.
363 if (!noParseRule) {
364 return false;
365 }
366
367 // we only have one rule to check
368 if (!Array.isArray(noParseRule)) {
369 // returns "true" if the module is !not! to be parsed
370 return this.applyNoParseRule(noParseRule, request);
371 }
372
373 for (let i = 0; i < noParseRule.length; i++) {
374 const rule = noParseRule[i];
375 // early exit on first truthy match
376 // this module is !not! to be parsed
377 if (this.applyNoParseRule(rule, request)) {
378 return true;
379 }
380 }
381 // no match found, so this module !should! be parsed
382 return false;
383 }
384
385 _initBuildHash(compilation) {
386 const hash = createHash(compilation.outputOptions.hashFunction);
387 if (this._source) {
388 hash.update("source");
389 this._source.updateHash(hash);
390 }
391 hash.update("meta");
392 hash.update(JSON.stringify(this.buildMeta));
393 this._buildHash = hash.digest("hex");
394 }
395
396 build(options, compilation, resolver, fs, callback) {
397 this.buildTimestamp = Date.now();
398 this.built = true;
399 this._source = null;
400 this._ast = null;
401 this._buildHash = "";
402 this.error = null;
403 this.errors.length = 0;
404 this.warnings.length = 0;
405 this.buildMeta = {};
406 this.buildInfo = {
407 cacheable: false,
408 fileDependencies: new Set(),
409 contextDependencies: new Set()
410 };
411
412 return this.doBuild(options, compilation, resolver, fs, err => {
413 this._cachedSources.clear();
414
415 // if we have an error mark module as failed and exit
416 if (err) {
417 this.markModuleAsErrored(err);
418 this._initBuildHash(compilation);
419 return callback();
420 }
421
422 // check if this module should !not! be parsed.
423 // if so, exit here;
424 const noParseRule = options.module && options.module.noParse;
425 if (this.shouldPreventParsing(noParseRule, this.request)) {
426 this._initBuildHash(compilation);
427 return callback();
428 }
429
430 const handleParseError = e => {
431 const source = this._source.source();
432 const error = new ModuleParseError(this, source, e);
433 this.markModuleAsErrored(error);
434 this._initBuildHash(compilation);
435 return callback();
436 };
437
438 const handleParseResult = result => {
439 this._lastSuccessfulBuildMeta = this.buildMeta;
440 this._initBuildHash(compilation);
441 return callback();
442 };
443
444 try {
445 const result = this.parser.parse(
446 this._ast || this._source.source(),
447 {
448 current: this,
449 module: this,
450 compilation: compilation,
451 options: options
452 },
453 (err, result) => {
454 if (err) {
455 handleParseError(err);
456 } else {
457 handleParseResult(result);
458 }
459 }
460 );
461 if (result !== undefined) {
462 // parse is sync
463 handleParseResult(result);
464 }
465 } catch (e) {
466 handleParseError(e);
467 }
468 });
469 }
470
471 getHashDigest(dependencyTemplates) {
472 // TODO webpack 5 refactor
473 let dtHash = dependencyTemplates.get("hash");
474 return `${this.hash}-${dtHash}`;
475 }
476
477 source(dependencyTemplates, runtimeTemplate, type = "javascript") {
478 const hashDigest = this.getHashDigest(dependencyTemplates);
479 const cacheEntry = this._cachedSources.get(type);
480 if (cacheEntry !== undefined && cacheEntry.hash === hashDigest) {
481 // We can reuse the cached source
482 return cacheEntry.source;
483 }
484
485 const source = this.generator.generate(
486 this,
487 dependencyTemplates,
488 runtimeTemplate,
489 type
490 );
491
492 const cachedSource = new CachedSource(source);
493 this._cachedSources.set(type, {
494 source: cachedSource,
495 hash: hashDigest
496 });
497 return cachedSource;
498 }
499
500 originalSource() {
501 return this._source;
502 }
503
504 needRebuild(fileTimestamps, contextTimestamps) {
505 // always try to rebuild in case of an error
506 if (this.error) return true;
507
508 // always rebuild when module is not cacheable
509 if (!this.buildInfo.cacheable) return true;
510
511 // Check timestamps of all dependencies
512 // Missing timestamp -> need rebuild
513 // Timestamp bigger than buildTimestamp -> need rebuild
514 for (const file of this.buildInfo.fileDependencies) {
515 const timestamp = fileTimestamps.get(file);
516 if (!timestamp) return true;
517 if (timestamp >= this.buildTimestamp) return true;
518 }
519 for (const file of this.buildInfo.contextDependencies) {
520 const timestamp = contextTimestamps.get(file);
521 if (!timestamp) return true;
522 if (timestamp >= this.buildTimestamp) return true;
523 }
524 // elsewise -> no rebuild needed
525 return false;
526 }
527
528 size() {
529 return this._source ? this._source.size() : -1;
530 }
531
532 /**
533 * @param {Hash} hash the hash used to track dependencies
534 * @returns {void}
535 */
536 updateHash(hash) {
537 hash.update(this._buildHash);
538 super.updateHash(hash);
539 }
540}
541
542module.exports = NormalModule;