UNPKG

@morjs/utils

Version:
476 lines 18.2 kB
"use strict"; 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