| /* |
| MIT License http://www.opensource.org/licenses/mit-license.php |
| Author Tobias Koppers @sokra |
| */ |
| "use strict"; |
| |
| const parseJson = require("json-parse-better-errors"); |
| const asyncLib = require("neo-async"); |
| const path = require("path"); |
| const { Source } = require("webpack-sources"); |
| const util = require("util"); |
| const { |
| Tapable, |
| SyncHook, |
| SyncBailHook, |
| AsyncParallelHook, |
| AsyncSeriesHook |
| } = require("tapable"); |
| |
| const Compilation = require("./Compilation"); |
| const Stats = require("./Stats"); |
| const Watching = require("./Watching"); |
| const NormalModuleFactory = require("./NormalModuleFactory"); |
| const ContextModuleFactory = require("./ContextModuleFactory"); |
| const ResolverFactory = require("./ResolverFactory"); |
| |
| const RequestShortener = require("./RequestShortener"); |
| const { makePathsRelative } = require("./util/identifier"); |
| const ConcurrentCompilationError = require("./ConcurrentCompilationError"); |
| |
| /** @typedef {import("../declarations/WebpackOptions").Entry} Entry */ |
| /** @typedef {import("../declarations/WebpackOptions").WebpackOptions} WebpackOptions */ |
| |
| /** |
| * @typedef {Object} CompilationParams |
| * @property {NormalModuleFactory} normalModuleFactory |
| * @property {ContextModuleFactory} contextModuleFactory |
| * @property {Set<string>} compilationDependencies |
| */ |
| |
| class Compiler extends Tapable { |
| constructor(context) { |
| super(); |
| this.hooks = { |
| /** @type {SyncBailHook<Compilation>} */ |
| shouldEmit: new SyncBailHook(["compilation"]), |
| /** @type {AsyncSeriesHook<Stats>} */ |
| done: new AsyncSeriesHook(["stats"]), |
| /** @type {AsyncSeriesHook<>} */ |
| additionalPass: new AsyncSeriesHook([]), |
| /** @type {AsyncSeriesHook<Compiler>} */ |
| beforeRun: new AsyncSeriesHook(["compiler"]), |
| /** @type {AsyncSeriesHook<Compiler>} */ |
| run: new AsyncSeriesHook(["compiler"]), |
| /** @type {AsyncSeriesHook<Compilation>} */ |
| emit: new AsyncSeriesHook(["compilation"]), |
| /** @type {AsyncSeriesHook<Compilation>} */ |
| afterEmit: new AsyncSeriesHook(["compilation"]), |
| |
| /** @type {SyncHook<Compilation, CompilationParams>} */ |
| thisCompilation: new SyncHook(["compilation", "params"]), |
| /** @type {SyncHook<Compilation, CompilationParams>} */ |
| compilation: new SyncHook(["compilation", "params"]), |
| /** @type {SyncHook<NormalModuleFactory>} */ |
| normalModuleFactory: new SyncHook(["normalModuleFactory"]), |
| /** @type {SyncHook<ContextModuleFactory>} */ |
| contextModuleFactory: new SyncHook(["contextModulefactory"]), |
| |
| /** @type {AsyncSeriesHook<CompilationParams>} */ |
| beforeCompile: new AsyncSeriesHook(["params"]), |
| /** @type {SyncHook<CompilationParams>} */ |
| compile: new SyncHook(["params"]), |
| /** @type {AsyncParallelHook<Compilation>} */ |
| make: new AsyncParallelHook(["compilation"]), |
| /** @type {AsyncSeriesHook<Compilation>} */ |
| afterCompile: new AsyncSeriesHook(["compilation"]), |
| |
| /** @type {AsyncSeriesHook<Compiler>} */ |
| watchRun: new AsyncSeriesHook(["compiler"]), |
| /** @type {SyncHook<Error>} */ |
| failed: new SyncHook(["error"]), |
| /** @type {SyncHook<string, string>} */ |
| invalid: new SyncHook(["filename", "changeTime"]), |
| /** @type {SyncHook} */ |
| watchClose: new SyncHook([]), |
| |
| // TODO the following hooks are weirdly located here |
| // TODO move them for webpack 5 |
| /** @type {SyncHook} */ |
| environment: new SyncHook([]), |
| /** @type {SyncHook} */ |
| afterEnvironment: new SyncHook([]), |
| /** @type {SyncHook<Compiler>} */ |
| afterPlugins: new SyncHook(["compiler"]), |
| /** @type {SyncHook<Compiler>} */ |
| afterResolvers: new SyncHook(["compiler"]), |
| /** @type {SyncBailHook<string, Entry>} */ |
| entryOption: new SyncBailHook(["context", "entry"]) |
| }; |
| |
| this._pluginCompat.tap("Compiler", options => { |
| switch (options.name) { |
| case "additional-pass": |
| case "before-run": |
| case "run": |
| case "emit": |
| case "after-emit": |
| case "before-compile": |
| case "make": |
| case "after-compile": |
| case "watch-run": |
| options.async = true; |
| break; |
| } |
| }); |
| |
| /** @type {string=} */ |
| this.name = undefined; |
| /** @type {Compilation=} */ |
| this.parentCompilation = undefined; |
| /** @type {string} */ |
| this.outputPath = ""; |
| |
| this.outputFileSystem = null; |
| this.inputFileSystem = null; |
| |
| /** @type {string|null} */ |
| this.recordsInputPath = null; |
| /** @type {string|null} */ |
| this.recordsOutputPath = null; |
| this.records = {}; |
| this.removedFiles = new Set(); |
| /** @type {Map<string, number>} */ |
| this.fileTimestamps = new Map(); |
| /** @type {Map<string, number>} */ |
| this.contextTimestamps = new Map(); |
| /** @type {ResolverFactory} */ |
| this.resolverFactory = new ResolverFactory(); |
| |
| // TODO remove in webpack 5 |
| this.resolvers = { |
| normal: { |
| plugins: util.deprecate((hook, fn) => { |
| this.resolverFactory.plugin("resolver normal", resolver => { |
| resolver.plugin(hook, fn); |
| }); |
| }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.plugin(/* … */);\n}); instead.'), |
| apply: util.deprecate((...args) => { |
| this.resolverFactory.plugin("resolver normal", resolver => { |
| resolver.apply(...args); |
| }); |
| }, "webpack: Using compiler.resolvers.normal is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver normal", resolver => {\n resolver.apply(/* … */);\n}); instead.') |
| }, |
| loader: { |
| plugins: util.deprecate((hook, fn) => { |
| this.resolverFactory.plugin("resolver loader", resolver => { |
| resolver.plugin(hook, fn); |
| }); |
| }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n resolver.plugin(/* … */);\n}); instead.'), |
| apply: util.deprecate((...args) => { |
| this.resolverFactory.plugin("resolver loader", resolver => { |
| resolver.apply(...args); |
| }); |
| }, "webpack: Using compiler.resolvers.loader is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver loader", resolver => {\n resolver.apply(/* … */);\n}); instead.') |
| }, |
| context: { |
| plugins: util.deprecate((hook, fn) => { |
| this.resolverFactory.plugin("resolver context", resolver => { |
| resolver.plugin(hook, fn); |
| }); |
| }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n resolver.plugin(/* … */);\n}); instead.'), |
| apply: util.deprecate((...args) => { |
| this.resolverFactory.plugin("resolver context", resolver => { |
| resolver.apply(...args); |
| }); |
| }, "webpack: Using compiler.resolvers.context is deprecated.\n" + 'Use compiler.resolverFactory.plugin("resolver context", resolver => {\n resolver.apply(/* … */);\n}); instead.') |
| } |
| }; |
| |
| /** @type {WebpackOptions} */ |
| this.options = /** @type {WebpackOptions} */ ({}); |
| |
| this.context = context; |
| |
| this.requestShortener = new RequestShortener(context); |
| |
| /** @type {boolean} */ |
| this.running = false; |
| |
| /** @type {boolean} */ |
| this.watchMode = false; |
| |
| /** @private @type {WeakMap<Source, { sizeOnlySource: SizeOnlySource, writtenTo: Map<string, number> }>} */ |
| this._assetEmittingSourceCache = new WeakMap(); |
| /** @private @type {Map<string, number>} */ |
| this._assetEmittingWrittenFiles = new Map(); |
| } |
| |
| watch(watchOptions, handler) { |
| if (this.running) return handler(new ConcurrentCompilationError()); |
| |
| this.running = true; |
| this.watchMode = true; |
| this.fileTimestamps = new Map(); |
| this.contextTimestamps = new Map(); |
| this.removedFiles = new Set(); |
| return new Watching(this, watchOptions, handler); |
| } |
| |
| run(callback) { |
| if (this.running) return callback(new ConcurrentCompilationError()); |
| |
| const finalCallback = (err, stats) => { |
| this.running = false; |
| |
| if (err) { |
| this.hooks.failed.call(err); |
| } |
| |
| if (callback !== undefined) return callback(err, stats); |
| }; |
| |
| const startTime = Date.now(); |
| |
| this.running = true; |
| |
| const onCompiled = (err, compilation) => { |
| if (err) return finalCallback(err); |
| |
| if (this.hooks.shouldEmit.call(compilation) === false) { |
| const stats = new Stats(compilation); |
| stats.startTime = startTime; |
| stats.endTime = Date.now(); |
| this.hooks.done.callAsync(stats, err => { |
| if (err) return finalCallback(err); |
| return finalCallback(null, stats); |
| }); |
| return; |
| } |
| |
| this.emitAssets(compilation, err => { |
| if (err) return finalCallback(err); |
| |
| if (compilation.hooks.needAdditionalPass.call()) { |
| compilation.needAdditionalPass = true; |
| |
| const stats = new Stats(compilation); |
| stats.startTime = startTime; |
| stats.endTime = Date.now(); |
| this.hooks.done.callAsync(stats, err => { |
| if (err) return finalCallback(err); |
| |
| this.hooks.additionalPass.callAsync(err => { |
| if (err) return finalCallback(err); |
| this.compile(onCompiled); |
| }); |
| }); |
| return; |
| } |
| |
| this.emitRecords(err => { |
| if (err) return finalCallback(err); |
| |
| const stats = new Stats(compilation); |
| stats.startTime = startTime; |
| stats.endTime = Date.now(); |
| this.hooks.done.callAsync(stats, err => { |
| if (err) return finalCallback(err); |
| return finalCallback(null, stats); |
| }); |
| }); |
| }); |
| }; |
| |
| this.hooks.beforeRun.callAsync(this, err => { |
| if (err) return finalCallback(err); |
| |
| this.hooks.run.callAsync(this, err => { |
| if (err) return finalCallback(err); |
| |
| this.readRecords(err => { |
| if (err) return finalCallback(err); |
| |
| this.compile(onCompiled); |
| }); |
| }); |
| }); |
| } |
| |
| runAsChild(callback) { |
| this.compile((err, compilation) => { |
| if (err) return callback(err); |
| |
| this.parentCompilation.children.push(compilation); |
| for (const name of Object.keys(compilation.assets)) { |
| this.parentCompilation.assets[name] = compilation.assets[name]; |
| } |
| |
| const entries = Array.from( |
| compilation.entrypoints.values(), |
| ep => ep.chunks |
| ).reduce((array, chunks) => { |
| return array.concat(chunks); |
| }, []); |
| |
| return callback(null, entries, compilation); |
| }); |
| } |
| |
| purgeInputFileSystem() { |
| if (this.inputFileSystem && this.inputFileSystem.purge) { |
| this.inputFileSystem.purge(); |
| } |
| } |
| |
| emitAssets(compilation, callback) { |
| let outputPath; |
| const emitFiles = err => { |
| if (err) return callback(err); |
| |
| asyncLib.forEachLimit( |
| compilation.assets, |
| 15, |
| (source, file, callback) => { |
| let targetFile = file; |
| const queryStringIdx = targetFile.indexOf("?"); |
| if (queryStringIdx >= 0) { |
| targetFile = targetFile.substr(0, queryStringIdx); |
| } |
| |
| const writeOut = err => { |
| if (err) return callback(err); |
| const targetPath = this.outputFileSystem.join( |
| outputPath, |
| targetFile |
| ); |
| // TODO webpack 5 remove futureEmitAssets option and make it on by default |
| if (this.options.output.futureEmitAssets) { |
| // check if the target file has already been written by this Compiler |
| const targetFileGeneration = this._assetEmittingWrittenFiles.get( |
| targetPath |
| ); |
| |
| // create an cache entry for this Source if not already existing |
| let cacheEntry = this._assetEmittingSourceCache.get(source); |
| if (cacheEntry === undefined) { |
| cacheEntry = { |
| sizeOnlySource: undefined, |
| writtenTo: new Map() |
| }; |
| this._assetEmittingSourceCache.set(source, cacheEntry); |
| } |
| |
| // if the target file has already been written |
| if (targetFileGeneration !== undefined) { |
| // check if the Source has been written to this target file |
| const writtenGeneration = cacheEntry.writtenTo.get(targetPath); |
| if (writtenGeneration === targetFileGeneration) { |
| // if yes, we skip writing the file |
| // as it's already there |
| // (we assume one doesn't remove files while the Compiler is running) |
| return callback(); |
| } |
| } |
| |
| // get the binary (Buffer) content from the Source |
| /** @type {Buffer} */ |
| let content; |
| if (typeof source.buffer === "function") { |
| content = source.buffer(); |
| } else { |
| const bufferOrString = source.source(); |
| if (Buffer.isBuffer(bufferOrString)) { |
| content = bufferOrString; |
| } else { |
| content = Buffer.from(bufferOrString, "utf8"); |
| } |
| } |
| |
| // Create a replacement resource which only allows to ask for size |
| // This allows to GC all memory allocated by the Source |
| // (expect when the Source is stored in any other cache) |
| cacheEntry.sizeOnlySource = new SizeOnlySource(content.length); |
| compilation.assets[file] = cacheEntry.sizeOnlySource; |
| |
| // Write the file to output file system |
| this.outputFileSystem.writeFile(targetPath, content, err => { |
| if (err) return callback(err); |
| |
| // information marker that the asset has been emitted |
| compilation.emittedAssets.add(file); |
| |
| // cache the information that the Source has been written to that location |
| const newGeneration = |
| targetFileGeneration === undefined |
| ? 1 |
| : targetFileGeneration + 1; |
| cacheEntry.writtenTo.set(targetPath, newGeneration); |
| this._assetEmittingWrittenFiles.set(targetPath, newGeneration); |
| callback(); |
| }); |
| } else { |
| if (source.existsAt === targetPath) { |
| source.emitted = false; |
| return callback(); |
| } |
| let content = source.source(); |
| |
| if (!Buffer.isBuffer(content)) { |
| content = Buffer.from(content, "utf8"); |
| } |
| |
| source.existsAt = targetPath; |
| source.emitted = true; |
| this.outputFileSystem.writeFile(targetPath, content, callback); |
| } |
| }; |
| |
| if (targetFile.match(/\/|\\/)) { |
| const dir = path.dirname(targetFile); |
| this.outputFileSystem.mkdirp( |
| this.outputFileSystem.join(outputPath, dir), |
| writeOut |
| ); |
| } else { |
| writeOut(); |
| } |
| }, |
| err => { |
| if (err) return callback(err); |
| |
| this.hooks.afterEmit.callAsync(compilation, err => { |
| if (err) return callback(err); |
| |
| return callback(); |
| }); |
| } |
| ); |
| }; |
| |
| this.hooks.emit.callAsync(compilation, err => { |
| if (err) return callback(err); |
| outputPath = compilation.getPath(this.outputPath); |
| this.outputFileSystem.mkdirp(outputPath, emitFiles); |
| }); |
| } |
| |
| emitRecords(callback) { |
| if (!this.recordsOutputPath) return callback(); |
| const idx1 = this.recordsOutputPath.lastIndexOf("/"); |
| const idx2 = this.recordsOutputPath.lastIndexOf("\\"); |
| let recordsOutputPathDirectory = null; |
| if (idx1 > idx2) { |
| recordsOutputPathDirectory = this.recordsOutputPath.substr(0, idx1); |
| } else if (idx1 < idx2) { |
| recordsOutputPathDirectory = this.recordsOutputPath.substr(0, idx2); |
| } |
| |
| const writeFile = () => { |
| this.outputFileSystem.writeFile( |
| this.recordsOutputPath, |
| JSON.stringify(this.records, undefined, 2), |
| callback |
| ); |
| }; |
| |
| if (!recordsOutputPathDirectory) { |
| return writeFile(); |
| } |
| this.outputFileSystem.mkdirp(recordsOutputPathDirectory, err => { |
| if (err) return callback(err); |
| writeFile(); |
| }); |
| } |
| |
| readRecords(callback) { |
| if (!this.recordsInputPath) { |
| this.records = {}; |
| return callback(); |
| } |
| this.inputFileSystem.stat(this.recordsInputPath, err => { |
| // It doesn't exist |
| // We can ignore this. |
| if (err) return callback(); |
| |
| this.inputFileSystem.readFile(this.recordsInputPath, (err, content) => { |
| if (err) return callback(err); |
| |
| try { |
| this.records = parseJson(content.toString("utf-8")); |
| } catch (e) { |
| e.message = "Cannot parse records: " + e.message; |
| return callback(e); |
| } |
| |
| return callback(); |
| }); |
| }); |
| } |
| |
| createChildCompiler( |
| compilation, |
| compilerName, |
| compilerIndex, |
| outputOptions, |
| plugins |
| ) { |
| const childCompiler = new Compiler(this.context); |
| if (Array.isArray(plugins)) { |
| for (const plugin of plugins) { |
| plugin.apply(childCompiler); |
| } |
| } |
| for (const name in this.hooks) { |
| if ( |
| ![ |
| "make", |
| "compile", |
| "emit", |
| "afterEmit", |
| "invalid", |
| "done", |
| "thisCompilation" |
| ].includes(name) |
| ) { |
| if (childCompiler.hooks[name]) { |
| childCompiler.hooks[name].taps = this.hooks[name].taps.slice(); |
| } |
| } |
| } |
| childCompiler.name = compilerName; |
| childCompiler.outputPath = this.outputPath; |
| childCompiler.inputFileSystem = this.inputFileSystem; |
| childCompiler.outputFileSystem = null; |
| childCompiler.resolverFactory = this.resolverFactory; |
| childCompiler.fileTimestamps = this.fileTimestamps; |
| childCompiler.contextTimestamps = this.contextTimestamps; |
| |
| const relativeCompilerName = makePathsRelative(this.context, compilerName); |
| if (!this.records[relativeCompilerName]) { |
| this.records[relativeCompilerName] = []; |
| } |
| if (this.records[relativeCompilerName][compilerIndex]) { |
| childCompiler.records = this.records[relativeCompilerName][compilerIndex]; |
| } else { |
| this.records[relativeCompilerName].push((childCompiler.records = {})); |
| } |
| |
| childCompiler.options = Object.create(this.options); |
| childCompiler.options.output = Object.create(childCompiler.options.output); |
| for (const name in outputOptions) { |
| childCompiler.options.output[name] = outputOptions[name]; |
| } |
| childCompiler.parentCompilation = compilation; |
| |
| compilation.hooks.childCompiler.call( |
| childCompiler, |
| compilerName, |
| compilerIndex |
| ); |
| |
| return childCompiler; |
| } |
| |
| isChild() { |
| return !!this.parentCompilation; |
| } |
| |
| createCompilation() { |
| return new Compilation(this); |
| } |
| |
| newCompilation(params) { |
| const compilation = this.createCompilation(); |
| compilation.fileTimestamps = this.fileTimestamps; |
| compilation.contextTimestamps = this.contextTimestamps; |
| compilation.name = this.name; |
| compilation.records = this.records; |
| compilation.compilationDependencies = params.compilationDependencies; |
| this.hooks.thisCompilation.call(compilation, params); |
| this.hooks.compilation.call(compilation, params); |
| return compilation; |
| } |
| |
| createNormalModuleFactory() { |
| const normalModuleFactory = new NormalModuleFactory( |
| this.options.context, |
| this.resolverFactory, |
| this.options.module || {} |
| ); |
| this.hooks.normalModuleFactory.call(normalModuleFactory); |
| return normalModuleFactory; |
| } |
| |
| createContextModuleFactory() { |
| const contextModuleFactory = new ContextModuleFactory(this.resolverFactory); |
| this.hooks.contextModuleFactory.call(contextModuleFactory); |
| return contextModuleFactory; |
| } |
| |
| newCompilationParams() { |
| const params = { |
| normalModuleFactory: this.createNormalModuleFactory(), |
| contextModuleFactory: this.createContextModuleFactory(), |
| compilationDependencies: new Set() |
| }; |
| return params; |
| } |
| |
| compile(callback) { |
| const params = this.newCompilationParams(); |
| this.hooks.beforeCompile.callAsync(params, err => { |
| if (err) return callback(err); |
| |
| this.hooks.compile.call(params); |
| |
| const compilation = this.newCompilation(params); |
| |
| this.hooks.make.callAsync(compilation, err => { |
| if (err) return callback(err); |
| |
| compilation.finish(err => { |
| if (err) return callback(err); |
| |
| compilation.seal(err => { |
| if (err) return callback(err); |
| |
| this.hooks.afterCompile.callAsync(compilation, err => { |
| if (err) return callback(err); |
| |
| return callback(null, compilation); |
| }); |
| }); |
| }); |
| }); |
| }); |
| } |
| } |
| |
| module.exports = Compiler; |
| |
| class SizeOnlySource extends Source { |
| constructor(size) { |
| super(); |
| this._size = size; |
| } |
| |
| _error() { |
| return new Error( |
| "Content and Map of this Source is no longer available (only size() is supported)" |
| ); |
| } |
| |
| size() { |
| return this._size; |
| } |
| |
| /** |
| * @param {any} options options |
| * @returns {string} the source |
| */ |
| source(options) { |
| throw this._error(); |
| } |
| |
| node() { |
| throw this._error(); |
| } |
| |
| listMap() { |
| throw this._error(); |
| } |
| |
| map() { |
| throw this._error(); |
| } |
| |
| listNode() { |
| throw this._error(); |
| } |
| |
| updateHash() { |
| throw this._error(); |
| } |
| } |