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);