node-hot-loader
Version:
Hot module replacement for Node.js applications
314 lines (303 loc) • 12.2 kB
JavaScript
"use strict";
exports.__esModule = true;
exports.default = void 0;
var _fs = _interopRequireDefault(require("fs"));
var _path = _interopRequireDefault(require("path"));
var _child_process = require("child_process");
var memfs = _interopRequireWildcard(require("memfs"));
var _LogColors = _interopRequireDefault(require("./LogColors"));
var _Logger = _interopRequireDefault(require("./Logger"));
var _LogLevel = require("./LogLevel");
var _messageActionType = _interopRequireDefault(require("./messageActionType"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
class HmrServer {
static defaultReporter({
context,
stateValid,
stats,
compilerOptions
}) {
// for check log level for webpack compiler only
const compilerLogLevel = (0, _LogLevel.parseLogLevel)(compilerOptions.stats);
if (!stateValid) {
if (compilerLogLevel >= _LogLevel.LogLevel.MINIMAL) {
context.webpackLogger.info('Compiling...');
}
return;
}
if (compilerLogLevel > _LogLevel.LogLevel.NONE) {
const statsInfo = stats.toString(compilerOptions.stats);
if (statsInfo) {
// To avoid log empty statsInfo, e.g. when options.stats is 'errors-only'.
context.webpackLogger.info(statsInfo);
}
}
if (compilerLogLevel >= _LogLevel.LogLevel.ERRORS) {
if (stats.hasErrors()) {
context.webpackLogger.error('Failed to compile.');
} else if (stats.hasWarnings()) {
context.webpackLogger.warn('Compiled with warnings.');
}
}
if (compilerLogLevel >= _LogLevel.LogLevel.MINIMAL) {
context.webpackLogger.info('Compiled successfully.');
}
}
/**
* @param {{
* compiler: import('webpack').Compiler;
* fork?: boolean | string[];
* args?: string[];
* autoRestart?: boolean;
* inMemory?: boolean;
* logLevel?: string;
* }} options
*/
constructor(options) {
_defineProperty(this, "context", {
/** When started compiled script contains process object in which script running. */
serverProcess: null,
/** Valid or invalid state. */
stateValid: false,
/** Last compiler stats. */
/** @type import('webpack').Stats */
webpackStats: undefined,
/** Compiler watching by compiler.watch(...). */
watching: undefined,
/**
* Do not use memory-fs because we can't fork bundle from in-memory file.
*/
fs: _fs.default,
reporter: HmrServer.defaultReporter,
logger: new _Logger.default(_LogColors.default.cyan('[HMR]')),
webpackLogger: new _Logger.default(_LogColors.default.magenta('Webpack')),
/** @type {import('webpack').Compiler} */
compiler: undefined,
fork: false,
args: undefined,
autoRestart: false,
inMemory: false,
logLevel: undefined
});
_defineProperty(this, "compilerStart", () => {
try {
this.sendMessage(_messageActionType.default.CompilerStart);
if (this.context.watching && this.context.stateValid) {
this.context.reporter({
stateValid: false,
context: this.context,
compilerOptions: this.context.compiler.options
});
}
// We are now in invalid state
this.context.stateValid = false;
} catch (ex) {
this.context.logger.error(ex);
}
});
/**
* @param {import('webpack').Stats} stats
*/
_defineProperty(this, "compilerDone", stats => new Promise(resolve => {
// We are now on valid state
this.context.stateValid = true;
this.context.webpackStats = stats;
// Do the stuff in nextTick, because bundle may be invalidated
// if a change happened while compiling
process.nextTick(() => {
// check if still in valid state
if (!this.context.stateValid) return;
// print webpack output
if (this.context.watching) {
this.context.reporter({
stateValid: true,
stats,
context: this.context,
compilerOptions: this.context.compiler.options
});
}
// Already has launched process
if (this.context.serverProcess) {
this.sendMessage(_messageActionType.default.CompilerDone);
}
// Start compiled files in child process (fork) or in current process.
else {
this.launchAssets(stats);
}
resolve();
});
}).catch(ex => {
this.context.logger.error(ex);
}));
_defineProperty(this, "startWatch", () => {
const {
compiler
} = this.context;
// start watching
this.context.watching = compiler.watch(compiler.options.watchOptions, err => {
if (err) {
this.context.logger.error(err.stack || err);
if (err.details) this.context.logger.error(err.details);
}
});
this.context.logger.info('Waiting webpack...');
});
_defineProperty(this, "run", (watch = true) => {
const {
compiler
} = this.context;
if (compiler.hooks) {
// webpack >= 4
compiler.hooks.invalid.tap('node-hot-loader', this.compilerStart);
compiler.hooks.done.tapPromise('node-hot-loader', this.compilerDone);
} else {
// webpack < 4
compiler.plugin('invalid', this.compilerStart);
compiler.plugin('done', this.compilerDone);
}
if (watch) {
this.startWatch();
}
return this;
});
this.context = {
...this.context,
...options
};
const {
compiler: _compiler,
inMemory
} = this.context;
if (inMemory) {
this.context.fs = memfs.createFsFromVolume(new memfs.Volume());
_compiler.outputFileSystem = this.context.fs;
}
// if (typeof compiler.outputPath === 'string' && !path.isAbsolute(compiler.outputPath)) {
// throw new Error('`output.path` needs to be an absolute path or `/`.');
// }
}
sendMessage(action) {
if (!this.context.serverProcess) {
return;
}
const logLevel = this.context.logLevel != null ? (0, _LogLevel.parseLogLevel)(this.context.logLevel) : (0, _LogLevel.parseLogLevel)(this.context.compiler.options.stats);
if (this.context.fork) {
this.context.serverProcess.send({
action,
stats: this.context.webpackStats.toJson(),
logLevel
});
} else {
this.context.serverProcess.emit('message', {
action,
stats: this.context.webpackStats.toJson(),
logLevel
});
}
}
getLauncherFileName(stats) {
const assets = Object.values(stats.toJson().entrypoints).reduce((acc, group) => {
return acc.concat(...group.assets.filter(asset => /\.[cm]?js$/i.test(asset.name || asset)).map(asset => _path.default.resolve(stats.compilation.compiler.outputPath, asset.name || asset)));
}, []);
if (assets.length === 1) {
// Only one valid assets, so just return it path
return assets[0];
}
// Create temp launcher file which aggregates all assets.
const launcherString = assets.map(asset => `require('${asset.replace(/\\/g, '/')}');`).join('\n');
const launcherFileName = _path.default.resolve(stats.compilation.compiler.outputPath, `launcher.${stats.hash}.js`);
this.context.fs.writeFileSync(launcherFileName, launcherString);
// If not launched yet (eg. if not a restart)
if (!this.context.serverProcess) {
// Delete created files on exit main process.
const deleteLauncher = () => this.context.fs.unlinkSync(launcherFileName);
process.on('exit', deleteLauncher);
process.on('SIGINT', deleteLauncher);
}
return launcherFileName;
}
requireFromMemory(filename) {
const Module = module.constructor.length > 1 ? module.constructor : require('module');
const {
_findPath
} = Module;
const {
readFileSync,
statSync
} = _fs.default;
const basedir = _path.default.dirname(filename);
Module._findPath = (request, ...rest) => {
if (request === filename || request.includes('.hot-update.js')) {
return _path.default.isAbsolute(request) ? request : _path.default.resolve(basedir, request);
}
return _findPath.call(Module, request, ...rest);
};
_fs.default.readFileSync = (fname, ...rest) => {
if (fname === filename || fname.includes('.hot-update.js')) {
return this.context.fs.readFileSync(fname, ...rest);
}
return readFileSync.call(_fs.default, fname, ...rest);
};
_fs.default.statSync = (fname, ...rest) => {
if (fname === filename || fname.includes('.hot-update.js')) {
return this.context.fs.statSync(fname, ...rest);
}
return statSync.call(_fs.default, fname, ...rest);
};
delete require.cache[filename];
return require(filename);
}
launchAssets(stats) {
return Promise.resolve().then(() => {
const launcherFileName = this.getLauncherFileName(stats);
// Execute built scripts
if (this.context.fork) {
/** @type import('child_process').ForkOptions */
const forkOptions = {
cwd: process.cwd(),
env: process.env,
execArgv: this.context.fork === true ? undefined : this.context.fork
};
if (process.getuid) {
forkOptions.uid = process.getuid();
forkOptions.gid = process.getgid();
}
// Launch in forked process
this.context.serverProcess = (0, _child_process.fork)(launcherFileName, this.context.args || process.argv, forkOptions);
// Exit main process when exit child process.
const onChildExit = code => process.exit(code);
this.context.serverProcess.on('exit', onChildExit);
if (this.context.autoRestart) {
// Listen messages from child process
this.context.serverProcess.on('message', ({
action
}) => {
if (action !== _messageActionType.default.RestartRequired) return;
this.context.logger.warn('AutoRestart is on. Restarting process...');
this.context.serverProcess.off('exit', onChildExit);
this.context.serverProcess.kill('SIGINT');
this.launchAssets(stats);
});
}
this.context.logger.info('Launch assets in forked process.');
return;
}
// Require in current process to launch script.
if (this.context.inMemory) {
this.requireFromMemory(launcherFileName);
} else {
require(`${launcherFileName}`);
}
this.context.serverProcess = process;
}).catch(err => {
this.context.logger.error(err);
process.exit();
});
}
}
exports.default = HmrServer;