UNPKG

9.14 kBJavaScriptView Raw
1#!/usr/bin/env node
2"use strict";
3
4const minOpts = {
5 boolean: [
6 "g", "gitless",
7 "h", "help",
8 "P", "packageless",
9 "p", "prefix",
10 "v", "version",
11 ],
12 string: [
13 "b", "base",
14 "c", "command",
15 "d", "date",
16 "r", "replace",
17 "_",
18 ],
19 alias: {
20 b: "base",
21 c: "command",
22 d: "date",
23 g: "gitless",
24 h: "help",
25 P: "packageless",
26 p: "prefix",
27 r: "replace",
28 v: "version",
29 }
30};
31
32const commands = ["patch", "minor", "major"];
33let args = require("minimist")(process.argv.slice(2), minOpts);
34args = fixArgs(commands, args, minOpts);
35let [level, ...files] = args._;
36
37if (args.version) {
38 console.info(require(require("path").join(__dirname, "package.json")).version);
39 process.exit(0);
40}
41
42if (!commands.includes(level) || args.help) {
43 console.info(`usage: ver [options] command [files...]
44
45 Semantically increment a project's version in multiple files.
46
47 Commands:
48 patch Increment patch 0.0.x version
49 minor Increment minor 0.x.0 version
50 major Increment major x.0.0 version
51
52 Arguments:
53 files Files to do version replacement in. The nearest package.json and
54 package-lock.json will always be included unless the -P argument is given.
55 Options:
56 -b, --base <version> Base version to use. Default is parsed from the nearest package.json
57 -c, --command <command> Run a command after files are updated but before git commit and tag
58 -d, --date [<date>] Replace dates in format YYYY-MM-DD with current or given date
59 -r, --replace <str> Additional replacement in the format "s#regexp#replacement#flags"
60 -P, --packageless Do not include package.json and package-lock.json unless explicitely given
61 -g, --gitless Do not create a git commit and tag
62 -p, --prefix Prefix git tags with a "v" character
63 -v, --version Print the version
64 -h, --help Print this help
65
66 Examples:
67 $ ver patch
68 $ ver minor build.js
69 $ ver major -p build.js
70 $ ver patch -c 'npm run build'`);
71 exit();
72}
73
74const replacements = [];
75if (args.replace) {
76 args.replace = Array.isArray(args.replace) ? args.replace : [args.replace];
77 for (const replaceStr of args.replace) {
78 let [_, re, replacement, flags] = (/^s#(.+?)#(.+?)#(.*?)$/.exec(replaceStr) || []);
79
80 if (!re || !replacement) {
81 exit(new Error(`Invalid replace string: ${replaceStr}`));
82 }
83
84 re = new RegExp(re, flags || undefined);
85 replacements.push({re, replacement});
86 }
87}
88
89let date = parseMixedArg(args.date);
90if (date) {
91 if (date === true) {
92 date = (new Date()).toISOString().substring(0, 10);
93 } else if (Array.isArray(date)) {
94 date = date[date.length - 1];
95 }
96
97 if (typeof date !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(date)) {
98 exit(`Invalid date argument: ${date}`);
99 }
100}
101
102const {promisify} = require("util");
103const readFile = promisify(require("fs").readFile);
104const writeFile = promisify(require("fs").writeFile);
105const truncate = promisify(require("fs").truncate);
106const stat = promisify(require("fs").stat);
107const realpath = promisify(require("fs").realpath);
108const semver = require("semver");
109const {basename} = require("path");
110const findUp = require("find-up");
111
112async function main() {
113 let packageFile = await findUp("package.json");
114 if (packageFile) packageFile = await realpath(packageFile);
115
116 // try to open package.json if it exists
117 let pkg, pkgStr;
118 if (packageFile) {
119 try {
120 pkgStr = await readFile(packageFile, "utf8");
121 pkg = JSON.parse(pkgStr);
122 } catch (err) {
123 throw new Error(`Error reading ${packageFile}: ${err.message}`);
124 }
125 }
126
127 // obtain old version
128 let baseVersion;
129 if (!args.base) {
130 if (pkg) {
131 if (pkg.version) {
132 baseVersion = pkg.version;
133 } else {
134 throw new Error(`No "version" field found in ${packageFile}`);
135 }
136 } else {
137 throw new Error(`Unable to obtain base version, either create package.json or specify --base`);
138 }
139 } else {
140 baseVersion = args.base;
141 }
142
143 // validate old version
144 if (!semver.valid(baseVersion)) {
145 throw new Error(`Invalid base version: ${baseVersion}`);
146 }
147
148 // de-glob files args which is needed for dumb shells like
149 // powershell that do not support globbing
150 files = await require("fast-glob")(files);
151
152 // convert paths to absolute
153 files = await Promise.all(files.map(file => realpath(file)));
154
155 // remove duplicate paths
156 files = Array.from(new Set(files));
157
158 if (!args.packageless) {
159 // include package.json if present
160 if (packageFile && !files.includes(packageFile)) {
161 files.push(packageFile);
162 }
163
164 // include package-lock.json if present
165 let packageLockFile = await findUp("package-lock.json");
166 if (packageLockFile) packageLockFile = await realpath(packageLockFile);
167 if (packageLockFile && !files.includes(packageLockFile)) {
168 files.push(packageLockFile);
169 }
170 }
171
172 if (!files.length) {
173 throw new Error(`Found no files to do replacements in`);
174 }
175
176 // verify files exist
177 for (const file of files) {
178 const stats = await stat(file);
179 if (!stats.isFile() && !stats.isSymbolicLink()) {
180 throw new Error(`${file} is not a file`);
181 }
182 }
183
184 // update files
185 const newVersion = semver.inc(baseVersion, level);
186 for (const file of files) {
187 if (basename(file) === "package.json") {
188 await updateFile({file, baseVersion, newVersion, replacements, pkgStr});
189 } else {
190 await updateFile({file, baseVersion, newVersion, replacements});
191 }
192 }
193
194 if (args.command) {
195 await run(args.command);
196 }
197
198 if (!args["gitless"]) {
199 // create git commit and tag
200 const tagName = args["prefix"] ? `v${newVersion}` : newVersion;
201 try {
202 await run(`git commit -a -m ${newVersion}`);
203 await run(`git tag -f -m ${newVersion} ${tagName}`);
204 } catch (err) {
205 return process.exit(1);
206 }
207 }
208
209 exit();
210}
211
212async function run(cmd) {
213 console.info(`+ ${cmd}`);
214 const child = require("execa")(cmd, {shell: true});
215 child.stdout.pipe(process.stdout);
216 child.stderr.pipe(process.stderr);
217 await child;
218}
219
220async function updateFile({file, baseVersion, newVersion, replacements, pkgStr}) {
221 let oldData;
222 if (pkgStr) {
223 oldData = pkgStr;
224 } else {
225 oldData = await readFile(file, "utf8");
226 }
227
228 let newData;
229 if (pkgStr) {
230 const re = new RegExp(`("version":[^]*?")${esc(baseVersion)}(")`);
231 newData = pkgStr.replace(re, (_, p1, p2) => `${p1}${newVersion}${p2}`);
232 } else if (basename(file) === "package-lock.json") {
233 // special case for package-lock.json which contains a lot of version
234 // strings which make regexp replacement risky. From a few tests on
235 // Node.js 12, key order seems to be preserved through parse and stringify.
236 newData = JSON.parse(oldData);
237 newData.version = newVersion;
238 newData = JSON.stringify(newData, null, 2) + "\n";
239 } else {
240 const re = new RegExp(esc(baseVersion), "g");
241 newData = oldData.replace(re, newVersion);
242 }
243
244 if (date) {
245 const re = new RegExp(`([^0-9]|^)[0-9]{4}-[0-9]{2}-[0-9]{2}([^0-9]|$)`, "g");
246 newData = newData.replace(re, (_, p1, p2) => `${p1}${date}${p2}`);
247 }
248
249 if (replacements.length) {
250 for (const replacement of replacements) {
251 newData = newData.replace(replacement.re, replacement.replacement);
252 }
253 }
254
255 if (oldData === newData) {
256 throw new Error(`No replacement made in ${file}`);
257 } else {
258 await write(file, newData);
259 }
260}
261
262async function write(file, content) {
263 if (require("os").platform() === "win32") {
264 // truncate and append on windows to preserve file metadata
265 await truncate(file, 0);
266 await writeFile(file, content, {encoding: "utf8", flag: "r+"});
267 } else {
268 await writeFile(file, content, {encoding: "utf8"});
269 }
270}
271
272function parseMixedArg(arg) {
273 if (arg === "") {
274 return true;
275 } else if (typeof arg === "string") {
276 return arg.includes(",") ? arg.split(",") : [arg];
277 } else if (Array.isArray(arg)) {
278 return arg;
279 } else {
280 return Boolean(arg);
281 }
282}
283
284// handle minimist parsing error like '-d patch'
285function fixArgs(commands, args, minOpts) {
286 for (const key of Object.keys(minOpts.alias)) {
287 delete args[key];
288 }
289
290 if (commands.includes(args.date)) {
291 args._ = [args.date, ...args._];
292 args.date = true;
293 }
294 if (commands.includes(args.base)) {
295 args._ = [args.base, ...args._];
296 args.base = true;
297 }
298 if (commands.includes(args.command)) {
299 args._ = [args.command, ...args._];
300 args.command = "";
301 }
302 if (commands.includes(args.replace)) {
303 args._ = [args.replace, ...args._];
304 args.replace = "";
305 }
306 if (commands.includes(args.packageless)) {
307 args._ = [args.packageless, ...args._];
308 args.packageless = true;
309 }
310
311 return args;
312}
313
314function esc(str) {
315 return str.replace(/[|\\{}()[\]^$+*?.-]/g, "\\$&");
316}
317
318function exit(err) {
319 if (err) {
320 console.info(String(err.message || err).trim());
321 }
322 process.exit(err ? 1 : 0);
323}
324
325main().then(exit).catch(exit);