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 +6 -0
- package/configs/cliff.toml +54 -0
- package/configs/cliff.user.toml +64 -0
- package/configs/dependency-policy.vue-workspace.json +19 -15
- package/package.json +20 -6
- package/release-notes/format.ts +82 -0
- package/release-notes/parseChangelog.ts +85 -0
- package/release-notes/serializeUserChangelog.ts +22 -0
- package/release-notes/sync-user-changelog.ts +84 -0
- package/release-notes/types.ts +10 -0
- package/scaffold/vue/Dockerfile.local +1 -1
- package/scaffold/vue/package.json +6 -6
- package/scripts/audit-override-commit.mjs +166 -0
- package/scripts/bump-pnpm-toolchain.mjs +281 -0
- package/scripts/check-dependency-policy.mjs +16 -0
- package/scripts/dependency-policy-toolchain.mjs +101 -0
- package/scripts/fg-devkit.mjs +71 -4
- package/scripts/fleet-bump-fg-devkit.mjs +3 -52
- package/scripts/fleet-vue-repos.mjs +54 -0
- package/scripts/generate-release-notes.mjs +84 -0
- package/scripts/refresh-dependency-policy-from-repos.mjs +14 -0
- package/scripts/release-notes-common.mjs +114 -0
- package/scripts/release.sh +152 -26
- package/scripts/sync-consumer-scripts.mjs +1 -0
- package/scripts/sync-dependency-policy.mjs +12 -1
- package/scripts/sync-release-notes-config.mjs +104 -0
- package/scripts/sync-scaffold-vue-deps.mjs +35 -2
- package/scripts/verify-release-notes.mjs +158 -0
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.
|
|
12
|
+
"@sentry/browser": "10.59.0",
|
|
10
13
|
"@sentry/tracing": "7.120.4",
|
|
11
|
-
"@sentry/vue": "10.
|
|
12
|
-
"@sentry/wasm": "10.
|
|
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.
|
|
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.
|
|
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.
|
|
55
|
+
"@intlify/unplugin-vue-i18n": "11.2.4",
|
|
53
56
|
"@playwright/test": "1.61.0",
|
|
54
|
-
"@storybook/addon-a11y": "10.4.
|
|
55
|
-
"@storybook/addon-docs": "10.4.
|
|
56
|
-
"@storybook/addon-vitest": "10.4.
|
|
57
|
-
"@storybook/vue3": "10.4.
|
|
58
|
-
"@storybook/vue3-vite": "10.4.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
81
|
-
"@typescript-eslint/parser": "8.61.
|
|
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.
|
|
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.
|
|
101
|
+
"fallow": "2.101.0",
|
|
102
|
+
"git-cliff": "2.13.1",
|
|
90
103
|
"lefthook": "2.1.9",
|
|
91
|
-
"npm-check-updates": "22.2.
|
|
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
|
+
}
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
41
|
+
"vite": "7.3.5"
|
|
42
42
|
},
|
|
43
|
-
"packageManager": "pnpm@
|
|
43
|
+
"packageManager": "pnpm@11.8.0"
|
|
44
44
|
}
|