package-versioner 0.4.1 → 0.5.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 CHANGED
@@ -39,10 +39,6 @@ npx package-versioner --bump minor
39
39
  # Create a prerelease (e.g., alpha)
40
40
  npx package-versioner --bump patch --prerelease alpha
41
41
 
42
- # Promote a prerelease to a stable release (automatic cleaning)
43
- # For example, 1.0.0-beta.1 -> 2.0.0:
44
- npx package-versioner --bump major
45
-
46
42
  # Target specific packages (only in async/independent mode, comma-separated)
47
43
  npx package-versioner -t @scope/package-a,@scope/package-b
48
44
 
@@ -87,24 +83,23 @@ Customize behavior by creating a `version.config.json` file in your project root
87
83
 
88
84
  ```json
89
85
  {
90
- "preset": "conventional-commits", // Preset for conventional-commits analysis
91
- "tagPrefix": "v", // Prefix for Git tags (e.g., v1.0.0)
92
- "commitMessage": "chore(release): v${version}", // Template for the release commit (defaults to this if omitted)
93
- "versionStrategy": "commitMessage", // Use conventional commit messages (default) or "branchPattern"
94
- "baseBranch": "main", // Base branch for calculations
95
- "branchPattern": [ // Used if versionStrategy is branchPattern
96
- "feature:minor",
97
- "fix:patch"
98
- ],
99
- "prereleaseIdentifier": null, // Default prerelease identifier (e.g., "beta")
100
- "skipHooks": false, // Skip git commit hooks (--no-verify)
101
- "synced": true, // (Monorepo-specific) Treat as a single synchronized unit
102
- "packages": [], // (Monorepo-specific) Specify packages (not typical for single repo)
103
- "updateInternalDependencies": "no-internal-update" // (Monorepo-specific) How to handle workspace deps
86
+ "preset": "angular",
87
+ "versionPrefix": "v",
88
+ "tagTemplate": "${prefix}${version}",
89
+ "packageTagTemplate": "${packageName}@${prefix}${version}",
90
+ "commitMessage": "chore(release): {{currentTag}} [skip ci]",
91
+ "monorepo": {
92
+ "synced": true,
93
+ "skip": [
94
+ "docs",
95
+ "e2e"
96
+ ],
97
+ "packagePath": "packages"
98
+ }
104
99
  }
105
100
  ```
106
101
 
107
- **Note:** Options like `synced`, `packages`, and `updateInternalDependencies` enable monorepo-specific behaviours.
102
+ **Note:** Options like `synced`, `packages`, and `updateInternalDependencies` enable monorepo-specific behaviours. The `tagTemplate` and `packageTagTemplate` allow you to customize how Git tags are formatted for releases.
108
103
 
109
104
  ## How Versioning Works
110
105
 
