/* 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 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} compilationDependencies */ class Compiler extends Tapable { constructor(context) { super(); this.hooks = { /** @type {SyncBailHook} */ shouldEmit: new SyncBailHook(["compilation"]), /** @type {AsyncSeriesHook} */ done: new AsyncSeriesHook(["stats"]), /** @type {AsyncSeriesHook<>} */ additionalPass: new AsyncSeriesHook([]), /** @type {AsyncSeriesHook} */ beforeRun: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook} */ run: new AsyncSeriesHook(["compiler"]), /** @type {AsyncSeriesHook} */ emit: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook} */ afterEmit: new AsyncSeriesHook(["compilation"]), /** @type {SyncHook} */ thisCompilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook} */ compilation: new SyncHook(["compilation", "params"]), /** @type {SyncHook} */ normalModuleFactory: new SyncHook(["normalModuleFactory"]), /** @type {SyncHook} */ contextModuleFactory: new SyncHook(["contextModulefactory"]), /** @type {AsyncSeriesHook} */ beforeCompile: new AsyncSeriesHook(["params"]), /** @type {SyncHook} */ compile: new SyncHook(["params"]), /** @type {AsyncParallelHook} */ make: new AsyncParallelHook(["compilation"]), /** @type {AsyncSeriesHook} */ afterCompile: new AsyncSeriesHook(["compilation"]), /** @type {AsyncSeriesHook} */ watchRun: new AsyncSeriesHook(["compiler"]), /** @type {SyncHook} */ failed: new SyncHook(["error"]), /** @type {SyncHook} */ 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} */ afterPlugins: new SyncHook(["compiler"]), /** @type {SyncHook} */ afterResolvers: new SyncHook(["compiler"]), /** @type {SyncBailHook} */ 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} */ this.fileTimestamps = new Map(); /** @type {Map} */ 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; } 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.forEach( compilation.assets, (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 ); 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(); 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;