fg-devkit 1.9.0 → 1.12.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
@@ -138,6 +138,12 @@ Upgrade one package.json dependency and auto-commit (`dep(package): bump to vers
138
138
  pnpm exec fgd policy-upgrade-commit
139
139
  ```
140
140
 
141
+ Fix one vulnerability override interactively and auto-commit (`vuln(package): override version`):
142
+
143
+ ```bash
144
+ pnpm exec fgd audit-override-commit
145
+ ```
146
+
141
147
  ## tsconfig in consuming repos
142
148
 
143
149
  Use `fgd sync-project-configs` (or `fgd sync-tooling-configs`) to write the local `tsconfig.json` template from `fg-devkit`.
@@ -0,0 +1,54 @@
1
+ # git-cliff ~ configuration file
2
+ # https://git-cliff.org/docs/configuration
3
+ # Commit types: feat, fix, perf, refactor, chore, dep, vuln, fix(security)
4
+
5
+ [changelog]
6
+ body = """
7
+ {% if version %}\
8
+ ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d %H:%M") }}
9
+ {% else %}\
10
+ ## [unreleased]
11
+ {% endif %}\
12
+ {% for group, commits in commits | group_by(attribute="group") %}
13
+ ### {{ group | striptags | trim | upper_first }}
14
+ {% for commit in commits %}
15
+ - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.raw_message }}
16
+ {% endfor %}
17
+ {% endfor %}
18
+ """
19
+ trim = true
20
+ render_always = true
21
+ postprocessors = []
22
+
23
+ [git]
24
+ conventional_commits = true
25
+ filter_unconventional = false
26
+ require_conventional = false
27
+ split_commits = false
28
+ protect_breaking_commits = false
29
+ commit_preprocessors = []
30
+ commit_parsers = [
31
+ { message = "^v\\d+\\.\\d+\\.\\d+$", skip = true },
32
+ { message = "^feat", group = "Added" },
33
+ { message = "^fix\\(security\\)", group = "Security" },
34
+ { message = "^fix", group = "Fixed" },
35
+ { message = "^perf", group = "Performance" },
36
+ { message = "^refactor", group = "Changed" },
37
+ { message = "^chore", group = "Changed" },
38
+ { message = "^dep", group = "Dependencies" },
39
+ { message = "^vuln", group = "Security dependency update" },
40
+ { message = "^doc", group = "Documentation" },
41
+ { message = "^style", group = "Styling" },
42
+ { message = "^test", group = "Testing" },
43
+ { message = "^ci", group = "Changed" },
44
+ { message = "^revert", group = "Revert" },
45
+ { message = ".*", group = "Other" },
46
+ ]
47
+ filter_commits = false
48
+ fail_on_unmatched_commit = false
49
+ link_parsers = []
50
+ use_branch_tags = false
51
+ topo_order = false
52
+ topo_order_commits = true
53
+ sort_commits = "oldest"
54
+ recurse_submodules = false
@@ -0,0 +1,64 @@
1
+ # User-facing changelog (feat, fix, vuln, fix(security)).
2
+ # CHANGELOG.user.md is edited manually and shown in the app.
3
+ # generate-release-notes writes CHANGELOG.user.draft.md and prepends only NEW versions to CHANGELOG.user.md.
4
+
5
+ [changelog]
6
+ body = """
7
+ {% if version %}\
8
+ ## Version {{ version | trim_start_matches(pat="v") }} - {{ timestamp | date(format="%Y-%m-%d %H:%M") }}
9
+ {% else %}\
10
+ ## [unreleased]
11
+ {% endif %}\
12
+ {% set feature_commits = commits | filter(attribute="group", value="Nytt") %}\
13
+ {% if feature_commits | length > 0 %}\
14
+ ### Nytt
15
+ {% for commit in feature_commits %}
16
+ - {{ commit.message | upper_first }}
17
+ {% endfor %}\
18
+ {% endif %}\
19
+ {% set security_commits = commits | filter(attribute="group", value="Säkerhet") %}\
20
+ {% if security_commits | length > 0 %}\
21
+ ### Säkerhet
22
+ {% for commit in security_commits %}
23
+ - {{ commit.message | upper_first }}
24
+ {% endfor %}\
25
+ {% endif %}\
26
+ {% set fix_commits = commits | filter(attribute="group", value="Fixar") %}\
27
+ {% if fix_commits | length > 0 %}\
28
+ ### Fixar
29
+ {% for commit in fix_commits %}
30
+ - {{ commit.message | upper_first }}
31
+ {% endfor %}\
32
+ {% endif %}
33
+ """
34
+ trim = true
35
+ render_always = true
36
+
37
+ [git]
38
+ conventional_commits = true
39
+ filter_unconventional = false
40
+ filter_commits = true
41
+ split_commits = false
42
+ protect_breaking_commits = false
43
+ commit_preprocessors = []
44
+ commit_parsers = [
45
+ { message = "^v\\d+\\.\\d+\\.\\d+$", skip = true },
46
+ { message = "^feat\\((repo|devtools|ci|deps|fg-devkit|fds-vue-core|vite)\\)", skip = true },
47
+ { message = "^fix\\((repo|devtools|ci|deps|fg-devkit|fds-vue-core|vite)\\)", skip = true },
48
+ { message = "^feat", group = "Nytt" },
49
+ { message = "^fix\\(security\\)", group = "Säkerhet" },
50
+ { message = "^vuln", group = "Säkerhet" },
51
+ { message = "^fix", group = "Fixar" },
52
+ { message = "^perf", skip = true },
53
+ { message = "^refactor", skip = true },
54
+ { message = "^chore", skip = true },
55
+ { message = "^dep", skip = true },
56
+ { message = ".*", skip = true },
57
+ ]
58
+ fail_on_unmatched_commit = false
59
+ link_parsers = []
60
+ use_branch_tags = false
61
+ topo_order = false
62
+ topo_order_commits = true
63
+ sort_commits = "oldest"
64
+ recurse_submodules = false
@@ -1,4 +1,7 @@
1
1
  {
2
+ "toolchain": {
3
+ "packageManager": "pnpm@11.8.0"
4
+ },
2
5
  "managedDependencies": {
3
6
  "dependencies": {
4
7
  "@element-plus/icons-vue": "2.3.2",
@@ -6,10 +9,10 @@
6
9
  "@lexical/list": "0.30.0",
7
10
  "@lexical/rich-text": "0.30.0",
8
11
  "@piwikpro/vue-piwik-pro": "1.7.0",
9
- "@sentry/browser": "10.58.0",
12
+ "@sentry/browser": "10.59.0",
10
13
  "@sentry/tracing": "7.120.4",
11
- "@sentry/vue": "10.58.0",
12
- "@sentry/wasm": "10.58.0",
14
+ "@sentry/vue": "10.59.0",
15
+ "@sentry/wasm": "10.59.0",
13
16
  "@tailwindcss/postcss": "4.3.1",
14
17
  "@tanstack/vue-query": "5.101.0",
15
18
  "@types/leaflet": "1.9.21",
@@ -23,7 +26,7 @@
23
26
  "content-disposition-header": "0.6.0",
24
27
  "date-fns": "4.4.0",
25
28
  "element-plus": "2.14.2",
26
- "fds-vue-core": "8.0.5",
29
+ "fds-vue-core": "8.1.1",
27
30
  "imask": "7.6.1",
28
31
  "leaflet": "1.9.4",
29
32
  "lexical": "0.30.0",
@@ -38,7 +41,7 @@
38
41
  "vega-embed": "7.1.0",
39
42
  "vega-lite": "6.4.3",
40
43
  "vue-chartjs": "5.3.3",
41
- "vue-i18n": "11.4.5",
44
+ "vue-i18n": "11.4.6",
42
45
  "vue-quilly": "1.1.6",
43
46
  "vue-router": "4.6.3",
44
47
  "vue3-touch-keyboard": "1.3.9",
@@ -49,13 +52,13 @@
49
52
  "devDependencies": {
50
53
  "@chromatic-com/storybook": "5.2.1",
51
54
  "@eslint/js": "9.39.1",
52
- "@intlify/unplugin-vue-i18n": "11.2.3",
55
+ "@intlify/unplugin-vue-i18n": "11.2.4",
53
56
  "@playwright/test": "1.61.0",
54
- "@storybook/addon-a11y": "10.4.5",
55
- "@storybook/addon-docs": "10.4.5",
56
- "@storybook/addon-vitest": "10.4.5",
57
- "@storybook/vue3": "10.4.5",
58
- "@storybook/vue3-vite": "10.4.5",
57
+ "@storybook/addon-a11y": "10.4.6",
58
+ "@storybook/addon-docs": "10.4.6",
59
+ "@storybook/addon-vitest": "10.4.6",
60
+ "@storybook/vue3": "10.4.6",
61
+ "@storybook/vue3-vite": "10.4.6",
59
62
  "@tanstack/vue-query-devtools": "6.1.34",
60
63
  "@tsconfig/node22": "22.0.5",
61
64
  "@types/lodash-es": "4.17.12",
@@ -68,13 +71,13 @@
68
71
  "@vuepic/vue-datepicker": "6.0.3",
69
72
  "arg": "5.0.2",
70
73
  "babel-eslint": "10.1.0",
71
- "eslint-plugin-storybook": "10.4.5",
74
+ "eslint-plugin-storybook": "10.4.6",
72
75
  "globals": "15.11.0",
73
76
  "jsdom": "26.1.0",
74
77
  "postcss": "8.5.15",
75
78
  "sass": "1.101.0",
76
79
  "start-server-and-test": "2.1.3",
77
- "storybook": "10.4.5",
80
+ "storybook": "10.4.6",
78
81
  "tsx": "4.22.4",
79
82
  "vega": "6.2.0",
80
83
  "vite-plugin-dts": "4.5.4",
@@ -90,13 +93,14 @@
90
93
  "dotenv": "16.4.5",
91
94
  "qrcode.vue": "3.10.0",
92
95
  "tailwindcss": "4.3.1",
93
- "vite": "7.3.2",
96
+ "vite": "7.3.5",
94
97
  "vue": "3.5.38"
95
98
  },
96
99
  "overrides": {
97
100
  "lodash@<=4.17.23": ">=4.18.0",
98
101
  "lodash@>=4.0.0 <=4.17.22": ">=4.17.23",
99
102
  "lodash@>=4.0.0 <=4.17.23": ">=4.18.0",
100
- "tmp@<=0.2.3": ">=0.2.4"
103
+ "tmp@<=0.2.3": ">=0.2.4",
104
+ "esbuild": ">=0.28.1"
101
105
  }
102
106
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fg-devkit",
3
- "version": "1.9.0",
3
+ "version": "1.12.0",
4
4
  "description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -33,8 +33,11 @@
33
33
  "./scripts/repo-vulns.mjs": "./scripts/repo-vulns.mjs",
34
34
  "./scripts/fleet-vulns.mjs": "./scripts/fleet-vulns.mjs",
35
35
  "./scripts/fleet-bump-fg-devkit.mjs": "./scripts/fleet-bump-fg-devkit.mjs",
36
+ "./scripts/bump-pnpm-toolchain.mjs": "./scripts/bump-pnpm-toolchain.mjs",
37
+ "./scripts/fleet-vue-repos.mjs": "./scripts/fleet-vue-repos.mjs",
36
38
  "./scripts/policy-upgrade.mjs": "./scripts/policy-upgrade.mjs",
37
39
  "./scripts/policy-upgrade-commit.mjs": "./scripts/policy-upgrade-commit.mjs",
40
+ "./scripts/audit-override-commit.mjs": "./scripts/audit-override-commit.mjs",
38
41
  "./scripts/check-dependency-policy.mjs": "./scripts/check-dependency-policy.mjs",
39
42
  "./scripts/sync-dependency-policy.mjs": "./scripts/sync-dependency-policy.mjs",
40
43
  "./scripts/resolve-dependency-policy-path.mjs": "./scripts/resolve-dependency-policy-path.mjs",
@@ -47,11 +50,20 @@
47
50
  "./scripts/fg-devkit.mjs": "./scripts/fg-devkit.mjs",
48
51
  "./scripts/resolve-candidate-version.mjs": "./scripts/resolve-candidate-version.mjs",
49
52
  "./scripts/release.sh": "./scripts/release.sh",
53
+ "./scripts/generate-release-notes.mjs": "./scripts/generate-release-notes.mjs",
54
+ "./scripts/verify-release-notes.mjs": "./scripts/verify-release-notes.mjs",
55
+ "./scripts/sync-release-notes-config.mjs": "./scripts/sync-release-notes-config.mjs",
56
+ "./release-notes/parseChangelog": "./release-notes/parseChangelog.ts",
57
+ "./release-notes/format": "./release-notes/format.ts",
58
+ "./release-notes/types": "./release-notes/types.ts",
59
+ "./configs/cliff.toml": "./configs/cliff.toml",
60
+ "./configs/cliff.user.toml": "./configs/cliff.user.toml",
50
61
  "./scripts/cz/.cz-config.cjs": "./scripts/cz/.cz-config.cjs",
51
62
  "./cursor-rules/core-coding-standards.mdc": "./.cursor/rules/core-coding-standards.mdc"
52
63
  },
53
64
  "files": [
54
65
  "configs",
66
+ "release-notes",
55
67
  "scaffold",
56
68
  "scripts",
57
69
  "tsconfig.json",
@@ -77,19 +89,21 @@
77
89
  "@inquirer/input": "5.1.2",
78
90
  "@inquirer/select": "5.2.1",
79
91
  "@intlify/eslint-plugin-vue-i18n": "4.5.1",
80
- "@typescript-eslint/eslint-plugin": "8.61.0",
81
- "@typescript-eslint/parser": "8.61.0",
92
+ "@typescript-eslint/eslint-plugin": "8.61.1",
93
+ "@typescript-eslint/parser": "8.61.1",
82
94
  "@vitest/eslint-plugin": "1.6.20",
83
95
  "@vue/eslint-config-prettier": "10.2.0",
84
- "@vue/eslint-config-typescript": "14.8.0",
96
+ "@vue/eslint-config-typescript": "14.9.0",
85
97
  "@vue/tsconfig": "0.9.1",
86
98
  "eslint": "10.5.0",
87
99
  "eslint-plugin-prettier": "5.5.6",
88
100
  "eslint-plugin-vue": "10.9.2",
89
- "fallow": "2.96.0",
101
+ "fallow": "2.101.0",
102
+ "git-cliff": "2.13.1",
90
103
  "lefthook": "2.1.9",
91
- "npm-check-updates": "22.2.3",
104
+ "npm-check-updates": "22.2.7",
92
105
  "prettier": "3.8.4",
106
+ "tsx": "4.22.4",
93
107
  "typescript": "5.9.3",
94
108
  "vue-eslint-parser": "10.4.1",
95
109
  "vue-tsc": "3.3.5"
@@ -0,0 +1,82 @@
1
+ import type { ChangelogRelease } from './types'
2
+
3
+ export const formatChangelogDate = (date: string): string => {
4
+ const dateTimeMatch = date.match(/^(\d{4}-\d{2}-\d{2})(?: (\d{2}:\d{2}))?$/)
5
+ if (!dateTimeMatch?.[1]) {
6
+ return date
7
+ }
8
+
9
+ const datePart = dateTimeMatch[1]
10
+ const timePart = dateTimeMatch[2]
11
+ const parsed = timePart ? new Date(`${datePart}T${timePart}:00`) : new Date(`${datePart}T12:00:00`)
12
+ const formattedDate = parsed.toLocaleDateString('sv-SE', { dateStyle: 'long' })
13
+
14
+ if (!timePart) {
15
+ return formattedDate
16
+ }
17
+
18
+ const formattedTime = parsed.toLocaleTimeString('sv-SE', { hour: '2-digit', minute: '2-digit' })
19
+ return `${formattedDate} ${formattedTime}`
20
+ }
21
+
22
+ const CONVENTIONAL_COMMIT_RE = /^(feat|fix|perf|refactor|chore|dep|vuln)(\([^)]+\))?:\s/
23
+
24
+ const SECTION_COMMIT_TYPE: Record<string, string> = {
25
+ Added: 'feat',
26
+ Fixed: 'fix',
27
+ Security: 'fix(security)',
28
+ Performance: 'perf',
29
+ Changed: 'chore',
30
+ Dependencies: 'dep',
31
+ 'Security dependency update': 'vuln',
32
+ }
33
+
34
+ /** Keeps `type(scope): message` lines and upgrades legacy git-cliff markdown items. */
35
+ export const formatTechnicalChangelogItem = (item: string, sectionTitle = ''): string => {
36
+ let text = item.trim()
37
+ const breakingPrefix = text.match(/^\[\*\*breaking\*\*\]\s*/i)?.[0] ?? ''
38
+ if (breakingPrefix.length > 0) {
39
+ text = text.slice(breakingPrefix.length).trim()
40
+ }
41
+
42
+ if (CONVENTIONAL_COMMIT_RE.test(text)) {
43
+ return breakingPrefix.length > 0 ? `[breaking] ${text}` : text
44
+ }
45
+
46
+ const scopedMatch = text.match(/^\*\(([^)]+)\)\*\s*(.*)$/)
47
+ if (scopedMatch) {
48
+ const commitType = SECTION_COMMIT_TYPE[sectionTitle]
49
+ const message = scopedMatch[2]?.trim() ?? ''
50
+
51
+ if (commitType === 'fix(security)') {
52
+ const formatted = `fix(security): ${message}`.trim()
53
+ return breakingPrefix.length > 0 ? `[breaking] ${formatted}` : formatted
54
+ }
55
+
56
+ if (commitType) {
57
+ const formatted = `${commitType}(${scopedMatch[1]}): ${message}`.trim()
58
+ return breakingPrefix.length > 0 ? `[breaking] ${formatted}` : formatted
59
+ }
60
+ }
61
+
62
+ return breakingPrefix.length > 0 ? `[breaking] ${text}` : text
63
+ }
64
+
65
+ export const hasChangelogContent = (release: ChangelogRelease): boolean =>
66
+ release.sections.some((section) => section.items.length > 0)
67
+
68
+ const USER_SECTION_ORDER = ['Nytt', 'Säkerhet', 'Fixar'] as const
69
+
70
+ export const sortUserReleaseSections = <T extends { title: string }>(sections: T[]): T[] =>
71
+ [...sections].sort((a, b) => {
72
+ const aIndex = USER_SECTION_ORDER.indexOf(a.title as (typeof USER_SECTION_ORDER)[number])
73
+ const bIndex = USER_SECTION_ORDER.indexOf(b.title as (typeof USER_SECTION_ORDER)[number])
74
+ const aRank = aIndex === -1 ? USER_SECTION_ORDER.length : aIndex
75
+ const bRank = bIndex === -1 ? USER_SECTION_ORDER.length : bIndex
76
+
77
+ if (aRank !== bRank) {
78
+ return aRank - bRank
79
+ }
80
+
81
+ return a.title.localeCompare(b.title, 'sv-SE')
82
+ })
@@ -0,0 +1,85 @@
1
+ import {
2
+ formatTechnicalChangelogItem,
3
+ hasChangelogContent,
4
+ sortUserReleaseSections,
5
+ } from './format'
6
+ import type { ChangelogRelease, ChangelogSection } from './types'
7
+
8
+ const VERSION_HEADING_RE =
9
+ /^## (?:Version (\d+\.\d+\.\d+)|\[(\d+\.\d+\.\d+)\]) - (\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)$/
10
+ const SECTION_HEADING_RE = /^### (.+)$/
11
+ const LIST_ITEM_RE = /^- (.+)$/
12
+
13
+ const parseChangelog = (
14
+ markdown: string,
15
+ options?: {
16
+ sortSections?: (sections: ChangelogSection[]) => ChangelogSection[]
17
+ cleanItem?: (item: string, sectionTitle: string) => string
18
+ },
19
+ ): ChangelogRelease[] => {
20
+ const releases: ChangelogRelease[] = []
21
+ let currentRelease: ChangelogRelease | null = null
22
+ let currentSection: ChangelogSection | null = null
23
+
24
+ for (const line of markdown.split('\n')) {
25
+ const trimmed = line.trim()
26
+ if (trimmed.length === 0) {
27
+ continue
28
+ }
29
+
30
+ const versionMatch = VERSION_HEADING_RE.exec(trimmed)
31
+ if (versionMatch) {
32
+ if (currentRelease) {
33
+ releases.push(currentRelease)
34
+ }
35
+
36
+ currentRelease = {
37
+ version: versionMatch[1] ?? versionMatch[2] ?? '',
38
+ date: versionMatch[3] ?? '',
39
+ sections: [],
40
+ }
41
+ currentSection = null
42
+ continue
43
+ }
44
+
45
+ const sectionMatch = SECTION_HEADING_RE.exec(trimmed)
46
+ if (sectionMatch && currentRelease) {
47
+ currentSection = {
48
+ title: sectionMatch[1]?.trim() ?? '',
49
+ items: [],
50
+ }
51
+ currentRelease.sections.push(currentSection)
52
+ continue
53
+ }
54
+
55
+ const itemMatch = LIST_ITEM_RE.exec(trimmed)
56
+ if (itemMatch && currentSection) {
57
+ const rawItem = itemMatch[1]?.trim() ?? ''
58
+ currentSection.items.push(
59
+ options?.cleanItem ? options.cleanItem(rawItem, currentSection.title) : rawItem,
60
+ )
61
+ }
62
+ }
63
+
64
+ if (currentRelease) {
65
+ releases.push(currentRelease)
66
+ }
67
+
68
+ return releases
69
+ .map((release) => ({
70
+ ...release,
71
+ sections: (options?.sortSections ? options.sortSections(release.sections) : release.sections).map(
72
+ (section) => ({
73
+ ...section,
74
+ items: section.items.filter((item) => item.length > 0),
75
+ }),
76
+ ),
77
+ }))
78
+ .filter((release) => hasChangelogContent(release))
79
+ }
80
+
81
+ export const parseUserChangelog = (markdown: string): ChangelogRelease[] =>
82
+ parseChangelog(markdown, { sortSections: sortUserReleaseSections })
83
+
84
+ export const parseTechnicalChangelog = (markdown: string): ChangelogRelease[] =>
85
+ parseChangelog(markdown, { cleanItem: formatTechnicalChangelogItem })
@@ -0,0 +1,22 @@
1
+ import { sortUserReleaseSections } from './format'
2
+ import type { ChangelogRelease } from './types'
3
+
4
+ export const serializeUserChangelogRelease = (release: ChangelogRelease): string => {
5
+ const lines: string[] = [`## Version ${release.version} - ${release.date}`, '']
6
+
7
+ for (const section of sortUserReleaseSections(release.sections)) {
8
+ if (section.items.length === 0) {
9
+ continue
10
+ }
11
+
12
+ lines.push(`### ${section.title}`, '')
13
+
14
+ for (const item of section.items) {
15
+ lines.push(`- ${item}`)
16
+ }
17
+
18
+ lines.push('')
19
+ }
20
+
21
+ return lines.join('\n').trimEnd()
22
+ }
@@ -0,0 +1,84 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { parseUserChangelog } from './parseChangelog.ts'
5
+ import { serializeUserChangelogRelease } from './serializeUserChangelog.ts'
6
+ import type { ChangelogRelease } from './types.ts'
7
+
8
+ const rootDir = process.env.FG_DEVKIT_CWD
9
+ ? path.resolve(process.env.FG_DEVKIT_CWD)
10
+ : path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
11
+ const draftPath = path.join(rootDir, 'CHANGELOG.user.draft.md')
12
+ const contentPath = path.join(rootDir, 'CHANGELOG.user.md')
13
+
14
+ const readMarkdown = (filePath: string): string =>
15
+ existsSync(filePath) ? readFileSync(filePath, 'utf-8') : ''
16
+
17
+ const updateVersionTimestamps = (
18
+ content: string,
19
+ draftReleases: ChangelogRelease[],
20
+ ): { content: string; versions: string[] } => {
21
+ const versions: string[] = []
22
+ let result = content
23
+
24
+ for (const release of draftReleases) {
25
+ const dateTimeMatch = release.date.match(/^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2})$/)
26
+ if (!dateTimeMatch) {
27
+ continue
28
+ }
29
+
30
+ const [, datePart, timePart] = dateTimeMatch
31
+ const headingWithoutTime = `## Version ${release.version} - ${datePart}`
32
+ const headingWithTime = `## Version ${release.version} - ${datePart} ${timePart}`
33
+
34
+ if (result.includes(headingWithTime)) {
35
+ continue
36
+ }
37
+
38
+ if (result.includes(headingWithoutTime)) {
39
+ result = result.replaceAll(headingWithoutTime, headingWithTime)
40
+ versions.push(release.version)
41
+ }
42
+ }
43
+
44
+ return { content: result, versions }
45
+ }
46
+
47
+ const draft = parseUserChangelog(readMarkdown(draftPath))
48
+ const existingContent = readMarkdown(contentPath).trimEnd()
49
+ const existing = existingContent.length > 0 ? parseUserChangelog(existingContent) : []
50
+
51
+ const existingVersions = new Set(existing.map((release) => release.version))
52
+ const newReleases = draft.filter((release) => !existingVersions.has(release.version))
53
+
54
+ let mergedContent = existingContent
55
+
56
+ if (newReleases.length > 0) {
57
+ const newBlocks = newReleases.map((release) => serializeUserChangelogRelease(release)).join('\n\n')
58
+ mergedContent =
59
+ existingContent.length > 0 ? `${newBlocks}\n\n${existingContent}` : newBlocks
60
+ }
61
+
62
+ const { content: contentWithTimestamps, versions: updatedTimestamps } = updateVersionTimestamps(
63
+ mergedContent,
64
+ draft,
65
+ )
66
+
67
+ if (newReleases.length === 0 && updatedTimestamps.length === 0) {
68
+ process.stdout.write('No user changelog changes to sync.\n')
69
+ process.exit(0)
70
+ }
71
+
72
+ writeFileSync(contentPath, `${contentWithTimestamps.trimEnd()}\n`)
73
+
74
+ if (newReleases.length > 0) {
75
+ process.stdout.write(
76
+ `Added user changelog version(s): ${newReleases.map((release) => release.version).join(', ')}\n`,
77
+ )
78
+ }
79
+
80
+ if (updatedTimestamps.length > 0) {
81
+ process.stdout.write(
82
+ `Updated timestamps in CHANGELOG.user.md for version(s): ${updatedTimestamps.join(', ')}\n`,
83
+ )
84
+ }
@@ -0,0 +1,10 @@
1
+ export interface ChangelogSection {
2
+ title: string
3
+ items: string[]
4
+ }
5
+
6
+ export interface ChangelogRelease {
7
+ version: string
8
+ date: string
9
+ sections: ChangelogSection[]
10
+ }
@@ -1,7 +1,7 @@
1
1
  FROM node:22-bookworm AS build
