Files
FSE-Ticket.sys/scripts/bump-web-asset-version.js
2026-06-21 10:00:13 +08:00

174 lines
5.7 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const projectRoot = path.resolve(__dirname, '..');
const webDir = path.join(projectRoot, 'web');
const versionsPath = path.join(webDir, 'asset-versions.json');
const statePath = path.join(webDir, '.asset-version-state.json');
const usage = `
用法:
node scripts/bump-web-asset-version.js --sync
node scripts/bump-web-asset-version.js --auto
node scripts/bump-web-asset-version.js --bump ai-assistant.js public-status.js
node scripts/bump-web-asset-version.js --bump-all
说明:
--sync 仅按 asset-versions.json 同步所有 HTML 中的 ?v= 引用
--auto 自动检查受管静态资源是否发生变更;有变更则递增对应版本并同步 HTML
--bump 仅递增指定静态资源版本号,并同步 HTML 引用
--bump-all 递增 asset-versions.json 中所有资源版本号,并同步 HTML 引用
`;
const args = process.argv.slice(2);
const hasFlag = (flag) => args.includes(flag);
const localPathPattern = /^\/?[^?#]+\.(?:js|css)(?:\?[^"]*)?$/i;
const readJson = (filePath) => JSON.parse(fs.readFileSync(filePath, 'utf8'));
const writeJson = (filePath, value) => {
fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
};
const readJsonIfExists = (filePath, fallback) => {
if (!fs.existsSync(filePath)) return fallback;
return readJson(filePath);
};
const collectHtmlFiles = (dir) => {
const files = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectHtmlFiles(fullPath));
continue;
}
if (entry.isFile() && entry.name.toLowerCase().endsWith('.html')) {
files.push(fullPath);
}
}
return files;
};
const normalizeAssetName = (value) => path.basename(String(value || '').trim());
const withVersion = (originalPath, version) => {
const [pathname] = originalPath.split('?');
return `${pathname}?v=${version}`;
};
const getManagedAssetStats = (versions) => {
const stats = {};
Object.keys(versions).forEach((assetName) => {
const assetPath = path.join(webDir, assetName);
if (!fs.existsSync(assetPath)) return;
const fileStat = fs.statSync(assetPath);
stats[assetName] = {
size: fileStat.size,
mtimeMs: fileStat.mtimeMs
};
});
return stats;
};
const syncHtmlReferences = (versions) => {
const htmlFiles = collectHtmlFiles(webDir);
const refPattern = /((?:src|href)\s*=\s*")([^"]+\.(?:js|css)(?:\?[^"]*)?)(")/gi;
let changedFiles = 0;
htmlFiles.forEach((filePath) => {
const source = fs.readFileSync(filePath, 'utf8');
const updated = source.replace(refPattern, (match, prefix, assetPath, suffix) => {
if (/^https?:\/\//i.test(assetPath) || !localPathPattern.test(assetPath)) return match;
const assetName = normalizeAssetName(assetPath.split('?')[0]);
const version = versions[assetName];
if (!version) return match;
return `${prefix}${withVersion(assetPath, version)}${suffix}`;
});
if (updated !== source) {
fs.writeFileSync(filePath, updated, 'utf8');
changedFiles += 1;
}
});
return changedFiles;
};
const versions = readJson(versionsPath);
if (hasFlag('--help') || hasFlag('-h') || !args.length) {
console.log(usage.trim());
process.exit(0);
}
if (hasFlag('--bump-all')) {
Object.keys(versions).forEach((key) => {
versions[key] += 1;
});
writeJson(versionsPath, versions);
const changed = syncHtmlReferences(versions);
console.log(`已递增全部资源版本,共 ${Object.keys(versions).length} 项;同步 HTML ${changed} 个文件。`);
process.exit(0);
}
if (hasFlag('--sync')) {
const changed = syncHtmlReferences(versions);
console.log(`已按 asset-versions.json 同步 HTML 引用,共更新 ${changed} 个文件。`);
process.exit(0);
}
if (hasFlag('--auto')) {
const previousState = readJsonIfExists(statePath, {});
const currentState = getManagedAssetStats(versions);
const changedAssets = Object.keys(currentState).filter((assetName) => {
const prev = previousState[assetName];
const curr = currentState[assetName];
if (!prev) return true;
return prev.size !== curr.size || prev.mtimeMs !== curr.mtimeMs;
});
if (!changedAssets.length) {
const synced = syncHtmlReferences(versions);
writeJson(statePath, currentState);
console.log(`未发现静态资源变更;已校准 HTML 引用 ${synced} 个文件。`);
process.exit(0);
}
changedAssets.forEach((assetName) => {
versions[assetName] += 1;
});
writeJson(versionsPath, versions);
writeJson(statePath, currentState);
const synced = syncHtmlReferences(versions);
console.log(`检测到资源变更: ${changedAssets.join(', ')};已自动递增版本并同步 HTML ${synced} 个文件。`);
process.exit(0);
}
const bumpIndex = args.indexOf('--bump');
if (bumpIndex !== -1) {
const requested = args.slice(bumpIndex + 1).map(normalizeAssetName).filter(Boolean);
if (!requested.length) {
console.error('请在 --bump 后提供至少一个静态资源文件名,例如 ai-assistant.js');
process.exit(1);
}
const unknown = requested.filter((name) => !(name in versions));
if (unknown.length) {
console.error(`以下资源尚未纳入版本管理: ${unknown.join(', ')}`);
console.error('请先在 web/asset-versions.json 中补充后再执行。');
process.exit(1);
}
requested.forEach((name) => {
versions[name] += 1;
});
writeJson(versionsPath, versions);
const changed = syncHtmlReferences(versions);
console.log(`已递增资源版本: ${requested.join(', ')};同步 HTML ${changed} 个文件。`);
process.exit(0);
}
console.error('未识别的参数。');
console.error(usage.trim());
process.exit(1);