@morjs/utils
Version:
mor utils
476 lines • 18.2 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.WebpackPlugin = exports.WebpackWrapper = void 0;
const enhanced_resolve_1 = require("enhanced-resolve");
const events_1 = __importDefault(require("events"));
const memfs_1 = require("memfs");
const takin_1 = require("takin");
const webpack_1 = __importDefault(require("webpack"));
// webpack-chain 官方还未支持 webpack 5 先拿 webpack-5-chain 代替下
const glob_to_regexp_1 = __importDefault(require("glob-to-regexp"));
const webpack_chain_5_1 = __importDefault(require("webpack-chain-5"));
const NodeWatchFileSystem_1 = __importDefault(require("webpack/lib/node/NodeWatchFileSystem"));
const logger_1 = require("./logger");
/**
* Webpack 相关用户配置
*/
const WebpackConfigSchema = takin_1.zod.object({
/**
* webpack chain 支持, 允许定制 webpack 配置
*/
webpackChain: takin_1.zod
.function()
.args(takin_1.zod.instanceof(webpack_chain_5_1.default))
.returns(takin_1.zod.promise(takin_1.zod.void()).or(takin_1.zod.void()))
.optional(),
/**
* 允许额外 watch 一些被 ignore 的目录或文件
*/
watchNodeModules: takin_1.zod
.string()
.or(takin_1.zod.instanceof(RegExp))
.or(takin_1.zod.array(takin_1.zod.string()))
.optional()
});
// copy from watchpack for logically consistent
const stringToRegexp = (ignored) => {
const source = (0, glob_to_regexp_1.default)(ignored, {
globstar: true,
extended: true
}).source;
const matchingStart = source.slice(0, source.length - 1) + '(?:$|\\/)';
return matchingStart;
};
const ignoredToFunction = (ignored) => {
if (Array.isArray(ignored)) {
const regexp = new RegExp(ignored.map((i) => stringToRegexp(i)).join('|'));
return (x) => regexp.test(x.replace(/\\/g, '/'));
}
else if (typeof ignored === 'string') {
const regexp = new RegExp(stringToRegexp(ignored));
return (x) => regexp.test(x.replace(/\\/g, '/'));
}
else if (ignored instanceof RegExp) {
return (x) => ignored.test(x.replace(/\\/g, '/'));
}
else if (ignored instanceof Function) {
return ignored;
}
else if (ignored) {
throw new Error(`Invalid option for 'ignored': ${ignored}`);
}
else {
return () => false;
}
};
const WebpackWrapperMap = new WeakMap();
function universalify(fn) {
return Object.defineProperty(function (...args) {
if (typeof args[args.length - 1] === 'function') {
fn(...args);
}
else {
return new Promise((resolve, reject) => {
fn(...args, (err, res) => err != null ? reject(err) : resolve(res));
});
}
}, 'name', { value: fn.name });
}
function combinedFn(fn1, fn2) {
return Object.defineProperty(function (...args) {
const callback = args[args.length - 1];
if (typeof callback === 'function') {
const _args = Array.from(args);
_args[args.length - 1] = (err, result) => {
// 这里需要额外判断 result 是否为空
// 原因是: enhanced-resolve 5.11.0 版本中写入了 { throwIfNoEntry: false }
// 导致 stat 方法调用失败后,不抛错,而是返回空值
// 参见: https://github.com/webpack/enhanced-resolve/commit/96be62de9ca9cd1fd6f79f0036a3cf5c3a6ef0b7
// https://github.com/webpack/enhanced-resolve/issues/362
err != null || result == null ? fn2(...args) : callback(err, result);
};
return fn1(..._args);
}
else {
try {
const result = fn1(...args);
if (result != null)
return result;
}
catch (err) {
/* do nothing */
}
return fn2(...args);
}
}, 'name', { value: fn2.name });
}
/**
* 创建自定义 fs
* 用于提供模拟文件以及
*/
function createCustomFS() {
const fileDataStore = new Map();
const fileJsonStore = new Map();
const fileLinkStore = new Map();
const memFs = (0, memfs_1.createFsFromVolume)(new memfs_1.Volume());
/**
* 生成 fs 的 promisified 实例
*/
function universalifyInputFileSystem(fs) {
const pstat = universalify(fs.stat);
return {
readFile: universalify(fs.readFile),
readJson: universalify(fs.readJson),
readlink: universalify(fs.readlink),
readdir: universalify(fs.readdir),
stat: pstat,
lstat: universalify(fs.lstat),
fileExists: async (arg) => {
try {
const stat = await pstat(arg);
if (stat.isFile())
return true;
return false;
}
catch (error) {
return false;
}
},
pathExists: async (arg) => {
try {
await pstat(arg);
return true;
}
catch (error) {
return false;
}
},
realpath: fs.realpath ? universalify(fs.realpath) : undefined,
purge: (what) => Promise.resolve(fs.purge(what)),
join: (arg, arg1) => Promise.resolve(fs.join(arg, arg1)),
relative: (arg, arg1) => Promise.resolve(fs.relative(arg, arg1)),
dirname: (arg) => Promise.resolve(fs.dirname(arg)),
mem: memFs
};
}
const _readFile = combinedFn(memFs.readFile, takin_1.fsExtra.readFile);
const inputFileSystem = new enhanced_resolve_1.CachedInputFileSystem({
...takin_1.fsExtra,
...{
readFile: _readFile,
readJson: function (filePath, callback) {
_readFile(filePath, function (err, content) {
if (err)
return callback(err, undefined);
try {
callback(null, JSON.parse(String(content)));
}
catch (error) {
callback(error, undefined);
}
});
},
readlink: combinedFn(memFs.readlink, takin_1.fsExtra.readlink),
/**
* 定制 readdir
* 优先从内存中读取文件夹列表,并与真实的文件夹列表合并
*/
readdir(path, ...args) {
const callback = args.pop();
const options = args[0];
memFs.readdir(path, options, function (err1, memDirs) {
if (err1) {
takin_1.fsExtra.readdir(path, ...args.concat(callback));
}
else {
takin_1.fsExtra.readdir(path, ...args.concat(function (err2, dirs) {
if (err2)
return callback(err2, dirs);
(memDirs || []).forEach(function (dir) {
if (dirs.includes(dir))
return;
dirs.push(dir);
});
callback(err2, dirs);
}));
}
});
},
stat: combinedFn(memFs.stat, takin_1.fsExtra.stat),
lstat: combinedFn(memFs.lstat, takin_1.fsExtra.lstat),
realpath: combinedFn(memFs.realpath, takin_1.fsExtra.realpath)
}
}, 60000);
// 追加清理内容
const originalPurge = inputFileSystem.purge;
inputFileSystem.purge = function (what) {
if (what) {
fileDataStore.delete(what);
fileJsonStore.delete(what);
fileLinkStore.delete(what);
}
else {
fileDataStore.clear();
fileJsonStore.clear();
fileLinkStore.clear();
}
return originalPurge.call(this, what);
};
return {
inputFileSystem,
fs: universalifyInputFileSystem(inputFileSystem)
};
}
/**
* 替换 webpack compiler 中的 inputFileSystem 为定制的 fs
*/
class ReplaceInputFileSystemPlugin {
constructor(inputFileSystem) {
this.name = 'ReplaceInputFileSystemPlugin';
this.inputFileSystem = inputFileSystem;
}
apply(compiler) {
const inputFileSystem = this.inputFileSystem;
compiler.inputFileSystem = inputFileSystem;
compiler.watchFileSystem = new NodeWatchFileSystem_1.default(compiler.inputFileSystem);
compiler.hooks.beforeRun.tap(this.name, (compiler) => {
if (compiler.inputFileSystem === inputFileSystem) {
compiler.fsStartTime = Date.now();
inputFileSystem.purge();
}
});
}
}
/**
* webpack 封装
* 主要目的是 共用 webpack 的能力
*/
class WebpackWrapper extends events_1.default {
constructor() {
super();
this.webpackChain = new webpack_chain_5_1.default();
const { inputFileSystem, fs } = createCustomFS();
this.inputFileSystem = inputFileSystem;
this.promisifiedFs = this.fs = fs;
}
/**
* 合并当前配置到 webpack 配置中
* @param cnf 需要合并的配置
*/
merge(cnf) {
this.webpackChain.merge(cnf);
return this;
}
/**
* 获取 webpackChain 实例, 用于修改 webpack 配置
*/
get chain() {
return this.webpackChain;
}
/**
* @private
* 准备 webpack 的 config
* 该方法调用之后 基于 chain 的修改不再生效
* NOTE: 该方法为 编译插件内部调用, 如无特别需要请勿自行调用
*/
buildConfig() {
if (this.config)
return this.config;
const config = this.webpackChain.toConfig();
this.config = config;
this.watchOptions = config.watchOptions;
this.watch = config.watch;
delete this.config.watchOptions;
delete this.config.watch;
return this.config;
}
/**
* @private
* 准备 webpack 相关实例
* NOTE: 该方法为 编译插件内部调用, 如无特别需要请勿自行调用
*/
prepare() {
const config = this.buildConfig();
const plugins = [
// 替换 inputFileSystem
new ReplaceInputFileSystemPlugin(this.inputFileSystem),
...(0, takin_1.asArray)(config.plugins)
];
this.compiler = (0, webpack_1.default)({ ...config, plugins });
return this;
}
/**
* 运行 webpack
* @param callback 回调函数
*/
async run(callback) {
if (!this.compiler)
this.prepare();
// 判断 watch 状态下第一次编译结束, 用于标记 promise 完成
let firstCompilation = true;
callback = callback || this.makeDefaultCallback();
// 这里额外保存一次 stats
// 原因是,某些 done hook 中提前抛错时, callback 中拿不到 stats
// 无法更加清晰的显示所有错误
let compileStats;
this.compiler.hooks.done.tap({
name: `MorWebpackWrapperBaseErrorHandlerPlugin`,
stage: Number.NEGATIVE_INFINITY
}, function (stats) {
compileStats = stats;
});
return await new Promise((resolve, reject) => {
var _a;
const cb = (err, stats) => {
if (callback)
callback(err, stats || compileStats);
// 首次编译完成后, 确保 promise 可以正常结束
if (firstCompilation) {
err ? reject(err) : resolve(stats || compileStats);
}
firstCompilation = false;
};
if (this.watch) {
const ignored = ignoredToFunction((_a = this.watchOptions) === null || _a === void 0 ? void 0 : _a.ignored);
const shouldWatch = ignoredToFunction(this.watchNodeModules);
const watchOptions = {
...(this.watchOptions || {}),
// watchpack 实际上是支持传入函数
// 但 webpack 本身的配置只允许传入 RegExp 或 string 或 string[]
// 所以这里在实际启动 watch 时通过替换 ignored 配置来实现额外监听
// 一些 node_modules 的手段
ignored(x) {
const shouldIgnore = ignored(x);
if (shouldIgnore === true) {
if (shouldWatch(x))
return false;
}
return shouldIgnore;
}
};
this.compiler.watch(watchOptions, cb);
// 触发 watch 事件
this.emit('watch', watchOptions);
this.watching = this.compiler.watching;
}
else {
this.compiler.run((err, stats) => {
this.compiler.close((err2) => {
let error = err || err2;
// 如果无 fatal 错误, 则检查是否有编译错误
// 并抛错出来
if (stats && stats.hasErrors()) {
error = error || new Error('编译错误, 请检查相关报错信息');
}
cb(error, stats);
});
});
}
});
}
/**
* 生成默认回调函数, 用于优化 stats 信息显示
* @returns 默认回调函数
*/
makeDefaultCallback() {
return function (_, stats) {
if (!stats)
return;
if (!stats.hasErrors())
return;
const info = stats.toJson();
const listedErrors = new Set();
info.errors.forEach((errMsg) => {
const moduleName = (errMsg === null || errMsg === void 0 ? void 0 : errMsg.moduleName) || (errMsg === null || errMsg === void 0 ? void 0 : errMsg.moduleId);
const message = moduleName
? `编译 ${moduleName} 失败:\n=> ${errMsg.message}`
: errMsg.message;
// 避免相同错误重复打印
if (!listedErrors.has(message)) {
listedErrors.add(message);
logger_1.logger.error(message, { error: errMsg });
}
});
};
}
}
exports.WebpackWrapper = WebpackWrapper;
/**
* Webpack 插件, 提供如下能力
* 1. hooks.webpackWrapper 的 hook 触发
* 2. 提供 webpackChain 配置支持
*/
class WebpackPlugin {
constructor() {
this.name = 'MorWebpackPlugin';
}
apply(runner) {
// 在插件完成加载之后触发 webpackWrapper hook
runner.hooks.initialize.tap(this.name, function () {
const webpackWrapper = new WebpackWrapper();
WebpackWrapperMap.set(runner, webpackWrapper);
runner.hooks.webpackWrapper.call(webpackWrapper);
});
// 注册 webpackChain 方法支持
runner.hooks.registerUserConfig.tap(this.name, (schema) => {
return schema.merge(WebpackConfigSchema);
});
runner.hooks.modifyUserConfig.tap({
name: this.name,
stage: Number.MAX_SAFE_INTEGER
}, (userConfig) => {
if (userConfig.watchNodeModules && userConfig.cache === true) {
logger_1.logger.warnOnce(`已开启 watchNodeModules 配置, 自动禁用 cache`);
userConfig.cache = false;
}
return userConfig;
});
runner.hooks.beforeRun.tapPromise(this.name, async () => {
const userConfig = runner.userConfig;
const webpackWrapper = WebpackWrapperMap.get(runner);
if (typeof (userConfig === null || userConfig === void 0 ? void 0 : userConfig.webpackChain) === 'function') {
await userConfig.webpackChain(webpackWrapper.chain);
}
if (userConfig === null || userConfig === void 0 ? void 0 : userConfig.watchNodeModules) {
webpackWrapper.watchNodeModules = userConfig.watchNodeModules;
}
});
// 关闭时自动清理 compiler 和 watching
runner.hooks.shutdown.tapPromise(this.name, async () => {
const wrapper = WebpackWrapperMap.get(runner);
await Promise.all([
// 关闭 compiler
new Promise(function (resolve, reject) {
if (wrapper === null || wrapper === void 0 ? void 0 : wrapper.compiler) {
wrapper.compiler.close(function (err) {
if (err)
return reject(err);
resolve();
});
}
else {
resolve();
}
}),
// 关闭 watching
new Promise(function (resolve, reject) {
if (wrapper === null || wrapper === void 0 ? void 0 : wrapper.watching) {
wrapper.watching.close(function (err) {
if (err)
return reject(err);
resolve();
});
}
else {
resolve();
}
})
]);
});
}
}
exports.WebpackPlugin = WebpackPlugin;
//# sourceMappingURL=webpack.js.map