UNPKG

goldpankit

Version:

GoldPanKit是一款极速研发套件,可在套件中快速构建各类技术框架和项目。开源作者可发布自己的项目,并为其设定金额,实现开源项目的盈利。

569 lines (568 loc) 25.1 kB
const fs = require('fs') const path = require('path') const root = process.cwd() const diffExp = require('./diff-express') const Const = require('../constants/constants') const ignore = require('ignore') const log = require('../utils/log') module.exports = { getRuntimeRoot() { return root }, // 重命名 rename (oldPath, newPath) { fs.renameSync(oldPath, newPath) }, // 清空目录 clearDir (dir) { this.deleteDirectory(dir, true) this.createDirectory(dir) }, // 获取文件列表 getFiles (dir) { dir = path.normalize(dir) return fs.readdirSync(dir) }, // 删除代码文件,卸载时调用 deleteFiles (files, project, service) { log.debug(`${project.name}: preparing to delete ${files.length} files.`) const diffFiles = [] for (const file of files) { // 排除掉已删除的文件 if (file.operaType === 'DELETED') { continue } const relativePath = file.filepath const filepath = path.join(project.codespace, relativePath) // 删除文件 if (file.filetype !== 'DIRECTORY') { let content = file.content // 读取本地文件(本地文件可能不存在,即使在卸载的情况,也有可能是纯删除语句反向后的纯增语句,从而让卸载产生新的文件) let fileInfo = null const fileExists = this.exists(filepath) if (fileExists) { fileInfo = this.readFile(filepath) } // 如果内容为差异表达式 if (diffExp.isDiffEllipsis(content)) { // 获取本地文件的内容,如本地文件不存在,则视为空(纯增情况本地文件可能不存在) let localFileContent = '' if (fileInfo != null) { localFileContent = fileInfo.content } const revertMergeResult = diffExp.revertMerge(content, localFileContent) content = revertMergeResult.content // 如果反向合并失败,则将差异表达式写入本地内容顶部 if (!revertMergeResult.success) { // 合并失败了,本地文件还不存在的情况,直接忽略 if (!fileExists) { continue } // 本地文件内容 = 表达式+本地文件内容 localFileContent = `[AUTOMERGE]\nThe following is the logic for merging the code, \nbut we are currently unable to merge according to this logic. \nPlease manually perform the merge.\n\n${revertMergeResult.errorExpress}\n\n${localFileContent}` } // 如果合并的结果与本地一致,直接忽略 if (content.trim() === localFileContent.trim()) { continue } // 合并后的内容为空 && 本地不存在该文件,直接忽略 if (content.trim() === '' && !fileExists) { continue } // 合并后的内容为空 && 本地存在该文件,则做删除 if (content.trim() === '' && fileExists) { file.operaType = 'DELETED' } // 合并后的内容不为空 && 本地不存在该文件,则标记为新增 if (content.trim() !== '' && !fileExists) { file.operaType = 'ADD' } // 加入差异队列 file.content = content file.localContent = localFileContent file.contentEncode = 'utf-8' diffFiles.push(file) continue } // 本地存在在该文件,做删除处理 if (fileExists) { // 将文件标记为“已删除”(指的是在新的服务或插件中代码中已被删除,并不是本地已被删除)并填充本地内容,加入差异文件队列 file.operaType = 'DELETED' file.localContent = '' if (fileInfo != null) { file.localContent = fileInfo.content } diffFiles.push(file) } } } return diffFiles }, /** * 写入代码文件 * @param files 需要写入的文件 * @param project 写入的项目 * @param service 安装的服务,只有安装时才有 * @param versionPath 安装的版本路径,如[1.0.0, 1.0.1] */ writeFiles (files, project, service = null, versionPath = []) { const currentFiles = this.getFiles(project.codespace) // 判断项目中是否存在文件 const isNotEmptyProject = currentFiles.filter(file => file !== Const.PROJECT_CONFIG_FILE && file !== Const.PROJECT_DATABASE_CONFIG_FILE).length > 0 log.debug(`${project.name}: prepare to process ${files.length} files.`) const diffFiles = [] let writeFileCount = 0 /* 如果项目中不存在文件,直接写入 */ if (!isNotEmptyProject) { log.debug(`${project.name}:project is empty, and the file contents are written directly.`) for (let i = 0; i < files.length; i++) { const file = files[i] // 目录,不做处理 if (file.filetype === 'DIRECTORY') { log.debug(`${project.name}: ${i}. ${file.filepath} is directory,ignored.`) log.traceFile(file, `is directory,ignored.`) continue } // 已删除的文件,不做处理 if (file.operaType === 'DELETED') { log.debug(`${project.name}: ${i}. ${file.filepath} is 'DELETED' file,ignored.`) log.traceFile(file, `is 'DELETED' file,ignored.`) continue } // 获取相对路径 const relativePath = file.filepath // kit.json和kit.db.json为项目配置文件,不允许操作 if (relativePath === Const.PROJECT_CONFIG_FILE || relativePath === Const.PROJECT_DATABASE_CONFIG_FILE) { continue } // 获取写入文件路径 const filepath = path.join(project.codespace, relativePath) // 写入文件 this.writeFileWithForce(filepath, file, () => { log.debug(`${project.name}: ${i}. ${file.filepath} is written.`) log.traceFile(file, ` is written.`) writeFileCount++ }) } } /* 项目中存在文件(要么是升级框架,要么是插件的安装、升级、卸载) 1. 对于删除状态的文件,如果本地存在,则视为删除,并加入差异队列;同时,如果文件中还存在相同文件的ADD操作,则忽略; 2. 对于最新内容为空的文件,如果本地存在,则视为删除,并加入差异队列 3. 对于最新文件,如果本地存在 && 为差异表达式,则做合并处理,并加入差异队列;否则做覆盖处理,并加入比差异队列 4. 对于最新文件,如果本地不存在 && 为差异表达式,则直接忽略;否则做新增处理,并加入比差异队列 */ else { log.debug(`${project.name}:project is not empty, will run the diff-express processor...`) for (let i = 0; i < files.length; i++) { const file = files[i] // 获取文件路径 const relativePath = file.filepath const filepath = path.join(project.codespace, relativePath) // 目录,不做处理 if (file.filetype === 'DIRECTORY' || (this.exists(filepath) && this.isDirectory(filepath))) { log.debug(`${project.name}: ${i}. ${filepath} is directory,ignored.`) continue } // kit.json为项目配置文件,不允许操作 if (relativePath === Const.PROJECT_CONFIG_FILE || relativePath === Const.PROJECT_DATABASE_CONFIG_FILE) { continue } // 获取文件信息,此处判断文件是否存在,需要区分大小写进行判断 const fileExists = this.exists(filepath, false) let localFile = null if (fileExists) { localFile = this.readFile(filepath) } // 已删除的文件,如果在本地找到文件,则加入差异队列 if (file.operaType === 'DELETED') { log.debug(`${project.name}${i}. ${filepath} will be deleted.`) log.traceFile(file, `will be deleted.`) if (fileExists) { /* 情况1: 一个插件版本可能对同一个文件即存在删除,又存在新增。 例如文件使用了变量A来作为文件名,此时修改文件名使用了变量B,且变量A与变量B产生的结果是一致的,则编译文件列表中会存在删除了该文件又新增了该文件。 此时新增文件和删除文件的顺序是不一定的,但是他们的serviceVersionId一定是一样的。此时删除动作应该被忽略 */ const anotherFileIndex = files.findIndex(f => f.filepath === relativePath && f.serviceVersionId === file.serviceVersionId && f.operaType !== 'DELETED') if (anotherFileIndex !== -1) { const anotherFile = files[anotherFileIndex] log.debug(`${project.name}${i}. ${relativePath} exists another file operation, deletion is stopped.`) log.traceFile(file, `exists another file operation, deletion is stopped.`) log.traceFile(file, ` current file index: ${i}`) log.traceFile(file, ` current file path: ${relativePath}`) log.traceFile(file, ` current file version: ${file.version}`) log.traceFile(file, ` another file index: ${anotherFileIndex}`) log.traceFile(file, ` another file path: ${anotherFile.filepath}`) log.traceFile(file, ` another file version: ${anotherFile.version}`) log.traceFile(file, ` another file opera type: ${anotherFile.operaType}`) continue } /* 情况2:一个插件存在多个版本(serviceVersionId不一样),且前面的版本出现了删除,后面的版本又出现了新增或者修改(如果出现了修改,可能是数据不正确), 那么删除动作应该被忽略。 */ const anotherVersionFileIndex = files.findIndex((f, index) => { if (index <= i) { return false } return f.filepath === relativePath && f.operaType !== 'DELETED' && f.serviceVersionId !== file.serviceVersionId }) if (anotherVersionFileIndex !== -1) { const anotherVersionFile = files[anotherVersionFileIndex] log.debug(`${project.name}${i}. ${relativePath} exists another version file operation, deletion is stopped.`) log.traceFile(file, `exists another version operation, deletion is stopped.`) log.traceFile(file, ` current version file index: ${i}`) log.traceFile(file, ` current version file path: ${relativePath}`) log.traceFile(file, ` current version file version: ${file.version}`) log.traceFile(file, ` another version file index: ${anotherVersionFileIndex}`) log.traceFile(file, ` another version file path: ${anotherVersionFile.filepath}`) log.traceFile(file, ` another version file version: ${anotherVersionFile.version}`) log.traceFile(file, ` another version file opera type: ${anotherVersionFile.operaType}`) continue } // 填充本地内容并加入差异队列 log.debug(`${project.name}${i}. ${filepath} joined the delete-diff-queue.`) log.traceFile(file, `joined the delete-diff-queue.`) // - 已删除的文件,contentEncode可能为null,需要填充,不填充会导致合并时无法判断预览 file.contentEncode = localFile.encode file.localContent = localFile.content diffFiles.push(file) } continue } // 如果最新内容为空,且本地存在该文件,则加入差异队列 if (file.content == null || file.content.trim() === '') { if (fileExists) { file.localContent = localFile.content // 文件操作类型调整为删除 file.operaType = 'DELETED' diffFiles.push(file) } continue } /* 文件在项目中存在(冲突文件) 1. 如果文件内容为差异表达式,则进行合并,并加入差异队列 2. 如果文件内容不为差异表达式,则直接加入差异队列 */ if (fileExists) { // 差异表达式 if (diffExp.isDiffEllipsis(file.content)) { log.debug(`${project.name}: ${i}. ${filepath} is diff-express,auto-merging...`) log.traceFile(file, `is diff-express,auto-merging...`) // 合并 const mergeResult = diffExp.merge(file.content, localFile.content) // 合并成功 if (mergeResult.success) { // 合并后的结果为空,加入删除队列 if (mergeResult.content.trim() === '') { log.debug(`${project.name}: ${i}. ${filepath} auto-merge result is blank,joined the delete-diff-queue.`) log.traceFile(file, `auto-merge result is blank,joined the delete-diff-queue.`) file.localContent = localFile.content // 文件操作类型调整为删除 file.operaType = 'DELETED' diffFiles.push(file) continue } // 合并成功 && 存在最新内容 file.content = mergeResult.content file.localContent = localFile.content file.operaType = 'MODIFIED' // 本地内容 != 最新内容,则加入差异队列 if (file.localContent !== file.content) { log.debug(`${project.name}: ${i}. ${filepath} auto-merge successfully,joined the diff-queue.`) log.traceFile(file, `auto-merge successfully,joined the diff-queue.`) diffFiles.push(file) } else { log.debug(`${project.name}: ${i}. ${filepath} auto-merge successfully,but the new content equals the local content,ignored.`) log.traceFile(file, `auto-merge successfully,but the new content equals the local content,ignored.`) } } // 合并失败 else { log.debug(`${project.name}: ${i}. ${filepath} auto-merge failed.`) log.traceFile(file, `auto-merge failed.`) file.operaType = 'MODIFIED' // 最新内容=合并后的内容(虽然合并失败,但并不是全部失败,能合并的还是会自动合并,不能合并的表达式通过错误表达式字段返回,在本地内容展示) file.content = mergeResult.content // 本地内容=差异表达式+本地内容 file.localContent = `[AUTOMERGE]\nThe following is the logic for merging the code, \nbut we are currently unable to merge according to this logic. \nPlease manually perform the merge.\n\n${mergeResult.errorExpress}\n\n${localFile.content}` diffFiles.push(file) } } // 非差异表达式 else { file.localContent = localFile.content file.operaType = 'MODIFIED' // 本地内容 != 最新内容,则加入差异列表 if (file.localContent !== file.content) { log.debug(`${project.name}: ${i}. ${filepath} has bean overwritten and joined the diff-queue.`) log.traceFile(file, `has bean overwritten and joined the diff-queue.`) diffFiles.push(file) } else { log.debug(`${project.name}: ${i}. ${filepath} content equals the local content,ignored.`) log.traceFile(file, `content equals the local content,ignored.`) } } } /* 文件在项目中不存在(新文件) 1. 如果文件内容为差异表达式,则忽略 2. 如果文件内容不为差异表达式,则直接加入差异队列 */ else { // 非差异表达式,直接加入差异队列 if (!diffExp.isDiffEllipsis(file.content)) { log.debug(`${project.name}: ${i}. ${filepath} is new file,joined diff-queue.`) log.traceFile(file, `is new file,joined diff-queue.`) file.operaType = 'ADD' diffFiles.push(file) } else { log.debug(`${project.name}: ${i}. ${filepath} is diff-express,and can't found in the local,ignored.`) log.traceFile(file, `is diff-express,and can't found in the local,ignored.`) } } } } // 给出文件写入提醒 if (writeFileCount > 0 && service != null) { log.success(`${service}: write ${writeFileCount} files to ${project.codespace} successfully.`) } return diffFiles }, /** * 强制写入文件 * @param filepath 文件路径 * @param file 文件对象 */ writeFileWithForce (filepath, file, callback) { if (file.contentEncode === 'base64') { this.createFile(filepath, Buffer.from(file.content, 'base64'), true) callback && callback() return } // - 项目中不存在文件,且为文本文件,直接写入到项目中 this.createFile(filepath, file.content, true) callback && callback() }, /** * 获取文件和子文件 * @param absolutePath 绝对路径 * @param parentIgnoreInstance 上级文件忽略实例,用于在当前目录下没有配置.gitignore时使用 * @returns {*[]} */ getFilesWithChildren (absolutePath, parentIgnoreInstance = null) { /* 创建ignore实例 如果当前目录下没有.gitignore文件配置,则以父级的ignore实例作为当前目录的ignore实例 */ let ignoreInstance = parentIgnoreInstance const ignoreFileConfig = this.getIgnoreFileConfig(absolutePath) if (ignoreInstance == null || ignoreFileConfig.ignoreFileConfig != null) { ignoreInstance = ignore().add(ignoreFileConfig.all) } let filePool = []; const files = fs.readdirSync(absolutePath); files.forEach(file => { const fullpath = path.join(absolutePath, file) // 忽略目录,目录需要在路径后增加'/' if (this.isDirectory(fullpath)) { if (ignoreInstance.ignores(file + '/')) { return } } // 忽略文件 if (ignoreInstance.ignores(file)) { return } // 全路径 filePool.push(fullpath); if (this.isDirectory(fullpath)) { const subFiles = this.getFilesWithChildren(fullpath, ignoreInstance); filePool = filePool.concat(subFiles); } }) return filePool }, isDirectory(filepath) { return fs.statSync(filepath).isDirectory() }, isFile(filepath) { return fs.statSync(filepath).isFile() }, /** * 读取文件 * @param filepath * @returns {string} */ readFile(filepath) { try { const buffer = fs.readFileSync(filepath) const encode = this.getContentEncode(buffer) return { encode, content: buffer.toString(encode) } } catch (e) { log.error(`read file ${filepath} failed`, e) } }, readAddFileArray (codespace) { const filepath = path.join(codespace, '.kit.addfile') if (!this.exists(filepath)) { return [] } return fs.readFileSync(filepath).toString().split('\n') }, readJSONFile(filepath) { if (!this.exists(filepath)) { return null } // 处理有文件但内容为空的情况 const fileContent = fs.readFileSync(filepath).toString() if (fileContent.trim() === '') { return {} } return JSON.parse(fs.readFileSync(filepath).toString()) }, createDirectory(filepath, force = false) { if (force) { /* 忽略大小写进行删除,场景说明如下: 某框架修改了目录名称,从Test转为了test并发布了新的版本,产生了如下两条记录 1. 新增test目录 在macos和windows下,都会认为Test目录就是test目录,因为默认情况下文件操作不区分大小写,于是并不会产生新的test目录; 2. 删除Test目录 此时就导致了没有产生新的test目录,旧的Test目录又被删除,升级动作缺少了整个目录 因此,此处删除目录,需要忽略大小写进行删除,则可以得到 1. 新增test目录(删除了Test目录,新增了test目录) 2. 删除Test目录(删除动作默认都是区分大小写的,所以会因为找不到Test目录而忽略删除动作) */ this.deleteDirectory(filepath, force, true) } if (!this.exists(filepath)) { fs.mkdirSync(filepath, {recursive: true}) log.debug(`+ created directory: ${filepath}`) } }, getDirectory(filepath) { return path.dirname(filepath) }, deleteDirectory(filepath, force = false, ignoreCase = false) { if (this.exists(filepath, ignoreCase)) { fs.rmdirSync(filepath, { recursive: force }) log.debug(`- deleted directory: ${filepath}`) } }, deleteFile(filepath, ignoreCase = false) { // 删除文件动作默认需要严格匹配文件路径和名称的大小写才可删除 if (this.exists(filepath, ignoreCase)) { fs.unlinkSync(filepath) log.debug(`- deleted file: ${filepath}`) } }, createFile(filepath, content, force = false) { if (force) { /* 忽略大小写进行删除,场景说明如下: 某框架修改了文件名称,从a.txt转为了A.txt并发布了新的版本,产生了如下两条记录 1. 新增A.txt 在macos和windows下,都会认为a.txt就是A.txt,因为默认情况下文件操作不区分大小写,于是A.txt的内容会写入a.txt中,并不会产生新的A.txt文件; 2. 删除a.txt 此时就导致了没有产生新的A.txt文件,旧的a.txt在写入A.txt内容后又被删除,升级动作缺少了文件! 因此,此处删除文件,需要忽略大小写进行删除,则可以得到 1. 新增A.txt(删除了旧的a.txt,新增了A.txt) 2. 删除a.txt(删除动作默认都是区分大小写的,所以会因为找不到a.txt而忽略删除动作) */ this.deleteFile(filepath, true) // 获取文件所在目录,如果目录不存在,则创建目录 const directory = this.getDirectory(filepath) if (!this.exists(directory)) { this.createDirectory(directory) } } fs.writeFileSync(filepath, content) log.debug(`+ created file: ${filepath}`) }, rewrite (filepath, content) { this.createFile(filepath, content, true) }, /** * 关于ignoreCase,参考createFile的场景说明 * * @param filepath 文件路径 * @param ignoreCase 是否忽略大小写 * @returns {boolean} */ exists(filepath, ignoreCase = true) { const exists = fs.existsSync(filepath) // 如果忽略大小写进行判断,则可以直接返回,因为existsSync本身就是忽略大小写判断 if (ignoreCase) { return exists } // 如果不忽略大小写,并且忽略大小写的情况下文件不存在,则直接返回false if (!exists) { return false } // 获取原始文件路径进行大小写对比 const realpath = fs.realpathSync.native(filepath) return realpath === filepath }, isEmptyDirectory(filepath) { return fs.readdirSync(filepath).length === 0 }, getFilename(filepath) { return path.basename(filepath) }, /** * 获取文件编码, */ getContentEncode (buffer) { if(buffer.toString('utf-8').indexOf('�') !== -1) { return 'base64' } return 'utf-8' }, toJSONFileString (json, space = 0) { return JSON.stringify(json, null, space) }, // 获取相对路径 getRelativePath(path, parentPath) { let relativePath = this.toLinuxPath(path).replace(this.toLinuxPath(parentPath), '') if (relativePath.startsWith('/')) { relativePath = relativePath.substring(1) } return relativePath }, // 转换为linux路径风格 toLinuxPath (path) { return path.replace(/\\/g, '/') }, /** * 获取忽略文件列表 * @param codespace 代码空间 */ getIgnoreFileConfig (codespace) { const defaultIgnoreFileConfig = `${Const.SERVICE_CONFIG_DIRECTORY}/\n${Const.IGNORE_FILES.join('\n')}` // 直接读取.gitignore文件路径 const ignoreFileConfigPath = path.join(codespace, '.gitignore') if (this.exists(ignoreFileConfigPath)) { const ignoreFileConfig = this.readFile(ignoreFileConfigPath).content return { defaultIgnoreFileConfig, ignoreFileConfig, all: `${defaultIgnoreFileConfig}\n${ignoreFileConfig}` } } return { defaultIgnoreFileConfig, all: defaultIgnoreFileConfig } } }