import path from 'node:path'; import fs from 'node:fs'; import { Config, SyncList, SyncConfigType, SyncConfig } from './type.ts'; import { fileIsExist } from '@/uitls/file.ts'; import { getHash } from '@/uitls/hash.ts'; import glob from 'fast-glob'; import { isMatch } from 'micromatch'; import { logger } from '@/module/logger.ts'; import { normalizeScriptPath } from "@/uitls/file.ts"; export type SyncOptions = { dir?: string; configFilename?: string; baseURL?: string; }; const checkAuth = (value: string = '', baseURL: string = '') => { if (value.startsWith(baseURL)) { return true; } return false; }; const DEFAULT_IGNORE = ['node_modules/**', '.git/**', '.next/**', '.astro/**', '.pack-dist/**']; export class SyncBase { config: Config; #filename: string; #dir: string; baseURL: string; defaultIgnore: string[] = DEFAULT_IGNORE; constructor(opts?: SyncOptions) { const filename = opts?.configFilename || 'kevisual.json'; const dir = opts?.dir || process.cwd(); this.#filename = filename; this.#dir = path.resolve(dir); this.baseURL = opts?.baseURL ?? ''; this.init(); } get dir() { return this.#dir; } get configFilename() { return this.#filename; } get configPath() { return path.join(this.#dir, this.#filename); } async init() { try { const dir = this.#dir; const filename = this.#filename; const filepath = path.join(dir, filename); if (!fileIsExist(filepath)) throw new Error('config file not found'); const config = JSON.parse(fs.readFileSync(filepath, 'utf-8')); const sync = config.sync || {}; const keys = Object.keys(sync); const newConfigSync: any = {}; for (let key of keys) { const keyPath = path.join(dir, key); const newKey = path.relative(dir, keyPath); newConfigSync[newKey] = sync[key]; } config.sync = newConfigSync; this.config = config; return config; } catch (err) { this.config = {} as Config; return {} as Config; } } getRelativePath(filename?: string) { if (!filename) return false; const dir = this.#dir; const file = path.join(dir, filename); const realFilename = path.basename(filename); return { relative: path.relative(dir, file), absolute: file, filename: realFilename }; } /** * * @param syncType * @param type * @returns */ async canDone(syncType: SyncConfigType, type?: SyncConfigType) { if (syncType === 'sync') return true; return syncType === type; } getIngore(ignore: string[] = []) { const defaultIgnore = [...this.defaultIgnore, ...ignore]; const set = new Set(defaultIgnore); return new Array(...set); } getMatchList(opts?: { matchList?: string[]; ignore: string[]; matchObjectList?: { path: string;[key: string]: any }[] }) { const { matchList = [], ignore = [], matchObjectList = [] } = opts || {}; const _ignore = this.getIngore(ignore); const _matchList = matchList.filter((file) => !isMatch(file, _ignore)); const _matchObjectList = matchObjectList.filter((item) => !isMatch(item.path, _ignore)); return { matchList: _matchList, matchObjectList: _matchObjectList }; } /** * * @param opts * @param opts.getFile 是否检测文件是否存在 * @returns */ async getSyncList(opts?: { getFile?: boolean }): Promise { const config = this.config!; let sync = config?.sync || {}; const syncDirectory = await this.getSyncDirectoryList(); sync = this.getMergeSync(sync, syncDirectory.sync); const syncKeys = Object.keys(sync); const baseURL = this.baseURL; const syncList = syncKeys.map((key) => { const value = sync[key]; const filepath = path.join(this.#dir, key); // 文件的路径 if (filepath.includes('node_modules') || filepath.includes('.git')) { return null; } if (typeof value === 'string') { const auth = checkAuth(value, baseURL); const type = auth ? 'sync' : 'none'; return { key, type: type as any, filepath, url: value, auth, }; } const auth = checkAuth(value.url, baseURL); const type = auth ? 'sync' : 'none'; return { key, filepath, ...value, type: value?.type ?? type, auth: checkAuth(value.url, baseURL), }; }).filter((item) => item); let resultSyncList: SyncList[] = [] if (opts?.getFile) { resultSyncList = await this.getSyncListFile(syncList); } else { resultSyncList = syncList; } return resultSyncList; } async getCheckList() { const checkDir = this.config?.clone || {}; const dirKeys = Object.keys(checkDir); const registry = this.config?.registry || ''; const files = dirKeys.map((key) => { return { key, ...this.getRelativePath(key) }; }); return files .map((item) => { if (!item) return; let url = checkDir[item.key]?.url || registry; let auth = checkAuth(url, this.baseURL); return { key: item.key, ...checkDir[item.key], url: url, filepath: item?.absolute, auth, }; }) .filter((item) => item); } /** * sync 是已有的,优先级高于 fileSync * * @param sync * @param fileSync * @returns */ getMergeSync(sync: Config['sync'] = {}, fileSync: Config['sync'] = {}) { const syncFileSyncKeys = Object.keys(fileSync); const syncKeys = Object.keys(sync); const config = this.config!; const registry = config?.registry; const keys = [...syncKeys, ...syncFileSyncKeys]; const obj: Config['sync'] = {}; const wrapperRegistry = (value: SyncConfig | string) => { if (typeof value === 'object') { const url = value.url; if (registry && !url.startsWith('http')) { return { ...value, url: registry.replace(/\/+$/g, '') + '/' + url.replace(/^\/+/g, ''), }; } return value; } const url = value; if (registry && !url.startsWith('http')) { return registry.replace(/\/+$/g, '') + '/' + url.replace(/^\/+/g, ''); } return url; } for (let key of keys) { const value = sync[key] ?? fileSync[key]; obj[key] = wrapperRegistry(value); } return obj; } async getSyncDirectoryList() { const config = this.config; const syncDirectory = config?.syncd || []; let obj: Record = {}; const keys: string[] = []; for (let item of syncDirectory) { const { registry, ignore = [], files = [], replace = {}, metadata } = item; const cwd = this.#dir; const glob_files = await glob(files, { ignore: this.getIngore(ignore), onlyFiles: true, cwd, dot: true, absolute: true, }); const registyURL = registry || config.registry; if (!registyURL) { logger.error('请配置 registry', item); continue; } for (let file of glob_files) { const key = path.relative(cwd, file); const _registryURL = new URL(registyURL); const replaceKeys = Object.keys(replace); let newKey = key; for (let replaceKey of replaceKeys) { const _replaceKey = normalizeScriptPath(replaceKey); if (newKey.startsWith(_replaceKey)) { newKey = key.replace(_replaceKey, replace[replaceKey]); } } const pathname = path.join(_registryURL.pathname, newKey); _registryURL.pathname = pathname; keys.push(key); obj[key] = { url: _registryURL.toString() }; if (metadata) { obj[key] = { ...obj[key], metadata }; } } } return { sync: obj, keys }; } /** * 获取文件列表,检测文件是否存在 * @param syncList * @returns */ async getSyncListFile(syncList: SyncList[]) { let syncListFile: SyncList[] = []; for (let item of syncList) { const { filepath, auth } = item; if (filepath && fileIsExist(filepath) && auth) { syncListFile.push({ ...item, exist: true, hash: getHash(filepath), }); } else { syncListFile.push({ ...item, exist: false }); } } return syncListFile; } getHash = getHash; async getDir(filepath: string, check = false) { const dir = path.dirname(filepath); if (check) { if (!fileIsExist(dir)) { fs.mkdirSync(dir, { recursive: true }); } } return dir; } async download() { // const syncList = await this.getSyncList(); // for (const item of syncList) { // } } async upload() { // need check permission } }