fg-devkit 1.8.3 → 1.8.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.8.3",
3
+ "version": "1.8.4",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -45,6 +45,7 @@
45
45
  "./scripts/poeditor-upload.mjs": "./scripts/poeditor-upload.mjs",
46
46
  "./scripts/scaffold-vue.mjs": "./scripts/scaffold-vue.mjs",
47
47
  "./scripts/fg-devkit.mjs": "./scripts/fg-devkit.mjs",
48
+ "./scripts/resolve-candidate-version.mjs": "./scripts/resolve-candidate-version.mjs",
48
49
  "./scripts/release.sh": "./scripts/release.sh",
49
50
  "./scripts/cz/.cz-config.cjs": "./scripts/cz/.cz-config.cjs",
50
51
  "./cursor-rules/core-coding-standards.mdc": "./.cursor/rules/core-coding-standards.mdc"
@@ -20,7 +20,7 @@
20
20
  * pnpm exec fg-devkit setup-lefthook
21
21
  * pnpm exec fg-devkit prune-consumer-deps [--dry-run]
22
22
  * pnpm exec fg-devkit full-consumer-setup
23
- * pnpm exec fg-devkit create-release [patch|minor|major]
23
+ * pnpm exec fg-devkit create-release [release|candidate] [patch|minor|major]
24
24
  * pnpm exec fg-devkit commit [cz-args...] (czg/cz-git with fg-devkit bundled config)
25
25
  * pnpm exec fg-devkit commit-vuln (staged package.json version bumps → vuln() commit)
26
26
  * pnpm exec fg-devkit lint [eslint-args...] (run ESLint with fg-devkit bundled packages)
