file-lane
Version:
File conversion tool, can be one-to-one, one to N, N to one
544 lines (527 loc) • 15.7 kB
JavaScript
;
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;