new-branch 0.3.0 → 0.4.0
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/README.md +107 -375
- package/dist/cli.js +19 -4
- package/dist/git/gitConfig.js +11 -0
- package/dist/pattern/transforms/index.js +18 -12
- package/dist/pattern/transforms/registry.js +10 -0
- package/dist/runtime/builtins.js +44 -0
- package/package.json +8 -1
- package/dist/config/loadProjectConfig.test.js +0 -34
- package/dist/git/sanitizeGitRef.test.js +0 -30
- package/dist/parseArgs.test.js +0 -50
- package/dist/pattern/parsePattern.test.js +0 -25
- package/dist/pattern/transforms/camel.test.js +0 -13
- package/dist/pattern/transforms/helpers/words.test.js +0 -35
- package/dist/pattern/transforms/kebab.test.js +0 -13
- package/dist/pattern/transforms/lower.test.js +0 -10
- package/dist/pattern/transforms/max.test.js +0 -13
- package/dist/pattern/transforms/renderPattern.test.js +0 -46
- package/dist/pattern/transforms/slugify.test.js +0 -14
- package/dist/pattern/transforms/snake.test.js +0 -13
- package/dist/pattern/transforms/title.test.js +0 -13
- package/dist/pattern/transforms/upper.test.js +0 -10
- package/dist/pattern/transforms/words.test.js +0 -16
- package/dist/runtime/resolveMissingValues.test.js +0 -64
package/README.md
CHANGED
|
@@ -1,299 +1,30 @@
|
|
|
1
|
-
# new-branch
|
|
2
|
-
|
|
3
|
-
## 1. Overview
|
|
4
|
-
|
|
5
|
-
`new-branch` is a CLI tool for generating standardized Git branch names
|
|
6
|
-
based on a configurable pattern and optional interactive prompts.
|
|
7
|
-
|
|
8
|
-
It can be executed in two ways:
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
npx new-branch
|
|
12
|
-
git nb
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 2. Goals
|
|
18
|
-
|
|
19
|
-
- Standardize branch naming across teams.
|
|
20
|
-
- Provide interactive and non-interactive modes.
|
|
21
|
-
- Support a composable transformation pipeline (functional style).
|
|
22
|
-
- Ensure generated names are always valid Git references.
|
|
23
|
-
- Be easily extensible via custom transforms.
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## 3. Usage
|
|
28
|
-
|
|
29
|
-
### 3.1 Basic
|
|
30
|
-
|
|
31
|
-
```bash
|
|
32
|
-
npx new-branch
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
Runs in interactive mode if required fields are missing.
|
|
36
|
-
|
|
37
|
-
---
|
|
38
|
-
|
|
39
|
-
### 3.2 With config file
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npx new-branch --config .newbranchrc
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
Alias:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
npx new-branch -c .newbranchrc
|
|
49
|
-
```
|
|
50
|
-
|
|
51
|
-
---
|
|
52
|
-
|
|
53
|
-
### 3.3 With custom pattern
|
|
54
|
-
|
|
55
|
-
```bash
|
|
56
|
-
npx new-branch --pattern "{type}/{title:slugify;max:25}-{id}"
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
Alias:
|
|
60
|
-
|
|
61
|
-
```bash
|
|
62
|
-
npx new-branch -p "{type}/{title:slugify;max:25}-{id}"
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
---
|
|
66
|
-
|
|
67
|
-
### 3.4 Non-interactive mode
|
|
68
|
-
|
|
69
|
-
```bash
|
|
70
|
-
npx new-branch --pattern "{type}/{title:slugify;max:25}-{id}" --id STK-123 --title "My very interesting task" --type feat
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
If all required variables are provided, no prompt is shown.
|
|
74
|
-
|
|
75
|
-
---
|
|
76
|
-
|
|
77
|
-
## 4. Configuration Precedence
|
|
78
|
-
|
|
79
|
-
Resolution order (highest priority first):
|
|
80
|
-
|
|
81
|
-
1. CLI flags
|
|
82
|
-
2. Environment variables
|
|
83
|
-
3. Config file (.newbranchrc)
|
|
84
|
-
4. Defaults
|
|
85
|
-
5. Interactive prompt (only if required values are missing)
|
|
86
|
-
|
|
87
|
-
---
|
|
88
|
-
|
|
89
|
-
## 5. Pattern Language
|
|
90
|
-
|
|
91
|
-
### 5.1 Syntax
|
|
92
|
-
|
|
93
|
-
Pattern example:
|
|
94
|
-
|
|
95
|
-
{type}/{title:slugify;max:25}-{id}
|
|
96
|
-
|
|
97
|
-
Structure:
|
|
98
|
-
|
|
99
|
-
{variable:transform1;transform2;transformWithArg:arg}
|
|
100
|
-
|
|
101
|
-
### 5.2 Parsing Model
|
|
102
|
-
|
|
103
|
-
Each token is parsed into:
|
|
104
|
-
|
|
105
|
-
- variable name
|
|
106
|
-
- ordered list of transforms
|
|
107
|
-
- optional arguments per transform
|
|
108
|
-
|
|
109
|
-
Example AST representation:
|
|
110
|
-
|
|
111
|
-
```json
|
|
112
|
-
{
|
|
113
|
-
"variable": "title",
|
|
114
|
-
"pipeline": [{ "fn": "slugify" }, { "fn": "max", "args": [25] }]
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## 6. Built-in Variables
|
|
121
|
-
|
|
122
|
-
| Variable | Description |
|
|
123
|
-
| -------- | ------------------------------- |
|
|
124
|
-
| type | Branch type (feat, fix, etc.) |
|
|
125
|
-
| title | Human-readable task title |
|
|
126
|
-
| id | Task identifier (e.g., STK-123) |
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## 7. Built-in Transforms
|
|
131
|
-
|
|
132
|
-
All transforms must be pure functions.
|
|
133
|
-
|
|
134
|
-
### 7.1 String Transforms
|
|
135
|
-
|
|
136
|
-
| Transform | Description |
|
|
137
|
-
| --------- | ------------------------- |
|
|
138
|
-
| slugify | Converts to URL-safe slug |
|
|
139
|
-
| lowercase | Converts to lowercase |
|
|
140
|
-
| uppercase | Converts to uppercase |
|
|
141
|
-
| trim | Trims whitespace |
|
|
142
|
-
| titlecase | Capitalizes words |
|
|
143
|
-
|
|
144
|
-
### 7.2 Argument-based Transforms
|
|
145
|
-
|
|
146
|
-
| Transform | Description | Example |
|
|
147
|
-
| --------- | ------------------------------ | ------- |
|
|
148
|
-
| max | Truncates string to max length | max:25 |
|
|
149
|
-
| pad | Pads string to length | pad:10 |
|
|
150
|
-
|
|
151
|
-
### 7.3 Validation Transforms
|
|
152
|
-
|
|
153
|
-
Validation transforms do not modify a value but throw errors if invalid.
|
|
154
|
-
|
|
155
|
-
| Transform | Description |
|
|
156
|
-
| --------- | -------------------------- |
|
|
157
|
-
| required | Ensures value is not empty |
|
|
158
|
-
| match | Validates value via regex |
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
## 8. Functional Pipeline Execution
|
|
163
|
-
|
|
164
|
-
Each variable pipeline is executed using reduce semantics:
|
|
165
|
-
|
|
166
|
-
```js
|
|
167
|
-
pipeline.reduce((acc, step) => {
|
|
168
|
-
return transforms[step.fn](acc, ...step.args);
|
|
169
|
-
}, baseValue);
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
All transforms must be registered in a dictionary:
|
|
173
|
-
|
|
174
|
-
```js
|
|
175
|
-
const transforms = {
|
|
176
|
-
slugify,
|
|
177
|
-
lowercase,
|
|
178
|
-
uppercase,
|
|
179
|
-
max,
|
|
180
|
-
trim,
|
|
181
|
-
};
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
---
|
|
185
|
-
|
|
186
|
-
## 9. Git Ref Sanitization
|
|
187
|
-
|
|
188
|
-
After full pattern rendering, a final sanitization step must run:
|
|
189
|
-
|
|
190
|
-
- Remove invalid Git characters
|
|
191
|
-
- Prevent:
|
|
192
|
-
- trailing slash
|
|
193
|
-
- double dots
|
|
194
|
-
- leading dash
|
|
195
|
-
- spaces
|
|
196
|
-
- Ensure valid Git ref format
|
|
197
|
-
|
|
198
|
-
Final step example:
|
|
199
|
-
|
|
200
|
-
```js
|
|
201
|
-
branchName = sanitizeGitRef(branchName);
|
|
202
|
-
```
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## 10. Branch Type Standardization
|
|
207
|
-
|
|
208
|
-
Supported types:
|
|
209
|
-
|
|
210
|
-
- feat
|
|
211
|
-
- fix
|
|
212
|
-
- chore
|
|
213
|
-
- docs
|
|
214
|
-
- refactor
|
|
215
|
-
- test
|
|
216
|
-
- perf
|
|
217
|
-
- build
|
|
218
|
-
- ci
|
|
219
|
-
|
|
220
|
-
Optional alias mapping:
|
|
221
|
-
|
|
222
|
-
- feature → feat
|
|
223
|
-
- bugfix → fix
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
## 11. Interactive Mode Behavior
|
|
228
|
-
|
|
229
|
-
If required values are missing:
|
|
230
|
-
|
|
231
|
-
Prompt user for: - type - id - title
|
|
232
|
-
|
|
233
|
-
Validation must occur immediately after input.
|
|
234
|
-
|
|
235
|
-
---
|
|
236
|
-
|
|
237
|
-
## 12. Optional Flags
|
|
238
|
-
|
|
239
|
-
| Flag | Description |
|
|
240
|
-
| ---------- | ------------------------------------------ |
|
|
241
|
-
| `--create` | Creates branch using `git switch -c` |
|
|
242
|
-
| `--print` | Prints branch name only (default behavior) |
|
|
243
|
-
|
|
244
|
-
---
|
|
245
|
-
|
|
246
|
-
## 13. Example Outputs
|
|
247
|
-
|
|
248
|
-
Input:
|
|
249
|
-
|
|
250
|
-
type = feat
|
|
251
|
-
title = My very interesting task
|
|
252
|
-
id = STK-123
|
|
253
|
-
|
|
254
|
-
Pattern:
|
|
255
|
-
|
|
256
|
-
{type}/{title:slugify;max:25}-{id}
|
|
1
|
+
# new-branch
|
|
257
2
|
|
|
258
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./logo.svg" width="180" alt="new-branch logo" />
|
|
5
|
+
</p>
|
|
259
6
|
|
|
260
|
-
|
|
7
|
+
> “Explicit is better than implicit.”
|
|
8
|
+
> — The Zen of Python (PEP 20)
|
|
261
9
|
|
|
262
|
-
|
|
10
|
+
A composable CLI to generate and optionally create standardized Git branch names using a pattern + transform pipeline.
|
|
263
11
|
|
|
264
|
-
|
|
12
|
+

|
|
265
13
|
|
|
266
|
-
-
|
|
267
|
-
-
|
|
268
|
-
- Branch existence check
|
|
269
|
-
- Automatic incremental suffixing
|
|
270
|
-
- Conventional commits integration
|
|
14
|
+
[](https://github.com/teles/new-branch/actions/workflows/ci.yml)
|
|
15
|
+
[](https://codecov.io/gh/teles/new-branch)
|
|
271
16
|
|
|
272
17
|
---
|
|
273
18
|
|
|
274
|
-
##
|
|
275
|
-
|
|
276
|
-
`new-branch` is a composable, functional, extensible CLI tool for
|
|
277
|
-
standardized Git branch naming.
|
|
278
|
-
|
|
279
|
-
Core principles:
|
|
280
|
-
|
|
281
|
-
- Functional pipeline transforms
|
|
282
|
-
- Deterministic output
|
|
283
|
-
- Git-safe sanitization
|
|
284
|
-
- Clear precedence rules
|
|
285
|
-
- Interactive fallback
|
|
286
|
-
|
|
287
|
-
# new-branch
|
|
19
|
+
## Why
|
|
288
20
|
|
|
289
|
-
|
|
290
|
-
based on a composable pattern language.
|
|
21
|
+
Keep branch names consistent across your team using a declarative pattern language.
|
|
291
22
|
|
|
292
23
|
---
|
|
293
24
|
|
|
294
|
-
##
|
|
25
|
+
## Install
|
|
295
26
|
|
|
296
|
-
|
|
27
|
+
Run without installing:
|
|
297
28
|
|
|
298
29
|
```bash
|
|
299
30
|
npx new-branch
|
|
@@ -309,78 +40,32 @@ npm install -g new-branch
|
|
|
309
40
|
|
|
310
41
|
## Usage
|
|
311
42
|
|
|
312
|
-
|
|
43
|
+
Generate a branch name:
|
|
313
44
|
|
|
314
45
|
```bash
|
|
315
46
|
new-branch \
|
|
316
|
-
--pattern "{type}/{title:slugify}-{id}" \
|
|
47
|
+
--pattern "{type}/{title:slugify;max:25}-{id}" \
|
|
317
48
|
--type feat \
|
|
318
49
|
--title "My task" \
|
|
319
50
|
--id STK-123
|
|
320
51
|
```
|
|
321
52
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
feat/minha-tarefa-STK-123
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
### Create the branch automatically
|
|
53
|
+
Create the branch automatically:
|
|
331
54
|
|
|
332
55
|
```bash
|
|
333
56
|
new-branch \
|
|
334
|
-
--pattern "{type
|
|
57
|
+
--pattern "{type}/{title:slugify}-{id}" \
|
|
335
58
|
--type feat \
|
|
336
59
|
--title "My task" \
|
|
337
60
|
--id STK-123 \
|
|
338
61
|
--create
|
|
339
62
|
```
|
|
340
63
|
|
|
341
|
-
This runs:
|
|
342
|
-
|
|
343
|
-
```
|
|
344
|
-
git switch -c <generated-branch>
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
---
|
|
348
|
-
|
|
349
|
-
### Interactive mode
|
|
350
|
-
|
|
351
|
-
If required variables in the pattern are missing, the CLI will prompt for them.
|
|
352
|
-
|
|
353
|
-
Example:
|
|
354
|
-
|
|
355
|
-
```bash
|
|
356
|
-
new-branch \
|
|
357
|
-
--pattern "{type:upper}/{title:slugify}-{id}" \
|
|
358
|
-
--title "My task" \
|
|
359
|
-
--id STK-123 \
|
|
360
|
-
--create
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
You will be prompted for `type`.
|
|
364
|
-
|
|
365
|
-
---
|
|
366
|
-
|
|
367
|
-
## CLI Options
|
|
368
|
-
|
|
369
|
-
| Option | Description |
|
|
370
|
-
| ------------------------- | -------------------------------------------- |
|
|
371
|
-
| `-p, --pattern <pattern>` | Branch name pattern |
|
|
372
|
-
| `--id <id>` | Task ID |
|
|
373
|
-
| `--title <title>` | Task title |
|
|
374
|
-
| `--type <type>` | Branch type |
|
|
375
|
-
| `--create` | Create the branch using `git switch -c` |
|
|
376
|
-
| `--no-prompt` | Fail instead of prompting for missing values |
|
|
377
|
-
| `--quiet` | Do not print any output |
|
|
378
|
-
|
|
379
64
|
---
|
|
380
65
|
|
|
381
66
|
## Pattern Language
|
|
382
67
|
|
|
383
|
-
|
|
68
|
+
Patterns are composed of variables and ordered transforms.
|
|
384
69
|
|
|
385
70
|
Example:
|
|
386
71
|
|
|
@@ -395,90 +80,137 @@ Example:
|
|
|
395
80
|
```
|
|
396
81
|
|
|
397
82
|
- Variables are wrapped in `{}`
|
|
398
|
-
- Transforms
|
|
399
|
-
-
|
|
83
|
+
- Transforms run left-to-right
|
|
84
|
+
- Multiple transforms are separated by `;`
|
|
85
|
+
- Transform arguments use `:`
|
|
400
86
|
|
|
401
87
|
---
|
|
402
88
|
|
|
403
89
|
## Built-in Variables
|
|
404
90
|
|
|
405
|
-
|
|
91
|
+
### Core Variables
|
|
406
92
|
|
|
407
93
|
- `type`
|
|
408
94
|
- `title`
|
|
409
95
|
- `id`
|
|
410
96
|
|
|
97
|
+
### Date Built-ins (derived from local system time)
|
|
98
|
+
|
|
99
|
+
- `year` → YYYY
|
|
100
|
+
- `month` → MM (zero padded)
|
|
101
|
+
- `day` → DD (zero padded)
|
|
102
|
+
- `date` → YYYY-MM-DD
|
|
103
|
+
- `dateCompact` → YYYYMMDD
|
|
104
|
+
|
|
411
105
|
---
|
|
412
106
|
|
|
413
107
|
## Built-in Transforms
|
|
414
108
|
|
|
415
|
-
|
|
109
|
+
| Transform | Description |
|
|
110
|
+
| --------- | -------------------------- |
|
|
111
|
+
| `slugify` | Convert to URL-safe slug |
|
|
112
|
+
| `lower` | Convert to lowercase |
|
|
113
|
+
| `upper` | Convert to uppercase |
|
|
114
|
+
| `camel` | Convert to camelCase |
|
|
115
|
+
| `kebab` | Convert to kebab-case |
|
|
116
|
+
| `snake` | Convert to snake_case |
|
|
117
|
+
| `title` | Convert to Title Case |
|
|
118
|
+
| `words:n` | Keep at most `n` words |
|
|
119
|
+
| `max:n` | Truncate to `n` characters |
|
|
416
120
|
|
|
417
|
-
|
|
121
|
+
All transforms are pure functions and composable.
|
|
418
122
|
|
|
419
|
-
|
|
420
|
-
| --------- | --------------------------- |
|
|
421
|
-
| `lower` | Lowercases the value |
|
|
422
|
-
| `upper` | Uppercases the value |
|
|
423
|
-
| `slugify` | Converts to a git-safe slug |
|
|
123
|
+
---
|
|
424
124
|
|
|
425
|
-
|
|
125
|
+
## Interactive Mode
|
|
426
126
|
|
|
427
|
-
|
|
428
|
-
| --------- | ------------------------------ | -------- |
|
|
429
|
-
| `max` | Truncates string to max length | `max:25` |
|
|
127
|
+
If variables referenced by the pattern are missing, the CLI prompts for them by default.
|
|
430
128
|
|
|
431
|
-
|
|
129
|
+
Disable prompts with:
|
|
432
130
|
|
|
433
|
-
```
|
|
434
|
-
|
|
131
|
+
```bash
|
|
132
|
+
--no-prompt
|
|
435
133
|
```
|
|
436
134
|
|
|
437
135
|
---
|
|
438
136
|
|
|
439
|
-
##
|
|
440
|
-
|
|
441
|
-
After rendering, branch names are:
|
|
442
|
-
|
|
443
|
-
1. Lightly sanitized
|
|
444
|
-
2. Validated using `git check-ref-format --branch`
|
|
137
|
+
## CLI Options
|
|
445
138
|
|
|
446
|
-
|
|
139
|
+
| Option | Description |
|
|
140
|
+
| ------------------------- | ----------------------------------- |
|
|
141
|
+
| `-p, --pattern <pattern>` | Branch pattern |
|
|
142
|
+
| `--type <type>` | Branch type |
|
|
143
|
+
| `--title <title>` | Task title |
|
|
144
|
+
| `--id <id>` | Task identifier |
|
|
145
|
+
| `--create` | Create branch using `git switch -c` |
|
|
146
|
+
| `--no-prompt` | Fail instead of prompting |
|
|
147
|
+
| `--quiet` | Suppress output |
|
|
447
148
|
|
|
448
149
|
---
|
|
449
150
|
|
|
450
|
-
##
|
|
151
|
+
## Project Configuration
|
|
451
152
|
|
|
452
|
-
|
|
153
|
+
You can define a default pattern in `package.json`:
|
|
453
154
|
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
155
|
+
```json
|
|
156
|
+
{
|
|
157
|
+
"new-branch": {
|
|
158
|
+
"pattern": "{type}/{title:slugify}-{id}"
|
|
159
|
+
}
|
|
160
|
+
}
|
|
458
161
|
```
|
|
459
162
|
|
|
460
|
-
|
|
163
|
+
### Git Configuration
|
|
461
164
|
|
|
462
|
-
|
|
463
|
-
|
|
165
|
+
You can also define a default pattern using Git config:
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
git config --local new-branch.pattern "{type}/{title:slugify}-{id}"
|
|
464
169
|
```
|
|
465
170
|
|
|
466
|
-
|
|
171
|
+
Or globally:
|
|
467
172
|
|
|
173
|
+
```bash
|
|
174
|
+
git config --global new-branch.pattern "{type}/{title:slugify}-{id}"
|
|
468
175
|
```
|
|
469
|
-
|
|
176
|
+
|
|
177
|
+
To remove the pattern from Git config:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Remove from local repository
|
|
181
|
+
git config --unset --local new-branch.pattern
|
|
182
|
+
|
|
183
|
+
# Remove from global config
|
|
184
|
+
git config --unset --global new-branch.pattern
|
|
470
185
|
```
|
|
471
186
|
|
|
187
|
+
When using Git config, the resolution order becomes:
|
|
188
|
+
|
|
189
|
+
1. CLI flags
|
|
190
|
+
2. `package.json` configuration
|
|
191
|
+
3. Git config (`new-branch.pattern`)
|
|
192
|
+
4. Interactive prompt (if enabled)
|
|
193
|
+
|
|
472
194
|
---
|
|
473
195
|
|
|
474
|
-
##
|
|
196
|
+
## Git Safety
|
|
197
|
+
|
|
198
|
+
After rendering, branch names are:
|
|
199
|
+
|
|
200
|
+
1. Lightly sanitized
|
|
201
|
+
2. Validated via `git check-ref-format --branch`
|
|
202
|
+
|
|
203
|
+
Invalid names cause the command to fail.
|
|
204
|
+
|
|
205
|
+
---
|
|
475
206
|
|
|
476
|
-
|
|
207
|
+
## Development
|
|
477
208
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
209
|
+
```bash
|
|
210
|
+
pnpm install
|
|
211
|
+
pnpm test:run
|
|
212
|
+
pnpm build
|
|
213
|
+
```
|
|
482
214
|
|
|
483
215
|
---
|
|
484
216
|
|
package/dist/cli.js
CHANGED
|
@@ -4,7 +4,9 @@ import { parsePattern } from "./pattern/parsePattern.js";
|
|
|
4
4
|
import { defaultTransforms } from "./pattern/transforms/index.js";
|
|
5
5
|
import { renderPattern } from "./pattern/transforms/renderPattern.js";
|
|
6
6
|
import { resolveMissingValues } from "./runtime/resolveMissingValues.js";
|
|
7
|
+
import { getBuiltinValues } from "./runtime/builtins.js";
|
|
7
8
|
import { loadProjectConfig } from "./config/loadProjectConfig.js";
|
|
9
|
+
import { getGitConfig } from "./git/gitConfig.js";
|
|
8
10
|
import { sanitizeGitRef } from "./git/sanitizeGitRef.js";
|
|
9
11
|
import { validateBranchName } from "./git/validateBranchName.js";
|
|
10
12
|
import { createBranch } from "./git/createBranch.js";
|
|
@@ -46,7 +48,7 @@ function toInitialValues(args) {
|
|
|
46
48
|
type: args.options.type,
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
|
-
async function run() {
|
|
51
|
+
export async function run() {
|
|
50
52
|
// Step 0: args/options
|
|
51
53
|
// Note: CAC prints help, but depending on our parseArgs wrapper we might not
|
|
52
54
|
// expose `help` in `args.options`. We still want to exit early and never
|
|
@@ -62,14 +64,23 @@ async function run() {
|
|
|
62
64
|
const prompt = args.options.prompt !== false;
|
|
63
65
|
// Pipeline: pattern -> AST -> resolve values -> render -> sanitize -> validate -> (optional) git -> output
|
|
64
66
|
const projectConfig = await loadProjectConfig();
|
|
65
|
-
|
|
67
|
+
// Git config (respects local -> global precedence automatically)
|
|
68
|
+
let gitPattern;
|
|
69
|
+
if (!args.options.pattern && !projectConfig.pattern) {
|
|
70
|
+
gitPattern = await getGitConfig("new-branch.pattern");
|
|
71
|
+
}
|
|
72
|
+
const resolvedPattern = args.options.pattern ?? projectConfig.pattern ?? gitPattern;
|
|
66
73
|
const patternRes = requirePattern(resolvedPattern);
|
|
67
74
|
if (!isOk(patternRes))
|
|
68
75
|
fail("Invalid CLI arguments.", patternRes.error);
|
|
69
76
|
const astRes = safe(() => parsePattern(patternRes.value));
|
|
70
77
|
if (!isOk(astRes))
|
|
71
78
|
fail("Invalid pattern.", astRes.error);
|
|
72
|
-
const
|
|
79
|
+
const builtinValues = getBuiltinValues();
|
|
80
|
+
const initialValues = {
|
|
81
|
+
...builtinValues,
|
|
82
|
+
...toInitialValues(args),
|
|
83
|
+
};
|
|
73
84
|
const valuesRes = await safeAsync(() => resolveMissingValues(astRes.value, initialValues, {
|
|
74
85
|
prompt,
|
|
75
86
|
}));
|
|
@@ -106,4 +117,8 @@ async function run() {
|
|
|
106
117
|
}
|
|
107
118
|
}
|
|
108
119
|
}
|
|
109
|
-
|
|
120
|
+
// Only execute the CLI automatically when not running tests.
|
|
121
|
+
// In tests, `run` can be imported and called directly.
|
|
122
|
+
if (process.env.NODE_ENV !== "test") {
|
|
123
|
+
run().catch((e) => fail("Unexpected error in CLI.", e));
|
|
124
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
export async function getGitConfig(key) {
|
|
3
|
+
try {
|
|
4
|
+
const { stdout } = await execa("git", ["config", "--get", key]);
|
|
5
|
+
const value = stdout.trim();
|
|
6
|
+
return value.length ? value : undefined;
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
import { buildRegistry } from "../../pattern/transforms/registry.js";
|
|
1
2
|
import { lower } from "../../pattern/transforms/lower.js";
|
|
2
3
|
import { upper } from "../../pattern/transforms/upper.js";
|
|
3
4
|
import { max } from "../../pattern/transforms/max.js";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
5
|
+
import { slugify } from "../../pattern/transforms/slugify.js";
|
|
6
|
+
import { camel } from "../../pattern/transforms/camel.js";
|
|
7
|
+
import { kebab } from "../../pattern/transforms/kebab.js";
|
|
8
|
+
import { snake } from "../../pattern/transforms/snake.js";
|
|
9
|
+
import { title } from "../../pattern/transforms/title.js";
|
|
10
|
+
import { words } from "../../pattern/transforms/words.js";
|
|
11
|
+
export const allTransforms = [
|
|
12
|
+
lower,
|
|
13
|
+
upper,
|
|
14
|
+
max,
|
|
15
|
+
slugify,
|
|
16
|
+
camel,
|
|
17
|
+
kebab,
|
|
18
|
+
snake,
|
|
19
|
+
title,
|
|
20
|
+
words,
|
|
21
|
+
];
|
|
16
22
|
export const defaultTransforms = buildRegistry(allTransforms);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pads a number with a leading zero when necessary.
|
|
3
|
+
*
|
|
4
|
+
* @param n - Number to pad.
|
|
5
|
+
* @returns A zero-padded string (e.g., 2 → "02").
|
|
6
|
+
*/
|
|
7
|
+
function pad(n) {
|
|
8
|
+
return String(n).padStart(2, "0");
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Returns built-in date-based variables derived from
|
|
12
|
+
* the local system time.
|
|
13
|
+
*
|
|
14
|
+
* This function is intentionally synchronous and pure.
|
|
15
|
+
* The optional `now` parameter allows deterministic testing.
|
|
16
|
+
*
|
|
17
|
+
* Provided variables:
|
|
18
|
+
*
|
|
19
|
+
* - `year` → YYYY
|
|
20
|
+
* - `month` → MM (zero padded)
|
|
21
|
+
* - `day` → DD (zero padded)
|
|
22
|
+
* - `date` → YYYY-MM-DD
|
|
23
|
+
* - `dateCompact` → YYYYMMDD
|
|
24
|
+
*
|
|
25
|
+
* @param now - Optional Date instance used to generate values.
|
|
26
|
+
* Defaults to the current system date/time.
|
|
27
|
+
* @returns An object containing built-in date variables.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* const builtins = getBuiltinValues(new Date("2026-02-19"));
|
|
31
|
+
* builtins.date; // "2026-02-19"
|
|
32
|
+
*/
|
|
33
|
+
export function getBuiltinValues(now = new Date()) {
|
|
34
|
+
const year = String(now.getFullYear());
|
|
35
|
+
const month = pad(now.getMonth() + 1);
|
|
36
|
+
const day = pad(now.getDate());
|
|
37
|
+
return {
|
|
38
|
+
year,
|
|
39
|
+
month,
|
|
40
|
+
day,
|
|
41
|
+
date: `${year}-${month}-${day}`,
|
|
42
|
+
dateCompact: `${year}${month}${day}`,
|
|
43
|
+
};
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "new-branch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Generate and create standardized git branch names from a pattern.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -48,10 +48,17 @@
|
|
|
48
48
|
"packageManager": "pnpm@10.22.0",
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@eslint/js": "10.0.1",
|
|
51
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
52
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
53
|
+
"@semantic-release/git": "^10.0.1",
|
|
54
|
+
"@semantic-release/github": "^12.0.6",
|
|
55
|
+
"@semantic-release/npm": "^13.1.4",
|
|
56
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
51
57
|
"@types/node": "25.2.3",
|
|
52
58
|
"@vitest/coverage-v8": "4.0.18",
|
|
53
59
|
"eslint": "10.0.0",
|
|
54
60
|
"prettier": "3.8.1",
|
|
61
|
+
"semantic-release": "^25.0.3",
|
|
55
62
|
"tsc-alias": "1.8.16",
|
|
56
63
|
"tsx": "4.21.0",
|
|
57
64
|
"typescript": "5.9.3",
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { vi, describe, it, expect, beforeEach } from "vitest";
|
|
2
|
-
vi.mock("node:fs/promises", () => ({
|
|
3
|
-
readFile: vi.fn(),
|
|
4
|
-
}));
|
|
5
|
-
import { readFile } from "node:fs/promises";
|
|
6
|
-
describe("loadProjectConfig", () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.resetAllMocks();
|
|
9
|
-
});
|
|
10
|
-
it("returns pattern when package.json contains new-branch.pattern as string", async () => {
|
|
11
|
-
readFile.mockResolvedValueOnce(JSON.stringify({ "new-branch": { pattern: "{type}/{title}-{id}" } }));
|
|
12
|
-
const { loadProjectConfig } = await import("./loadProjectConfig.js");
|
|
13
|
-
const cfg = await loadProjectConfig();
|
|
14
|
-
expect(cfg).toEqual({ pattern: "{type}/{title}-{id}" });
|
|
15
|
-
});
|
|
16
|
-
it("returns empty object when package.json has no new-branch key", async () => {
|
|
17
|
-
readFile.mockResolvedValueOnce(JSON.stringify({ name: "pkg" }));
|
|
18
|
-
const { loadProjectConfig } = await import("./loadProjectConfig.js");
|
|
19
|
-
const cfg = await loadProjectConfig();
|
|
20
|
-
expect(cfg).toEqual({});
|
|
21
|
-
});
|
|
22
|
-
it("ignores non-string pattern values", async () => {
|
|
23
|
-
readFile.mockResolvedValueOnce(JSON.stringify({ "new-branch": { pattern: 123 } }));
|
|
24
|
-
const { loadProjectConfig } = await import("./loadProjectConfig.js");
|
|
25
|
-
const cfg = await loadProjectConfig();
|
|
26
|
-
expect(cfg).toEqual({});
|
|
27
|
-
});
|
|
28
|
-
it("returns empty object if reading package.json throws", async () => {
|
|
29
|
-
readFile.mockRejectedValueOnce(new Error("enoent"));
|
|
30
|
-
const { loadProjectConfig } = await import("./loadProjectConfig.js");
|
|
31
|
-
const cfg = await loadProjectConfig();
|
|
32
|
-
expect(cfg).toEqual({});
|
|
33
|
-
});
|
|
34
|
-
});
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { sanitizeGitRef } from "./sanitizeGitRef.js";
|
|
3
|
-
describe("sanitizeGitRef", () => {
|
|
4
|
-
it("trims input and replaces whitespace with dashes", () => {
|
|
5
|
-
expect(sanitizeGitRef(" feature new ")).toBe("feature-new");
|
|
6
|
-
});
|
|
7
|
-
it("removes forbidden characters and sequences", () => {
|
|
8
|
-
const input = "~^:??*[\\]name@{weird}";
|
|
9
|
-
// forbidden chars removed and @{ removed
|
|
10
|
-
expect(sanitizeGitRef(input)).toBe("nameweird}");
|
|
11
|
-
});
|
|
12
|
-
it("collapses multiple slashes and removes leading/trailing slashes", () => {
|
|
13
|
-
expect(sanitizeGitRef("///a//b/c///")).toBe("a/b/c");
|
|
14
|
-
});
|
|
15
|
-
it("collapses repeated dots and trims trailing dots", () => {
|
|
16
|
-
expect(sanitizeGitRef("v1..0...")).toBe("v1.0");
|
|
17
|
-
});
|
|
18
|
-
it("prevents leading dash or slash and trailing dot or slash", () => {
|
|
19
|
-
expect(sanitizeGitRef("-/--abc/.")).toBe("abc");
|
|
20
|
-
});
|
|
21
|
-
it("removes trailing .lock suffix", () => {
|
|
22
|
-
expect(sanitizeGitRef("release.lock")).toBe("release");
|
|
23
|
-
// multiple .lock occurrences only remove final suffix
|
|
24
|
-
expect(sanitizeGitRef("a.lock.lock")).toBe("a.lock");
|
|
25
|
-
});
|
|
26
|
-
it("works with an empty-ish result (keeps dot/punctuations according to sanitizer)", () => {
|
|
27
|
-
// Implementation collapses dots and slashes but does not remove the leading dot
|
|
28
|
-
expect(sanitizeGitRef(" ....///--- ")).toBe("./---");
|
|
29
|
-
});
|
|
30
|
-
});
|
package/dist/parseArgs.test.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parseArgs } from "./parseArgs.js";
|
|
3
|
-
describe("parseArgs", () => {
|
|
4
|
-
it("parses long options", () => {
|
|
5
|
-
const argv = [
|
|
6
|
-
"node",
|
|
7
|
-
"cli",
|
|
8
|
-
"--pattern",
|
|
9
|
-
"{type}/{title}-{id}",
|
|
10
|
-
"--id",
|
|
11
|
-
"STK-123",
|
|
12
|
-
"--title",
|
|
13
|
-
"Minha tarefa",
|
|
14
|
-
"--type",
|
|
15
|
-
"feat",
|
|
16
|
-
"--create",
|
|
17
|
-
];
|
|
18
|
-
const res = parseArgs(argv);
|
|
19
|
-
expect(res.options.pattern).toBe("{type}/{title}-{id}");
|
|
20
|
-
expect(res.options.id).toBe("STK-123");
|
|
21
|
-
expect(res.options.title).toBe("Minha tarefa");
|
|
22
|
-
expect(res.options.type).toBe("feat");
|
|
23
|
-
expect(res.options.create).toBe(true);
|
|
24
|
-
expect(res.args).toEqual([]);
|
|
25
|
-
});
|
|
26
|
-
it("parses short -p as pattern", () => {
|
|
27
|
-
const argv = ["node", "cli", "-p", "{type}/{title}-{id}"];
|
|
28
|
-
const res = parseArgs(argv);
|
|
29
|
-
expect(res.options.pattern).toBe("{type}/{title}-{id}");
|
|
30
|
-
});
|
|
31
|
-
it("strips tsx double-dash separator", () => {
|
|
32
|
-
const argv = [
|
|
33
|
-
"node",
|
|
34
|
-
"cli",
|
|
35
|
-
"--",
|
|
36
|
-
"--pattern",
|
|
37
|
-
"{type}/{title}-{id}",
|
|
38
|
-
"--id",
|
|
39
|
-
"STK-123",
|
|
40
|
-
];
|
|
41
|
-
const res = parseArgs(argv);
|
|
42
|
-
expect(res.options.pattern).toBe("{type}/{title}-{id}");
|
|
43
|
-
expect(res.options.id).toBe("STK-123");
|
|
44
|
-
});
|
|
45
|
-
it("supports --no-prompt", () => {
|
|
46
|
-
const argv = ["node", "cli", "--no-prompt"];
|
|
47
|
-
const res = parseArgs(argv);
|
|
48
|
-
expect(res.options.prompt).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { parsePattern } from "./parsePattern.js";
|
|
3
|
-
describe("parsePattern", () => {
|
|
4
|
-
it("parses literals + variables + transforms", () => {
|
|
5
|
-
const res = parsePattern("{type}/{title:slugify;max:25}-{id}");
|
|
6
|
-
expect(res.variablesUsed).toEqual(["type", "title", "id"]);
|
|
7
|
-
expect(res.nodes).toEqual([
|
|
8
|
-
{ kind: "variable", name: "type", transforms: [] },
|
|
9
|
-
{ kind: "literal", value: "/" },
|
|
10
|
-
{
|
|
11
|
-
kind: "variable",
|
|
12
|
-
name: "title",
|
|
13
|
-
transforms: [
|
|
14
|
-
{ name: "slugify", args: [] },
|
|
15
|
-
{ name: "max", args: ["25"] },
|
|
16
|
-
],
|
|
17
|
-
},
|
|
18
|
-
{ kind: "literal", value: "-" },
|
|
19
|
-
{ kind: "variable", name: "id", transforms: [] },
|
|
20
|
-
]);
|
|
21
|
-
});
|
|
22
|
-
it("throws on missing closing brace", () => {
|
|
23
|
-
expect(() => parsePattern("{type/{id}")).toThrow(/missing "}"|Invalid pattern/);
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { camel } from "./camel.js";
|
|
3
|
-
describe("camel transform", () => {
|
|
4
|
-
it("converts spaced text to camelCase", () => {
|
|
5
|
-
expect(camel.fn("My Task", [])).toBe("myTask");
|
|
6
|
-
});
|
|
7
|
-
it("handles punctuation and multiple separators", () => {
|
|
8
|
-
expect(camel.fn("hello-world_test", [])).toBe("helloWorldTest");
|
|
9
|
-
});
|
|
10
|
-
it("returns empty string for empty input", () => {
|
|
11
|
-
expect(camel.fn("", [])).toBe("");
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { splitWords, upperFirst, lowerFirst } from "./words.js";
|
|
3
|
-
describe("splitWords", () => {
|
|
4
|
-
it("returns empty array for empty or whitespace-only input", () => {
|
|
5
|
-
expect(splitWords("")).toEqual([]);
|
|
6
|
-
expect(splitWords(" ")).toEqual([]);
|
|
7
|
-
});
|
|
8
|
-
it("splits on separators and trims words", () => {
|
|
9
|
-
expect(splitWords("hello world-test")).toEqual(["hello", "world", "test"]);
|
|
10
|
-
expect(splitWords(" leading and trailing ")).toEqual(["leading", "and", "trailing"]);
|
|
11
|
-
});
|
|
12
|
-
it("handles camelCase boundaries", () => {
|
|
13
|
-
expect(splitWords("myTask")).toEqual(["my", "Task"]);
|
|
14
|
-
expect(splitWords("version2Beta")).toEqual(["version2", "Beta"]);
|
|
15
|
-
});
|
|
16
|
-
it("handles ALLCAPS followed by capitalized word", () => {
|
|
17
|
-
expect(splitWords("HTTPServer")).toEqual(["HTTP", "Server"]);
|
|
18
|
-
expect(splitWords("XMLHttpRequest")).toEqual(["XML", "Http", "Request"]);
|
|
19
|
-
});
|
|
20
|
-
it("keeps Unicode letters and accents", () => {
|
|
21
|
-
expect(splitWords("Título grande")).toEqual(["Título", "grande"]);
|
|
22
|
-
});
|
|
23
|
-
});
|
|
24
|
-
describe("upperFirst / lowerFirst", () => {
|
|
25
|
-
it("upperFirst capitalizes only the first character", () => {
|
|
26
|
-
expect(upperFirst("hello")).toBe("Hello");
|
|
27
|
-
expect(upperFirst("")).toBe("");
|
|
28
|
-
expect(upperFirst("éclair")).toBe("Éclair");
|
|
29
|
-
});
|
|
30
|
-
it("lowerFirst lowercases only the first character", () => {
|
|
31
|
-
expect(lowerFirst("Hello")).toBe("hello");
|
|
32
|
-
expect(lowerFirst("")).toBe("");
|
|
33
|
-
expect(lowerFirst("Éclair")).toBe("éclair");
|
|
34
|
-
});
|
|
35
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { kebab } from "./kebab.js";
|
|
3
|
-
describe("kebab transform", () => {
|
|
4
|
-
it("converts spaced text to kebab-case", () => {
|
|
5
|
-
expect(kebab.fn("My Task", [])).toBe("my-task");
|
|
6
|
-
});
|
|
7
|
-
it("handles camelCase and punctuation", () => {
|
|
8
|
-
expect(kebab.fn("myTaskHTTP Server", [])).toBe("my-task-http-server");
|
|
9
|
-
});
|
|
10
|
-
it("returns empty string for empty input", () => {
|
|
11
|
-
expect(kebab.fn("", [])).toBe("");
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { lower } from "./lower.js";
|
|
3
|
-
describe("lower transform", () => {
|
|
4
|
-
it("converts to lowercase", () => {
|
|
5
|
-
expect(lower.fn("Hello WORLD", [])).toBe("hello world");
|
|
6
|
-
});
|
|
7
|
-
it("keeps non-letter characters", () => {
|
|
8
|
-
expect(lower.fn("123-ÁÉ!", [])).toBe("123-áé!");
|
|
9
|
-
});
|
|
10
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { max } from "./max.js";
|
|
3
|
-
describe("max transform", () => {
|
|
4
|
-
it("truncates the value to the given length", () => {
|
|
5
|
-
expect(max.fn("abcdef", ["3"])).toBe("abc");
|
|
6
|
-
expect(max.fn("short", ["10"])).toBe("short");
|
|
7
|
-
});
|
|
8
|
-
it("throws on negative or non-finite sizes", () => {
|
|
9
|
-
expect(() => max.fn("abc", ["-1"])).toThrow();
|
|
10
|
-
expect(() => max.fn("abc", ["not-a-number"])).toThrow();
|
|
11
|
-
expect(() => max.fn("abc", [])).toThrow();
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { renderPattern } from "./renderPattern.js";
|
|
3
|
-
import { parsePattern } from "../parsePattern.js";
|
|
4
|
-
import { lower } from "./lower.js";
|
|
5
|
-
import { upper } from "./upper.js";
|
|
6
|
-
import { max } from "./max.js";
|
|
7
|
-
import { slugify } from "./slugify.js";
|
|
8
|
-
const defaultTransforms = {
|
|
9
|
-
[lower.name]: lower.fn,
|
|
10
|
-
[upper.name]: upper.fn,
|
|
11
|
-
[max.name]: max.fn,
|
|
12
|
-
[slugify.name]: slugify.fn,
|
|
13
|
-
};
|
|
14
|
-
describe("renderPattern", () => {
|
|
15
|
-
it("renders literals and variables without transforms", () => {
|
|
16
|
-
const parsed = parsePattern("{type}/{title}-{id}");
|
|
17
|
-
const out = renderPattern(parsed, { type: "feat", title: "My Task", id: "123" }, { transforms: defaultTransforms });
|
|
18
|
-
expect(out).toBe("feat/My Task-123");
|
|
19
|
-
});
|
|
20
|
-
it("applies transforms in order (slugify then max)", () => {
|
|
21
|
-
const parsed = parsePattern("{title:slugify;max:5}");
|
|
22
|
-
const out = renderPattern(parsed, { title: "Título grande" }, { transforms: defaultTransforms });
|
|
23
|
-
// slugify -> "titulo-grande" then max:5 -> "titul"
|
|
24
|
-
expect(out).toBe("titul");
|
|
25
|
-
});
|
|
26
|
-
it("throws when an unknown transform is used", () => {
|
|
27
|
-
const parsed = {
|
|
28
|
-
nodes: [{ kind: "variable", name: "foo", transforms: [{ name: "nope", args: [] }] }],
|
|
29
|
-
variablesUsed: ["foo"],
|
|
30
|
-
};
|
|
31
|
-
expect(() => renderPattern(parsed, { foo: "bar" }, { transforms: defaultTransforms })).toThrow(/Unknown transform/i);
|
|
32
|
-
});
|
|
33
|
-
it("throws on missing variable in strict mode (default)", () => {
|
|
34
|
-
const parsed = parsePattern("{id}");
|
|
35
|
-
expect(() => renderPattern(parsed, {}, { transforms: defaultTransforms })).toThrow(/Missing value/);
|
|
36
|
-
});
|
|
37
|
-
it("returns empty for missing variable when strict is false", () => {
|
|
38
|
-
const parsed = parsePattern("pre-{id}-post");
|
|
39
|
-
const out = renderPattern(parsed, {}, { transforms: defaultTransforms, strict: false });
|
|
40
|
-
expect(out).toBe("pre--post");
|
|
41
|
-
});
|
|
42
|
-
it("propagates transform errors (invalid max arg)", () => {
|
|
43
|
-
const parsed = parsePattern("{title:max:-1}");
|
|
44
|
-
expect(() => renderPattern(parsed, { title: "abc" }, { transforms: defaultTransforms })).toThrow();
|
|
45
|
-
});
|
|
46
|
-
});
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { slugify } from "./slugify.js";
|
|
3
|
-
describe("slugify transform", () => {
|
|
4
|
-
it("removes accents, lowercases and replaces non-alphanum with dashes", () => {
|
|
5
|
-
const input = "Título com Acentos & símbolos!";
|
|
6
|
-
const out = slugify.fn(input, []);
|
|
7
|
-
expect(out).toBe("titulo-com-acentos-simbolos");
|
|
8
|
-
});
|
|
9
|
-
it("collapses multiple separators into one and trims dashes", () => {
|
|
10
|
-
const input = " --Hello___World-- ";
|
|
11
|
-
const out = slugify.fn(input, []);
|
|
12
|
-
expect(out).toBe("hello-world");
|
|
13
|
-
});
|
|
14
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { snake } from "./snake.js";
|
|
3
|
-
describe("snake transform", () => {
|
|
4
|
-
it("converts spaced text to snake_case", () => {
|
|
5
|
-
expect(snake.fn("My Task", [])).toBe("my_task");
|
|
6
|
-
});
|
|
7
|
-
it("handles camelCase and punctuation", () => {
|
|
8
|
-
expect(snake.fn("myTaskHTTP Server", [])).toBe("my_task_http_server");
|
|
9
|
-
});
|
|
10
|
-
it("returns empty string for empty input", () => {
|
|
11
|
-
expect(snake.fn("", [])).toBe("");
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { title } from "./title.js";
|
|
3
|
-
describe("title transform", () => {
|
|
4
|
-
it("converts text to Title Case", () => {
|
|
5
|
-
expect(title.fn("hello WORLD", [])).toBe("Hello World");
|
|
6
|
-
});
|
|
7
|
-
it("handles punctuation and multiple separators", () => {
|
|
8
|
-
expect(title.fn("my-task_HTTP server", [])).toBe("My Task Http Server");
|
|
9
|
-
});
|
|
10
|
-
it("returns empty string for empty input", () => {
|
|
11
|
-
expect(title.fn("", [])).toBe("");
|
|
12
|
-
});
|
|
13
|
-
});
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { upper } from "./upper.js";
|
|
3
|
-
describe("upper transform", () => {
|
|
4
|
-
it("converts to uppercase", () => {
|
|
5
|
-
expect(upper.fn("Hello world", [])).toBe("HELLO WORLD");
|
|
6
|
-
});
|
|
7
|
-
it("keeps non-letter characters", () => {
|
|
8
|
-
expect(upper.fn("123-áé!", [])).toBe("123-ÁÉ!");
|
|
9
|
-
});
|
|
10
|
-
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { words } from "./words.js";
|
|
3
|
-
describe("words transform", () => {
|
|
4
|
-
it("limits the number of words when a positive number is provided", () => {
|
|
5
|
-
expect(words.fn("one two three four", ["2"]).trim()).toBe("one two");
|
|
6
|
-
expect(words.fn("hello world", ["5"]).trim()).toBe("hello world");
|
|
7
|
-
});
|
|
8
|
-
it("returns empty string when 0 is provided", () => {
|
|
9
|
-
expect(words.fn("some text here", ["0"]).trim()).toBe("");
|
|
10
|
-
});
|
|
11
|
-
it("throws when argument is missing or invalid", () => {
|
|
12
|
-
expect(() => words.fn("a b c", [])).toThrow();
|
|
13
|
-
expect(() => words.fn("a b c", ["-1"])).toThrow();
|
|
14
|
-
expect(() => words.fn("a b c", ["not-a-number"])).toThrow();
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { resolveMissingValues } from "./resolveMissingValues.js";
|
|
3
|
-
import { parsePattern } from "../pattern/parsePattern.js";
|
|
4
|
-
// Mock prompts module so tests can control interactive behavior
|
|
5
|
-
vi.mock("@inquirer/prompts", () => {
|
|
6
|
-
return {
|
|
7
|
-
input: vi.fn(),
|
|
8
|
-
select: vi.fn(),
|
|
9
|
-
};
|
|
10
|
-
});
|
|
11
|
-
// Provide a lightweight mock for the runtime enums so module resolution
|
|
12
|
-
// does not fail in the test environment (we only need the shape used
|
|
13
|
-
// by the code under test).
|
|
14
|
-
vi.mock("@/runtime/enums.js", () => ({
|
|
15
|
-
TYPE_CHOICES: [
|
|
16
|
-
{ name: "Feature", value: "feat" },
|
|
17
|
-
{ name: "Fix", value: "fix" },
|
|
18
|
-
],
|
|
19
|
-
}));
|
|
20
|
-
import { input, select } from "@inquirer/prompts";
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
// resetAllMocks clears implementations and mock queues (e.g. mockResolvedValueOnce)
|
|
23
|
-
// which prevents cross-test leakage of one-off mock return values.
|
|
24
|
-
vi.resetAllMocks();
|
|
25
|
-
});
|
|
26
|
-
describe("resolveMissingValues", () => {
|
|
27
|
-
it("returns values unchanged when all required vars are present", async () => {
|
|
28
|
-
const parsed = parsePattern("{type}/{title}-{id}");
|
|
29
|
-
const initial = { type: "feat", title: "My Task", id: "123" };
|
|
30
|
-
const out = await resolveMissingValues(parsed, initial, { prompt: true });
|
|
31
|
-
expect(out).toEqual(initial);
|
|
32
|
-
expect(input).not.toHaveBeenCalled();
|
|
33
|
-
expect(select).not.toHaveBeenCalled();
|
|
34
|
-
});
|
|
35
|
-
it("prompts for a missing non-type variable using input", async () => {
|
|
36
|
-
const parsed = parsePattern("{type}/{title}");
|
|
37
|
-
// input should be used for `title`
|
|
38
|
-
input.mockResolvedValueOnce("Provided Title");
|
|
39
|
-
// Type is present so select should not be called
|
|
40
|
-
const out = await resolveMissingValues(parsed, { type: "fix" }, { prompt: true });
|
|
41
|
-
expect(input).toHaveBeenCalled();
|
|
42
|
-
expect(select).not.toHaveBeenCalled();
|
|
43
|
-
expect(out.title).toBe("Provided Title");
|
|
44
|
-
});
|
|
45
|
-
it("uses select for the `type` variable", async () => {
|
|
46
|
-
const parsed = parsePattern("{type}/{title}");
|
|
47
|
-
select.mockResolvedValueOnce("feat");
|
|
48
|
-
input.mockResolvedValueOnce("Some title");
|
|
49
|
-
const out = await resolveMissingValues(parsed, { title: "Some title" }, { prompt: true });
|
|
50
|
-
expect(select).toHaveBeenCalled();
|
|
51
|
-
expect(out.type).toBe("feat");
|
|
52
|
-
});
|
|
53
|
-
it("throws when prompt is false and a required variable is missing", async () => {
|
|
54
|
-
const parsed = parsePattern("{id}");
|
|
55
|
-
await expect(resolveMissingValues(parsed, {}, { prompt: false })).rejects.toThrow(/Missing required value/i);
|
|
56
|
-
});
|
|
57
|
-
it("treats whitespace-only values as missing and prompts", async () => {
|
|
58
|
-
const parsed = parsePattern("{title}");
|
|
59
|
-
input.mockResolvedValueOnce("Trimmed");
|
|
60
|
-
const out = await resolveMissingValues(parsed, { title: " " }, { prompt: true });
|
|
61
|
-
expect(input).toHaveBeenCalled();
|
|
62
|
-
expect(out.title).toBe("Trimmed");
|
|
63
|
-
});
|
|
64
|
-
});
|