dev-dump 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +189 -0
- package/index.js +529 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# dev-dump
|
|
2
|
+
|
|
3
|
+
Flexible, semver-compliant snapshot & prerelease version generator.
|
|
4
|
+
|
|
5
|
+
Generate deterministic, sortable prerelease versions for CI/CD pipelines, canary releases, feature branch deploys, and multi-stage release workflows — with zero dependencies.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g dev-dump # global
|
|
11
|
+
npm install -D dev-dump # devDependency
|
|
12
|
+
npx dev-dump # one-off
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Default: {preid}.{timestamp}.{hash}
|
|
19
|
+
npx dev-dump
|
|
20
|
+
# → 1.2.3-dev.202603171950.abc1234
|
|
21
|
+
|
|
22
|
+
# Alpha prerelease
|
|
23
|
+
npx dev-dump -p alpha
|
|
24
|
+
# → 1.2.3-alpha.202603171950.abc1234
|
|
25
|
+
|
|
26
|
+
# Auto-increment from npm registry
|
|
27
|
+
npx dev-dump -f "{preid}.{inc}" --from-registry
|
|
28
|
+
# → 1.2.3-dev.0 (next time: dev.1, dev.2, …)
|
|
29
|
+
|
|
30
|
+
# Canary release
|
|
31
|
+
npx dev-dump -f "canary.{hash}"
|
|
32
|
+
# → 1.2.3-canary.abc1234
|
|
33
|
+
|
|
34
|
+
# Feature branch snapshot
|
|
35
|
+
npx dev-dump -f "{branch}.{timestamp}.{hash}"
|
|
36
|
+
# → 1.2.3-feat-login.202603171950.abc1234
|
|
37
|
+
|
|
38
|
+
# Dry run (don't write package.json)
|
|
39
|
+
npx dev-dump --dry-run
|
|
40
|
+
|
|
41
|
+
# Capture for CI pipelines
|
|
42
|
+
VERSION=$(npx dev-dump --stdout-only)
|
|
43
|
+
echo "Publishing $VERSION"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Template Variables
|
|
47
|
+
|
|
48
|
+
The `-f` / `--format` flag defines the **prerelease** portion of the version. The base `MAJOR.MINOR.PATCH` is read from `package.json` (or overridden with `--base`).
|
|
49
|
+
|
|
50
|
+
| Variable | Description | Example |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `{preid}` | Prerelease identifier (`--preid`) | `dev`, `alpha`, `beta`, `rc` |
|
|
53
|
+
| `{timestamp}` | `YYYYMMDDHHmm` | `202603171950` |
|
|
54
|
+
| `{ts}` | `YYMMDDHHmm` | `2603171950` |
|
|
55
|
+
| `{date}` | `YYYYMMDD` | `20260317` |
|
|
56
|
+
| `{time}` | `HHmm` | `1950` |
|
|
57
|
+
| `{hash}` | Git short hash (7 chars) | `abc1234` |
|
|
58
|
+
| `{hash-long}` | Git full hash (40 chars) | `abc1234567890…` |
|
|
59
|
+
| `{inc}` | Auto-increment counter (needs `--from-registry`) | `0`, `1`, `2` |
|
|
60
|
+
| `{branch}` | Git branch name (sanitised) | `feat-login` |
|
|
61
|
+
| `{base}` | `MAJOR.MINOR.PATCH` | `1.2.3` |
|
|
62
|
+
|
|
63
|
+
## Options
|
|
64
|
+
|
|
65
|
+
| Option | Short | Default | Description |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| `--preid <id>` | `-p` | `dev` | Prerelease identifier |
|
|
68
|
+
| `--format <tpl>` | `-f` | `{preid}.{timestamp}.{hash}` | Prerelease template |
|
|
69
|
+
| `--base <ver>` | `-b` | from `package.json` | Override base version |
|
|
70
|
+
| `--from-registry` | `-r` | `false` | Resolve base / `{inc}` from npm registry |
|
|
71
|
+
| `--tag <tag>` | `-t` | `latest` | dist-tag for `--from-registry` |
|
|
72
|
+
| `--dry-run` | `-d` | `false` | Print version without writing files |
|
|
73
|
+
| `--stdout-only` | `-s` | `false` | Only output version to stdout |
|
|
74
|
+
| `--no-git-tag` | | `true` | Don't create a git tag |
|
|
75
|
+
| `--cwd <path>` | | `.` | Working directory |
|
|
76
|
+
| `--help` | `-h` | | Show help |
|
|
77
|
+
| `--version` | `-v` | | Show tool version |
|
|
78
|
+
|
|
79
|
+
## Common Recipes
|
|
80
|
+
|
|
81
|
+
### CI/CD Snapshot (default)
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
npx dev-dump
|
|
85
|
+
# 1.2.3-dev.202603171950.abc1234
|
|
86
|
+
npm publish --tag dev
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Standard Prerelease Cycle
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
# alpha phase
|
|
93
|
+
npx dev-dump -p alpha -f "{preid}.{inc}" -r
|
|
94
|
+
# 1.2.3-alpha.0 → alpha.1 → alpha.2
|
|
95
|
+
|
|
96
|
+
# beta phase
|
|
97
|
+
npx dev-dump -p beta -f "{preid}.{inc}" -r
|
|
98
|
+
# 1.2.3-beta.0 → beta.1
|
|
99
|
+
|
|
100
|
+
# release candidate
|
|
101
|
+
npx dev-dump -p rc -f "{preid}.{inc}" -r
|
|
102
|
+
# 1.2.3-rc.0 → rc.1
|
|
103
|
+
|
|
104
|
+
# final release
|
|
105
|
+
npm version 1.2.3
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Feature Branch Deploy
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
npx dev-dump -f "{branch}.{timestamp}.{hash}"
|
|
112
|
+
# 1.2.3-feat-login.202603171950.abc1234
|
|
113
|
+
npm publish --tag "$(git branch --show-current | tr '/' '-')"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Canary Release
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
npx dev-dump -f "canary.{hash}"
|
|
120
|
+
# 1.2.3-canary.abc1234
|
|
121
|
+
npm publish --tag canary
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Override Base for Next Major
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx dev-dump -b 2.0.0 -p alpha
|
|
128
|
+
# 2.0.0-alpha.202603171950.abc1234
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### GitHub Actions Example
|
|
132
|
+
|
|
133
|
+
```yaml
|
|
134
|
+
- name: Publish snapshot
|
|
135
|
+
run: |
|
|
136
|
+
VERSION=$(npx dev-dump --stdout-only)
|
|
137
|
+
npm version "$VERSION" --no-git-tag-version
|
|
138
|
+
npm publish --tag dev
|
|
139
|
+
env:
|
|
140
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## SemVer Compliance
|
|
144
|
+
|
|
145
|
+
All generated versions are fully compliant with [Semantic Versioning 2.0.0](https://semver.org/):
|
|
146
|
+
|
|
147
|
+
- Prerelease identifiers use only `[0-9A-Za-z-]`
|
|
148
|
+
- No leading zeros on numeric identifiers
|
|
149
|
+
- Empty identifiers are automatically removed
|
|
150
|
+
- Branch names and custom strings are sanitised (`/` `_` → `-`)
|
|
151
|
+
- Versions are validated with a strict SemVer regex before output
|
|
152
|
+
- Ordering is verified: timestamps naturally sort correctly, `{inc}` is monotonically increasing
|
|
153
|
+
|
|
154
|
+
## Programmatic API
|
|
155
|
+
|
|
156
|
+
```js
|
|
157
|
+
const {
|
|
158
|
+
parseSemver,
|
|
159
|
+
isValidSemver,
|
|
160
|
+
semverCompare,
|
|
161
|
+
sanitise,
|
|
162
|
+
interpolate,
|
|
163
|
+
buildVariables,
|
|
164
|
+
resolveInc,
|
|
165
|
+
resolveBaseFromRegistry,
|
|
166
|
+
} = require('dev-dump');
|
|
167
|
+
|
|
168
|
+
// Parse
|
|
169
|
+
parseSemver('1.2.3-dev.1');
|
|
170
|
+
// → { major: 1, minor: 2, patch: 3, prerelease: 'dev.1', build: '', base: '1.2.3' }
|
|
171
|
+
|
|
172
|
+
// Compare
|
|
173
|
+
semverCompare('1.0.0-alpha', '1.0.0-beta'); // → -1
|
|
174
|
+
|
|
175
|
+
// Sanitise branch names
|
|
176
|
+
sanitise('feature/my_branch'); // → 'feature-my-branch'
|
|
177
|
+
|
|
178
|
+
// Interpolate custom template
|
|
179
|
+
interpolate('{preid}.{timestamp}', { preid: 'dev', timestamp: '202603171950' });
|
|
180
|
+
// → 'dev.202603171950'
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Zero Dependencies
|
|
184
|
+
|
|
185
|
+
This package has **no runtime dependencies**. It includes a built-in semver parser, comparator, and validator — everything needed in a single `index.js` file.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
package/index.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* dev-dump
|
|
5
|
+
*
|
|
6
|
+
* A flexible CLI tool for generating semver-compliant snapshot / prerelease
|
|
7
|
+
* version strings. Supports customisable templates with variables such as
|
|
8
|
+
* {preid}, {timestamp}, {hash}, {branch}, {inc} and more.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx dev-dump [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* -p, --preid <id> Prerelease identifier (default: "dev")
|
|
15
|
+
* -f, --format <tpl> Prerelease template (default: "{preid}.{timestamp}.{hash}")
|
|
16
|
+
* -b, --base <ver> Override base MAJOR.MINOR.PATCH
|
|
17
|
+
* -r, --from-registry Resolve base / inc from the npm registry
|
|
18
|
+
* -t, --tag <tag> dist-tag used with --from-registry (default: "latest")
|
|
19
|
+
* -d, --dry-run Print the version without writing package.json
|
|
20
|
+
* -s, --stdout-only Only print the version string to stdout (implies --dry-run)
|
|
21
|
+
* --no-git-tag Skip creating a git tag (default: true)
|
|
22
|
+
* --cwd <path> Working directory (default: ".")
|
|
23
|
+
* -h, --help Show this help message
|
|
24
|
+
* -v, --version Show tool version
|
|
25
|
+
*
|
|
26
|
+
* Template variables:
|
|
27
|
+
* {preid} --preid value e.g. dev, alpha, beta
|
|
28
|
+
* {timestamp} YYYYMMDDHHmm e.g. 202603171950
|
|
29
|
+
* {ts} YYMMDDHHmm e.g. 2603171950
|
|
30
|
+
* {date} YYYYMMDD e.g. 20260317
|
|
31
|
+
* {time} HHmm e.g. 1950
|
|
32
|
+
* {hash} git short hash (7) e.g. abc1234
|
|
33
|
+
* {hash-long} git full hash (40) e.g. abc1234...
|
|
34
|
+
* {inc} auto-increment (needs --from-registry or local scan)
|
|
35
|
+
* {branch} current git branch (sanitised) e.g. feat-login
|
|
36
|
+
* {base} MAJOR.MINOR.PATCH e.g. 1.2.3
|
|
37
|
+
*
|
|
38
|
+
* @license MIT
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
'use strict';
|
|
42
|
+
|
|
43
|
+
const { execSync } = require('child_process');
|
|
44
|
+
const fs = require('fs');
|
|
45
|
+
const path = require('path');
|
|
46
|
+
|
|
47
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
48
|
+
// Minimal semver helpers (no external dependency)
|
|
49
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
const SEMVER_RE =
|
|
52
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|[\da-zA-Z-]*[a-zA-Z-][\da-zA-Z-]*)(?:\.(?:0|[1-9]\d*|[\da-zA-Z-]*[a-zA-Z-][\da-zA-Z-]*))*))?(?:\+([\da-zA-Z-]+(?:\.[\da-zA-Z-]+)*))?$/;
|
|
53
|
+
|
|
54
|
+
function parseSemver(v) {
|
|
55
|
+
const m = String(v).replace(/^[=v]+/, '').match(SEMVER_RE);
|
|
56
|
+
if (!m) return null;
|
|
57
|
+
return {
|
|
58
|
+
major: Number(m[1]),
|
|
59
|
+
minor: Number(m[2]),
|
|
60
|
+
patch: Number(m[3]),
|
|
61
|
+
prerelease: m[4] || '',
|
|
62
|
+
build: m[5] || '',
|
|
63
|
+
base: `${m[1]}.${m[2]}.${m[3]}`,
|
|
64
|
+
raw: m[0],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function isValidSemver(v) {
|
|
69
|
+
return parseSemver(v) !== null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Compare two semver strings. Returns -1 | 0 | 1.
|
|
74
|
+
* Implements the full SemVer 2.0.0 precedence rules including prerelease.
|
|
75
|
+
*/
|
|
76
|
+
function semverCompare(a, b) {
|
|
77
|
+
const pa = parseSemver(a);
|
|
78
|
+
const pb = parseSemver(b);
|
|
79
|
+
if (!pa || !pb) return 0;
|
|
80
|
+
|
|
81
|
+
// Compare MAJOR.MINOR.PATCH
|
|
82
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
83
|
+
if (pa[k] > pb[k]) return 1;
|
|
84
|
+
if (pa[k] < pb[k]) return -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Both have no prerelease → equal
|
|
88
|
+
if (!pa.prerelease && !pb.prerelease) return 0;
|
|
89
|
+
// One has prerelease, the other doesn't → the one WITHOUT is greater
|
|
90
|
+
if (!pa.prerelease) return 1;
|
|
91
|
+
if (!pb.prerelease) return -1;
|
|
92
|
+
|
|
93
|
+
// Compare prerelease identifiers
|
|
94
|
+
const aParts = pa.prerelease.split('.');
|
|
95
|
+
const bParts = pb.prerelease.split('.');
|
|
96
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
97
|
+
for (let i = 0; i < len; i++) {
|
|
98
|
+
if (i >= aParts.length) return -1; // fewer fields → lower
|
|
99
|
+
if (i >= bParts.length) return 1;
|
|
100
|
+
const ai = aParts[i];
|
|
101
|
+
const bi = bParts[i];
|
|
102
|
+
const aNum = /^\d+$/.test(ai);
|
|
103
|
+
const bNum = /^\d+$/.test(bi);
|
|
104
|
+
if (aNum && bNum) {
|
|
105
|
+
const diff = Number(ai) - Number(bi);
|
|
106
|
+
if (diff !== 0) return diff > 0 ? 1 : -1;
|
|
107
|
+
} else if (aNum) {
|
|
108
|
+
return -1; // numeric < alphanumeric
|
|
109
|
+
} else if (bNum) {
|
|
110
|
+
return 1;
|
|
111
|
+
} else {
|
|
112
|
+
if (ai > bi) return 1;
|
|
113
|
+
if (ai < bi) return -1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
120
|
+
// Helpers
|
|
121
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function run(cmd, cwd) {
|
|
124
|
+
try {
|
|
125
|
+
return execSync(cmd, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })
|
|
126
|
+
.toString()
|
|
127
|
+
.trim();
|
|
128
|
+
} catch {
|
|
129
|
+
return '';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Sanitise a string so that it only contains characters allowed by SemVer in
|
|
135
|
+
* prerelease identifiers: [0-9A-Za-z-].
|
|
136
|
+
*/
|
|
137
|
+
function sanitise(str) {
|
|
138
|
+
return str
|
|
139
|
+
.replace(/[^0-9A-Za-z-]+/g, '-') // replace illegal chars
|
|
140
|
+
.replace(/--+/g, '-') // collapse repeated dashes
|
|
141
|
+
.replace(/^-|-$/g, ''); // trim leading/trailing dash
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Remove leading zeros from a pure-numeric identifier (SemVer rule: numeric
|
|
146
|
+
* identifiers MUST NOT include leading zeroes).
|
|
147
|
+
* Non-numeric identifiers are returned as-is.
|
|
148
|
+
*/
|
|
149
|
+
function stripLeadingZero(id) {
|
|
150
|
+
if (/^\d+$/.test(id)) {
|
|
151
|
+
return String(Number(id));
|
|
152
|
+
}
|
|
153
|
+
return id;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Pad a number to `n` digits.
|
|
158
|
+
*/
|
|
159
|
+
const pad = (v, n = 2) => String(v).padStart(n, '0');
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Build a map of all template variables available for the current environment.
|
|
163
|
+
*/
|
|
164
|
+
function buildVariables(opts) {
|
|
165
|
+
const now = new Date();
|
|
166
|
+
const yyyy = now.getFullYear();
|
|
167
|
+
const MM = pad(now.getMonth() + 1);
|
|
168
|
+
const dd = pad(now.getDate());
|
|
169
|
+
const HH = pad(now.getHours());
|
|
170
|
+
const mm = pad(now.getMinutes());
|
|
171
|
+
|
|
172
|
+
const timestamp = `${yyyy}${MM}${dd}${HH}${mm}`;
|
|
173
|
+
const ts = `${String(yyyy).slice(2)}${MM}${dd}${HH}${mm}`;
|
|
174
|
+
const date = `${yyyy}${MM}${dd}`;
|
|
175
|
+
const time = `${HH}${mm}`;
|
|
176
|
+
|
|
177
|
+
const hash = run('git rev-parse --short HEAD', opts.cwd);
|
|
178
|
+
const hashLong = run('git rev-parse HEAD', opts.cwd);
|
|
179
|
+
const rawBranch = run('git rev-parse --abbrev-ref HEAD', opts.cwd);
|
|
180
|
+
const branch = sanitise(rawBranch || 'unknown');
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
preid: sanitise(opts.preid),
|
|
184
|
+
timestamp,
|
|
185
|
+
ts,
|
|
186
|
+
date,
|
|
187
|
+
time,
|
|
188
|
+
hash: hash || '0000000',
|
|
189
|
+
'hash-long': hashLong || '0'.repeat(40),
|
|
190
|
+
branch,
|
|
191
|
+
base: opts._resolvedBase || '0.0.0',
|
|
192
|
+
inc: String(opts._resolvedInc ?? 0),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Resolve {inc} by scanning the npm registry for existing versions that share
|
|
198
|
+
* the same base and preid pattern, then returning max + 1.
|
|
199
|
+
*/
|
|
200
|
+
function resolveInc(pkgName, base, preid, cwd) {
|
|
201
|
+
if (!pkgName) return 0;
|
|
202
|
+
const raw = run(`npm view ${pkgName} versions --json 2>/dev/null`, cwd);
|
|
203
|
+
if (!raw) return 0;
|
|
204
|
+
|
|
205
|
+
let versions;
|
|
206
|
+
try {
|
|
207
|
+
versions = JSON.parse(raw);
|
|
208
|
+
} catch {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
if (!Array.isArray(versions)) {
|
|
212
|
+
versions = [versions]; // single version comes as string
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Filter to versions whose base matches and prerelease starts with preid
|
|
216
|
+
const prefix = `${base}-${preid}.`;
|
|
217
|
+
let maxInc = -1;
|
|
218
|
+
for (const v of versions) {
|
|
219
|
+
if (!v.startsWith(prefix)) continue;
|
|
220
|
+
const rest = v.slice(prefix.length);
|
|
221
|
+
// Try to extract leading numeric inc value
|
|
222
|
+
const firstDotOrEnd = rest.indexOf('.');
|
|
223
|
+
const candidate = firstDotOrEnd === -1 ? rest : rest.slice(0, firstDotOrEnd);
|
|
224
|
+
if (/^\d+$/.test(candidate)) {
|
|
225
|
+
const n = Number(candidate);
|
|
226
|
+
if (n > maxInc) maxInc = n;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return maxInc + 1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Resolve the base version from the npm registry when --from-registry is set.
|
|
234
|
+
*/
|
|
235
|
+
function resolveBaseFromRegistry(pkgName, tag, cwd) {
|
|
236
|
+
if (!pkgName) return null;
|
|
237
|
+
const v = run(
|
|
238
|
+
`npm view ${pkgName}@${tag} version 2>/dev/null`,
|
|
239
|
+
cwd,
|
|
240
|
+
);
|
|
241
|
+
if (v && isValidSemver(v)) {
|
|
242
|
+
return parseSemver(v).base;
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Interpolate the format template with the variable map, then validate &
|
|
249
|
+
* clean each prerelease identifier.
|
|
250
|
+
*/
|
|
251
|
+
function interpolate(format, vars) {
|
|
252
|
+
let result = format.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
253
|
+
const k = key.trim().toLowerCase();
|
|
254
|
+
if (vars.hasOwnProperty(k)) return vars[k];
|
|
255
|
+
// Unknown variable – leave as sanitised literal
|
|
256
|
+
return sanitise(key);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Split into identifiers, sanitise each, drop empties
|
|
260
|
+
const ids = result.split('.').map(id => {
|
|
261
|
+
const s = sanitise(id);
|
|
262
|
+
return stripLeadingZero(s);
|
|
263
|
+
}).filter(Boolean);
|
|
264
|
+
|
|
265
|
+
return ids.join('.');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
269
|
+
// CLI argument parser
|
|
270
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
function parseArgs(argv) {
|
|
273
|
+
const opts = {
|
|
274
|
+
preid: 'dev',
|
|
275
|
+
format: '{preid}.{timestamp}.{hash}',
|
|
276
|
+
base: null,
|
|
277
|
+
fromRegistry: false,
|
|
278
|
+
tag: 'latest',
|
|
279
|
+
dryRun: false,
|
|
280
|
+
stdoutOnly: false,
|
|
281
|
+
noGitTag: true,
|
|
282
|
+
cwd: process.cwd(),
|
|
283
|
+
help: false,
|
|
284
|
+
showVersion: false,
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const args = argv.slice(2);
|
|
288
|
+
for (let i = 0; i < args.length; i++) {
|
|
289
|
+
const a = args[i];
|
|
290
|
+
switch (a) {
|
|
291
|
+
case '-p': case '--preid':
|
|
292
|
+
opts.preid = args[++i] || 'dev';
|
|
293
|
+
break;
|
|
294
|
+
case '-f': case '--format':
|
|
295
|
+
opts.format = args[++i] || opts.format;
|
|
296
|
+
break;
|
|
297
|
+
case '-b': case '--base':
|
|
298
|
+
opts.base = args[++i] || null;
|
|
299
|
+
break;
|
|
300
|
+
case '-r': case '--from-registry':
|
|
301
|
+
opts.fromRegistry = true;
|
|
302
|
+
break;
|
|
303
|
+
case '-t': case '--tag':
|
|
304
|
+
opts.tag = args[++i] || 'latest';
|
|
305
|
+
break;
|
|
306
|
+
case '-d': case '--dry-run':
|
|
307
|
+
opts.dryRun = true;
|
|
308
|
+
break;
|
|
309
|
+
case '-s': case '--stdout-only':
|
|
310
|
+
opts.stdoutOnly = true;
|
|
311
|
+
opts.dryRun = true;
|
|
312
|
+
break;
|
|
313
|
+
case '--no-git-tag':
|
|
314
|
+
opts.noGitTag = true;
|
|
315
|
+
break;
|
|
316
|
+
case '--cwd':
|
|
317
|
+
opts.cwd = path.resolve(args[++i] || '.');
|
|
318
|
+
break;
|
|
319
|
+
case '-h': case '--help':
|
|
320
|
+
opts.help = true;
|
|
321
|
+
break;
|
|
322
|
+
case '-v': case '--version':
|
|
323
|
+
opts.showVersion = true;
|
|
324
|
+
break;
|
|
325
|
+
default:
|
|
326
|
+
// Positional: treat as preid shorthand
|
|
327
|
+
if (!a.startsWith('-')) {
|
|
328
|
+
opts.preid = a;
|
|
329
|
+
}
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return opts;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
337
|
+
// Help text
|
|
338
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
const HELP = `
|
|
341
|
+
dev-dump — Flexible semver-compliant snapshot version generator
|
|
342
|
+
|
|
343
|
+
Usage:
|
|
344
|
+
npx dev-dump [options] [preid]
|
|
345
|
+
|
|
346
|
+
Options:
|
|
347
|
+
-p, --preid <id> Prerelease identifier (default: "dev")
|
|
348
|
+
-f, --format <tpl> Prerelease template (default: "{preid}.{timestamp}.{hash}")
|
|
349
|
+
-b, --base <ver> Override base MAJOR.MINOR.PATCH
|
|
350
|
+
-r, --from-registry Resolve base / inc from npm registry
|
|
351
|
+
-t, --tag <tag> dist-tag for --from-registry (default: "latest")
|
|
352
|
+
-d, --dry-run Print version without modifying files
|
|
353
|
+
-s, --stdout-only Only output the version string (implies --dry-run)
|
|
354
|
+
--no-git-tag Do not create a git tag (default)
|
|
355
|
+
--cwd <path> Working directory (default: ".")
|
|
356
|
+
-h, --help Show this help message
|
|
357
|
+
-v, --version Show tool version
|
|
358
|
+
|
|
359
|
+
Template variables:
|
|
360
|
+
{preid} Prerelease id dev, alpha, beta, rc, canary
|
|
361
|
+
{timestamp} YYYYMMDDHHmm 202603171950
|
|
362
|
+
{ts} YYMMDDHHmm 2603171950
|
|
363
|
+
{date} YYYYMMDD 20260317
|
|
364
|
+
{time} HHmm 1950
|
|
365
|
+
{hash} Git short hash (7) abc1234
|
|
366
|
+
{hash-long} Git full hash (40) abc1234567890...
|
|
367
|
+
{inc} Auto-increment counter 0, 1, 2, ... (needs --from-registry)
|
|
368
|
+
{branch} Git branch (sanitised) feat-login
|
|
369
|
+
{base} MAJOR.MINOR.PATCH 1.2.3
|
|
370
|
+
|
|
371
|
+
Examples:
|
|
372
|
+
npx dev-dump # 1.2.3-dev.202603171950.abc1234
|
|
373
|
+
npx dev-dump -p alpha # 1.2.3-alpha.202603171950.abc1234
|
|
374
|
+
npx dev-dump -f "{preid}.{inc}" -r # 1.2.3-dev.0 (auto-increment from registry)
|
|
375
|
+
npx dev-dump -f "{branch}.{ts}.{hash}" # 1.2.3-feat-login.2603171950.abc1234
|
|
376
|
+
npx dev-dump -f "canary.{hash}" # 1.2.3-canary.abc1234
|
|
377
|
+
npx dev-dump -b 2.0.0 -p beta # 2.0.0-beta.202603171950.abc1234
|
|
378
|
+
npx dev-dump --dry-run # prints version without writing
|
|
379
|
+
npx dev-dump --stdout-only # only outputs version string
|
|
380
|
+
`.trim();
|
|
381
|
+
|
|
382
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
383
|
+
// Main
|
|
384
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
function main() {
|
|
387
|
+
const opts = parseArgs(process.argv);
|
|
388
|
+
|
|
389
|
+
// --help
|
|
390
|
+
if (opts.help) {
|
|
391
|
+
console.log(HELP);
|
|
392
|
+
process.exit(0);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// --version
|
|
396
|
+
if (opts.showVersion) {
|
|
397
|
+
const self = path.join(__dirname, 'package.json');
|
|
398
|
+
if (fs.existsSync(self)) {
|
|
399
|
+
console.log(JSON.parse(fs.readFileSync(self, 'utf8')).version);
|
|
400
|
+
} else {
|
|
401
|
+
console.log('0.0.0');
|
|
402
|
+
}
|
|
403
|
+
process.exit(0);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 1. Locate package.json
|
|
407
|
+
const pkgPath = path.resolve(opts.cwd, 'package.json');
|
|
408
|
+
let pkg = {};
|
|
409
|
+
let hasPkg = false;
|
|
410
|
+
if (fs.existsSync(pkgPath)) {
|
|
411
|
+
try {
|
|
412
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
413
|
+
hasPkg = true;
|
|
414
|
+
} catch (e) {
|
|
415
|
+
error(`Failed to parse ${pkgPath}: ${e.message}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// 2. Determine base version
|
|
420
|
+
let base;
|
|
421
|
+
if (opts.base) {
|
|
422
|
+
// User-supplied
|
|
423
|
+
const p = parseSemver(opts.base);
|
|
424
|
+
if (!p) error(`Invalid --base version: ${opts.base}`);
|
|
425
|
+
base = p.base;
|
|
426
|
+
} else if (opts.fromRegistry && pkg.name) {
|
|
427
|
+
const regBase = resolveBaseFromRegistry(pkg.name, opts.tag, opts.cwd);
|
|
428
|
+
if (regBase) {
|
|
429
|
+
base = regBase;
|
|
430
|
+
if (!opts.stdoutOnly) {
|
|
431
|
+
console.error(`ℹ Base from registry (${opts.tag}): ${base}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!base && hasPkg && pkg.version) {
|
|
436
|
+
const p = parseSemver(pkg.version);
|
|
437
|
+
if (p) base = p.base;
|
|
438
|
+
}
|
|
439
|
+
if (!base) {
|
|
440
|
+
error(
|
|
441
|
+
'Could not determine base version. Provide --base, ensure package.json has a version, or use --from-registry.',
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
opts._resolvedBase = base;
|
|
445
|
+
|
|
446
|
+
// 3. Resolve {inc} if the template uses it
|
|
447
|
+
if (opts.format.includes('{inc}')) {
|
|
448
|
+
if (!pkg.name) {
|
|
449
|
+
error('{inc} requires a package name in package.json (or --from-registry).');
|
|
450
|
+
}
|
|
451
|
+
opts._resolvedInc = resolveInc(pkg.name, base, sanitise(opts.preid), opts.cwd);
|
|
452
|
+
if (!opts.stdoutOnly) {
|
|
453
|
+
console.error(`ℹ Resolved {inc} = ${opts._resolvedInc}`);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// 4. Build variable map & interpolate
|
|
458
|
+
const vars = buildVariables(opts);
|
|
459
|
+
const prerelease = interpolate(opts.format, vars);
|
|
460
|
+
|
|
461
|
+
if (!prerelease) {
|
|
462
|
+
error('Template produced an empty prerelease string.');
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const newVersion = `${base}-${prerelease}`;
|
|
466
|
+
|
|
467
|
+
// 5. Validate
|
|
468
|
+
if (!isValidSemver(newVersion)) {
|
|
469
|
+
error(`Generated version is not valid semver: ${newVersion}`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// 6. Verify ordering: new version should be >= base-prerelease level
|
|
473
|
+
// (this is a sanity check, not a hard gate)
|
|
474
|
+
if (hasPkg && pkg.version && isValidSemver(pkg.version)) {
|
|
475
|
+
if (semverCompare(newVersion, pkg.version) < 0) {
|
|
476
|
+
if (!opts.stdoutOnly) {
|
|
477
|
+
console.error(
|
|
478
|
+
`⚠ Warning: generated version ${newVersion} is lower than current ${pkg.version}`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// 7. Output
|
|
485
|
+
if (opts.stdoutOnly) {
|
|
486
|
+
process.stdout.write(newVersion);
|
|
487
|
+
process.exit(0);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (opts.dryRun) {
|
|
491
|
+
console.log(newVersion);
|
|
492
|
+
process.exit(0);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Write to package.json
|
|
496
|
+
if (!hasPkg) {
|
|
497
|
+
error('No package.json found. Use --dry-run or --stdout-only.');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
pkg.version = newVersion;
|
|
501
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
502
|
+
console.log(`✔ ${newVersion}`);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function error(msg) {
|
|
506
|
+
console.error(`✖ ${msg}`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
511
|
+
// Export for programmatic use
|
|
512
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
513
|
+
|
|
514
|
+
module.exports = {
|
|
515
|
+
parseSemver,
|
|
516
|
+
isValidSemver,
|
|
517
|
+
semverCompare,
|
|
518
|
+
sanitise,
|
|
519
|
+
stripLeadingZero,
|
|
520
|
+
interpolate,
|
|
521
|
+
buildVariables,
|
|
522
|
+
resolveInc,
|
|
523
|
+
resolveBaseFromRegistry,
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
// Run when executed directly
|
|
527
|
+
if (require.main === module) {
|
|
528
|
+
main();
|
|
529
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dev-dump",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Zero-dependency CLI tool to generate timestamp and git-hash based development versions",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dev-dump": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"semver",
|
|
14
|
+
"version",
|
|
15
|
+
"snapshot",
|
|
16
|
+
"prerelease",
|
|
17
|
+
"dev",
|
|
18
|
+
"alpha",
|
|
19
|
+
"beta",
|
|
20
|
+
"canary",
|
|
21
|
+
"ci",
|
|
22
|
+
"npm-version",
|
|
23
|
+
"git",
|
|
24
|
+
"hash",
|
|
25
|
+
"timestamp",
|
|
26
|
+
"bump"
|
|
27
|
+
],
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "https://github.com/gitaiQAQ/dev-dump.git"
|
|
36
|
+
},
|
|
37
|
+
"author": "gitaiQAQ",
|
|
38
|
+
"license": "MIT",
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=14.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|