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