-
-
Notifications
You must be signed in to change notification settings - Fork 902
Expand file tree
/
Copy pathbump-version.js
More file actions
261 lines (220 loc) · 9.22 KB
/
bump-version.js
File metadata and controls
261 lines (220 loc) · 9.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
#!/usr/bin/env node
"use strict";
/**
* Bump the project version (patch / minor / major).
*
* Updates:
* - public/versioning.js — VERSION constant
* - package.json — "version" field
* - package-lock.json — top-level "version" and packages[""].version fields
* - src/index.html — ?v= cache-busting hashes for changed public/*.js files
*
* Usage:
* node scripts/bump-version.js # interactive prompt
* node scripts/bump-version.js patch # non-interactive
* node scripts/bump-version.js minor # non-interactive
* node scripts/bump-version.js major # non-interactive
* node scripts/bump-version.js --dry-run # preview only, no writes
*/
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const {execSync} = require("child_process");
// ---------------------------------------------------------------------------
// Paths
// ---------------------------------------------------------------------------
const repoRoot = path.resolve(__dirname, "..");
const packageJsonPath = path.join(repoRoot, "package.json");
const packageLockJsonPath = path.join(repoRoot, "package-lock.json");
const versioningPath = path.join(repoRoot, "public", "versioning.js");
const indexHtmlPath = path.join(repoRoot, "src", "index.html");
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function readFile(filePath) {
return fs.readFileSync(filePath, "utf8");
}
function writeFile(filePath, content) {
fs.writeFileSync(filePath, content, "utf8");
}
function parseCurrentVersion() {
const content = readFile(versioningPath);
const match = content.match(/const VERSION = "(\d+\.\d+\.\d+)";/);
if (!match) throw new Error("Could not find VERSION constant in public/versioning.js");
return match[1];
}
function bumpVersion(version, type) {
const [major, minor, patch] = version.split(".").map(Number);
if (type === "major") return `${major + 1}.0.0`;
if (type === "minor") return `${major}.${minor + 1}.0`;
return `${major}.${minor}.${patch + 1}`;
}
function escapeRegExp(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/** Returns true if versionA is strictly greater than versionB (semver). */
function isVersionGreater(versionA, versionB) {
const a = versionA.split(".").map(Number);
const b = versionB.split(".").map(Number);
for (let i = 0; i < 3; i++) {
if (a[i] > b[i]) return true;
if (a[i] < b[i]) return false;
}
return false; // equal
}
/**
* Returns public/*.js paths (relative to repo root) that have changed.
* Checks (in order, deduplicating):
* 1. Upstream branch diff — catches everything on a feature/PR branch
* 2. Staged (index) diff — catches files staged but not yet committed
* 3. Last-commit diff — fallback for main / detached HEAD
*/
function getChangedPublicJsFiles() {
const run = cmd => execSync(cmd, {encoding: "utf8", cwd: repoRoot});
const parseFiles = output =>
output
.split("\n")
.map(f => f.trim())
.filter(f => f.startsWith("public/") && f.endsWith(".js"));
const seen = new Set();
const collect = files => files.forEach(f => seen.add(f));
// 1. Upstream branch diff
try {
const upstream = run("git rev-parse --abbrev-ref --symbolic-full-name @{upstream}").trim();
collect(parseFiles(run(`git diff --name-only ${upstream}...HEAD`)));
} catch {
/* no upstream */
}
// 2. Staged changes (useful when building before committing)
try {
collect(parseFiles(run("git diff --name-only --cached")));
} catch {
/* ignore */
}
if (seen.size > 0) return [...seen];
// 3. Fallback: last commit diff
try {
return parseFiles(run("git diff --name-only HEAD~1 HEAD"));
} catch {
/* shallow / single-commit repo */
}
return [];
}
// ---------------------------------------------------------------------------
// File updaters
// ---------------------------------------------------------------------------
function updateVersioningJs(newVersion, dry) {
const original = readFile(versioningPath);
const updated = original.replace(/const VERSION = "\d+\.\d+\.\d+";/, `const VERSION = "${newVersion}";`);
if (original === updated) throw new Error("Failed to update VERSION in public/versioning.js");
if (!dry) writeFile(versioningPath, updated);
console.log(` public/versioning.js → ${newVersion}`);
}
function updatePackageJson(newVersion, dry) {
const original = readFile(packageJsonPath);
const pkg = JSON.parse(original);
const oldVersion = pkg.version;
pkg.version = newVersion;
if (!dry) writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}\n`);
console.log(` package.json ${oldVersion} → ${newVersion}`);
}
function updatePackageLockJson(newVersion, dry) {
if (!fs.existsSync(packageLockJsonPath)) {
console.log(" package-lock.json (not found, skipping)");
return;
}
const original = readFile(packageLockJsonPath);
const lock = JSON.parse(original);
const oldVersion = lock.version;
lock.version = newVersion;
if (lock.packages && lock.packages[""]) {
lock.packages[""].version = newVersion;
}
if (!dry) writeFile(packageLockJsonPath, `${JSON.stringify(lock, null, 2)}\n`);
console.log(` package-lock.json ${oldVersion} → ${newVersion}`);
}
function updateIndexHtmlHashes(newVersion, dry) {
const changedFiles = getChangedPublicJsFiles();
if (changedFiles.length === 0) {
console.log(" src/index.html (no changed public/*.js files detected)");
return;
}
let html = readFile(indexHtmlPath);
const updated = [];
for (const publicPath of changedFiles) {
const htmlPath = publicPath.replace(/^public\//, "");
const pattern = new RegExp(`${escapeRegExp(htmlPath)}\\?v=[0-9.]+`, "g");
if (pattern.test(html)) {
html = html.replace(pattern, `${htmlPath}?v=${newVersion}`);
updated.push(htmlPath);
}
}
if (updated.length > 0) {
if (!dry) writeFile(indexHtmlPath, html);
console.log(` src/index.html hashes updated for:\n - ${updated.join("\n - ")}`);
} else {
console.log(
` src/index.html (changed files not referenced: ${changedFiles.map(f => f.replace("public/", "")).join(", ")})`
);
}
}
// ---------------------------------------------------------------------------
// Prompt
// ---------------------------------------------------------------------------
function promptBumpType(currentVersion) {
return new Promise(resolve => {
const rl = readline.createInterface({input: process.stdin, output: process.stdout});
process.stdout.write(`\nCurrent version: ${currentVersion}\nBump type (patch / minor / major) [patch]: `);
rl.once("line", answer => {
rl.close();
const input = answer.trim().toLowerCase();
if (input === "minor" || input === "mi") return resolve("minor");
if (input === "major" || input === "maj") return resolve("major");
resolve("patch");
});
});
}
// ---------------------------------------------------------------------------
// Main
// ---------------------------------------------------------------------------
async function main() {
const argv = process.argv.slice(2);
const args = argv.map(a => a.toLowerCase());
const dry = args.includes("--dry-run");
// --base-version X.Y.Z — version on master before this PR was merged.
// When provided, the script checks whether the developer already bumped
// the version manually in their branch. If so, the increment is skipped
// and only the ?v= hashes in index.html are refreshed.
const baseVersionFlagIdx = argv.findIndex(a => a === "--base-version");
const baseVersion = baseVersionFlagIdx !== -1 ? argv[baseVersionFlagIdx + 1] : null;
if (dry) console.log("\n[bump-version] DRY RUN — no files will be changed\n");
const currentVersion = parseCurrentVersion();
if (baseVersion && isVersionGreater(currentVersion, baseVersion)) {
// Developer already bumped the version manually in their branch.
console.log(
`\n[bump-version] Version already updated manually: ${baseVersion} → ${currentVersion} (base was ${baseVersion})\n`
);
console.log(" Skipping version increment — updating ?v= hashes only.\n");
updateIndexHtmlHashes(currentVersion, dry);
console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`);
return;
}
// Determine bump type: CLI arg → stdin prompt → default patch
let bumpType;
if (args.includes("major")) bumpType = "major";
else if (args.includes("minor")) bumpType = "minor";
else if (args.includes("patch")) bumpType = "patch";
else if (process.stdin.isTTY) bumpType = await promptBumpType(currentVersion);
else bumpType = "patch"; // non-interactive (CI / pipe)
const newVersion = bumpVersion(currentVersion, bumpType);
console.log(`\n[bump-version] ${bumpType}: ${currentVersion} → ${newVersion}\n`);
updateVersioningJs(newVersion, dry);
updatePackageJson(newVersion, dry);
updatePackageLockJson(newVersion, dry);
updateIndexHtmlHashes(newVersion, dry);
console.log(`\n[bump-version] ${dry ? "(dry run) " : ""}done.\n`);
}
main().catch(err => {
console.error("\n[bump-version] Error:", err.message || err);
process.exit(1);
});