new-branch 0.2.0 → 0.3.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/README.md +73 -382
- package/dist/pattern/transforms/camel.js +25 -0
- package/dist/pattern/transforms/helpers/words.js +64 -0
- package/dist/pattern/transforms/kebab.js +19 -0
- package/dist/pattern/transforms/snake.js +19 -0
- package/dist/pattern/transforms/title.js +19 -0
- package/dist/pattern/transforms/words.js +27 -0
- package/dist/runtime/resolveMissingValues.js +27 -4
- package/package.json +4 -2
- 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/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/upper.test.js +0 -10
package/README.md
CHANGED
|
@@ -1,299 +1,24 @@
|
|
|
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}
|
|
257
|
-
|
|
258
|
-
Output:
|
|
259
|
-
|
|
260
|
-
feat/my-very-interesting-task-STK-123
|
|
1
|
+
# new-branch
|
|
261
2
|
|
|
262
|
-
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="./logo.svg" width="180" alt="new-branch logo" />
|
|
5
|
+
</p>
|
|
263
6
|
|
|
264
|
-
|
|
7
|
+
A composable CLI to generate and optionally create standardized Git branch names using a pattern + transform pipeline.
|
|
265
8
|
|
|
266
|
-
|
|
267
|
-
- Jira integration (auto-fetch title from ID)
|
|
268
|
-
- Branch existence check
|
|
269
|
-
- Automatic incremental suffixing
|
|
270
|
-
- Conventional commits integration
|
|
9
|
+