@@ -72,7 +72,7 @@ function runNode(scriptRelative, args = [], options = {}) {
72
72
 
73
73
  /**
74
74
  * Run release.sh (pnpm version bump, git push, gh release). Uses consumer cwd.
75
- * Optional first arg: patch | minor | major (otherwise the script prompts interactively).
75
+ * Args: [release|candidate] [patch|minor|major] or [patch|minor|major] [release|candidate].
76
76
  */
77
77
  function runReleaseScript(args = []) {
78
78
  const shPath = path.join(scriptDir, 'release.sh')
@@ -493,7 +493,8 @@ Commands:
493
493
  setup-lefthook Setup lefthook and migrate from husky/lint-staged
494
494
  prune-consumer-deps [--dry-run] Remove consumer tooling deps bundled by fg-devkit
495
495
  full-consumer-setup Run: step 7 -> scripts -> lefthook -> step 3 -> step 2 -> pnpm install
496
- create-release [bump] Version bump, git push, GitHub release (patch|minor|major; omit = prompt)
496
+ create-release [release|candidate] [patch|minor|major]
497
+ Stable release or candidate (X.Y.Z-rc.N); omit = interactive menu
497
498
  (alias: createRelease)
498
499
  commit [args...] czg/cz-git with fg-devkit bundled config
499
500
  commit-vuln Commit staged dependency bumps as vuln() (alias: vuln-commit)
@@ -635,8 +636,20 @@ async function runInteractiveMenu() {
635
636
  }
636
637
 
637
638
  if (group === 'release') {
638
- const releaseChoice = await selectPrompt({
639
+ const releaseKind = await selectPrompt({
639
640
  message: 'Create release',
641
+ choices: [
642
+ { name: 'Release', value: 'release' },
643
+ { name: 'Candidate', value: 'candidate' },
644
+ { name: 'Cancel', value: 'cancel' },
645
+ ],
646
+ default: 'release',
647
+ loop: false,
648
+ })
649
+ if (releaseKind === 'cancel') continue
650
+
651
+ const bumpChoice = await selectPrompt({
652
+ message: releaseKind === 'candidate' ? 'Candidate' : 'Release',
640
653
  choices: [
641
654
  { name: 'Patch', value: 'patch' },
642
655
  { name: 'Minor', value: 'minor' },
@@ -646,8 +659,11 @@ async function runInteractiveMenu() {
646
659
  default: 'patch',
647
660
  loop: false,
648
661
  })
649
- if (releaseChoice === 'cancel') continue
650
- return runSelectedAction(() => runReleaseScript([releaseChoice]))
662
+ if (bumpChoice === 'cancel') continue
663
+
664
+ const releaseArgs =
665
+ releaseKind === 'candidate' ? [bumpChoice, 'candidate'] : [bumpChoice]
666
+ return runSelectedAction(() => runReleaseScript(releaseArgs))
651
667
  }
652
668
 
653
669
  if (group === 'lint-typecheck') {
@@ -3,12 +3,29 @@
3
3
  # Interactive or scripted release for this package: bumps version with pnpm, pushes commits/tags,
4
4
  # and creates a GitHub Release with generated notes (requires gh CLI, authenticated).
5
5
  #
6
- # Usage: ./scripts/release.sh [major|minor|patch]
7
- # With no argument, prompts for patch/minor/major.
6
+ # Usage:
7
+ # ./scripts/release.sh [patch|minor|major] # stable release (default mode)
8
+ # ./scripts/release.sh [patch|minor|major] candidate # prerelease tag X.Y.Z-rc.N
9
+ # ./scripts/release.sh candidate [patch|minor|major]
10
+ #
11
+ # Candidate tags follow SemVer prerelease format: <version>-rc.<number> (e.g. 2.8.0-rc.1).
8
12
 
9
13
  set -euo pipefail
10
14
 
11
- bump_type="${1:-}"
15
+ bump_type=""
16
+ release_mode="release"
17
+
18
+ for arg in "$@"; do
19
+ case "$arg" in
20
+ patch | minor | major) bump_type="$arg" ;;
21
+ release | candidate) release_mode="$arg" ;;
22
+ *)
23
+ echo "Unknown argument: $arg"
24
+ echo "Usage: ./scripts/release.sh [patch|minor|major] [release|candidate]"
25
+ exit 1
26
+ ;;
27
+ esac
28
+ done
12
29
 
13
30
  if [[ -z "$bump_type" ]]; then
14
31
  echo "Select release type:"
@@ -29,7 +46,7 @@ if [[ -z "$bump_type" ]]; then
29
46
  fi
30
47
 
31
48
  if [[ "$bump_type" != "major" && "$bump_type" != "minor" && "$bump_type" != "patch" ]]; then
32
- echo "Usage: ./scripts/release.sh [major|minor|patch]"
49
+ echo "Usage: ./scripts/release.sh [patch|minor|major] [release|candidate]"
33
50
  exit 1
34
51
  fi
35
52
 
@@ -50,22 +67,61 @@ fi
50
67
 
51
68
  previous_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)"
52
69
 
53
- pnpm version "$bump_type" --no-git-tag-version
54
-
55
- new_version="$(node -p "require('./package.json').version")"
56
- new_tag="v${new_version}"
57
-
58
70
  is_fg_devkit_repo="$(
59
71
  node -e "const fs=require('fs'); try { const p=JSON.parse(fs.readFileSync('./package.json','utf8')); process.stdout.write(p.name==='fg-devkit' ? 'yes' : 'no'); } catch { process.stdout.write('no'); }"
60
72
  )"
61
73
 
62
- if [[ "$is_fg_devkit_repo" == "yes" && -f "./scripts/sync-scaffold-vue-deps.mjs" && -f "./scaffold/vue/package.json" ]]; then
63
- node ./scripts/sync-scaffold-vue-deps.mjs
64
- git add package.json scaffold/vue/package.json
65
- else
66
- git add package.json
74
+ sync_fg_devkit_package_files() {
75
+ if [[ "$is_fg_devkit_repo" == "yes" && -f "./scripts/sync-scaffold-vue-deps.mjs" && -f "./scaffold/vue/package.json" ]]; then
76
+ node ./scripts/sync-scaffold-vue-deps.mjs
77
+ git add package.json scaffold/vue/package.json
78
+ else
79
+ git add package.json
80
+ fi
81
+ }
82
+
83
+ if [[ "$release_mode" == "candidate" ]]; then
84
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
85
+ new_version="$(node "${script_dir}/resolve-candidate-version.mjs" "$bump_type")"
86
+
87
+ node -e "
88
+ const fs = require('fs');
89
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'));
90
+ pkg.version = process.argv[1];
91
+ fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
92
+ " "$new_version"
93
+
94
+ sync_fg_devkit_package_files
95
+
96
+ new_tag="v${new_version}"
97
+ git commit -m "${new_tag}"
98
+ git tag "${new_tag}"
99
+
100
+ git push
101
+ git push --tags
102
+
103
+ if [[ -n "$previous_tag" ]]; then
104
+ gh release create "$new_tag" \
105
+ --prerelease \
106
+ --generate-notes \
107
+ --notes-start-tag "$previous_tag"
108
+ else
109
+ gh release create "$new_tag" \
110
+ --prerelease \
111
+ --generate-notes
112
+ fi
113
+
114
+ echo "Candidate release created: ${new_tag}"
115
+ exit 0
67
116
  fi
68
117
 
118
+ pnpm version "$bump_type" --no-git-tag-version
119
+
120
+ new_version="$(node -p "require('./package.json').version")"
121
+ new_tag="v${new_version}"
122
+
123
+ sync_fg_devkit_package_files
124
+
69
125
  git commit -m "${new_tag}"
70
126
  git tag "${new_tag}"
71
127
 
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Resolves next candidate version: <semver>-rc.<n> (e.g. 2.8.0-rc.2).
4
+ * Usage: node resolve-candidate-version.mjs <patch|minor|major>
5
+ */
6
+ import fs from 'node:fs'
7
+ import { execSync } from 'node:child_process'
8
+
9
+ const bump = process.argv[2]
10
+ if (bump !== 'patch' && bump !== 'minor' && bump !== 'major') {
11
+ console.error('[resolve-candidate-version] Usage: node resolve-candidate-version.mjs <patch|minor|major>')
12
+ process.exit(1)
13
+ }
14
+
15
+ const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8'))
16
+ const v = pkg.version
17
+
18
+ /** @param {string} ver @param {'patch'|'minor'|'major'} type */
19
+ function bumpSemver(ver, type) {
20
+ const m = ver.match(/^(\d+)\.(\d+)\.(\d+)$/)
21
+ if (!m) throw new Error(`Invalid semver base: ${ver}`)
22
+ let maj = Number(m[1])
23
+ let min = Number(m[2])
24
+ let pat = Number(m[3])
25
+ if (type === 'major') {
26
+ maj += 1
27
+ min = 0
28
+ pat = 0
29
+ } else if (type === 'minor') {
30
+ min += 1
31
+ pat = 0
32
+ } else if (type === 'patch') {
33
+ pat += 1
34
+ }
35
+ return `${maj}.${min}.${pat}`
36
+ }
37
+
38
+ const plainMatch = v.match(/^(\d+\.\d+\.\d+)/)
39
+ if (!plainMatch) {
40
+ console.error(`[resolve-candidate-version] Could not parse version: ${v}`)
41
+ process.exit(1)
42
+ }
43
+ const plain = plainMatch[1]
44
+
45
+ /** @type {string} */
46
+ let targetBase
47
+ if (/^\d+\.\d+\.\d+-rc\.\d+$/.test(v)) {
48
+ targetBase = plain
49
+ } else {
50
+ targetBase = bumpSemver(plain, bump)
51
+ }
52
+
53
+ /** @param {string} base @param {string[]} tags */
54
+ function maxRcForBase(base, tags) {
55
+ const prefixes = [`${base}-rc.`, `v${base}-rc.`]
56
+ let max = 0
57
+ for (const tag of tags) {
58
+ for (const prefix of prefixes) {
59
+ if (tag.startsWith(prefix)) {
60
+ const n = Number(tag.slice(prefix.length))
61
+ if (!Number.isNaN(n)) max = Math.max(max, n)
62
+ }
63
+ }
64
+ }
65
+ return max
66
+ }
67
+
68
+ /** @type {string[]} */
69
+ let tags = []
70
+ try {
71
+ tags = execSync('git tag -l', { encoding: 'utf8' })
72
+ .split('\n')
73
+ .map((t) => t.trim())
74
+ .filter(Boolean)
75
+ } catch {
76
+ tags = []
77
+ }
78
+
79
+ const nextRc = maxRcForBase(targetBase, tags) + 1
80
+ process.stdout.write(`${targetBase}-rc.${nextRc}`)