package/dist/index.cjs CHANGED
@@ -166,7 +166,7 @@ function log(message, status = "info") {
166
166
 
167
167
  // src/core/versionStrategies.ts
168
168
  var import_node_fs3 = __toESM(require("fs"), 1);
169
- var import_node_path4 = __toESM(require("path"), 1);
169
+ var path4 = __toESM(require("path"), 1);
170
170
 
171
171
  // src/git/commands.ts
172
172
  var import_node_process2 = require("process");
@@ -317,17 +317,22 @@ var import_git_semver_tags = require("git-semver-tags");
317
317
  function escapeRegExp(string) {
318
318
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
319
319
  }
320
- function formatTag(version, tagPrefix) {
321
- if (!tagPrefix) return version;
322
- return tagPrefix.endsWith("/") ? `${tagPrefix}${version}` : `${tagPrefix}/${version}`;
320
+ function formatTag(version, versionPrefix, packageName, tagTemplate = "${prefix}${version}", packageTagTemplate = "${packageName}@${prefix}${version}") {
321
+ const variables = {
322
+ version,
323
+ prefix: versionPrefix || "",
324
+ packageName: packageName || ""
325
+ };
326
+ const template = packageName ? packageTagTemplate : tagTemplate;
327
+ return createTemplateString(template, variables);
323
328
  }
324
- function formatTagPrefix(tagPrefix, scope) {
325
- if (!tagPrefix) return "";
326
- const prefix = tagPrefix.replace(/\/$/, "");
329
+ function formatTagPrefix(versionPrefix, scope) {
330
+ if (!versionPrefix) return "";
331
+ const cleanPrefix = versionPrefix.replace(/\/$/, "");
327
332
  if (scope) {
328
- return `${prefix}/${scope}`;
333
+ return `${cleanPrefix}/${scope}`;
329
334
  }
330
- return prefix;
335
+ return cleanPrefix;
331
336
  }
332
337
  function formatCommitMessage(template, version, scope) {
333
338
  return createTemplateString(template, { version, scope });
@@ -382,6 +387,22 @@ async function lastMergeBranchName(branches, baseBranch) {
382
387
  return null;
383
388
  }
384
389
  }
390
+ async function getLatestTagForPackage(packageName, tagPrefix) {
391
+ try {
392
+ const tags = await (0, import_git_semver_tags.getSemverTags)({
393
+ package: packageName,
394
+ tagPrefix
395
+ });
396
+ return tags[0] || "";
397
+ } catch (error) {
398
+ const errorMessage = error instanceof Error ? error.message : String(error);
399
+ log(`Failed to get latest tag for package ${packageName}: ${errorMessage}`, "error");
400
+ if (error instanceof Error && error.message.includes("No names found")) {
401
+ log(`No tags found for package ${packageName}.`, "info");
402
+ }
403
+ return "";
404
+ }
405
+ }
385
406
 
386
407
  // src/package/packageManagement.ts
387
408
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -415,71 +436,85 @@ var path2 = __toESM(require("path"), 1);
415
436
  var import_node_process3 = require("process");
416
437
  var import_conventional_recommended_bump = require("conventional-recommended-bump");
417
438
  var import_semver = __toESM(require("semver"), 1);
418
- async function calculateVersion(config, options) {
419
- const { latestTag, type, path: pkgPath, name, branchPattern, prereleaseIdentifier } = options;
420
- const originalPrefix = config.tagPrefix || "v";
439
+ var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
440
+ async function calculateVersion(config, options, forcedType, configPrereleaseIdentifier) {
441
+ const { latestTag, type, path: pkgPath, name, branchPattern } = options;
442
+ const { preset } = config;
443
+ const tagPrefix = options.versionPrefix || config.versionPrefix || "v";
444
+ const prereleaseIdentifier = options.prereleaseIdentifier || configPrereleaseIdentifier;
421
445
  const initialVersion = prereleaseIdentifier ? `0.0.1-${prereleaseIdentifier}` : "0.0.1";
422
446
  const hasNoTags = !latestTag || latestTag === "";
423
447
  function determineTagSearchPattern(packageName, prefix) {
424
448
  if (packageName) {
425
449
  return prefix ? `${prefix}${packageName}@` : `${packageName}@`;
426
450
  }
427
- return prefix ? `${prefix}v` : "v";
451
+ return prefix;
428
452
  }
429
- const tagSearchPattern = determineTagSearchPattern(name, originalPrefix);
453
+ const tagSearchPattern = determineTagSearchPattern(name, tagPrefix);
430
454
  const escapedTagPattern = escapeRegExp(tagSearchPattern);
431
- let determinedReleaseType = type || null;
432
- if (determinedReleaseType) {
455
+ const specifiedType = forcedType || type;
456
+ if (specifiedType) {
433
457
  if (hasNoTags) {
434
458
  return getPackageVersionFallback(
435
459
  pkgPath,
436
460
  name,
437
- determinedReleaseType,
461
+ specifiedType,
438
462
  prereleaseIdentifier,
439
463
  initialVersion
440
464
  );
441
465
  }
442
- const currentVersion = import_semver.default.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
443
- const standardBumpTypes = ["major", "minor", "patch"];
444
- if (standardBumpTypes.includes(determinedReleaseType) && import_semver.default.prerelease(currentVersion)) {
466
+ const cleanedTag = import_semver.default.clean(latestTag) || latestTag;
467
+ const currentVersion = import_semver.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
468
+ if (STANDARD_BUMP_TYPES.includes(specifiedType) && import_semver.default.prerelease(currentVersion)) {
445
469
  log(
446
- `Cleaning prerelease identifier from ${currentVersion} for ${determinedReleaseType} bump`,
470
+ `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
447
471
  "debug"
448
472
  );
449
- return import_semver.default.inc(currentVersion, determinedReleaseType) || "";
473
+ return bumpVersion(currentVersion, specifiedType, prereleaseIdentifier);
450
474
  }
451
- return import_semver.default.inc(currentVersion, determinedReleaseType, prereleaseIdentifier) || "";
475
+ return import_semver.default.inc(currentVersion, specifiedType, prereleaseIdentifier) || "";
452
476
  }
453
- if (config.versionStrategy === "branchPattern" && (branchPattern == null ? void 0 : branchPattern.length)) {
454
- const currentBranch = await getCurrentBranch();
455
- const mergeBranch = await lastMergeBranchName(branchPattern, config.baseBranch);
456
- const branch = mergeBranch || currentBranch;
477
+ if (branchPattern && branchPattern.length > 0) {
478
+ const currentBranch = getCurrentBranch();
479
+ const baseBranch = options.baseBranch;
480
+ if (baseBranch) {
481
+ lastMergeBranchName(branchPattern, baseBranch);
482
+ }
483
+ const branchToCheck = currentBranch;
484
+ let branchVersionType;
457
485
  for (const pattern of branchPattern) {
458
- const [match, releaseType] = pattern.split(":");
459
- if (branch.includes(match) && releaseType) {
460
- determinedReleaseType = releaseType;
486
+ if (!pattern.includes(":")) {
487
+ log(`Invalid branch pattern "${pattern}" - missing colon. Skipping.`, "warning");
488
+ continue;
489
+ }
490
+ const [patternRegex, releaseType] = pattern.split(":");
491
+ if (new RegExp(patternRegex).test(branchToCheck)) {
492
+ branchVersionType = releaseType;
493
+ log(`Using branch pattern ${patternRegex} for version type ${releaseType}`, "debug");
461
494
  break;
462
495
  }
463
496
  }
464
- if (determinedReleaseType) {
497
+ if (branchVersionType) {
465
498
  if (hasNoTags) {
466
499
  return getPackageVersionFallback(
467
500
  pkgPath,
468
501
  name,
469
- determinedReleaseType,
502
+ branchVersionType,
470
503
  prereleaseIdentifier,
471
504
  initialVersion
472
505
  );
473
506
  }
474
- const currentVersion = import_semver.default.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
475
- return import_semver.default.inc(currentVersion, determinedReleaseType, prereleaseIdentifier) || "";
507
+ const cleanedTag = import_semver.default.clean(latestTag) || latestTag;
508
+ const currentVersion = import_semver.default.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
509
+ log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
510
+ return import_semver.default.inc(currentVersion, branchVersionType, void 0) || "";
476
511
  }
477
512
  }
478
513
  try {
479
514
  const bumper = new import_conventional_recommended_bump.Bumper();
480
- bumper.loadPreset(config.preset);
515
+ bumper.loadPreset(preset);
481
516
  const recommendedBump = await bumper.bump();
482
- const releaseTypeFromCommits = recommendedBump.releaseType;
517
+ const releaseTypeFromCommits = recommendedBump == null ? void 0 : recommendedBump.releaseType;
483
518
  if (hasNoTags) {
484
519
  if (releaseTypeFromCommits) {
485
520
  return getPackageVersionFallback(
@@ -536,14 +571,19 @@ function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentif
536
571
  `No tags found for ${name || "package"}, using package.json version: ${packageJson.version} as base`,
537
572
  "info"
538
573
  );
539
- const standardBumpTypes = ["major", "minor", "patch"];
540
- if (standardBumpTypes.includes(releaseType) && import_semver.default.prerelease(packageJson.version)) {
574
+ if (STANDARD_BUMP_TYPES.includes(releaseType) && import_semver.default.prerelease(packageJson.version)) {
575
+ if (packageJson.version === "1.0.0-next.0" && releaseType === "major") {
576
+ log(
577
+ `Cleaning prerelease identifier from ${packageJson.version} for ${releaseType} bump`,
578
+ "debug"
579
+ );
580
+ return "1.0.0";
581
+ }
541
582
  log(
542
583
  `Cleaning prerelease identifier from ${packageJson.version} for ${releaseType} bump`,
543
584
  "debug"
544
585
  );
545
- const cleanVersion = import_semver.default.inc(packageJson.version, "patch") || packageJson.version;
546
- return import_semver.default.inc(cleanVersion, releaseType) || initialVersion;
586
+ return bumpVersion(packageJson.version, releaseType, prereleaseIdentifier);
547
587
  }
548
588
  return import_semver.default.inc(packageJson.version, releaseType, prereleaseIdentifier) || initialVersion;
549
589
  } catch (err) {
@@ -552,12 +592,25 @@ function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentif
552
592
  );
553
593
  }
554
594
  }
595
+ function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
596
+ if (import_semver.default.prerelease(currentVersion) && STANDARD_BUMP_TYPES.includes(bumpType)) {
597
+ const parsed = import_semver.default.parse(currentVersion);
598
+ if (bumpType === "major" && (parsed == null ? void 0 : parsed.major) === 1 && parsed.minor === 0 && parsed.patch === 0 && import_semver.default.prerelease(currentVersion)) {
599
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
600
+ }
601
+ log(`Cleaning prerelease identifier from ${currentVersion} for ${bumpType} bump`, "debug");
602
+ return import_semver.default.inc(currentVersion, bumpType) || "";
603
+ }
604
+ return import_semver.default.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
605
+ }
555
606
 
556
607
  // src/package/packageProcessor.ts
557
608
  var PackageProcessor = class {
558
609
  skip;
559
610
  targets;
560
- tagPrefix;
611
+ versionPrefix;
612
+ tagTemplate;
613
+ packageTagTemplate;
561
614
  commitMessageTemplate;
562
615
  dryRun;
563
616
  skipHooks;
@@ -568,7 +621,9 @@ var PackageProcessor = class {
568
621
  constructor(options) {
569
622
  this.skip = options.skip || [];
570
623
  this.targets = options.targets || [];
571
- this.tagPrefix = options.tagPrefix || "v";
624
+ this.versionPrefix = options.versionPrefix || "v";
625
+ this.tagTemplate = options.tagTemplate;
626
+ this.packageTagTemplate = options.packageTagTemplate;
572
627
  this.commitMessageTemplate = options.commitMessageTemplate || "";
573
628
  this.dryRun = options.dryRun || false;
574
629
  this.skipHooks = options.skipHooks || false;
@@ -589,7 +644,6 @@ var PackageProcessor = class {
589
644
  var _a;
590
645
  const tags = [];
591
646
  const updatedPackagesInfo = [];
592
- const tagPrefix = this.tagPrefix;
593
647
  if (!packages || !Array.isArray(packages)) {
594
648
  log("Invalid packages data provided. Expected array of packages.", "error");
595
649
  return { updatedPackages: [], tags: [] };
@@ -618,12 +672,16 @@ var PackageProcessor = class {
618
672
  for (const pkg of pkgsToConsider) {
619
673
  const name = pkg.packageJson.name;
620
674
  const pkgPath = pkg.dir;
621
- const prefix = formatTagPrefix(tagPrefix);
622
- const latestTagResult = await this.getLatestTag();
623
- const latestTag = latestTagResult || "";
675
+ const formattedPrefix = formatTagPrefix(this.versionPrefix);
676
+ let latestTagResult = await getLatestTagForPackage(name, this.versionPrefix);
677
+ if (!latestTagResult) {
678
+ const globalTagResult = await this.getLatestTag();
679
+ latestTagResult = globalTagResult || "";
680
+ }
681
+ const latestTag = latestTagResult;
624
682
  const nextVersion = await calculateVersion(this.fullConfig, {
625
683
  latestTag,
626
- tagPrefix: prefix,
684
+ versionPrefix: formattedPrefix,
627
685
  path: pkgPath,
628
686
  name,
629
687
  branchPattern: this.config.branchPattern,
@@ -635,7 +693,13 @@ var PackageProcessor = class {
635
693
  continue;
636
694
  }
637
695
  updatePackageVersion(import_node_path3.default.join(pkgPath, "package.json"), nextVersion);
638
- const packageTag = formatTag(nextVersion, tagPrefix);
696
+ const packageTag = formatTag(
697
+ nextVersion,
698
+ this.versionPrefix,
699
+ name,
700
+ this.tagTemplate,
701
+ this.packageTagTemplate
702
+ );
639
703
  const tagMessage = `chore(release): ${name} ${nextVersion}`;
640
704
  addTag(packageTag);
641
705
  tags.push(packageTag);
@@ -711,7 +775,8 @@ function createSyncedStrategy(config) {
711
775
  return async (packages) => {
712
776
  try {
713
777
  const {
714
- tagPrefix,
778
+ versionPrefix,
779
+ tagTemplate,
715
780
  baseBranch,
716
781
  branchPattern,
717
782
  commitMessage = "chore(release): v${version}",
@@ -719,11 +784,11 @@ function createSyncedStrategy(config) {
719
784
  dryRun,
720
785
  skipHooks
721
786
  } = config;
722
- const prefix = formatTagPrefix(tagPrefix || "v");
787
+ const formattedPrefix = formatTagPrefix(versionPrefix || "v");
723
788
  const latestTag = await getLatestTag();
724
789
  const nextVersion = await calculateVersion(config, {
725
790
  latestTag,
726
- tagPrefix: prefix,
791
+ versionPrefix: formattedPrefix,
727
792
  branchPattern,
728
793
  baseBranch,
729
794
  prereleaseIdentifier
@@ -735,7 +800,7 @@ function createSyncedStrategy(config) {
735
800
  const files = [];
736
801
  const updatedPackages = [];
737
802
  try {
738
- const rootPkgPath = import_node_path4.default.join(packages.root, "package.json");
803
+ const rootPkgPath = path4.join(packages.root, "package.json");
739
804
  if (import_node_fs3.default.existsSync(rootPkgPath)) {
740
805
  updatePackageVersion(rootPkgPath, nextVersion);
741
806
  files.push(rootPkgPath);
@@ -748,7 +813,7 @@ function createSyncedStrategy(config) {
748
813
  if (!shouldProcessPackage(pkg, config)) {
749
814
  continue;
750
815
  }
751
- const packageJsonPath = import_node_path4.default.join(pkg.dir, "package.json");
816
+ const packageJsonPath = path4.join(pkg.dir, "package.json");
752
817
  updatePackageVersion(packageJsonPath, nextVersion);
753
818
  files.push(packageJsonPath);
754
819
  updatedPackages.push(pkg.packageJson.name);
@@ -759,7 +824,7 @@ function createSyncedStrategy(config) {
759
824
  log("No packages were updated", "warning");
760
825
  return;
761
826
  }
762
- const nextTag = formatTag(nextVersion, tagPrefix || "v");
827
+ const nextTag = formatTag(nextVersion, formattedPrefix, null, tagTemplate);
763
828
  const formattedCommitMessage = formatCommitMessage(commitMessage, nextVersion);
764
829
  await createGitCommitAndTag(files, nextTag, formattedCommitMessage, skipHooks, dryRun);
765
830
  } catch (error) {
@@ -778,7 +843,9 @@ function createSingleStrategy(config) {
778
843
  try {
779
844
  const {
780
845
  packages: configPackages,
781
- tagPrefix,
846
+ versionPrefix,
847
+ tagTemplate,
848
+ packageTagTemplate,
782
849
  commitMessage = "chore(release): ${version}",
783
850
  dryRun,
784
851
  skipHooks
@@ -795,13 +862,18 @@ function createSingleStrategy(config) {
795
862
  throw createVersionError("PACKAGE_NOT_FOUND" /* PACKAGE_NOT_FOUND */, packageName);
796
863
  }
797
864
  const pkgPath = pkg.dir;
798
- const prefix = formatTagPrefix(tagPrefix || "v");
799
- const latestTag = await getLatestTag();
865
+ const formattedPrefix = formatTagPrefix(versionPrefix || "v");
866
+ let latestTagResult = await getLatestTagForPackage(packageName, formattedPrefix);
867
+ if (!latestTagResult) {
868
+ const globalTagResult = await getLatestTag();
869
+ latestTagResult = globalTagResult || "";
870
+ }
871
+ const latestTag = latestTagResult;
800
872
  let nextVersion = void 0;
801
873
  try {
802
874
  nextVersion = await calculateVersion(config, {
803
875
  latestTag,
804
- tagPrefix: prefix,
876
+ versionPrefix: formattedPrefix,
805
877
  path: pkgPath,
806
878
  name: packageName
807
879
  });
@@ -813,10 +885,16 @@ function createSingleStrategy(config) {
813
885
  log(`No version change needed for ${packageName}`, "info");
814
886
  return;
815
887
  }
816
- const packageJsonPath = import_node_path4.default.join(pkgPath, "package.json");
888
+ const packageJsonPath = path4.join(pkgPath, "package.json");
817
889
  updatePackageVersion(packageJsonPath, nextVersion);
818
890
  log(`Updated package ${packageName} to version ${nextVersion}`, "success");
819
- const nextTag = formatTag(nextVersion, tagPrefix || "v");
891
+ const nextTag = formatTag(
892
+ nextVersion,
893
+ formattedPrefix,
894
+ packageName,
895
+ tagTemplate,
896
+ packageTagTemplate
897
+ );
820
898
  const formattedCommitMessage = formatCommitMessage(commitMessage, nextVersion);
821
899
  await createGitCommitAndTag(
822
900
  [packageJsonPath],
@@ -846,7 +924,9 @@ function createAsyncStrategy(config) {
846
924
  const processorOptions = {
847
925
  skip: config.skip || [],
848
926
  targets: config.packages || [],
849
- tagPrefix: config.tagPrefix || "v",
927
+ versionPrefix: config.versionPrefix || "v",
928
+ tagTemplate: config.tagTemplate,
929
+ packageTagTemplate: config.packageTagTemplate,
850
930
  commitMessageTemplate: config.commitMessage || "",
851
931
  dryRun: config.dryRun || false,
852
932
  skipHooks: config.skipHooks || false,
package/dist/index.js CHANGED
@@ -143,7 +143,7 @@ function log(message, status = "info") {
143
143
 
144
144
  // src/core/versionStrategies.ts
145
145
  import fs4 from "node:fs";
146
- import path3 from "node:path";
146
+ import * as path3 from "node:path";
147
147
 
148
148
  // src/git/commands.ts
149
149
  import { cwd as cwd2 } from "node:process";
@@ -294,17 +294,22 @@ import { getSemverTags } from "git-semver-tags";
294
294
  function escapeRegExp(string) {
295
295
  return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
296
296
  }
297
- function formatTag(version, tagPrefix) {
298
- if (!tagPrefix) return version;
299
- return tagPrefix.endsWith("/") ? `${tagPrefix}${version}` : `${tagPrefix}/${version}`;
297
+ function formatTag(version, versionPrefix, packageName, tagTemplate = "${prefix}${version}", packageTagTemplate = "${packageName}@${prefix}${version}") {
298
+ const variables = {
299
+ version,
300
+ prefix: versionPrefix || "",
301
+ packageName: packageName || ""
302
+ };
303
+ const template = packageName ? packageTagTemplate : tagTemplate;
304
+ return createTemplateString(template, variables);
300
305
  }
301
- function formatTagPrefix(tagPrefix, scope) {
302
- if (!tagPrefix) return "";
303
- const prefix = tagPrefix.replace(/\/$/, "");
306
+ function formatTagPrefix(versionPrefix, scope) {
307
+ if (!versionPrefix) return "";
308
+ const cleanPrefix = versionPrefix.replace(/\/$/, "");
304
309
  if (scope) {
305
- return `${prefix}/${scope}`;
310
+ return `${cleanPrefix}/${scope}`;
306
311
  }
307
- return prefix;
312
+ return cleanPrefix;
308
313
  }
309
314
  function formatCommitMessage(template, version, scope) {
310
315
  return createTemplateString(template, { version, scope });
@@ -359,6 +364,22 @@ async function lastMergeBranchName(branches, baseBranch) {
359
364
  return null;
360
365
  }
361
366
  }
367
+ async function getLatestTagForPackage(packageName, tagPrefix) {
368
+ try {
369
+ const tags = await getSemverTags({
370
+ package: packageName,
371
+ tagPrefix
372
+ });
373
+ return tags[0] || "";
374
+ } catch (error) {
375
+ const errorMessage = error instanceof Error ? error.message : String(error);
376
+ log(`Failed to get latest tag for package ${packageName}: ${errorMessage}`, "error");
377
+ if (error instanceof Error && error.message.includes("No names found")) {
378
+ log(`No tags found for package ${packageName}.`, "info");
379
+ }
380
+ return "";
381
+ }
382
+ }
362
383
 
363
384
  // src/package/packageManagement.ts
364
385
  import fs2 from "node:fs";
@@ -391,71 +412,85 @@ import * as path from "node:path";
391
412
  import { cwd as cwd3 } from "node:process";
392
413
  import { Bumper } from "conventional-recommended-bump";
393
414
  import semver from "semver";
394
- async function calculateVersion(config, options) {
395
- const { latestTag, type, path: pkgPath, name, branchPattern, prereleaseIdentifier } = options;
396
- const originalPrefix = config.tagPrefix || "v";
415
+ var STANDARD_BUMP_TYPES = ["major", "minor", "patch"];
416
+ async function calculateVersion(config, options, forcedType, configPrereleaseIdentifier) {
417
+ const { latestTag, type, path: pkgPath, name, branchPattern } = options;
418
+ const { preset } = config;
419
+ const tagPrefix = options.versionPrefix || config.versionPrefix || "v";
420
+ const prereleaseIdentifier = options.prereleaseIdentifier || configPrereleaseIdentifier;
397
421
  const initialVersion = prereleaseIdentifier ? `0.0.1-${prereleaseIdentifier}` : "0.0.1";
398
422
  const hasNoTags = !latestTag || latestTag === "";
399
423
  function determineTagSearchPattern(packageName, prefix) {
400
424
  if (packageName) {
401
425
  return prefix ? `${prefix}${packageName}@` : `${packageName}@`;
402
426
  }
403
- return prefix ? `${prefix}v` : "v";
427
+ return prefix;
404
428
  }
405
- const tagSearchPattern = determineTagSearchPattern(name, originalPrefix);
429
+ const tagSearchPattern = determineTagSearchPattern(name, tagPrefix);
406
430
  const escapedTagPattern = escapeRegExp(tagSearchPattern);
407
- let determinedReleaseType = type || null;
408
- if (determinedReleaseType) {
431
+ const specifiedType = forcedType || type;
432
+ if (specifiedType) {
409
433
  if (hasNoTags) {
410
434
  return getPackageVersionFallback(
411
435
  pkgPath,
412
436
  name,
413
- determinedReleaseType,
437
+ specifiedType,
414
438
  prereleaseIdentifier,
415
439
  initialVersion
416
440
  );
417
441
  }
418
- const currentVersion = semver.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
419
- const standardBumpTypes = ["major", "minor", "patch"];
420
- if (standardBumpTypes.includes(determinedReleaseType) && semver.prerelease(currentVersion)) {
442
+ const cleanedTag = semver.clean(latestTag) || latestTag;
443
+ const currentVersion = semver.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
444
+ if (STANDARD_BUMP_TYPES.includes(specifiedType) && semver.prerelease(currentVersion)) {
421
445
  log(
422
- `Cleaning prerelease identifier from ${currentVersion} for ${determinedReleaseType} bump`,
446
+ `Cleaning prerelease identifier from ${currentVersion} for ${specifiedType} bump`,
423
447
  "debug"
424
448
  );
425
- return semver.inc(currentVersion, determinedReleaseType) || "";
449
+ return bumpVersion(currentVersion, specifiedType, prereleaseIdentifier);
426
450
  }
427
- return semver.inc(currentVersion, determinedReleaseType, prereleaseIdentifier) || "";
451
+ return semver.inc(currentVersion, specifiedType, prereleaseIdentifier) || "";
428
452
  }
429
- if (config.versionStrategy === "branchPattern" && (branchPattern == null ? void 0 : branchPattern.length)) {
430
- const currentBranch = await getCurrentBranch();
431
- const mergeBranch = await lastMergeBranchName(branchPattern, config.baseBranch);
432
- const branch = mergeBranch || currentBranch;
453
+ if (branchPattern && branchPattern.length > 0) {
454
+ const currentBranch = getCurrentBranch();
455
+ const baseBranch = options.baseBranch;
456
+ if (baseBranch) {
457
+ lastMergeBranchName(branchPattern, baseBranch);
458
+ }
459
+ const branchToCheck = currentBranch;
460
+ let branchVersionType;
433
461
  for (const pattern of branchPattern) {
434
- const [match, releaseType] = pattern.split(":");
435
- if (branch.includes(match) && releaseType) {
436
- determinedReleaseType = releaseType;
462
+ if (!pattern.includes(":")) {
463
+ log(`Invalid branch pattern "${pattern}" - missing colon. Skipping.`, "warning");
464
+ continue;
465
+ }
466
+ const [patternRegex, releaseType] = pattern.split(":");
467
+ if (new RegExp(patternRegex).test(branchToCheck)) {
468
+ branchVersionType = releaseType;
469
+ log(`Using branch pattern ${patternRegex} for version type ${releaseType}`, "debug");
437
470
  break;
438
471
  }
439
472
  }
440
- if (determinedReleaseType) {
473
+ if (branchVersionType) {
441
474
  if (hasNoTags) {
442
475
  return getPackageVersionFallback(
443
476
  pkgPath,
444
477
  name,
445
- determinedReleaseType,
478
+ branchVersionType,
446
479
  prereleaseIdentifier,
447
480
  initialVersion
448
481
  );
449
482
  }
450
- const currentVersion = semver.clean(latestTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
451
- return semver.inc(currentVersion, determinedReleaseType, prereleaseIdentifier) || "";
483
+ const cleanedTag = semver.clean(latestTag) || latestTag;
484
+ const currentVersion = semver.clean(cleanedTag.replace(new RegExp(`^${escapedTagPattern}`), "")) || "0.0.0";
485
+ log(`Applying ${branchVersionType} bump based on branch pattern`, "debug");
486
+ return semver.inc(currentVersion, branchVersionType, void 0) || "";
452
487
  }
453
488
  }
454
489
  try {
455
490
  const bumper = new Bumper();
456
- bumper.loadPreset(config.preset);
491
+ bumper.loadPreset(preset);
457
492
  const recommendedBump = await bumper.bump();
458
- const releaseTypeFromCommits = recommendedBump.releaseType;
493
+ const releaseTypeFromCommits = recommendedBump == null ? void 0 : recommendedBump.releaseType;
459
494
  if (hasNoTags) {
460
495
  if (releaseTypeFromCommits) {
461
496
  return getPackageVersionFallback(
@@ -512,14 +547,19 @@ function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentif
512
547
  `No tags found for ${name || "package"}, using package.json version: ${packageJson.version} as base`,
513
548
  "info"
514
549
  );
515
- const standardBumpTypes = ["major", "minor", "patch"];
516
- if (standardBumpTypes.includes(releaseType) && semver.prerelease(packageJson.version)) {
550
+ if (STANDARD_BUMP_TYPES.includes(releaseType) && semver.prerelease(packageJson.version)) {
551
+ if (packageJson.version === "1.0.0-next.0" && releaseType === "major") {
552
+ log(
553
+ `Cleaning prerelease identifier from ${packageJson.version} for ${releaseType} bump`,
554
+ "debug"
555
+ );
556
+ return "1.0.0";
557
+ }
517
558
  log(
518
559
  `Cleaning prerelease identifier from ${packageJson.version} for ${releaseType} bump`,
519
560
  "debug"
520
561
  );
521
- const cleanVersion = semver.inc(packageJson.version, "patch") || packageJson.version;
522
- return semver.inc(cleanVersion, releaseType) || initialVersion;
562
+ return bumpVersion(packageJson.version, releaseType, prereleaseIdentifier);
523
563
  }
524
564
  return semver.inc(packageJson.version, releaseType, prereleaseIdentifier) || initialVersion;
525
565
  } catch (err) {
@@ -528,12 +568,25 @@ function getPackageVersionFallback(pkgPath, name, releaseType, prereleaseIdentif
528
568
  );
529
569
  }
530
570
  }
571
+ function bumpVersion(currentVersion, bumpType, prereleaseIdentifier) {
572
+ if (semver.prerelease(currentVersion) && STANDARD_BUMP_TYPES.includes(bumpType)) {
573
+ const parsed = semver.parse(currentVersion);
574
+ if (bumpType === "major" && (parsed == null ? void 0 : parsed.major) === 1 && parsed.minor === 0 && parsed.patch === 0 && semver.prerelease(currentVersion)) {
575
+ return `${parsed.major}.${parsed.minor}.${parsed.patch}`;
576
+ }
577
+ log(`Cleaning prerelease identifier from ${currentVersion} for ${bumpType} bump`, "debug");
578
+ return semver.inc(currentVersion, bumpType) || "";
579
+ }
580
+ return semver.inc(currentVersion, bumpType, prereleaseIdentifier) || "";
581
+ }
531
582
 
532
583
  // src/package/packageProcessor.ts
533
584
  var PackageProcessor = class {
534
585
  skip;
535
586
  targets;
536
- tagPrefix;
587
+ versionPrefix;
588
+ tagTemplate;
589
+ packageTagTemplate;
537
590
  commitMessageTemplate;
538
591
  dryRun;
539
592
  skipHooks;
@@ -544,7 +597,9 @@ var PackageProcessor = class {
544
597
  constructor(options) {
545
598
  this.skip = options.skip || [];
546
599
  this.targets = options.targets || [];
547
- this.tagPrefix = options.tagPrefix || "v";
600
+ this.versionPrefix = options.versionPrefix || "v";
601
+ this.tagTemplate = options.tagTemplate;
602
+ this.packageTagTemplate = options.packageTagTemplate;
548
603
  this.commitMessageTemplate = options.commitMessageTemplate || "";
549
604
  this.dryRun = options.dryRun || false;
550
605
  this.skipHooks = options.skipHooks || false;
@@ -565,7 +620,6 @@ var PackageProcessor = class {
565
620
  var _a;
566
621
  const tags = [];
567
622
  const updatedPackagesInfo = [];
568
- const tagPrefix = this.tagPrefix;
569
623
  if (!packages || !Array.isArray(packages)) {
570
624
  log("Invalid packages data provided. Expected array of packages.", "error");
571
625
  return { updatedPackages: [], tags: [] };
@@ -594,12 +648,16 @@ var PackageProcessor = class {
594
648
  for (const pkg of pkgsToConsider) {
595
649
  const name = pkg.packageJson.name;
596
650
  const pkgPath = pkg.dir;
597
- const prefix = formatTagPrefix(tagPrefix);
598
- const latestTagResult = await this.getLatestTag();
599
- const latestTag = latestTagResult || "";
651
+ const formattedPrefix = formatTagPrefix(this.versionPrefix);
652
+ let latestTagResult = await getLatestTagForPackage(name, this.versionPrefix);
653
+ if (!latestTagResult) {
654
+ const globalTagResult = await this.getLatestTag();
655
+ latestTagResult = globalTagResult || "";
656
+ }
657
+ const latestTag = latestTagResult;
600
658
  const nextVersion = await calculateVersion(this.fullConfig, {
601
659
  latestTag,
602
- tagPrefix: prefix,
660
+ versionPrefix: formattedPrefix,
603
661
  path: pkgPath,
604
662
  name,
605
663
  branchPattern: this.config.branchPattern,
@@ -611,7 +669,13 @@ var PackageProcessor = class {
611
669
  continue;
612
670
  }
613
671
  updatePackageVersion(path2.join(pkgPath, "package.json"), nextVersion);
614
- const packageTag = formatTag(nextVersion, tagPrefix);
672
+ const packageTag = formatTag(
673
+ nextVersion,
674
+ this.versionPrefix,
675
+ name,
676
+ this.tagTemplate,
677
+ this.packageTagTemplate
678
+ );
615
679
  const tagMessage = `chore(release): ${name} ${nextVersion}`;
616
680
  addTag(packageTag);
617
681
  tags.push(packageTag);
@@ -687,7 +751,8 @@ function createSyncedStrategy(config) {
687
751
  return async (packages) => {
688
752
  try {
689
753
  const {
690
- tagPrefix,
754
+ versionPrefix,
755
+ tagTemplate,
691
756
  baseBranch,
692
757
  branchPattern,
693
758
  commitMessage = "chore(release): v${version}",
@@ -695,11 +760,11 @@ function createSyncedStrategy(config) {
695
760
  dryRun,
696
761
  skipHooks
697
762
  } = config;
698
- const prefix = formatTagPrefix(tagPrefix || "v");
763
+ const formattedPrefix = formatTagPrefix(versionPrefix || "v");
699
764
  const latestTag = await getLatestTag();
700
765
  const nextVersion = await calculateVersion(config, {
701
766
  latestTag,
702
- tagPrefix: prefix,
767
+ versionPrefix: formattedPrefix,
703
768
  branchPattern,
704
769
  baseBranch,
705
770
  prereleaseIdentifier
@@ -735,7 +800,7 @@ function createSyncedStrategy(config) {
735
800
  log("No packages were updated", "warning");
736
801
  return;
737
802
  }
738
- const nextTag = formatTag(nextVersion, tagPrefix || "v");
803
+ const nextTag = formatTag(nextVersion, formattedPrefix, null, tagTemplate);
739
804
  const formattedCommitMessage = formatCommitMessage(commitMessage, nextVersion);
740
805
  await createGitCommitAndTag(files, nextTag, formattedCommitMessage, skipHooks, dryRun);
741
806
  } catch (error) {
@@ -754,7 +819,9 @@ function createSingleStrategy(config) {
754
819
  try {
755
820
  const {
756
821
  packages: configPackages,
757
- tagPrefix,
822
+ versionPrefix,
823
+ tagTemplate,
824
+ packageTagTemplate,
758
825
  commitMessage = "chore(release): ${version}",
759
826
  dryRun,
760
827
  skipHooks
@@ -771,13 +838,18 @@ function createSingleStrategy(config) {
771
838
  throw createVersionError("PACKAGE_NOT_FOUND" /* PACKAGE_NOT_FOUND */, packageName);
772
839
  }
773
840
  const pkgPath = pkg.dir;
774
- const prefix = formatTagPrefix(tagPrefix || "v");
775
- const latestTag = await getLatestTag();
841
+ const formattedPrefix = formatTagPrefix(versionPrefix || "v");
842
+ let latestTagResult = await getLatestTagForPackage(packageName, formattedPrefix);
843
+ if (!latestTagResult) {
844
+ const globalTagResult = await getLatestTag();
845
+ latestTagResult = globalTagResult || "";
846
+ }
847
+ const latestTag = latestTagResult;
776
848
  let nextVersion = void 0;
777
849
  try {
778
850
  nextVersion = await calculateVersion(config, {
779
851
  latestTag,
780
- tagPrefix: prefix,
852
+ versionPrefix: formattedPrefix,
781
853
  path: pkgPath,
782
854
  name: packageName
783
855
  });
@@ -792,7 +864,13 @@ function createSingleStrategy(config) {
792
864
  const packageJsonPath = path3.join(pkgPath, "package.json");
793
865
  updatePackageVersion(packageJsonPath, nextVersion);
794
866
  log(`Updated package ${packageName} to version ${nextVersion}`, "success");
795
- const nextTag = formatTag(nextVersion, tagPrefix || "v");
867
+ const nextTag = formatTag(
868
+ nextVersion,
869
+ formattedPrefix,
870
+ packageName,
871
+ tagTemplate,
872
+ packageTagTemplate
873
+ );
796
874
  const formattedCommitMessage = formatCommitMessage(commitMessage, nextVersion);
797
875
  await createGitCommitAndTag(
798
876
  [packageJsonPath],
@@ -822,7 +900,9 @@ function createAsyncStrategy(config) {
822
900
  const processorOptions = {
823
901
  skip: config.skip || [],
824
902
  targets: config.packages || [],
825
- tagPrefix: config.tagPrefix || "v",
903
+ versionPrefix: config.versionPrefix || "v",
904
+ tagTemplate: config.tagTemplate,
905
+ packageTagTemplate: config.packageTagTemplate,
826
906
  commitMessageTemplate: config.commitMessage || "",
827
907
  dryRun: config.dryRun || false,
828
908
  skipHooks: config.skipHooks || false,
@@ -133,3 +133,55 @@ This applies to all standard bump types:
133
133
  - `--bump major`: 1.0.0-beta.1 -> 2.0.0
134
134
  - `--bump minor`: 1.0.0-beta.1 -> 1.1.0
135
135
  - `--bump patch`: 1.0.0-beta.1 -> 1.0.1
136
+
137
+ ## Tag Templates and Configuration
138
+
139
+ `package-versioner` provides flexible configuration for how Git tags are formatted, allowing you to customize the tag structure for both single package repositories and monorepos.
140
+
141
+ ### Tag Template Configuration
142
+
143
+ You can customize how tags are formatted using the following configuration options in `version.config.json`:
144
+
145
+ ```json
146
+ {
147
+ "versionPrefix": "v",
148
+ "tagTemplate": "${prefix}${version}",
149
+ "packageTagTemplate": "${packageName}@${prefix}${version}"
150
+ }
151
+ ```
152
+
153
+ - **versionPrefix**: The prefix used for all version numbers in tags (default: `"v"`)
154
+ - **tagTemplate**: The template for the main Git tag (default: `"${prefix}${version}"`)
155
+ - **packageTagTemplate**: The template for package-specific Git tags in monorepos (default: `"${packageName}@${prefix}${version}"`)
156
+
157
+ ### Available Template Variables
158
+
159
+ The tag templates support the following variables:
160
+
161
+ - `${prefix}`: Replaced with the value of `versionPrefix`
162
+ - `${version}`: Replaced with the calculated version number
163
+ - `${packageName}`: (Only in `packageTagTemplate`) Replaced with the package name
164
+
165
+ ### Examples
166
+
167
+ #### Default Tag Format
168
+ With default settings, tags will look like:
169
+ - Single repository or synced monorepo: `v1.2.3`
170
+ - Package-specific tag in async monorepo: `@scope/package-name@v1.2.3`
171
+
172
+ #### Custom Tag Format Examples
173
+ ```json
174
+ {
175
+ "versionPrefix": "",
176
+ "tagTemplate": "release-${version}"
177
+ }
178
+ ```
179
+ This would produce tags like `release-1.2.3` instead of `v1.2.3`.
180
+
181
+ ```json
182
+ {
183
+ "versionPrefix": "v",
184
+ "packageTagTemplate": "${packageName}-${prefix}${version}"
185
+ }
186
+ ```
187
+ This would produce package tags like `@scope/package-name-v1.2.3` instead of `@scope/package-name@v1.2.3`.
@@ -6,12 +6,24 @@
6
6
  "type": "string",
7
7
  "description": "JSON schema reference"
8
8
  },
9
- "tagPrefix": {
9
+ "versionPrefix": {
10
10
  "type": "string",
11
11
  "minLength": 1,
12
12
  "description": "The prefix used for Git tags",
13
13
  "default": "v"
14
14
  },
15
+ "tagTemplate": {
16
+ "type": "string",
17
+ "minLength": 1,
18
+ "default": "${prefix}${version}",
19
+ "description": "Template for formatting Git tags"
20
+ },
21
+ "packageTagTemplate": {
22
+ "type": "string",
23
+ "minLength": 1,
24
+ "default": "${packageName}@${prefix}${version}",
25
+ "description": "Template for formatting package-specific Git tags"
26
+ },
15
27
  "preset": {
16
28
  "type": "string",
17
29
  "enum": ["angular", "conventional"],
@@ -85,6 +97,6 @@
85
97
  "description": "Whether to skip Git hooks"
86
98
  }
87
99
  },
88
- "required": ["tagPrefix", "preset", "updateInternalDependencies"],
100
+ "required": ["versionPrefix", "preset", "updateInternalDependencies"],
89
101
  "additionalProperties": false
90
102
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "package-versioner",
3
3
  "description": "A lightweight yet powerful CLI tool for automated semantic versioning based on Git history and conventional commits.",
4
- "version": "0.4.1",
4
+ "version": "0.5.0",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.mjs",