UNPKG

file-lane

Version:

File conversion tool, can be one-to-one, one to N, N to one

544 lines (527 loc) 15.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _sharedUtils = require("@aiot-toolkit/shared-utils"); var _chokidar = _interopRequireDefault(require("chokidar")); var _fsExtra = _interopRequireDefault(require("fs-extra")); var _path = _interopRequireDefault(require("path")); var _FileLaneCompilation = _interopRequireDefault(require("./FileLaneCompilation")); var _CompilationEvent = _interopRequireDefault(require("./event/CompilationEvent")); var _FileEvent = _interopRequireDefault(require("./event/FileEvent")); var _FileLaneUtil = _interopRequireDefault(require("./utils/FileLaneUtil")); var _IChangedFile = require("./interface/IChangedFile"); var _FileLaneTriggerType = _interopRequireDefault(require("./enum/FileLaneTriggerType")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } /** * FileLane * * 文件车道,用于将文件进行对应转换,支持1对1、1对N、N对1 * * @example * ``` * new FileLane<IJavascriptCompileOption>( * fileLaneConfig, * projectPath, * compilerOption, * { * onBuildSuccess: (data) => {}, * onBuildError: (data) => {}, * onLog: (logs) => {} * }).start() * ``` * * @description */ class FileLane { // 创建监听实例 /** * 每次编译的数据对象 */ changeFileList = []; triggerCount = 0; building = false; nextBuildParam = null; fileHashCache = {}; /** * 实例化FileLane * @param config fileLane 配置 * @param projectPath 项目路径 * @param compilerOption 编译参数,不同语言的项目具有的参数不同,即使同一项目开发者也会设置不同的参数 * @param events 事件监听器 */ constructor(config, projectPath, compilerOption, events) { this.config = config; this.compilerOption = compilerOption; this.events = events; this.context = _FileLaneUtil.default.createContext(config.output, projectPath); this.addProcessListener(); } addProcessListener() { process.on('beforeExit', this.processBeforeExitHandler); process.on('SIGINT', this.sigintHandler); } /** * 运行 * @param params * @returns */ async start(params) { const errorList = this.validateConfig(); if (errorList && errorList.length) { this.handlerLogs([{ level: _sharedUtils.Loglevel.ERROR, message: `### file-lane ### ${errorList.map((item, index) => `${index + 1}. ${item}`).join('\r\n')}` }], () => { throw new Error(`validate error`); }); return; } await this.complyBeforeWorks(); const fileList = this.collectFile(); if (!fileList || !fileList.length) { return; } if (params?.watch) { this.watch(); } await this.build({ fileList, trigger: _FileLaneTriggerType.default.START }); } validateConfig() { const result = []; // 校验上下文路径,路径是否真实存在,是否为文件夹 const projectPath = this.context.projectPath; if (_fsExtra.default.existsSync(projectPath)) { if (!_fsExtra.default.statSync(projectPath).isDirectory()) { result.push(`ProjectPath is not a folder`); } } else { result.push(`ProjectPath is not a real path`); } if (!this.config) { result.push(`Missing config`); } else { const { output, module } = this.config; if (!output) { result.push(`Config missing output attribute`); } if (!module) { result.push(`Config missing module attribute`); } else { if (!module.rules) { result.push(`module missing rules attribute`); } else if (!module.rules.length) { result.push(`module.rules must have at least one item`); } else { module.rules.forEach((item, index) => { if (!item.test) { result.push(`rules[${index}] missing test attribute`); } if (!item.loader) { result.push(`rules[${index}] missing loader attribute`); } else if (!item.loader.length) { result.push(`rules[${index}]'loader must have at least one item`); } }); } } } return result; } processBeforeExitHandler = () => { this.dispose(); }; sigintHandler = async () => { await this.dispose(); process.exit(); }; async dispose() { if (this.watcher) { this.watcher.removeAllListeners(); await this.watcher.close(); } await this.complyAfterWork(); await this.cleanOutput(); } /** * 触发编译 * * 如果:在编译过程中,则记录统参数,待本次 build 完成后,开始下一次 * 否则:直接触发编译 */ async triggerBuild(param) { if (this.building) { this.nextBuildParam = param; } else { this.build(param); } } /** * * @param fileList 原始文件列表 */ async build(param) { const { fileList, trigger } = param; const { onBuildSuccess, onBuildError, onBuildStart } = this.events || {}; const timeStart = Date.now(); this.initCompilation(trigger); this.building = true; onBuildStart?.(); try { await this.complyBeforeCompile(); this.triggerPlugins(new _CompilationEvent.default(_CompilationEvent.default.PROJECT_START)); for (let item of fileList) { this.triggerPlugins(new _FileEvent.default(_FileEvent.default.FILE_START_COMPILATION, _FileLaneUtil.default.pathToFileParam(item))); await this.buildFile(item); this.triggerPlugins(new _FileEvent.default(_FileEvent.default.FILE_END_COMPILATION, _FileLaneUtil.default.pathToFileParam(item))); } this.triggerPlugins(new _CompilationEvent.default(_CompilationEvent.default.PROJECT_END)); // 执行extra this.triggerPlugins(new _CompilationEvent.default(_CompilationEvent.default.FLLOW_WORK_START)); await this.complyAfterCompile(); this.triggerPlugins(new _CompilationEvent.default(_CompilationEvent.default.FLLOW_WORK_END)); const costTime = Date.now() - timeStart; if (onBuildSuccess) { onBuildSuccess({ costTime, info: this.compilation?.info }); } else { _sharedUtils.ColorConsole.success({ word: 'build success:' }, `${costTime}ms`); } } catch (error) { if (onBuildError) { onBuildError?.(error); } else { _sharedUtils.ColorConsole.throw(`ERROR: `, { word: 'build error' }, `, ${error || 'unknown error'}`); } } finally { this.building = false; if (this.nextBuildParam) { this.build(this.nextBuildParam); this.nextBuildParam = null; } } } initCompilation(trigger) { const { plugins } = this.config; const compilation = new _FileLaneCompilation.default(); compilation.trigger = trigger; compilation.triggerCount = ++this.triggerCount; compilation.info.trigger = trigger; plugins?.forEach(item => { item.compilation = compilation; item.apply(); }); this.compilation = compilation; } async buildFile(filePath) { const fileList = await this.runLoaders(filePath); await this.writeFiles(fileList); } async runLoaders(filePath) { // 拾取要处理的虚拟文件 const { fileCollector } = this.config; const collectedFileList = fileCollector ? fileCollector(filePath).map(item => _FileLaneUtil.default.pathToFileParam(item, true)) : [_FileLaneUtil.default.pathToFileParam(filePath, true)]; // 查找到匹配的 loader const loaderList = this.findLoader(filePath); // 循环loader,转换文件 let result = collectedFileList.concat(); for (let item of loaderList) { // 转换 result 为 string 或流 result.forEach(fileItem => { const { content } = fileItem; if (content) { if (!item.raw && typeof content !== 'string') { fileItem.content = content.toString(); } else if (item.raw && !(content instanceof Buffer)) { fileItem.content = Buffer.from(content); } } }); const loader = new item(); result = await this.runLoader(result, loader); if (loader.logs) { this.handlerLogs(loader.logs, () => { throw new Error(`loader error: ${item.name}`); }); } } return result; } /** * 处理日志 * 1. 触发onLog事件 * 2. 如果有错误日志,则抛出错误 * @param logs * @param onError * @returns */ handlerLogs(logs, onError) { if (!logs?.length) { return; } const onLog = this.events?.onLog; if (onLog) { onLog(logs); } else { logs.forEach(item => { _sharedUtils.ColorConsole.logger(item); }); } const hasError = logs.find(item => item.level && [_sharedUtils.Loglevel.ERROR, _sharedUtils.Loglevel.THROW].includes(item.level)); if (hasError) { onError?.(); } } async writeFiles(fileList) { if (!fileList || !fileList.length) { return; } const buildPath = _FileLaneUtil.default.getOutputPath(this.context); if (!_fsExtra.default.existsSync(buildPath)) { _sharedUtils.FileUtil.mkdirSync(buildPath, { hidden: true }); } return Promise.all(fileList.map(item => { const resolvePath = _path.default.relative(this.context.projectPath, item.path); // 将相对路径与输出路径拼接,形成最新的文件路径 const outputPath = _path.default.join(buildPath, resolvePath); return _fsExtra.default.outputFile(outputPath, Buffer.from(item.content || '')); })); } async runLoader(fileList, loader) { loader.context = this.context; loader.compilerOption = this.compilerOption; return loader.parser(fileList); } /** * 匹配符合条件的loader * @param filePath * @returns */ findLoader(filePath) { const parse = _path.default.basename(filePath); const rules = this.config.module.rules; let loaders = []; for (let rule of rules) { // 1. 检查filePath是否符合规则test if (_sharedUtils.FileUtil.match(parse, rule.test) && _sharedUtils.FileUtil.include(filePath, rule.include, rule.exclude)) { // 2. 符合规则就添加到返回列表中 loaders.push(...rule.loader); } } return loaders; } async triggerPlugins(event) { if (this.compilation) { return this.compilation.dispatch(event); } } /** * start开始时的准备工作 */ async complyBeforeWorks() { const { beforeWorks } = this.config; if (beforeWorks) { for (let item of beforeWorks) { await item(this.context); } } } /** * start结束后的收尾工作 */ async complyAfterWork() { const { afterWorks } = this.config; if (afterWorks) { for (let item of afterWorks) { await item(this.context); } } } /** * 执行项目转换的前置工作 */ async complyBeforeCompile() { const { beforeCompile } = this.config; if (beforeCompile) { for (let item of beforeCompile) { await item({ context: this.context, config: this.config, compilerOption: this.compilerOption, compalition: this.compilation }); } } } /** * 执行项目转换的后续工作 */ async complyAfterCompile() { const { afterCompile } = this.config; if (afterCompile) { for (let item of afterCompile) { try { this.handlerLogs([{ level: _sharedUtils.Loglevel.INFO, message: `afterCompile: ${item.workerDescribe}` }]); await item.worker({ context: this.context, config: this.config, compilerOption: this.compilerOption, compalition: this.compilation, onLog: logs => this.handlerLogs(logs, () => { throw new Error(`${item.workerDescribe} error}`); }) }); } catch (error) { throw new Error(`afterCompile: ${item.workerDescribe} error: ${error}`); } } } } async watch() { // 监听文件变化,并触发 build const onChange = async () => { const fileList = this.config.collectFile ? this.config.collectFile(this.changeFileList) : this.collectFile(); // 开始编译前,记录列表置空 this.changeFileList = []; await this.triggerBuild({ fileList, trigger: _FileLaneTriggerType.default.UPDATE }); }; this.listenFileChange(onChange); } /** * 采集所有要处理的真实文件路径列表 * @param entryFileList * @returns */ collectFile(entryFileList) { // 使用include projectPath获取所有要处理的真实文件 let files = []; if (entryFileList) { entryFileList.forEach(filePath => { if (_sharedUtils.FileUtil.include(filePath, this.config.include, this.config.exclude)) { _sharedUtils.ColorConsole.log(`### file-lane ### file change: ${filePath}`); files.push(filePath); } }); } else { // 1.取出projectPath const projectPath = this.context.projectPath; // 2. 循环文件夹,取出所有匹配的文件路径 files = _sharedUtils.FileUtil.readAlldirSync(projectPath, this.config.include, this.config.exclude); } return files; } /** * 监听文件变化 * @param onChange */ listenFileChange(onChange) { const watcher = _chokidar.default.watch(this.context.projectPath, { ignored: this.getIgnoreConfig() }); const handler = (path, type) => { const { exclude, include } = this.config; // 执行onChange回调 const validFile = _sharedUtils.FileUtil.include(path, include, exclude); if (validFile) { this.changeFileList.push({ path, type }); onChange(); } }; watcher.on('ready', () => { watcher.on('add', path => { // 监听文件添加事件 handler(path, _IChangedFile.HandlerType.ADD); }).on('change', path => { // 监听文件修改事件 handler(path, _IChangedFile.HandlerType.CHANGE); }).on('unlink', filePath => { // 监听文件删除事件 handler(filePath, _IChangedFile.HandlerType.UNLINK); }); }).on('error', error => { // 监听错误 _FileLaneUtil.default.checkError(error.message); watcher.close(); }); if (this.watcher) { this.watcher.close(); } this.watcher = watcher; } /** * 清除输出文件夹 */ async cleanOutput() { return _sharedUtils.FileUtil.del(_FileLaneUtil.default.getOutputPath(this.context)); } /** * 1. 配置的忽略文件夹 * 2. 默认应该忽略的文件及文件夹 * @returns */ getIgnoreConfig() { let ignoreList = []; // 1. if (this.config.watchIgnores) { if (Array.isArray(this.config.watchIgnores)) { ignoreList.push(...this.config.watchIgnores); } else { ignoreList.push(this.config.watchIgnores); } } // 2. 忽略.开头的隐藏文件夹 ignoreList.push(/(^|[\/\\])\../); // 3. 忽略output目录 ignoreList.push(_path.default.join(this.context.projectPath, this.config.output)); return ignoreList; } } var _default = exports.default = FileLane;