@edcalderon/versioning 1.3.0 → 1.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 CHANGED
@@ -5,6 +5,7 @@ A comprehensive versioning and changelog management tool designed for monorepos
5
5
  ## Features
6
6
 
7
7
  - 🚀 Automated version bumping (patch, minor, major, prerelease)
8
+ - 🌿 Optional branch-aware versioning (`main`, `develop`, `feature/*`, `hotfix/*`)
8
9
  - 📝 Conventional commit-based changelog generation
9
10
  - 🔄 Version synchronization across monorepo packages
10
11
  - 🎯 Works with both monorepos and single repositories
@@ -407,7 +408,41 @@ For single repositories:
407
408
  "changelogFile": "CHANGELOG.md",
408
409
  "conventionalCommits": true,
409
410
  "syncDependencies": false,
410
- "ignorePackages": []
411
+ "ignorePackages": [],
412
+ "branchAwareness": {
413
+ "enabled": false,
414
+ "defaultBranch": "main",
415
+ "branches": {
416
+ "main": {
417
+ "versionFormat": "semantic",
418
+ "tagFormat": "v{version}",
419
+ "syncFiles": ["package.json", "version.production.json"],
420
+ "environment": "production",
421
+ "bumpStrategy": "semantic"
422
+ },
423
+ "develop": {
424
+ "versionFormat": "dev",
425
+ "tagFormat": "v{version}",
426
+ "syncFiles": ["version.development.json"],
427
+ "environment": "development",
428
+ "bumpStrategy": "dev-build"
429
+ },
430
+ "feature/*": {
431
+ "versionFormat": "feature",
432
+ "tagFormat": "v{version}",
433
+ "syncFiles": ["version.development.json"],
434
+ "environment": "development",
435
+ "bumpStrategy": "feature-branch"
436
+ },
437
+ "hotfix/*": {
438
+ "versionFormat": "hotfix",
439
+ "tagFormat": "v{version}",
440
+ "syncFiles": ["version.development.json"],
441
+ "environment": "development",
442
+ "bumpStrategy": "hotfix"
443
+ }
444
+ }
445
+ }
411
446
  }
