template-syncer
Version:
智能模板同步工具 - 让你的项目与模板仓库保持同步,支持智能合并、差异对比和交互式更新
220 lines (219 loc) • 8.4 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.Scanner = void 0;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const glob_1 = require("glob");
const minimatch_1 = require("minimatch");
const platform_1 = require("./platform");
const categorizer_1 = require("./categorizer");
let native = null;
try {
native = require('../../native/index');
}
catch {
// fallback to pure JS
}
const DEFAULT_IGNORE = [
'node_modules/**',
'.git/**',
'dist/**',
'build/**',
'out/**',
'.output/**',
'coverage/**',
'.nyc_output/**',
'.cache/**',
'.nuxt/**',
'.next/**',
'.svelte-kit/**',
'.vercel/**',
'.netlify/**',
'logs/**',
'*.log',
'.DS_Store',
'Thumbs.db',
'*.tmp',
'*.temp',
'*.swp',
'*~',
'.template-sync.json'
];
class Scanner {
constructor(customIgnore = [], customCategories = []) {
this.ignorePatterns = [...DEFAULT_IGNORE, ...customIgnore];
this.categorizer = new categorizer_1.Categorizer(customCategories);
}
async scan(dir) {
if (!fs.existsSync(dir))
throw new Error(`目录不存在: ${dir}`);
const files = await (0, glob_1.glob)('**/*', {
cwd: dir,
ignore: this.ignorePatterns,
dot: true,
nodir: true
});
// 并发处理所有文件的 stat + 分类
const results = await Promise.allSettled(files.map(async (file) => {
const normalizedPath = file.replace(/\\/g, '/');
const fullPath = path.join(dir, file);
const stat = await fs.promises.stat(fullPath);
if (!stat.isFile())
return null;
const { category, icon } = this.categorizer.categorize(normalizedPath);
return {
path: normalizedPath,
fullPath,
size: stat.size,
isBinary: platform_1.platform.isBinary(fullPath),
category,
icon
};
}));
return results
.filter((r) => r.status === 'fulfilled' && r.value !== null)
.map(r => r.value)
.sort((a, b) => a.path.localeCompare(b.path));
}
async compare(templateDir, currentDir, detectOrphans = false) {
// 优先使用 Rust native 实现
if (native) {
return this.compareNative(templateDir, currentDir, detectOrphans);
}
return this.compareJs(templateDir, currentDir, detectOrphans);
}
/** Rust native 实现:并行 hash 比较 */
async compareNative(templateDir, currentDir, detectOrphans) {
const ignoreNames = ['node_modules', '.git', 'dist', 'build', '.cache', '.template-sync.json'];
const diffs = native.compareDirs(templateDir, currentDir, ignoreNames, detectOrphans);
// 并发补充 FileChange 所需的 category/icon/size 信息
const results = await Promise.allSettled(diffs.map(async (diff) => {
const isDeleted = diff.status === 'deleted';
const basePath = isDeleted ? currentDir : templateDir;
const fullPath = path.join(basePath, diff.path);
let size = 0;
try {
size = (await fs.promises.stat(fullPath)).size;
}
catch { }
const { category, icon } = this.categorizer.categorize(diff.path);
return {
path: diff.path,
fullPath,
size,
isBinary: platform_1.platform.isBinary(fullPath),
category,
icon,
status: diff.status,
templatePath: isDeleted ? '' : fullPath,
currentPath: isDeleted ? fullPath : path.join(currentDir, diff.path),
selected: diff.status !== 'deleted',
};
}));
return results
.filter((r) => r.status === 'fulfilled' && r.value !== null)
.map((r) => r.value);
}
/** 纯 JS fallback 实现 */
async compareJs(templateDir, currentDir, detectOrphans) {
const [templateFiles, currentFiles] = await Promise.all([
this.scan(templateDir),
this.scan(currentDir)
]);
const currentMap = new Map(currentFiles.map(f => [f.path, f]));
const templateMap = new Map(templateFiles.map(f => [f.path, f]));
// 并发检查所有模板文件的变更状态
const changeResults = await Promise.allSettled(templateFiles.map(async (templateFile) => {
const currentFile = currentMap.get(templateFile.path);
const templatePath = templateFile.fullPath;
const currentPath = path.join(currentDir, templateFile.path);
if (!currentFile) {
return { ...templateFile, status: 'new', templatePath, currentPath, selected: true };
}
const changed = await this.hasChanged(templatePath, currentFile.fullPath);
if (changed) {
return { ...templateFile, status: 'modified', templatePath, currentPath: currentFile.fullPath, selected: true };
}
return null;
}));
const changes = changeResults
.filter((r) => r.status === 'fulfilled' && r.value !== null)
.map(r => r.value);
if (detectOrphans) {
for (const currentFile of currentFiles) {
if (!templateMap.has(currentFile.path)) {
changes.push({
...currentFile,
status: 'deleted',
templatePath: '',
currentPath: currentFile.fullPath,
selected: false
});
}
}
}
return changes;
}
async hasChanged(file1, file2) {
try {
const [stat1, stat2] = await Promise.all([
fs.promises.stat(file1),
fs.promises.stat(file2)
]);
if (stat1.size !== stat2.size)
return true;
const [buf1, buf2] = await Promise.all([
fs.promises.readFile(file1),
fs.promises.readFile(file2)
]);
return !buf1.equals(buf2);
}
catch {
return true;
}
}
filterOrphans(orphans, deletePatterns = ['**'], protectPatterns = []) {
return orphans.filter(file => {
const isProtected = protectPatterns.some(p => (0, minimatch_1.minimatch)(file.path, p, { dot: true, matchBase: true }));
if (isProtected)
return false;
return deletePatterns.some(p => p === '**' || (0, minimatch_1.minimatch)(file.path, p, { dot: true, matchBase: true }));
});
}
addIgnore(pattern) { this.ignorePatterns.push(pattern); }
addCategory(rule) { this.categorizer.addRule(rule); }
}
exports.Scanner = Scanner;