|
|
271
10
|
|
|
272
11
|
---
|
|
273
12
|
|
|
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
|
|
13
|
+
## Why
|
|
288
14
|
|
|
289
|
-
|
|
290
|
-
based on a composable pattern language.
|
|
15
|
+
Keep branch names consistent across your team using a declarative pattern language.
|
|
291
16
|
|
|
292
17
|
---
|
|
293
18
|
|
|
294
|
-
##
|
|
19
|
+
## Install
|
|
295
20
|
|
|
296
|
-
|
|
21
|
+
Run without installing:
|
|
297
22
|
|
|
298
23
|
```bash
|
|
299
24
|
npx new-branch
|
|
@@ -309,78 +34,32 @@ npm install -g new-branch
|
|
|
309
34
|
|
|
310
35
|
## Usage
|
|
311
36
|
|
|
312
|
-
|
|
37
|
+
Generate a branch name:
|
|
313
38
|
|
|
314
39
|
```bash
|
|
315
40
|
new-branch \
|
|
316
|
-
--pattern "{type}/{title:slugify}-{id}" \
|
|
41
|
+
--pattern "{type}/{title:slugify;max:25}-{id}" \
|
|
317
42
|
--type feat \
|
|
318
43
|
--title "My task" \
|
|
319
44
|
--id STK-123
|
|
320
45
|
```
|
|
321
46
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
```
|
|
325
|
-
feat/minha-tarefa-STK-123
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
### Create the branch automatically
|
|
47
|
+
Create the branch automatically:
|
|
331
48
|
|
|
332
49
|
```bash
|
|
333
50
|
new-branch \
|
|
334
|
-
--pattern "{type
|
|
51
|
+
--pattern "{type}/{title:slugify}-{id}" \
|
|
335
52
|
--type feat \
|
|
336
53
|
--title "My task" \
|
|
337
54
|
--id STK-123 \
|
|
338
55
|
--create
|
|
339
56
|
```
|
|
340
57
|
|
|
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
58
|
---
|
|
380
59
|
|
|
381
60
|
## Pattern Language
|
|
382
61
|
|
|
383
|
-
|
|
62
|
+
Patterns are composed of variables and ordered transforms.
|
|
384
63
|
|
|
385
64
|
Example:
|
|
386
65
|
|
|
@@ -395,15 +74,14 @@ Example:
|
|
|
395
74
|
```
|
|
396
75
|
|
|
397
76
|
- Variables are wrapped in `{}`
|
|
398
|
-
- Transforms
|
|
399
|
-
-
|
|
77
|
+
- Transforms run left-to-right
|
|
78
|
+
- Multiple transforms are separated by `;`
|
|
79
|
+
- Transform arguments use `:`
|
|
400
80
|
|
|
401
81
|
---
|
|
402
82
|
|
|
403
83
|
## Built-in Variables
|
|
404
84
|
|
|
405
|
-
These variables are currently supported:
|
|
406
|
-
|
|
407
85
|
- `type`
|
|
408
86
|
- `title`
|
|
409
87
|
- `id`
|
|
@@ -412,73 +90,86 @@ These variables are currently supported:
|
|
|
412
90
|
|
|
413
91
|
## Built-in Transforms
|
|
414
92
|
|
|
415
|
-
|
|
93
|
+
| Transform | Description |
|
|
94
|
+
| --------- | -------------------------- |
|
|
95
|
+
| `slugify` | Convert to URL-safe slug |
|
|
96
|
+
| `lower` | Convert to lowercase |
|
|
97
|
+
| `upper` | Convert to uppercase |
|
|
98
|
+
| `camel` | Convert to camelCase |
|
|
99
|
+
| `kebab` | Convert to kebab-case |
|
|
100
|
+
| `snake` | Convert to snake_case |
|
|
101
|
+
| `title` | Convert to Title Case |
|
|
102
|
+
| `words:n` | Keep at most `n` words |
|
|
103
|
+
| `max:n` | Truncate to `n` characters |
|
|
416
104
|
|
|
417
|
-
|
|
105
|
+
All transforms are pure functions and composable.
|
|
418
106
|
|
|
419
|
-
|
|
420
|
-
| --------- | --------------------------- |
|
|
421
|
-
| `lower` | Lowercases the value |
|
|
422
|
-
| `upper` | Uppercases the value |
|
|
423
|
-
| `slugify` | Converts to a git-safe slug |
|
|
107
|
+
---
|
|
424
108
|
|
|
425
|
-
|
|
109
|
+
## Interactive Mode
|
|
426
110
|
|
|
427
|
-
|
|
428
|
-
| --------- | ------------------------------ | -------- |
|
|
429
|
-
| `max` | Truncates string to max length | `max:25` |
|
|
111
|
+
If variables referenced by the pattern are missing, the CLI prompts for them by default.
|
|
430
112
|
|
|
431
|
-
|
|
113
|
+
Disable prompts with:
|
|
432
114
|
|
|
433
|
-
```
|
|
434
|
-
|
|
115
|
+
```bash
|
|
116
|
+
--no-prompt
|
|
435
117
|
```
|
|
436
118
|
|
|
437
119
|
---
|
|
438
120
|
|
|
439
|
-
##
|
|
440
|
-
|
|
441
|
-
After rendering, branch names are:
|
|
442
|
-
|
|
443
|
-
1. Lightly sanitized
|
|
444
|
-
2. Validated using `git check-ref-format --branch`
|
|
121
|
+
## CLI Options
|
|
445
122
|
|
|
446
|
-
|
|
123
|
+
| Option | Description |
|
|
124
|
+
| ------------------------- | ----------------------------------- |
|
|
125
|
+
| `-p, --pattern <pattern>` | Branch pattern |
|
|
126
|
+
| `--type <type>` | Branch type |
|
|
127
|
+
| `--title <title>` | Task title |
|
|
128
|
+
| `--id <id>` | Task identifier |
|
|
129
|
+
| `--create` | Create branch using `git switch -c` |
|
|
130
|
+
| `--no-prompt` | Fail instead of prompting |
|
|
131
|
+
| `--quiet` | Suppress output |
|
|
447
132
|
|
|
448
133
|
---
|
|
449
134
|
|
|
450
|
-
##
|
|
135
|
+
## Project Configuration
|
|
451
136
|
|
|
452
|
-
|
|
137
|
+
You can define a default pattern in `package.json`:
|
|
453
138
|
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
139
|
+
```json
|
|
140
|
+
{
|
|
141
|
+
"new-branch": {
|
|
142
|
+
"pattern": "{type}/{title:slugify}-{id}"
|
|
143
|
+
}
|
|
144
|
+
}
|
|
458
145
|
```
|
|
459
146
|
|
|
460
|
-
|
|
147
|
+
Resolution order:
|
|
461
148
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
149
|
+
1. CLI flags
|
|
150
|
+
2. `package.json` configuration
|
|
151
|
+
3. Interactive prompt (if enabled)
|
|
465
152
|
|
|
466
|
-
|
|
153
|
+
---
|
|
467
154
|
|
|
468
|
-
|
|
469
|
-
feat/my-task-STK-123
|
|
470
|
-
```
|
|
155
|
+
## Git Safety
|
|
471
156
|
|
|
472
|
-
|
|
157
|
+
After rendering, branch names are:
|
|
473
158
|
|
|
474
|
-
|
|
159
|
+
1. Lightly sanitized
|
|
160
|
+
2. Validated via `git check-ref-format --branch`
|
|
161
|
+
|
|
162
|
+
Invalid names cause the command to fail.
|
|
163
|
+
|
|
164
|
+
---
|
|
475
165
|
|
|
476
|
-
|
|
166
|
+
## Development
|
|
477
167
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
168
|
+
```bash
|
|
169
|
+
pnpm install
|
|
170
|
+
pnpm test:run
|
|
171
|
+
pnpm build
|
|
172
|
+
```
|
|
482
173
|
|
|
483
174
|
---
|
|
484
175
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { splitWords, upperFirst } from "./helpers/words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Transform: camel
|
|
4
|
+
*
|
|
5
|
+
* Converts an input string into camelCase. Uses `splitWords` to extract word
|
|
6
|
+
* boundaries and lower-cases the words before joining. The first word is kept
|
|
7
|
+
* in lower-case; subsequent words are capitalized using `upperFirst`.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - "My Task" -> "myTask"
|
|
11
|
+
* - "HTTP Server" -> "httpServer"
|
|
12
|
+
*/
|
|
13
|
+
export const camel = {
|
|
14
|
+
name: "camel",
|
|
15
|
+
fn: (value) => {
|
|
16
|
+
const words = splitWords(value).map((w) => w.toLowerCase());
|
|
17
|
+
if (!words.length)
|
|
18
|
+
return "";
|
|
19
|
+
return words[0] + words.slice(1).map(upperFirst).join("");
|
|
20
|
+
},
|
|
21
|
+
doc: {
|
|
22
|
+
summary: "Converts value to camelCase.",
|
|
23
|
+
usage: ["{title:camel}"],
|
|
24
|
+
},
|
|
25
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits an input string into word-like segments.
|
|
3
|
+
*
|
|
4
|
+
* The function attempts to be Unicode-aware and supports the following
|
|
5
|
+
* heuristics for identifying boundaries:
|
|
6
|
+
* - Splits on any run of non-letter/number characters (spaces, punctuation).
|
|
7
|
+
* - Inserts boundaries for camelCase (e.g. `myTask` -> `my Task`).
|
|
8
|
+
* - Inserts a boundary between an ALL-CAPS acronym and a following
|
|
9
|
+
* capitalized word (e.g. `HTTPServer` -> `HTTP Server`).
|
|
10
|
+
*
|
|
11
|
+
* Returned words are trimmed and empty segments are discarded.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* splitWords("myTask") // => ["my", "Task"]
|
|
15
|
+
* splitWords("HTTPServer") // => ["HTTP", "Server"]
|
|
16
|
+
* splitWords("Título grande") // => ["Título", "grande"]
|
|
17
|
+
*
|
|
18
|
+
* @param input - The string to split into words.
|
|
19
|
+
* @returns An array of word segments (possibly empty).
|
|
20
|
+
*/
|
|
21
|
+
export function splitWords(input) {
|
|
22
|
+
const cleaned = input.trim();
|
|
23
|
+
if (!cleaned)
|
|
24
|
+
return [];
|
|
25
|
+
const withBoundaries = cleaned
|
|
26
|
+
// camelCase boundary: myTask -> my Task
|
|
27
|
+
.replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, "$1 $2")
|
|
28
|
+
// ALLCAPS followed by lowercase: HTTPServer -> HTTP Server
|
|
29
|
+
.replace(/(\p{Lu})(\p{Lu}\p{Ll})/gu, "$1 $2");
|
|
30
|
+
return withBoundaries
|
|
31
|
+
.split(/[^\p{L}\p{N}]+/u)
|
|
32
|
+
.map((w) => w.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Upper-cases the first character of the provided string.
|
|
37
|
+
*
|
|
38
|
+
* Does not modify the remainder of the string.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* upperFirst("hello") // => "Hello"
|
|
42
|
+
* upperFirst("") // => ""
|
|
43
|
+
*
|
|
44
|
+
* @param s - Input string.
|
|
45
|
+
* @returns String with the first character upper-cased (if present).
|
|
46
|
+
*/
|
|
47
|
+
export function upperFirst(s) {
|
|
48
|
+
return s.length ? s[0].toUpperCase() + s.slice(1) : s;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Lower-cases the first character of the provided string.
|
|
52
|
+
*
|
|
53
|
+
* Does not modify the remainder of the string.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* lowerFirst("Hello") // => "hello"
|
|
57
|
+
* lowerFirst("") // => ""
|
|
58
|
+
*
|
|
59
|
+
* @param s - Input string.
|
|
60
|
+
* @returns String with the first character lower-cased (if present).
|
|
61
|
+
*/
|
|
62
|
+
export function lowerFirst(s) {
|
|
63
|
+
return s.length ? s[0].toLowerCase() + s.slice(1) : s;
|
|
64
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { splitWords } from "./helpers/words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Transform: kebab
|
|
4
|
+
*
|
|
5
|
+
* Converts an input string into kebab-case (lowercased words joined with
|
|
6
|
+
* hyphens). Uses `splitWords` to determine word boundaries.
|
|
7
|
+
*
|
|
8
|
+
* Example: "My Task" -> "my-task"
|
|
9
|
+
*/
|
|
10
|
+
export const kebab = {
|
|
11
|
+
name: "kebab",
|
|
12
|
+
fn: (value) => splitWords(value)
|
|
13
|
+
.map((w) => w.toLowerCase())
|
|
14
|
+
.join("-"),
|
|
15
|
+
doc: {
|
|
16
|
+
summary: "Converts value to kebab-case.",
|
|
17
|
+
usage: ["{title:kebab}"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { splitWords } from "./helpers/words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Transform: snake
|
|
4
|
+
*
|
|
5
|
+
* Converts an input string into snake_case (lowercased words joined with
|
|
6
|
+
* underscores). Uses `splitWords` to determine boundaries.
|
|
7
|
+
*
|
|
8
|
+
* Example: "My Task" -> "my_task"
|
|
9
|
+
*/
|
|
10
|
+
export const snake = {
|
|
11
|
+
name: "snake",
|
|
12
|
+
fn: (value) => splitWords(value)
|
|
13
|
+
.map((w) => w.toLowerCase())
|
|
14
|
+
.join("_"),
|
|
15
|
+
doc: {
|
|
16
|
+
summary: "Converts value to snake_case.",
|
|
17
|
+
usage: ["{title:snake}"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { splitWords, upperFirst } from "./helpers/words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Transform: title
|
|
4
|
+
*
|
|
5
|
+
* Converts an input string into Title Case where each word's first
|
|
6
|
+
* character is upper-cased and the remainder lower-cased. Uses `splitWords`.
|
|
7
|
+
*
|
|
8
|
+
* Example: "hello WORLD" -> "Hello World"
|
|
9
|
+
*/
|
|
10
|
+
export const title = {
|
|
11
|
+
name: "title",
|
|
12
|
+
fn: (value) => splitWords(value)
|
|
13
|
+
.map((w) => upperFirst(w.toLowerCase()))
|
|
14
|
+
.join(" "),
|
|
15
|
+
doc: {
|
|
16
|
+
summary: "Converts value to Title Case.",
|
|
17
|
+
usage: ["{title:title}"],
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { splitWords } from "./helpers/words.js";
|
|
2
|
+
/**
|
|
3
|
+
* Transform: words
|
|
4
|
+
*
|
|
5
|
+
* Limits the input to at most `n` words. The transform expects a single
|
|
6
|
+
* numeric argument that indicates the maximum number of words to keep. The
|
|
7
|
+
* returned value is the first `n` words joined by a single space.
|
|
8
|
+
*
|
|
9
|
+
* Examples:
|
|
10
|
+
* - `{title:words:2}` applied to "My big title" -> "My big"
|
|
11
|
+
*
|
|
12
|
+
* @throws If the provided argument is missing or not a non-negative number.
|
|
13
|
+
*/
|
|
14
|
+
export const words = {
|
|
15
|
+
name: "words",
|
|
16
|
+
fn: (value, [n]) => {
|
|
17
|
+
const count = Number(n);
|
|
18
|
+
if (!Number.isFinite(count) || count < 0) {
|
|
19
|
+
throw new Error(`words expects a non-negative number, got "${n ?? ""}"`);
|
|
20
|
+
}
|
|
21
|
+
return splitWords(value).slice(0, count).join(" ");
|
|
22
|
+
},
|
|
23
|
+
doc: {
|
|
24
|
+
summary: "Limits value to a maximum number of words.",
|
|
25
|
+
usage: ["{title:words:3}"],
|
|
26
|
+
},
|
|
27
|
+
};
|
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { input, select } from "@inquirer/prompts";
|
|
2
2
|
import { TYPE_CHOICES } from "../runtime/enums.js";
|
|
3
3
|
/**
|
|
4
|
-
* Resolves missing variable values required by
|
|
4
|
+
* Resolves missing variable values required by a parsed pattern.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* The function inspects `parsed.variablesUsed` and ensures each required
|
|
7
|
+
* variable has a non-empty value in the returned object. If a variable is
|
|
8
|
+
* missing or contains only whitespace and `opts.prompt` is `true`, the
|
|
9
|
+
* function will prompt the user for the value. The special variable name
|
|
10
|
+
* `type` is resolved with a select prompt pre-populated with
|
|
11
|
+
* {@link TYPE_CHOICES}.
|
|
12
|
+
*
|
|
13
|
+
* Behavior summary:
|
|
14
|
+
* - Any variable present in `parsed.variablesUsed` is considered required.
|
|
15
|
+
* - If a value exists and is non-empty (after trimming) it is preserved.
|
|
16
|
+
* - If a value is missing or blank and `opts.prompt` is `false`, an error is
|
|
17
|
+
* thrown listing the missing variable.
|
|
18
|
+
* - If `opts.prompt` is `true`, the function will:
|
|
19
|
+
* - use a select prompt for the variable named `type` (choices from
|
|
20
|
+
* {@link TYPE_CHOICES});
|
|
21
|
+
* - use a text input prompt for any other variable.
|
|
22
|
+
*
|
|
23
|
+
* @param parsed - Parsed pattern containing `variablesUsed`.
|
|
24
|
+
* @param initialValues - Existing values that may satisfy requirements.
|
|
25
|
+
* @param opts - Options controlling prompting behavior.
|
|
26
|
+
* @returns A promise resolving to a `RenderValues` object containing all
|
|
27
|
+
* required variables (original values preserved when present).
|
|
28
|
+
* @throws When a required variable is missing and `opts.prompt` is false.
|
|
29
|
+
* @example
|
|
30
|
+
* const parsed = parsePattern('{type}/{title}');
|
|
31
|
+
* await resolveMissingValues(parsed, { title: 'Hello' }, { prompt: true });
|
|
9
32
|
*/
|
|
10
33
|
export async function resolveMissingValues(parsed, initialValues, opts) {
|
|
11
34
|
const requiredVars = parsed.variablesUsed;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "new-branch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Generate and create standardized git branch names from a pattern.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"git",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"prepack": "pnpm build && pnpm test:run",
|
|
21
21
|
"test": "vitest --dir src --exclude **/dist/**",
|
|
22
22
|
"test:run": "vitest run --dir src --exclude **/dist/**",
|
|
23
|
+
"test:coverage": "vitest run --coverage --dir src --exclude **/dist/**",
|
|
23
24
|
"format": "prettier . --write",
|
|
24
25
|
"format:check": "prettier . --check",
|
|
25
26
|
"lint": "eslint src --ext .ts"
|
|
@@ -41,13 +42,14 @@
|
|
|
41
42
|
"url": "https://github.com/teles/new-branch/issues"
|
|
42
43
|
},
|
|
43
44
|
"new-branch": {
|
|
44
|
-
"pattern": "{type}/{title}-{id}"
|
|
45
|
+
"pattern": "{type}/{title:lower}-{id}"
|
|
45
46
|
},
|
|
46
47
|
"homepage": "https://github.com/teles/new-branch#readme",
|
|
47
48
|
"packageManager": "pnpm@10.22.0",
|
|
48
49
|
"devDependencies": {
|
|
49
50
|
"@eslint/js": "10.0.1",
|
|
50
51
|
"@types/node": "25.2.3",
|
|
52
|
+
"@vitest/coverage-v8": "4.0.18",
|
|
51
53
|
"eslint": "10.0.0",
|
|
52
54
|
"prettier": "3.8.1",
|
|
53
55
|
"tsc-alias": "1.8.16",
|
|
@@ -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,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,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
|
-
});
|