versions
Version:
CLI to flexibly increment a project's version
29 lines (24 loc) • 10.7 kB
JavaScript
import{execFile as e}from"node:child_process";import{parseArgs as t}from"node:util";import{basename as n,dirname as r,join as i,relative as a}from"node:path";import{cwd as o,exit as s,stdout as c}from"node:process";import{EOL as l,platform as u}from"node:os";import{accessSync as d,readFileSync as f,statSync as p,truncateSync as m,writeFileSync as h}from"node:fs";var g=class extends Error{stdout;stderr;output;constructor(e,t=``,n=``){super(e),this.name=`SubprocessError`,this.stdout=t,this.stderr=n,this.output=[n,t].filter(Boolean).join(`
`)}};function _(e,t,n){let r=!1;for(let i of e.split(/\r?\n/)){let e=i.trim();if(!(!e||e[0]===`#`)){if(e[0]===`[`){let n=/^\[([^[\]]+)\]/.exec(e);r=n?n[1].trim()===t:!1;continue}if(r){let t=RegExp(`^${n}\\s*=\\s*["']([^"']+)["']`).exec(e);if(t)return t[1]}}}}function v(t,n,r){return new Promise((i,a)=>{let o=e(t,n,{encoding:`utf8`,shell:r?.shell,windowsHide:!0,cwd:r?.cwd,env:r?.env},(e,t,n)=>{e?a(new g(e.message.split(/\r?\n/)[0],t,n)):i({stdout:t.trimEnd(),stderr:n.trimEnd()})});r?.stdin&&o.stdin.end(r.stdin.string)})}var y=`14.2.6`;const b=/[|\\{}()[\]^$+*?.-]/g,x=/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/,S=/^v/,C=/_VER_/g,w=/_MAJOR_/g,T=/_MINOR_/g,E=/_PATCH_/g,D=/([0-9]+)\.[0-9]+\.[0-9]+(.*)/,O=/([0-9]+\.)([0-9]+)\.[0-9]+(.*)/,k=/([0-9]+\.[0-9]+\.)([0-9]+)(.*)/,A=/^(\d+)\.(\d+)\.(\d+)(?:-([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*))?/,j=/^([a-zA-Z0-9-]+)\.(\d+)$/,M=/\r?\n/,N=/([^0-9]|^)[0-9]{4}-[0-9]{2}-[0-9]{2}([^0-9]|$)/g,P=/^s#([^#]+)#([^#]+)#(.*)$/;function F(e){return e.replace(b,`\\$&`)}function I(e){return x.test(e.replace(S,``))}function L(e){return Array.from(new Set(e))}function R(e,t){let[n,r,i]=t.split(`.`);return e.replace(C,t).replace(w,n).replace(T,r).replace(E,i)}function z(e,t,n){if(!I(e))throw Error(`Invalid semver: ${e}`);if(t===`major`){let t=e.replace(D,(e,t)=>`${Number(t)+1}.0.0`);return n?`${t}-${n}.0`:t}if(t===`minor`){let t=e.replace(O,(e,t,n)=>`${t}${Number(n)+1}.0`);return n?`${t}-${n}.0`:t}if(t===`patch`){let t=e.replace(k,(e,t,n)=>`${t}${Number(n)+1}`);return n?`${t}-${n}.0`:t}if(t===`prerelease`){if(!n)throw Error(`prerelease requires --preid option`);let t=A.exec(e);if(!t)throw Error(`Invalid semver: ${e}`);let[,r,i,a,o]=t;if(!o)return`${r}.${i}.${Number(a)+1}-${n}.0`;let s=j.exec(o);if(s){let[,e,t]=s;if(e===n)return`${r}.${i}.${a}-${n}.${Number(t)+1}`}return`${r}.${i}.${a}-${n}.0`}return e.replace(k,(e,t,n,r)=>`${t}${Number(n)+1}${r}`)}function B(e,t,n){let a=i(t,e);try{return d(a),a}catch{}let o=r(t);return n&&a===n||o===t?null:B(e,o,n)}function V(e){let t=B(`package.json`,e);if(!t)return null;try{let e=f(t,`utf8`),n=JSON.parse(e);if(n.version&&I(n.version))return n.version.replace(S,``)}catch{}return null}function H(e){let t=B(`pyproject.toml`,e);if(!t)return null;try{let e=f(t,`utf8`),n=_(e,`project`,`version`);if(n&&I(n))return n.replace(S,``);let r=_(e,`tool.poetry`,`version`);if(r&&I(r))return r.replace(S,``)}catch{}return null}async function U(e){let t;try{t=await v(`git`,[`check-ignore`,`--`,...e])}catch{return e}let n=new Set(t.stdout.split(M));return e.filter(e=>!n.has(e))}function W({file:e,baseVersion:t,newVersion:r,replacements:i,date:a}){let o=n(e);if((/lock/i.test(o)||o===`go.sum`)&&o!==`package-lock.json`&&o!==`uv.lock`)return[e,null];let s=f(e,`utf8`),c;if(o===`package.json`)c=s.replace(/("version":[^]*?")\d+\.\d+\.\d+(?:[^"\d][^"]*)?(")/,(e,t,n)=>`${t}${r}${n}`);else if(o===`package-lock.json`){let e=JSON.parse(s);e.version&&=r,e?.packages?.[``]?.version&&(e.packages[``].version=r),c=`${JSON.stringify(e,null,2)}\n`}else if(o===`pyproject.toml`)c=s.replace(/(^version ?= ?["'])\d+\.\d+\.\d+(?:[^"'\d][^"']*)?(["'].*)/gm,(e,t,n)=>`${t}${r}${n}`);else if(o===`uv.lock`){let t=_(f(e.replace(/uv\.lock$/,`pyproject.toml`),`utf8`),`project`,`name`),n=RegExp(`(\\[\\[package\\]\\]\r?\n.+${F(t)}.+\r?\nversion = ").+?(")`);c=s.replace(n,(e,t,n)=>`${t}${r}${n}`)}else{let e=new RegExp(F(t),`g`);c=s.replace(e,r)}if(a){let e=N;c=c.replace(e,(e,t,n)=>`${t}${a}${n}`)}if(i?.length)for(let e of i)c=c.replace(e.re,e.replacement);return[e,c]}function G(e,t){if(u()===`win32`)try{m(e),h(e,t,{flag:`r+`})}catch{h(e,t)}else h(e,t)}function K(e,t){let n=[];for(let t of e)t&&n.push(t);return n.join(t).trim()}function q(e){e instanceof g?console.info(`${e.message}\n${e.output}`):e instanceof Error?console.info(String(e.stack||e.message||e).trim()):e&&console.info(e),s(e?1:0)}function J(e){return e.endsWith(l)?e:`${e}${l}`}function Y(){return process.env.VERSIONS_FORGE_TOKEN||process.env.GITHUB_API_TOKEN||process.env.GITHUB_TOKEN||process.env.GH_TOKEN||process.env.HOMEBREW_GITHUB_API_TOKEN||null}function X(){return process.env.VERSIONS_FORGE_TOKEN||process.env.GITEA_API_TOKEN||process.env.GITEA_AUTH_TOKEN||process.env.GITEA_TOKEN||null}async function Z(){try{let{stdout:e}=await v(`git`,[`remote`,`get-url`,`origin`]),t=e.trim(),n=/https:\/\/([^/]+)\/([^/]+)\/([^/.]+)/.exec(t),r=/git@([^:]+):([^/]+)\/([^/.]+)/.exec(t),i=n||r;return i?{owner:i[2],repo:i[3],host:i[1],type:i[1]===`github.com`?`github`:`gitea`}:null}catch{return null}}async function Q(e,t,n,r){let i=e.type===`github`?`https://api.github.com/repos/${e.owner}/${e.repo}/releases`:`https://${e.host}/api/v1/repos/${e.owner}/${e.repo}/releases`,a={tag_name:t,name:t,body:n,draft:!1,prerelease:A.test(t)&&t.includes(`-`)},o={"Content-Type":`application/json`,Authorization:e.type===`github`?`Bearer ${r}`:`token ${r}`},s=await fetch(i,{method:`POST`,headers:o,body:JSON.stringify(a)});if(!s.ok){let e=await s.text();throw Error(`Failed to create release: ${s.status} ${s.statusText}\n${e}`)}let c=await s.json();c.html_url?console.info(`Created release: ${c.html_url}`):console.info(`Created release`)}function $(e){e.stdout&&c.write(J(e.stdout)),e.stderr&&c.write(J(e.stderr))}async function ee(){let e=new Set([`patch`,`minor`,`major`,`prerelease`]),n=t({strict:!1,allowPositionals:!0,options:{all:{short:`a`,type:`boolean`},dry:{short:`D`,type:`boolean`},gitless:{short:`g`,type:`boolean`},help:{short:`h`,type:`boolean`},packageless:{short:`P`,type:`boolean`},prefix:{short:`p`,type:`boolean`},version:{short:`v`,type:`boolean`},date:{short:`d`,type:`boolean`},release:{short:`R`,type:`boolean`},base:{short:`b`,type:`string`},command:{short:`c`,type:`string`},replace:{short:`r`,type:`string`,multiple:!0},message:{short:`m`,type:`string`,multiple:!0},preid:{short:`i`,type:`string`}}}),i=n.values,[s,...c]=n.positionals;c=L(c),i.version&&(console.info(y||`0.0.0`),q()),(!e.has(s)||i.help)&&(console.info(`usage: versions [options] patch|minor|major|prerelease [files...]
Options:
-a, --all Add all changed files to the commit
-b, --base <version> Base version. Default is from latest git tag, package.json, pyproject.toml, or 0.0.0
-p, --prefix Prefix version string with a "v" character. Default is none
-c, --command <cmd> Run command after files are updated but before git commit and tag
-d, --date Replace dates in format YYYY-MM-DD with current date
-i, --preid <id> Prerelease identifier, e.g., alpha, beta, rc
-m, --message <str> Custom tag and commit message
-r, --replace <str> Additional replacements in the format "s#regexp#replacement#flags"
-g, --gitless Do not perform any git action like creating commit and tag
-D, --dry Do not create a tag or commit, just print what would be done
-R, --release Create a GitHub or Gitea release with the changelog as body
-v, --version Print the version
-h, --help Print this help
The message and replacement strings accept tokens _VER_, _MAJOR_, _MINOR_, _PATCH_.
Examples:
$ versions patch
$ versions prerelease --preid=alpha
$ versions -c 'npm run build' -m 'Release _VER_' minor file.css`),q());let l=``;i.date&&(l=new Date().toISOString().substring(0,10));let u=o(),d=B(`.git`,u),f=d?r(d):null;f||=u;let m=``,h=``;if(i.base)m=String(i.base);else{let e=``;if(!i.gitless){try{h=(await v(`git`,[`describe`,`--tags`,`--abbrev=0`])).stdout.trim(),I(h)&&(m=h.replace(S,``))}catch{}if(!m){try{({stdout:e}=await v(`git`,[`tag`,`--list`,`--sort=-creatordate`]))}catch{}for(let t of e.split(M).map(e=>e.trim()).filter(Boolean))if(I(t)){m=t.replace(S,``);break}}}if(!m){if(m=V(f)||H(f)||``,!m&&i.gitless)return q(Error(`--gitless requires --base to be set or a version in package.json or pyproject.toml`));m||=`0.0.0`}}if(m.startsWith(`v`)&&(m=m.substring(1)),!I(m))throw Error(`Invalid base version: ${m}`);if(c=c.map(e=>a(u,e)),s===`prerelease`&&!i.preid)return q(Error(`prerelease requires --preid option`));let g=z(m,s,typeof i.preid==`string`?i.preid:void 0),_=[];if(i.replace?.length){let e=i.replace.filter(e=>typeof e==`string`);for(let t of e){let[e,n,r,i]=P.exec(t)||[];(!n||!r)&&q(Error(`Invalid replace string: ${t}`)),r=R(r,g),_.push({re:new RegExp(n,i||void 0),replacement:r})}}let b=!i.gitless&&i.release?Z():null,x=!i.gitless&&!i.all&&c.length?U(c):null;if(c.length){for(let e of c){let t=p(e);if(!t.isFile()&&!t.isSymbolicLink())throw Error(`${e} is not a file`)}let e=[];for(let t of c)e.push(W({file:t,baseVersion:m,newVersion:g,replacements:_,date:l}));for(let[t,n]of e)n!==null&&G(t,n)}if(typeof i.command==`string`&&$(await v(i.command,[],{shell:!0})),i.gitless)return;let C=(i.message||[]).filter(e=>typeof e==`string`),w=i.prefix?`v${g}`:g,T=``;await v(`git`,[`rev-parse`,`--verify`,`refs/tags/${w}`]).then(()=>!0,()=>!1)?T=`${w}..HEAD`:h&&(T=`${h}..HEAD`);let E;try{let e=[`log`];T&&e.push(T);let{stdout:t}=await v(`git`,[...e,`--pretty=format:* %s (%aN)`]);t?.length&&(E=t)}catch{}if(i.dry)return console.info(`Would create new tag and commit: ${w}`);let D=K([w,...C,E],`
`);if(i.all)$(await v(`git`,[`commit`,`-a`,`--allow-empty`,`-F`,`-`],{stdin:{string:D}}));else{let e=x?await x:[];e.length?$(await v(`git`,[`commit`,`-i`,`-F`,`-`,`--`,...e],{stdin:{string:D}})):$(await v(`git`,[`commit`,`--allow-empty`,`-F`,`-`],{stdin:{string:D}}))}let O=K([...C,E],`
`);if($(await v(`git`,[`tag`,`-f`,`-F`,`-`,w],{stdin:{string:O}})),i.release){let e=await b;if(!e)throw Error(`Could not determine repository type from git remote. Only GitHub and Gitea repositories are supported for release creation.`);let t=E||w;if(e.type===`github`){let n=Y();if(!n)throw Error(`GitHub release requested but no token found in environment`);await Q(e,w,t,n)}else if(e.type===`gitea`){let n=X();if(!n)throw Error(`Gitea release requested but no token found in environment`);await Q(e,w,t,n)}}}ee().then(q).catch(q);export{};