UNPKG

5.83 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 "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
27if (args.version) {
28 console.info(require(require("path").join(__dirname, "package.json")).version);
29 process.exit(0);
30}
31
32const commands = ["patch", "minor", "major"];
33let [level, ...files] = args._;
34
35if (!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
65const replacements = [];
66if (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
80const fs = require("fs-extra");
81const esc = require("escape-string-regexp");
82const semver = require("semver");
83const {basename} = require("path");
84
85async function main() {
86 const packageFile = await require("find-up")("package.json");
87
88 // try to open package.json if it exists
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 // obtain old version
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 // validate old version
116 if (!semver.valid(baseVersion)) {
117 throw new Error(`Invalid base version: ${baseVersion}`);
118 }
119
120 // de-glob files args which is needed for dumb shells like
121 // powershell that do not support globbing
122 files = await require("fast-glob")(files);
123
124 // convert paths to absolute
125 files = await Promise.all(files.map(file => fs.realpath(file)));
126
127 // remove duplicate paths
128 files = Array.from(new Set(files));
129
130 // make sure package.json is included if present
131 if (!files.length) {
132 files = [packageFile];
133 } else if (packageFile && !files.includes(packageFile)) {
134 files.push(packageFile);
135 }
136
137 // verify files exist
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 // update files
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 // create git commit and tag
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
173async 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
181async 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
211function exit(err) {
212 if (err) {
213 console.info(String(err.message || err).trim());
214 }
215 process.exit(err ? 1 : 0);
216}
217
218main().then(exit).catch(exit);