UNPKG

6.7 kBJavaScriptView Raw
1#!/usr/bin/env node
2"use strict";
3
4const args = require("minimist")(process.argv.slice(2), {
5 boolean: [
6 "g", "no-git",
7 "h", "help",
8 "p", "prefix",
9 "v", "version",
10 ],
11 string: [
12 "c", "command",
13 "d", "date",
14 "r", "replace",
15 "_",
16 ],
17 alias: {
18 b: "base",
19 c: "command",
20 d: "date",
21 g: "no-git",
22 h: "help",
23 p: "prefix",
24 r: "replace",
25 v: "version",
26 }
27});
28
29if (args.version) {
30 console.info(require(require("path").join(__dirname, "package.json")).version);
31 process.exit(0);
32}
33
34const commands = ["patch", "minor", "major"];
35let [level, ...files] = args._;
36
37if (!commands.includes(level) || args.help) {
38 console.info(`usage: ver [options] command [files...]
39
40 Semantically increment a project's version in multiple files.
41
42 Commands:
43 patch Increment patch 0.0.x version
44 minor Increment minor 0.x.0 version
45 major Increment major x.0.0 version
46
47 Arguments:
48 files Files to handle. Default is the nearest package.json which if
49 present, will always be included.
50 Options:
51 -b, --base <version> Base version to use. Default is parsed from the nearest package.json
52 -c, --command <command> Run a command after files are updated but before git commit and tag
53 -d, --date [<date>] Replace dates in format YYYY-MM-DD with current or given date
54 -r, --replace <str> Additional replacement in the format "s#regexp#replacement#flags"
55 -g, --no-git Do not create a git commit and tag
56 -p, --prefix Prefix git tags with a "v" character
57 -v, --version Print the version
58 -h, --help Print this help
59
60 Examples:
61 $ ver patch
62 $ ver -g minor build.js
63 $ ver -p major build.js
64 $ ver patch -c 'npm run build'`);
65 exit();
66}
67
68const replacements = [];
69if (args.replace) {
70 args.replace = Array.isArray(args.replace) ? args.replace : [args.replace];
71 for (const replaceStr of args.replace) {
72 let [_, re, replacement, flags] = (/^s#(.+?)#(.+?)#(.*?)$/.exec(replaceStr) || []);
73
74 if (!re || !replacement) {
75 exit(new Error(`Invalid replace string: ${replaceStr}`));
76 }
77
78 re = new RegExp(re, flags || undefined);
79 replacements.push({re, replacement});
80 }
81}
82
83const fs = require("fs-extra");
84const esc = require("escape-string-regexp");
85const semver = require("semver");
86const {basename} = require("path");
87
88let date = parseMixedArg(args.date);
89if (date) {
90 if (date === true) {
91 date = (new Date()).toISOString().substring(0, 10);
92 } else if (Array.isArray(date)) {
93 date = date[date.length - 1];
94 }
95
96 if (typeof date !== "string" || !/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/.test(date)) {
97 exit(`Invalid date argument: ${date}`);
98 }
99}
100
101async function main() {
102 const packageFile = await require("find-up")("package.json");
103
104 // try to open package.json if it exists
105 let pkg, pkgStr;
106 if (packageFile) {
107 try {
108 pkgStr = await fs.readFile(packageFile, "utf8");
109 pkg = JSON.parse(pkgStr);
110 } catch (err) {
111 throw new Error(`Error reading ${packageFile}: ${err.message}`);
112 }
113 }
114
115 // obtain old version
116 let baseVersion;
117 if (!args.base) {
118 if (pkg) {
119 if (pkg.version) {
120 baseVersion = pkg.version;
121 } else {
122 throw new Error(`No "version" field found in ${packageFile}`);
123 }
124 } else {
125 throw new Error(`Unable to obtain base version, either create package.json or specify --base`);
126 }
127 } else {
128 baseVersion = args.base;
129 }
130
131 // validate old version
132 if (!semver.valid(baseVersion)) {
133 throw new Error(`Invalid base version: ${baseVersion}`);
134 }
135
136 // de-glob files args which is needed for dumb shells like
137 // powershell that do not support globbing
138 files = await require("fast-glob")(files);
139
140 // convert paths to absolute
141 files = await Promise.all(files.map(file => fs.realpath(file)));
142
143 // remove duplicate paths
144 files = Array.from(new Set(files));
145
146 // make sure package.json is included if present
147 if (!files.length) {
148 files = [packageFile];
149 } else if (packageFile && !files.includes(packageFile)) {
150 files.push(packageFile);
151 }
152
153 // verify files exist
154 for (const file of files) {
155 const stat = await fs.stat(file);
156 if (!stat.isFile() && !stat.isSymbolicLink()) {
157 throw new Error(`${file} is not a file`);
158 }
159 }
160
161 // update files
162 const newVersion = semver.inc(baseVersion, level);
163 for (const file of files) {
164 if (basename(file) === "package.json") {
165 await updateFile({file, baseVersion, newVersion, replacements, pkgStr});
166 } else {
167 await updateFile({file, baseVersion, newVersion, replacements});
168 }
169 }
170
171 if (args.command) {
172 await run(args.command);
173 }
174
175 if (!args["no-git"]) {
176 // create git commit and tag
177 const tagName = args["prefix"] ? `v${newVersion}` : newVersion;
178 try {
179 await run(`git commit -a -m ${newVersion}`);
180 await run(`git tag -f -m ${newVersion} ${tagName}`);
181 } catch (err) {
182 return process.exit(1);
183 }
184 }
185
186 exit();
187}
188
189async function run(cmd) {
190 console.info(`+ ${cmd}`);
191 const child = require("execa").shell(cmd);
192 child.stdout.pipe(process.stdout);
193 child.stderr.pipe(process.stderr);
194 await child;
195}
196
197async function updateFile({file, baseVersion, newVersion, replacements, pkgStr}) {
198 let oldData;
199 if (pkgStr) {
200 oldData = pkgStr;
201 } else {
202 oldData = await fs.readFile(file, "utf8");
203 }
204
205 let newData;
206 if (pkgStr) {
207 const re = new RegExp(`("version":[^]*?")${esc(baseVersion)}(")`);
208 newData = pkgStr.replace(re, (_, p1, p2) => `${p1}${newVersion}${p2}`);
209 } else {
210 const re = new RegExp(esc(baseVersion), "g");
211 newData = oldData.replace(re, newVersion);
212 }
213
214 if (date) {
215 const re = new RegExp(`([^0-9]|^)[0-9]{4}-[0-9]{2}-[0-9]{2}([^0-9]|$)`, "g");
216 newData = newData.replace(re, (_, p1, p2) => `${p1}${date}${p2}`);
217 }
218
219 if (replacements.length) {
220 for (const replacement of replacements) {
221 newData = newData.replace(replacement.re, replacement.replacement);
222 }
223 }
224
225 if (oldData === newData) {
226 throw new Error(`No replacement made in ${file}`);
227 } else {
228 await fs.writeFile(file, newData);
229 }
230}
231
232function parseMixedArg(arg) {
233 if (arg === "") {
234 return true;
235 } else if (typeof arg === "string") {
236 return arg.includes(",") ? arg.split(",") : [arg];
237 } else if (Array.isArray(arg)) {
238 return arg;
239 } else {
240 return false;
241 }
242}
243
244function exit(err) {
245 if (err) {
246 console.info(String(err.message || err).trim());
247 }
248 process.exit(err ? 1 : 0);
249}
250
251main().then(exit).catch(exit);