1 | #!/usr/bin/env node
|
2 | "use strict";
|
3 |
|
4 | const 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 |
|
29 | if (args.version) {
|
30 | console.info(require(require("path").join(__dirname, "package.json")).version);
|
31 | process.exit(0);
|
32 | }
|
33 |
|
34 | const commands = ["patch", "minor", "major"];
|
35 | let [level, ...files] = args._;
|
36 |
|
37 | if (!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 |
|
68 | const replacements = [];
|
69 | if (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 |
|
83 | const fs = require("fs-extra");
|
84 | const esc = require("escape-string-regexp");
|
85 | const semver = require("semver");
|
86 | const {basename} = require("path");
|
87 |
|
88 | let date = parseMixedArg(args.date);
|
89 | if (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 |
|
101 | async function main() {
|
102 | const packageFile = await require("find-up")("package.json");
|
103 |
|
104 |
|
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 |
|
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 |
|
132 | if (!semver.valid(baseVersion)) {
|
133 | throw new Error(`Invalid base version: ${baseVersion}`);
|
134 | }
|
135 |
|
136 |
|
137 |
|
138 | files = await require("fast-glob")(files);
|
139 |
|
140 |
|
141 | files = await Promise.all(files.map(file => fs.realpath(file)));
|
142 |
|
143 |
|
144 | files = Array.from(new Set(files));
|
145 |
|
146 |
|
147 | if (!files.length) {
|
148 | files = [packageFile];
|
149 | } else if (packageFile && !files.includes(packageFile)) {
|
150 | files.push(packageFile);
|
151 | }
|
152 |
|
153 |
|
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 |
|
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 |
|
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 |
|
189 | async 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 |
|
197 | async 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 |
|
232 | function 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 |
|
244 | function exit(err) {
|
245 | if (err) {
|
246 | console.info(String(err.message || err).trim());
|
247 | }
|
248 | process.exit(err ? 1 : 0);
|
249 | }
|
250 |
|
251 | main().then(exit).catch(exit);
|