Files
cli/src/command/sync/modules/base.ts
2026-01-16 01:39:01 +08:00

287 lines
8.6 KiB
TypeScript

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<SyncList[]> {
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<string, any> = {};
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
}
}