412
447
  ```
413
448
 
@@ -431,6 +466,28 @@ For monorepos:
431
466
  - `conventionalCommits`: Whether to use conventional commits for changelog
432
467
  - `syncDependencies`: Whether to sync internal dependencies
433
468
  - `ignorePackages`: Array of package names to ignore during sync
469
+ - `branchAwareness`: Optional branch-aware release rules and file sync targets
470
+ - `branchAwareness.branches.<pattern>.versionFormat`: `semantic`, `dev`, `feature`, `hotfix`, or custom
471
+ - `branchAwareness.branches.<pattern>.syncFiles`: Only these files are updated when branch-aware mode is enabled
472
+
473
+ ### Branch-Aware Releases
474
+
475
+ Enable branch-aware mode per command:
476
+
477
+ ```bash
478
+ versioning patch --branch-aware
479
+ versioning patch --branch-aware --target-branch develop
480
+ versioning patch --branch-aware --format dev --build 396
481
+ versioning minor --branch-aware
482
+ versioning major --branch-aware
483
+ ```
484
+
485
+ Behavior summary:
486
+ - Exact branch names are checked first (e.g. `main`, `develop`)
487
+ - Wildcard patterns are checked next (e.g. `feature/*`, `hotfix/*`)
488
+ - If no match is found, the `defaultBranch` rule is used
489
+ - `--force-branch-aware` enables branch-aware behavior even when `branchAwareness.enabled` is `false`
490
+ - Full config template: `packages/versioning/examples/versioning.config.branch-aware.json`
434
491
 
435
492
  ## Commands
436
493
 
@@ -471,13 +528,25 @@ versioning release 1.2.3 --message "Custom release"
471
528
  versioning release 2.0.0-beta.1 --skip-sync
472
529
  ```
473
530
 
474
- **Options for release commands:**
531
+ **Options for `patch`, `minor`, and `major`:**
532
+ - `-p, --packages <packages>`: Comma-separated list of packages to sync
533
+ - `-m, --message <message>`: Release commit message
534
+ - `-c, --config <file>`: Config file path (default: versioning.config.json)
535
+ - `--branch-aware`: Enable branch-aware release behavior
536
+ - `--force-branch-aware`: Force branch-aware mode even if disabled in config
537
+ - `--target-branch <branch>`: Explicit branch to resolve branch rules
538
+ - `--format <format>`: Override the configured branch version format
539
+ - `--build <number>`: Override build number for non-semantic branch formats
540
+ - `--no-tag`: Do not create git tag
541
+ - `--no-commit`: Do not commit changes
542
+
543
+ **Options for `release <version>`:**
475
544
  - `-p, --packages <packages>`: Comma-separated list of packages to sync
476
545
  - `-m, --message <message>`: Release commit message
477
546
  - `-c, --config <file>`: Config file path (default: versioning.config.json)
478
547
  - `--no-tag`: Do not create git tag
479
548
  - `--no-commit`: Do not commit changes
480
- - `--skip-sync`: Skip version synchronization (for `release` command)
549
+ - `--skip-sync`: Skip version synchronization
481
550
 
482
551
  ### Other Commands
483
552
 
@@ -624,4 +693,4 @@ Tags should follow the format `v{major}.{minor}.{patch}` (e.g., `v1.0.0`, `v1.1.
624
693
  The `create-tag` script will:
625
694
  - Read the version from `package.json`
626
695
  - Create an annotated git tag
627
- - Push the tag to trigger the publish workflow
696
+ - Push the tag to trigger the publish workflow
package/dist/cli.js CHANGED
@@ -161,6 +161,11 @@ program
161
161
  .option('-p, --packages <packages>', 'Comma-separated list of packages to sync')
162
162
  .option('-m, --message <message>', 'Release commit message')
163
163
  .option('-c, --config <file>', 'Config file path', 'versioning.config.json')
164
+ .option('--branch-aware', 'Enable branch-aware versioning')
165
+ .option('--force-branch-aware', 'Force branch-aware mode even if disabled in config')
166
+ .option('--target-branch <branch>', 'Explicit branch to apply branch-aware rules')
167
+ .option('--format <format>', 'Override version format (semantic, dev, feature, hotfix)')
168
+ .option('--build <number>', 'Override build number for non-semantic formats', parseBuildOption)
164
169
  .option('--no-tag', 'Do not create git tag')
165
170
  .option('--no-commit', 'Do not commit changes')
166
171
  .action(async (options) => {
@@ -179,7 +184,12 @@ program
179
184
  const packages = options.packages ? options.packages.split(',').map((p) => p.trim()) : undefined;
180
185
  const newVersion = await releaseManager.patchRelease({
181
186
  packages,
182
- message: options.message
187
+ message: options.message,
188
+ branchAware: options.branchAware,
189
+ forceBranchAware: options.forceBranchAware,
190
+ targetBranch: options.targetBranch,
191
+ format: options.format,
192
+ build: options.build
183
193
  });
184
194
  console.log(`✅ Patch release v${newVersion} completed`);
185
195
  }
@@ -194,6 +204,11 @@ program
194
204
  .option('-p, --packages <packages>', 'Comma-separated list of packages to sync')
195
205
  .option('-m, --message <message>', 'Release commit message')
196
206
  .option('-c, --config <file>', 'Config file path', 'versioning.config.json')
207
+ .option('--branch-aware', 'Enable branch-aware versioning')
208
+ .option('--force-branch-aware', 'Force branch-aware mode even if disabled in config')
209
+ .option('--target-branch <branch>', 'Explicit branch to apply branch-aware rules')
210
+ .option('--format <format>', 'Override version format (semantic, dev, feature, hotfix)')
211
+ .option('--build <number>', 'Override build number for non-semantic formats', parseBuildOption)
197
212
  .option('--no-tag', 'Do not create git tag')
198
213
  .option('--no-commit', 'Do not commit changes')
199
214
  .action(async (options) => {
@@ -212,7 +227,12 @@ program
212
227
  const packages = options.packages ? options.packages.split(',').map((p) => p.trim()) : undefined;
213
228
  const newVersion = await releaseManager.minorRelease({
214
229
  packages,
215
- message: options.message
230
+ message: options.message,
231
+ branchAware: options.branchAware,
232
+ forceBranchAware: options.forceBranchAware,
233
+ targetBranch: options.targetBranch,
234
+ format: options.format,
235
+ build: options.build
216
236
  });
217
237
  console.log(`✅ Minor release v${newVersion} completed`);
218
238
  }
@@ -225,7 +245,13 @@ program
225
245
  .command('major')
226
246
  .description('Create a major release')
227
247
  .option('-p, --packages <packages>', 'Comma-separated list of packages to sync')
248
+ .option('-m, --message <message>', 'Release commit message')
228
249
  .option('-c, --config <file>', 'Config file path', 'versioning.config.json')
250
+ .option('--branch-aware', 'Enable branch-aware versioning')
251
+ .option('--force-branch-aware', 'Force branch-aware mode even if disabled in config')
252
+ .option('--target-branch <branch>', 'Explicit branch to apply branch-aware rules')
253
+ .option('--format <format>', 'Override version format (semantic, dev, feature, hotfix)')
254
+ .option('--build <number>', 'Override build number for non-semantic formats', parseBuildOption)
229
255
  .option('--no-tag', 'Do not create git tag')
230
256
  .option('--no-commit', 'Do not commit changes')
231
257
  .action(async (options) => {
@@ -244,7 +270,12 @@ program
244
270
  const packages = options.packages ? options.packages.split(',').map((p) => p.trim()) : undefined;
245
271
  const newVersion = await releaseManager.majorRelease({
246
272
  packages,
247
- message: options.message
273
+ message: options.message,
274
+ branchAware: options.branchAware,
275
+ forceBranchAware: options.forceBranchAware,
276
+ targetBranch: options.targetBranch,
277
+ format: options.format,
278
+ build: options.build
248
279
  });
249
280
  console.log(`✅ Major release v${newVersion} completed`);
250
281
  }
@@ -306,7 +337,41 @@ program
306
337
  conventionalCommits: true,
307
338
  syncDependencies: false,
308
339
  ignorePackages: [],
309
- extensions: [] // Add extensions array to config
340
+ extensions: [], // Add extensions array to config
341
+ branchAwareness: {
342
+ enabled: false,
343
+ defaultBranch: 'main',
344
+ branches: {
345
+ main: {
346
+ versionFormat: 'semantic',
347
+ tagFormat: 'v{version}',
348
+ syncFiles: ['package.json'],
349
+ environment: 'production',
350
+ bumpStrategy: 'semantic'
351
+ },
352
+ develop: {
353
+ versionFormat: 'dev',
354
+ tagFormat: 'v{version}',
355
+ syncFiles: ['version.development.json'],
356
+ environment: 'development',
357
+ bumpStrategy: 'dev-build'
358
+ },
359
+ 'feature/*': {
360
+ versionFormat: 'feature',
361
+ tagFormat: 'v{version}',
362
+ syncFiles: ['version.development.json'],
363
+ environment: 'development',
364
+ bumpStrategy: 'feature-branch'
365
+ },
366
+ 'hotfix/*': {
367
+ versionFormat: 'hotfix',
368
+ tagFormat: 'v{version}',
369
+ syncFiles: ['version.development.json'],
370
+ environment: 'development',
371
+ bumpStrategy: 'hotfix'
372
+ }
373
+ }
374
+ }
310
375
  };
311
376
  await fs.writeJson(configPath, defaultConfig, { spaces: 2 });
312
377
  console.log('✅ Initialized versioning config at versioning.config.json');
@@ -322,6 +387,13 @@ async function loadConfig(configPath) {
322
387
  }
323
388
  return await fs.readJson(configPath);
324
389
  }
390
+ function parseBuildOption(value) {
391
+ const parsed = Number.parseInt(value, 10);
392
+ if (!Number.isInteger(parsed) || parsed < 0) {
393
+ throw new Error(`Invalid build number "${value}". Use a non-negative integer.`);
394
+ }
395
+ return parsed;
396
+ }
325
397
  async function main() {
326
398
  try {
327
399
  // Load and register extensions
package/dist/release.d.ts CHANGED
@@ -17,17 +17,21 @@ export declare class ReleaseManager {
17
17
  skipSync?: boolean;
18
18
  }): Promise<void>;
19
19
  private publishPackages;
20
- patchRelease(options?: {
21
- packages?: string[];
22
- message?: string;
23
- }): Promise<string>;
24
- minorRelease(options?: {
25
- packages?: string[];
26
- message?: string;
27
- }): Promise<string>;
28
- majorRelease(options?: {
29
- packages?: string[];
30
- message?: string;
31
- }): Promise<string>;
20
+ patchRelease(options?: ReleaseOptions): Promise<string>;
21
+ minorRelease(options?: ReleaseOptions): Promise<string>;
22
+ majorRelease(options?: ReleaseOptions): Promise<string>;
23
+ private shouldUseBranchAwareFlow;
24
+ private releaseBranchAware;
25
+ }
26
+ interface ReleaseOptions {
27
+ message?: string;
28
+ packages?: string[];
29
+ skipSync?: boolean;
30
+ branchAware?: boolean;
31
+ forceBranchAware?: boolean;
32
+ targetBranch?: string;
33
+ format?: string;
34
+ build?: number;
32
35
  }
36
+ export {};
33
37
  //# sourceMappingURL=release.d.ts.map
package/dist/release.js CHANGED
@@ -81,21 +81,55 @@ class ReleaseManager {
81
81
  console.log('Publishing packages:', packages || 'all');
82
82
  }
83
83
  async patchRelease(options = {}) {
84
- const currentVersion = await this.config.versionManager.getCurrentVersion();
84
+ if (this.shouldUseBranchAwareFlow(options)) {
85
+ return await this.releaseBranchAware('patch', options);
86
+ }
85
87
  const newVersion = await this.config.versionManager.bumpVersion('patch');
86
88
  await this.release(newVersion, options);
87
89
  return newVersion;
88
90
  }
89
91
  async minorRelease(options = {}) {
92
+ if (this.shouldUseBranchAwareFlow(options)) {
93
+ return await this.releaseBranchAware('minor', options);
94
+ }
90
95
  const newVersion = await this.config.versionManager.bumpVersion('minor');
91
96
  await this.release(newVersion, options);
92
97
  return newVersion;
93
98
  }
94
99
  async majorRelease(options = {}) {
100
+ if (this.shouldUseBranchAwareFlow(options)) {
101
+ return await this.releaseBranchAware('major', options);
102
+ }
95
103
  const newVersion = await this.config.versionManager.bumpVersion('major');
96
104
  await this.release(newVersion, options);
97
105
  return newVersion;
98
106
  }
107
+ shouldUseBranchAwareFlow(options) {
108
+ return options.branchAware === true
109
+ || options.forceBranchAware === true
110
+ || typeof options.targetBranch === 'string'
111
+ || typeof options.format === 'string'
112
+ || typeof options.build === 'number';
113
+ }
114
+ async releaseBranchAware(releaseType, options) {
115
+ const result = await this.config.versionManager.bumpVersionBranchAware(releaseType, {
116
+ targetBranch: options.targetBranch,
117
+ forceBranchAware: options.forceBranchAware,
118
+ format: options.format,
119
+ build: options.build
120
+ });
121
+ await this.config.changelogManager.generate();
122
+ if (this.config.createCommit) {
123
+ await this.config.versionManager.commitChanges(result.version);
124
+ }
125
+ if (this.config.createTag) {
126
+ await this.config.versionManager.createGitTagWithFormat(result.version, result.tagFormat, options.message);
127
+ }
128
+ if (this.config.publish) {
129
+ await this.publishPackages(options.packages);
130
+ }
131
+ return result.version;
132
+ }
99
133
  }
100
134
  exports.ReleaseManager = ReleaseManager;
101
135
  //# sourceMappingURL=release.js.map
@@ -1,10 +1,37 @@
1
1
  import * as semver from 'semver';
2
+ export interface BranchRuleConfig {
3
+ versionFormat?: 'semantic' | 'dev' | 'feature' | 'hotfix' | string;
4
+ tagFormat?: string;
5
+ syncFiles?: string[];
6
+ environment?: string;
7
+ bumpStrategy?: 'semantic' | 'dev-build' | 'feature-branch' | 'hotfix' | string;
8
+ }
9
+ export interface BranchAwarenessConfig {
10
+ enabled?: boolean;
11
+ defaultBranch?: string;
12
+ branches?: Record<string, BranchRuleConfig>;
13
+ }
14
+ export interface BranchAwareBumpOptions {
15
+ targetBranch?: string;
16
+ forceBranchAware?: boolean;
17
+ format?: string;
18
+ build?: number;
19
+ }
20
+ export interface BranchAwareBumpResult {
21
+ version: string;
22
+ branch: string;
23
+ matchPattern: string;
24
+ versionFormat: string;
25
+ tagFormat: string;
26
+ syncFiles: string[];
27
+ }
2
28
  export interface VersionConfig {
3
29
  rootPackageJson: string;
4
30
  packages: string[];
5
31
  changelogFile?: string;
6
32
  conventionalCommits?: boolean;
7
33
  extensionConfig?: Record<string, any>;
34
+ branchAwareness?: BranchAwarenessConfig;
8
35
  }
9
36
  export declare class VersionManager {
10
37
  private config;
@@ -12,9 +39,30 @@ export declare class VersionManager {
12
39
  constructor(config: VersionConfig);
13
40
  getCurrentVersion(): Promise<string>;
14
41
  bumpVersion(releaseType: semver.ReleaseType, preRelease?: string): Promise<string>;
42
+ bumpVersionBranchAware(releaseType: semver.ReleaseType, options?: BranchAwareBumpOptions): Promise<BranchAwareBumpResult>;
15
43
  updateVersion(newVersion: string): Promise<void>;
16
44
  private updatePackageJson;
17
45
  createGitTag(version: string, message?: string): Promise<void>;
46
+ createGitTagWithFormat(version: string, tagFormat?: string, message?: string): Promise<string>;
18
47
  commitChanges(version: string): Promise<void>;
48
+ private getCurrentBranch;
49
+ private resolveBranchConfig;
50
+ private getBranchAwarenessConfig;
51
+ private getDefaultBranchRules;
52
+ private normalizeBranchRule;
53
+ private getDefaultBumpStrategy;
54
+ private buildBranchAwareVersion;
55
+ private shouldBumpSemantic;
56
+ private coerceBaseVersion;
57
+ private incrementSemanticVersion;
58
+ private resolveBuildNumber;
59
+ private getDefaultSyncFiles;
60
+ private applyVersionFormat;
61
+ private extractBuildNumber;
62
+ private readVersionFromFile;
63
+ private updateVersionFile;
64
+ private matchesPattern;
65
+ private escapeRegex;
66
+ private renderTag;
19
67
  }
20
68
  //# sourceMappingURL=versioning.d.ts.map
@@ -65,6 +65,32 @@ class VersionManager {
65
65
  await this.updateVersion(newVersion);
66
66
  return newVersion;
67
67
  }
68
+ async bumpVersionBranchAware(releaseType, options = {}) {
69
+ if (options.build !== undefined && (!Number.isInteger(options.build) || options.build < 0)) {
70
+ throw new Error(`Invalid build number "${options.build}". Expected a non-negative integer.`);
71
+ }
72
+ const branch = options.targetBranch || await this.getCurrentBranch();
73
+ const { matchPattern, resolvedConfig } = this.resolveBranchConfig(branch, options.forceBranchAware === true);
74
+ const versionFormat = options.format || resolvedConfig.versionFormat;
75
+ const version = await this.buildBranchAwareVersion(releaseType, branch, versionFormat, resolvedConfig, options.build);
76
+ const syncFiles = resolvedConfig.syncFiles.length > 0
77
+ ? resolvedConfig.syncFiles
78
+ : this.getDefaultSyncFiles(versionFormat);
79
+ if (syncFiles.length === 0) {
80
+ throw new Error(`No sync files configured for branch "${branch}".`);
81
+ }
82
+ for (const filePath of syncFiles) {
83
+ await this.updateVersionFile(filePath, version);
84
+ }
85
+ return {
86
+ version,
87
+ branch,
88
+ matchPattern,
89
+ versionFormat,
90
+ tagFormat: resolvedConfig.tagFormat,
91
+ syncFiles
92
+ };
93
+ }
68
94
  async updateVersion(newVersion) {
69
95
  // Update root package.json
70
96
  await this.updatePackageJson(this.config.rootPackageJson, newVersion);
@@ -82,8 +108,13 @@ class VersionManager {
82
108
  await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
83
109
  }
84
110
  async createGitTag(version, message) {
85
- const tagMessage = message || `Release v${version}`;
86
- await this.git.addAnnotatedTag(`v${version}`, tagMessage);
111
+ await this.createGitTagWithFormat(version, 'v{version}', message);
112
+ }
113
+ async createGitTagWithFormat(version, tagFormat = 'v{version}', message) {
114
+ const tagName = this.renderTag(tagFormat, version);
115
+ const tagMessage = message || `Release ${tagName}`;
116
+ await this.git.addAnnotatedTag(tagName, tagMessage);
117
+ return tagName;
87
118
  }
88
119
  async commitChanges(version) {
89
120
  const commitMessage = this.config.conventionalCommits
@@ -92,6 +123,286 @@ class VersionManager {
92
123
  await this.git.add('.');
93
124
  await this.git.commit(commitMessage);
94
125
  }
126
+ async getCurrentBranch() {
127
+ const branch = (await this.git.revparse(['--abbrev-ref', 'HEAD'])).trim();
128
+ if (!branch) {
129
+ throw new Error('Unable to detect current Git branch.');
130
+ }
131
+ return branch;
132
+ }
133
+ resolveBranchConfig(branch, forceBranchAware) {
134
+ const branchAwareness = this.getBranchAwarenessConfig(forceBranchAware);
135
+ const { branches } = branchAwareness;
136
+ if (branches[branch]) {
137
+ return {
138
+ matchPattern: branch,
139
+ resolvedConfig: this.normalizeBranchRule(branches[branch])
140
+ };
141
+ }
142
+ const wildcardPatterns = Object.keys(branches)
143
+ .filter((pattern) => pattern.includes('*'))
144
+ .sort((a, b) => b.length - a.length);
145
+ for (const pattern of wildcardPatterns) {
146
+ if (this.matchesPattern(branch, pattern)) {
147
+ return {
148
+ matchPattern: pattern,
149
+ resolvedConfig: this.normalizeBranchRule(branches[pattern])
150
+ };
151
+ }
152
+ }
153
+ const fallbackPattern = branchAwareness.defaultBranch;
154
+ if (fallbackPattern && branches[fallbackPattern]) {
155
+ return {
156
+ matchPattern: fallbackPattern,
157
+ resolvedConfig: this.normalizeBranchRule(branches[fallbackPattern])
158
+ };
159
+ }
160
+ throw new Error(`No branch configuration matched "${branch}".`);
161
+ }
162
+ getBranchAwarenessConfig(forceBranchAware) {
163
+ const configured = this.config.branchAwareness;
164
+ const defaultBranch = configured?.defaultBranch || 'main';
165
+ const configuredBranches = configured?.branches && Object.keys(configured.branches).length > 0
166
+ ? configured.branches
167
+ : this.getDefaultBranchRules(defaultBranch);
168
+ if (!configured?.enabled && !forceBranchAware) {
169
+ throw new Error('Branch awareness is not enabled in versioning.config.json.');
170
+ }
171
+ return {
172
+ defaultBranch,
173
+ branches: configuredBranches
174
+ };
175
+ }
176
+ getDefaultBranchRules(defaultBranch) {
177
+ const rootPackageJson = this.config.rootPackageJson || 'package.json';
178
+ const defaults = {
179
+ main: {
180
+ versionFormat: 'semantic',
181
+ tagFormat: 'v{version}',
182
+ syncFiles: [rootPackageJson],
183
+ environment: 'production',
184
+ bumpStrategy: 'semantic'
185
+ },
186
+ develop: {
187
+ versionFormat: 'dev',
188
+ tagFormat: 'v{version}',
189
+ syncFiles: ['version.development.json'],
190
+ environment: 'development',
191
+ bumpStrategy: 'dev-build'
192
+ },
193
+ 'feature/*': {
194
+ versionFormat: 'feature',
195
+ tagFormat: 'v{version}',
196
+ syncFiles: ['version.development.json'],
197
+ environment: 'development',
198
+ bumpStrategy: 'feature-branch'
199
+ },
200
+ 'hotfix/*': {
201
+ versionFormat: 'hotfix',
202
+ tagFormat: 'v{version}',
203
+ syncFiles: ['version.development.json'],
204
+ environment: 'development',
205
+ bumpStrategy: 'hotfix'
206
+ }
207
+ };
208
+ if (!defaults[defaultBranch]) {
209
+ defaults[defaultBranch] = {
210
+ versionFormat: 'semantic',
211
+ tagFormat: 'v{version}',
212
+ syncFiles: [rootPackageJson],
213
+ environment: 'production',
214
+ bumpStrategy: 'semantic'
215
+ };
216
+ }
217
+ return defaults;
218
+ }
219
+ normalizeBranchRule(branchRule) {
220
+ const versionFormat = branchRule?.versionFormat || 'semantic';
221
+ return {
222
+ ...branchRule,
223
+ versionFormat,
224
+ tagFormat: branchRule?.tagFormat || 'v{version}',
225
+ syncFiles: branchRule?.syncFiles || [],
226
+ bumpStrategy: branchRule?.bumpStrategy || this.getDefaultBumpStrategy(versionFormat)
227
+ };
228
+ }
229
+ getDefaultBumpStrategy(versionFormat) {
230
+ switch (versionFormat) {
231
+ case 'semantic':
232
+ return 'semantic';
233
+ case 'feature':
234
+ return 'feature-branch';
235
+ case 'hotfix':
236
+ return 'hotfix';
237
+ case 'dev':
238
+ default:
239
+ return 'dev-build';
240
+ }
241
+ }
242
+ async buildBranchAwareVersion(releaseType, branch, versionFormat, branchConfig, explicitBuild) {
243
+ const currentVersion = await this.getCurrentVersion();
244
+ const baseVersion = this.coerceBaseVersion(currentVersion);
245
+ const shouldBumpSemantic = this.shouldBumpSemantic(releaseType, versionFormat, branchConfig.bumpStrategy);
246
+ const semanticVersion = shouldBumpSemantic ? this.incrementSemanticVersion(baseVersion, releaseType) : baseVersion;
247
+ if (versionFormat === 'semantic') {
248
+ return semanticVersion;
249
+ }
250
+ const buildNumber = explicitBuild ?? await this.resolveBuildNumber(branchConfig.syncFiles, versionFormat, branch);
251
+ return this.applyVersionFormat(versionFormat, semanticVersion, branch, buildNumber);
252
+ }
253
+ shouldBumpSemantic(releaseType, versionFormat, bumpStrategy) {
254
+ if (versionFormat === 'semantic') {
255
+ return true;
256
+ }
257
+ if (bumpStrategy === 'semantic') {
258
+ return true;
259
+ }
260
+ return releaseType !== 'patch';
261
+ }
262
+ coerceBaseVersion(version) {
263
+ const parsed = semver.parse(version) || semver.coerce(version);
264
+ if (!parsed) {
265
+ throw new Error(`Unable to parse semantic base version from "${version}".`);
266
+ }
267
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
268
+ }
269
+ incrementSemanticVersion(version, releaseType) {
270
+ const incremented = semver.inc(version, releaseType);
271
+ if (!incremented) {
272
+ throw new Error(`Invalid semantic version bump: ${releaseType} from ${version}`);
273
+ }
274
+ return incremented;
275
+ }
276
+ async resolveBuildNumber(syncFiles, versionFormat, branch) {
277
+ const candidateFiles = syncFiles.length > 0 ? syncFiles : this.getDefaultSyncFiles(versionFormat);
278
+ let highestBuild = 0;
279
+ for (const filePath of candidateFiles) {
280
+ const existingVersion = await this.readVersionFromFile(filePath);
281
+ if (!existingVersion) {
282
+ continue;
283
+ }
284
+ const existingBuild = this.extractBuildNumber(existingVersion, versionFormat, branch);
285
+ if (existingBuild !== null) {
286
+ highestBuild = Math.max(highestBuild, existingBuild);
287
+ }
288
+ }
289
+ const rootVersion = await this.getCurrentVersion();
290
+ const rootBuild = this.extractBuildNumber(rootVersion, versionFormat, branch);
291
+ if (rootBuild !== null) {
292
+ highestBuild = Math.max(highestBuild, rootBuild);
293
+ }
294
+ return highestBuild + 1;
295
+ }
296
+ getDefaultSyncFiles(versionFormat) {
297
+ if (versionFormat === 'semantic') {
298
+ return [this.config.rootPackageJson];
299
+ }
300
+ return ['version.development.json'];
301
+ }
302
+ applyVersionFormat(versionFormat, semanticVersion, branch, build) {
303
+ if (versionFormat === 'dev') {
304
+ return `${semanticVersion}-dev.${build}`;
305
+ }
306
+ if (versionFormat === 'feature' || versionFormat === 'hotfix') {
307
+ return `${semanticVersion}-${branch}.${build}`;
308
+ }
309
+ const normalizedFormat = versionFormat.includes('{branch}')
310
+ ? versionFormat.replace(/\{branch\}/g, branch)
311
+ : versionFormat;
312
+ return `${semanticVersion}-${normalizedFormat}.${build}`;
313
+ }
314
+ extractBuildNumber(version, versionFormat, branch) {
315
+ let match = null;
316
+ if (versionFormat === 'dev') {
317
+ match = version.match(/-dev\.(\d+)$/);
318
+ }
319
+ else if (versionFormat === 'feature') {
320
+ match = version.match(/-feature\/.+\.(\d+)$/);
321
+ }
322
+ else if (versionFormat === 'hotfix') {
323
+ match = version.match(/-hotfix\/.+\.(\d+)$/);
324
+ }
325
+ else {
326
+ const normalizedFormat = versionFormat.includes('{branch}')
327
+ ? versionFormat.replace(/\{branch\}/g, branch)
328
+ : versionFormat;
329
+ const escapedFormat = this.escapeRegex(normalizedFormat);
330
+ match = version.match(new RegExp(`-${escapedFormat}\\.(\\d+)$`));
331
+ }
332
+ if (!match) {
333
+ return null;
334
+ }
335
+ return Number.parseInt(match[1], 10);
336
+ }
337
+ async readVersionFromFile(filePath) {
338
+ if (!(await fs.pathExists(filePath))) {
339
+ return null;
340
+ }
341
+ const extension = path.extname(filePath).toLowerCase();
342
+ if (extension === '.json') {
343
+ try {
344
+ const jsonContent = await fs.readJson(filePath);
345
+ if (typeof jsonContent === 'string') {
346
+ return jsonContent;
347
+ }
348
+ if (jsonContent && typeof jsonContent === 'object') {
349
+ const version = jsonContent.version;
350
+ if (typeof version === 'string') {
351
+ return version;
352
+ }
353
+ }
354
+ }
355
+ catch {
356
+ return null;
357
+ }
358
+ return null;
359
+ }
360
+ const content = await fs.readFile(filePath, 'utf8');
361
+ const normalized = content.trim();
362
+ return normalized.length > 0 ? normalized : null;
363
+ }
364
+ async updateVersionFile(filePath, version) {
365
+ await fs.ensureDir(path.dirname(filePath));
366
+ const extension = path.extname(filePath).toLowerCase();
367
+ if (extension === '.json') {
368
+ let jsonContent = {};
369
+ if (await fs.pathExists(filePath)) {
370
+ try {
371
+ jsonContent = await fs.readJson(filePath);
372
+ }
373
+ catch {
374
+ jsonContent = {};
375
+ }
376
+ }
377
+ if (!jsonContent || typeof jsonContent !== 'object' || Array.isArray(jsonContent)) {
378
+ jsonContent = {};
379
+ }
380
+ const nextContent = jsonContent;
381
+ nextContent.version = version;
382
+ await fs.writeJson(filePath, nextContent, { spaces: 2 });
383
+ return;
384
+ }
385
+ await fs.writeFile(filePath, `${version}\n`, 'utf8');
386
+ }
387
+ matchesPattern(branch, pattern) {
388
+ if (!pattern.includes('*')) {
389
+ return branch === pattern;
390
+ }
391
+ const escapedPattern = pattern
392
+ .split('*')
393
+ .map((part) => this.escapeRegex(part))
394
+ .join('.*');
395
+ const regex = new RegExp(`^${escapedPattern}$`);
396
+ return regex.test(branch);
397
+ }
398
+ escapeRegex(value) {
399
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
400
+ }
401
+ renderTag(tagFormat, version) {
402
+ return tagFormat.includes('{version}')
403
+ ? tagFormat.replace(/\{version\}/g, version)
404
+ : tagFormat;
405
+ }
95
406
  }
96
407
  exports.VersionManager = VersionManager;
97
408
  //# sourceMappingURL=versioning.js.map
@@ -0,0 +1,51 @@
1
+ {
2
+ "rootPackageJson": "package.json",
3
+ "packages": [],
4
+ "changelogFile": "CHANGELOG.md",
5
+ "conventionalCommits": true,
6
+ "syncDependencies": false,
7
+ "ignorePackages": [],
8
+ "branchAwareness": {
9
+ "enabled": true,
10
+ "defaultBranch": "main",
11
+ "branches": {
12
+ "main": {
13
+ "versionFormat": "semantic",
14
+ "tagFormat": "v{version}",
15
+ "syncFiles": [
16
+ "package.json",
17
+ "version.production.json"
18
+ ],
19
+ "environment": "production",
20
+ "bumpStrategy": "semantic"
21
+ },
22
+ "develop": {
23
+ "versionFormat": "dev",
24
+ "tagFormat": "v{version}",
25
+ "syncFiles": [
26
+ "version.development.json"
27
+ ],
28
+ "environment": "development",
29
+ "bumpStrategy": "dev-build"
30
+ },
31
+ "feature/*": {
32
+ "versionFormat": "feature",
33
+ "tagFormat": "v{version}",
34
+ "syncFiles": [
35
+ "version.development.json"
36
+ ],
37
+ "environment": "development",
38
+ "bumpStrategy": "feature-branch"
39
+ },
40
+ "hotfix/*": {
41
+ "versionFormat": "hotfix",
42
+ "tagFormat": "v{version}",
43
+ "syncFiles": [
44
+ "version.development.json"
45
+ ],
46
+ "environment": "development",
47
+ "bumpStrategy": "hotfix"
48
+ }
49
+ }
50
+ }
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edcalderon/versioning",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "A comprehensive versioning and changelog management tool for monorepos",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -78,4 +78,4 @@
78
78
  "url": "git+https://github.com/edcalderon/my-second-brain.git",
79
79
  "directory": "packages/versioning"
80
80
  }
81
- }
81
+ }
@@ -5,6 +5,49 @@
5
5
  "conventionalCommits": true,
6
6
  "syncDependencies": false,
7
7
  "ignorePackages": [],
8
+ "branchAwareness": {
9
+ "enabled": false,
10
+ "defaultBranch": "main",
11
+ "branches": {
12
+ "main": {
13
+ "versionFormat": "semantic",
14
+ "tagFormat": "v{version}",
15
+ "syncFiles": [
16
+ "package.json",
17
+ "version.production.json"
18
+ ],
19
+ "environment": "production",
20
+ "bumpStrategy": "semantic"
21
+ },
22
+ "develop": {
23
+ "versionFormat": "dev",
24
+ "tagFormat": "v{version}",
25
+ "syncFiles": [
26
+ "version.development.json"
27
+ ],
28
+ "environment": "development",
29
+ "bumpStrategy": "dev-build"
30
+ },
31
+ "feature/*": {
32
+ "versionFormat": "feature",
33
+ "tagFormat": "v{version}",
34
+ "syncFiles": [
35
+ "version.development.json"
36
+ ],
37
+ "environment": "development",
38
+ "bumpStrategy": "feature-branch"
39
+ },
40
+ "hotfix/*": {
41
+ "versionFormat": "hotfix",
42
+ "tagFormat": "v{version}",
43
+ "syncFiles": [
44
+ "version.development.json"
45
+ ],
46
+ "environment": "development",
47
+ "bumpStrategy": "hotfix"
48
+ }
49
+ }
50
+ },
8
51
  "changelog": {
9
52
  "preset": "angular",
10
53
  "releaseCount": 0
@@ -12,4 +55,4 @@
12
55
  "sync": {
13
56
  "includeRoot": true
14
57
  }
15
- }
58
+ }