2
2
  WORKDIR /src
3
3
  COPY . /src
4
- RUN corepack enable && corepack prepare pnpm@10.17.1 --activate
4
+ RUN corepack enable && corepack prepare pnpm@11.8.0 --activate
5
5
  RUN pnpm install
6
6
  RUN pnpm build
7
7
 
@@ -22,23 +22,23 @@
22
22
  "content-disposition-header": "0.6.0",
23
23
  "date-fns": "4.4.0",
24
24
  "dotenv": "16.4.5",
25
- "fds-vue-core": "8.0.5",
25
+ "fds-vue-core": "8.1.1",
26
26
  "pinia": "3.0.4",
27
27
  "qrcode.vue": "3.10.0",
28
28
  "vue": "3.5.38",
29
- "vue-i18n": "11.4.5",
29
+ "vue-i18n": "11.4.6",
30
30
  "vue-router": "4.6.3"
31
31
  },
32
32
  "devDependencies": {
33
- "@intlify/unplugin-vue-i18n": "11.2.3",
33
+ "@intlify/unplugin-vue-i18n": "11.2.4",
34
34
  "@tanstack/vue-query-devtools": "6.1.34",
35
35
  "@types/node": "22.16.5",
36
36
  "@vitejs/plugin-vue": "6.0.7",
37
- "fg-devkit": "1.9.0",
37
+ "fg-devkit": "1.12.0",
38
38
  "start-server-and-test": "2.1.3",
39
39
  "tailwindcss": "4.3.1",
40
40
  "tsx": "4.22.4",
41
- "vite": "7.3.2"
41
+ "vite": "7.3.5"
42
42
  },
43
- "packageManager": "pnpm@10.17.1"
43
+ "packageManager": "pnpm@11.8.0"
44
44
  }