@itcase/config 1.6.63 → 1.6.65
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 +34 -7
- package/husky-hooks/_lib.sh +57 -0
- package/husky-hooks/_run.sh +22 -0
- package/husky-hooks/commit-msg +23 -0
- package/husky-hooks/config.example +10 -0
- package/husky-hooks/install-launcher.js +67 -0
- package/husky-hooks/post-commit +4 -0
- package/husky-hooks/post-merge +38 -0
- package/husky-hooks/pre-commit +37 -0
- package/husky-hooks/pre-push +31 -0
- package/husky-hooks/prepare-commit-msg +42 -0
- package/husky-hooks/run.js +70 -0
- package/husky-hooks/set-release-message +161 -0
- package/husky-hooks/setup_tty.sh +22 -0
- package/lint-staged/index.js +1 -1
- package/lint-staged-ts/index.js +1 -1
- package/package.json +21 -16
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@ npm install --save-dev @itcase/config
|
|
|
17
17
|
- `createCommitlintReleaseConfig(SCOPES)` — для релизных веток
|
|
18
18
|
- `commitlint/commitizenAdapter.js` — адаптер Commitizen (type → scope → subScope → subject)
|
|
19
19
|
|
|
20
|
-
- **Husky** — husky-hooks
|
|
20
|
+
- **Husky** — общие git-хуки в `husky-hooks/`
|
|
21
|
+
- Логика хуков живёт в пакете; бинарь `itcase-config-husky-hooks` ставится при `npm i`. В репозитории остаются файлы `.husky/<hook>` и локальный `.husky/config`
|
|
21
22
|
|
|
22
23
|
- **Lint-staged** — запуск линтеров для измененных файлов
|
|
23
24
|
- `lint-staged/index.js` — базовая конфигурация
|
|
@@ -68,11 +69,7 @@ export default process.env.RELEASE_BRANCH === 'true'
|
|
|
68
69
|
: createCommitlintProjectConfig(SCOPES)
|
|
69
70
|
```
|
|
70
71
|
|
|
71
|
-
3.
|
|
72
|
-
|
|
73
|
-
```bash
|
|
74
|
-
npx commitlint --edit "$1"
|
|
75
|
-
```
|
|
72
|
+
3. Git-хуки подключаются из `husky-hooks/` — см. раздел **Подключение Husky** ниже.
|
|
76
73
|
|
|
77
74
|
4. **(Опционально)** Интерактивное оформление коммитов через Commitizen. Установите:
|
|
78
75
|
|
|
@@ -84,4 +81,34 @@ npm install --save-dev commitizen @commitlint/cz-commitlint
|
|
|
84
81
|
|
|
85
82
|
```json
|
|
86
83
|
{ "path": "@itcase/config/commitlint/commitizenAdapter.js" }
|
|
87
|
-
```
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Подключение Husky
|
|
87
|
+
|
|
88
|
+
В репозитории остаются файлы, на которые опираются git и husky. Логика лежит в `@itcase/config/husky-hooks`. При установке пакета npm кладёт бинарь `itcase-config-husky-hooks` в `node_modules/.bin` — husky уже добавляет этот путь в `PATH`.
|
|
89
|
+
|
|
90
|
+
При `npm i` пакет сам копирует `.husky/_run.sh` (git его не вызывает). Добавьте в `.gitignore`:
|
|
91
|
+
|
|
92
|
+
```gitignore
|
|
93
|
+
.husky/_run.sh
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Репы без обёрток скрипт не трогает. `npm i --ignore-scripts` файл не создаст.
|
|
97
|
+
|
|
98
|
+
В каждом git-hook оставьте обёртку:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
#!/usr/bin/env sh
|
|
102
|
+
. "$(dirname "$0")/_run.sh"
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Нужны: `pre-commit`, `commit-msg`, `prepare-commit-msg`, `pre-push`, `post-merge`, `post-commit`. `set-release-message` тоже можно обернуть — его вызывает `post-merge`.
|
|
106
|
+
|
|
107
|
+
Локальный `.husky/config` не оборачивайте. Образец: `husky-hooks/config.example`.
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
PROTECTED_BRANCHES="master"
|
|
111
|
+
RELEASE_BRANCHES="master"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`prepare` в `package.json` как обычно: `"prepare": "husky"`.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Shared helpers for @itcase/config husky-hooks.
|
|
2
|
+
# HUSKY_DIR — consumer .husky (set by the repo wrapper).
|
|
3
|
+
|
|
4
|
+
PACKAGE_HOOKS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
|
|
6
|
+
# Только локальный .husky/config потребителя, не example из пакета
|
|
7
|
+
if [ -n "${HUSKY_DIR:-}" ] && [ -f "${HUSKY_DIR}/config" ]; then
|
|
8
|
+
# shellcheck disable=SC1091
|
|
9
|
+
. "${HUSKY_DIR}/config"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
itcase_is_ci() {
|
|
13
|
+
[ -n "${GIT_WORKFLOW:-}" ] || [ -n "${CI:-}" ] || [ -n "${GITHUB_ACTIONS:-}" ]
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
itcase_current_branch() {
|
|
17
|
+
git rev-parse --abbrev-ref HEAD 2>/dev/null || echo ''
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
itcase_is_release_branch() {
|
|
21
|
+
local branch="$1"
|
|
22
|
+
local release_branch
|
|
23
|
+
|
|
24
|
+
if [ -z "${RELEASE_BRANCHES:-}" ]; then
|
|
25
|
+
[ "$branch" = "master" ]
|
|
26
|
+
return $?
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
for release_branch in $RELEASE_BRANCHES; do
|
|
30
|
+
if [ "$branch" = "$release_branch" ]; then
|
|
31
|
+
return 0
|
|
32
|
+
fi
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
return 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
itcase_export_release_branch_env() {
|
|
39
|
+
if itcase_is_release_branch "$(itcase_current_branch)"; then
|
|
40
|
+
export RELEASE_BRANCH=true
|
|
41
|
+
fi
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
itcase_conflict_marker_files() {
|
|
45
|
+
git grep -lI --untracked --exclude-standard \
|
|
46
|
+
-e '^<<<<<<< ' -e '^=======$' -e '^>>>>>>> ' \
|
|
47
|
+
-- . ':!node_modules' ':!dist' 2>/dev/null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
itcase_run_lint_staged() {
|
|
51
|
+
if [ -f "./node_modules/lint-staged/bin/lint-staged.js" ]; then
|
|
52
|
+
node ./node_modules/lint-staged/bin/lint-staged.js
|
|
53
|
+
return $?
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
npx lint-staged
|
|
57
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
# Shared launcher for @itcase/config husky hooks. Git does not run this file.
|
|
3
|
+
export HUSKY_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
|
4
|
+
HOOK_NAME="$(basename "$0")"
|
|
5
|
+
|
|
6
|
+
if command -v itcase-config-husky-hooks >/dev/null 2>&1; then
|
|
7
|
+
exec itcase-config-husky-hooks "$HOOK_NAME" "$@"
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
ROOT="$(CDPATH= cd -- "$HUSKY_DIR/.." && pwd)"
|
|
11
|
+
for candidate in \
|
|
12
|
+
"$ROOT/husky-hooks/run.js" \
|
|
13
|
+
"$ROOT/node_modules/@itcase/config/husky-hooks/run.js" \
|
|
14
|
+
"$ROOT/../itcase-config/husky-hooks/run.js"
|
|
15
|
+
do
|
|
16
|
+
if [ -f "$candidate" ]; then
|
|
17
|
+
exec node "$candidate" "$HOOK_NAME" "$@"
|
|
18
|
+
fi
|
|
19
|
+
done
|
|
20
|
+
|
|
21
|
+
echo "❌ itcase-config-husky-hooks not found. Install @itcase/config." >&2
|
|
22
|
+
exit 1
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# shellcheck disable=SC1091
|
|
4
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_lib.sh"
|
|
5
|
+
|
|
6
|
+
# Пропускаем в CI окружении
|
|
7
|
+
if itcase_is_ci; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
commit_msg=$(cat "$1")
|
|
12
|
+
|
|
13
|
+
# Если сообщение коммита пустое или содержит только пробелы/комментарии, отменяем коммит
|
|
14
|
+
clean_msg=$(echo "$commit_msg" | sed '/^#/d; s/^[[:space:]]*//; s/[[:space:]]*$//')
|
|
15
|
+
[ -z "$clean_msg" ] && echo "Aborting commit due to empty commit message." && exit 1
|
|
16
|
+
|
|
17
|
+
# Пропускаем специальные коммиты
|
|
18
|
+
echo "$commit_msg" | grep -qE "^release: publishing new version|^Merge (branch|commit|remote-tracking)|^(Merge pull request|Squash and merge|Rebase and merge)|^Revert |^\(cherry picked from commit" && exit 0
|
|
19
|
+
|
|
20
|
+
# Устанавливаем переменную окружения для выбора конфига commitlint
|
|
21
|
+
itcase_export_release_branch_env
|
|
22
|
+
|
|
23
|
+
npx commitlint --edit "$1"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Пример локального .husky/config — скопируйте в репозиторий и поправьте ветки.
|
|
4
|
+
# Пакет этот файл не подключает.
|
|
5
|
+
|
|
6
|
+
# Защищенные ветки - в них можно делать только merge коммиты (push после merge)
|
|
7
|
+
PROTECTED_BRANCHES="master"
|
|
8
|
+
|
|
9
|
+
# Ветки релизов - для них используется специальная конфигурация
|
|
10
|
+
RELEASE_BRANCHES="master"
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Copies husky-hooks/_run.sh into the project .husky/ on install.
|
|
4
|
+
* Skips repos that do not use the wrappers.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'node:fs'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
|
|
9
|
+
const GIT_HOOKS = [
|
|
10
|
+
'pre-commit',
|
|
11
|
+
'commit-msg',
|
|
12
|
+
'prepare-commit-msg',
|
|
13
|
+
'pre-push',
|
|
14
|
+
'post-merge',
|
|
15
|
+
'post-commit',
|
|
16
|
+
'set-release-message',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
function projectRoot() {
|
|
20
|
+
const initCwd = process.env.INIT_CWD
|
|
21
|
+
if (
|
|
22
|
+
initCwd &&
|
|
23
|
+
path.isAbsolute(initCwd) &&
|
|
24
|
+
!initCwd.includes(`${path.sep}node_modules${path.sep}`)
|
|
25
|
+
) {
|
|
26
|
+
return initCwd
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const cwd = process.cwd()
|
|
30
|
+
const marker = `${path.sep}node_modules${path.sep}@itcase${path.sep}config`
|
|
31
|
+
const index = cwd.lastIndexOf(marker)
|
|
32
|
+
if (index !== -1) {
|
|
33
|
+
return cwd.slice(0, index)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return cwd
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function usesItcaseHooks(huskyDir) {
|
|
40
|
+
if (fs.existsSync(path.join(huskyDir, '_run.sh'))) {
|
|
41
|
+
return true
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return GIT_HOOKS.some((name) => {
|
|
45
|
+
try {
|
|
46
|
+
const text = fs.readFileSync(path.join(huskyDir, name), 'utf8')
|
|
47
|
+
return text.includes('_run.sh')
|
|
48
|
+
} catch {
|
|
49
|
+
return false
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const root = projectRoot()
|
|
55
|
+
const huskyDir = path.join(root, '.husky')
|
|
56
|
+
if (!fs.existsSync(huskyDir) || !usesItcaseHooks(huskyDir)) {
|
|
57
|
+
process.exit(0)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const src = path.join(import.meta.dirname, '_run.sh')
|
|
61
|
+
const dest = path.join(huskyDir, '_run.sh')
|
|
62
|
+
fs.copyFileSync(src, dest)
|
|
63
|
+
try {
|
|
64
|
+
fs.chmodSync(dest, 0o755)
|
|
65
|
+
} catch {
|
|
66
|
+
// Windows may not support chmod; Git Bash still runs the file via sh.
|
|
67
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# shellcheck disable=SC1091
|
|
4
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_lib.sh"
|
|
5
|
+
|
|
6
|
+
# Ранний выход для различных условий
|
|
7
|
+
itcase_is_ci && exit 0
|
|
8
|
+
|
|
9
|
+
branch="$(itcase_current_branch)"
|
|
10
|
+
itcase_is_release_branch "$branch" || exit 0
|
|
11
|
+
|
|
12
|
+
commit_msg=$(git log -1 --pretty=%B 2>/dev/null || echo '')
|
|
13
|
+
[ -z "$commit_msg" ] && exit 0
|
|
14
|
+
|
|
15
|
+
# Проверка формата коммита (major/minor/patch или release: publishing)
|
|
16
|
+
echo "$commit_msg" | grep -qE "^(major|minor|patch)(\([^)]+\))?:|^release: publishing" && exit 0
|
|
17
|
+
|
|
18
|
+
# Проверка наличия коммита в origin
|
|
19
|
+
git branch -r --contains HEAD 2>/dev/null | grep -q "origin/" && exit 0
|
|
20
|
+
|
|
21
|
+
# Проверка на автоматические merge коммиты
|
|
22
|
+
echo "$commit_msg" | grep -qE "^Merge (remote-tracking|made by|branch .* of (http|https|git@))" && exit 0
|
|
23
|
+
|
|
24
|
+
# Проверка merge parent в origin
|
|
25
|
+
if git rev-parse --verify HEAD^2 >/dev/null 2>&1; then
|
|
26
|
+
merge_parent=$(git rev-parse HEAD^2 2>/dev/null)
|
|
27
|
+
git branch -r --contains "$merge_parent" 2>/dev/null | grep -q . && exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Если дошли сюда, нужно установить release message
|
|
31
|
+
export RELEASE_BRANCH=true
|
|
32
|
+
|
|
33
|
+
# Проверка на merge коммит или невалидный commitlint
|
|
34
|
+
if echo "$commit_msg" | grep -qE "^Merge (branch|commit|remote-tracking)|^(Merge pull request|Squash and merge|Rebase and merge)" || \
|
|
35
|
+
! echo "$commit_msg" | npx commitlint --no-edit 2>/dev/null; then
|
|
36
|
+
bash "${PACKAGE_HOOKS}/set-release-message"
|
|
37
|
+
exit $?
|
|
38
|
+
fi
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# shellcheck disable=SC1091
|
|
4
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_lib.sh"
|
|
5
|
+
|
|
6
|
+
# Пропускаем в CI окружении
|
|
7
|
+
if itcase_is_ci; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
BRANCH="$(itcase_current_branch)"
|
|
12
|
+
|
|
13
|
+
# Проверяем, является ли текущая ветка защищенной
|
|
14
|
+
if [ -n "${PROTECTED_BRANCHES:-}" ]; then
|
|
15
|
+
for protected_branch in $PROTECTED_BRANCHES; do
|
|
16
|
+
if [ "$BRANCH" = "$protected_branch" ]; then
|
|
17
|
+
# Если это merge операция - разрешаем
|
|
18
|
+
if [ -f "$(git rev-parse --git-dir)/MERGE_HEAD" ]; then
|
|
19
|
+
break
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
echo "❌ Прямые коммиты в ветку '$BRANCH' запрещены!"
|
|
23
|
+
echo " Разрешены только merge коммиты из других веток."
|
|
24
|
+
echo " Создайте новую ветку и сделайте merge через pull request."
|
|
25
|
+
exit 1
|
|
26
|
+
fi
|
|
27
|
+
done
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Проверка на конфликты слияния
|
|
31
|
+
if conflict_files="$(itcase_conflict_marker_files)"; then
|
|
32
|
+
echo 'Fix conflicts'
|
|
33
|
+
printf '%s\n' "$conflict_files" | sed 's/^/ /'
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
itcase_run_lint_staged
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# shellcheck disable=SC1091
|
|
4
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_lib.sh"
|
|
5
|
+
|
|
6
|
+
if itcase_is_ci; then
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
branch="$(itcase_current_branch)"
|
|
11
|
+
|
|
12
|
+
if ! itcase_is_release_branch "$branch"; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
commit_msg=$(git log --format=%B -n 1 2>/dev/null || echo '')
|
|
17
|
+
|
|
18
|
+
if [ -z "$commit_msg" ]; then
|
|
19
|
+
exit 0
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
if echo "$commit_msg" | grep -qE "^(major|minor|patch).*:"; then
|
|
23
|
+
exit 0
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Пропускаем мерж-коммиты
|
|
27
|
+
if echo "$commit_msg" | grep -qE "^Merge (branch|commit|remote-tracking)|^(Merge pull request|Squash and merge|Rebase and merge)"; then
|
|
28
|
+
exit 0
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
echo "$commit_msg" | npx commitlint
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# shellcheck disable=SC1091
|
|
4
|
+
. "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_lib.sh"
|
|
5
|
+
|
|
6
|
+
# Пропускаем в CI окружении
|
|
7
|
+
if itcase_is_ci; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
if [ -n "$2" ] && [ "$2" != "message" ]; then
|
|
12
|
+
exit 0
|
|
13
|
+
fi
|
|
14
|
+
|
|
15
|
+
if [ "$2" = "merge" ] || [ "$2" = "squash" ] || [ "$2" = "commit" ]; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Проверяем, является ли текущая ветка веткой релиза
|
|
20
|
+
itcase_export_release_branch_env
|
|
21
|
+
|
|
22
|
+
if [ -f "$1" ]; then
|
|
23
|
+
commit_msg=$(cat "$1" 2>/dev/null | sed '/^#/d' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')
|
|
24
|
+
if [ -n "$commit_msg" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Импортируем утилиты для настройки TTY
|
|
30
|
+
if [ -f "${PACKAGE_HOOKS}/setup_tty.sh" ]; then
|
|
31
|
+
# shellcheck disable=SC1091
|
|
32
|
+
. "${PACKAGE_HOOKS}/setup_tty.sh"
|
|
33
|
+
# Настраиваем TTY для интерактивного ввода
|
|
34
|
+
setup_tty
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if ! npx cz --hook; then
|
|
38
|
+
if [ -f "$1" ]; then
|
|
39
|
+
> "$1"
|
|
40
|
+
fi
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
const ALLOWED_HOOKS = new Set([
|
|
7
|
+
'pre-commit',
|
|
8
|
+
'commit-msg',
|
|
9
|
+
'prepare-commit-msg',
|
|
10
|
+
'pre-push',
|
|
11
|
+
'post-merge',
|
|
12
|
+
'post-commit',
|
|
13
|
+
'set-release-message',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
const hookName = process.argv[2]
|
|
17
|
+
const hookArgs = process.argv.slice(3)
|
|
18
|
+
|
|
19
|
+
if (!hookName || !ALLOWED_HOOKS.has(hookName)) {
|
|
20
|
+
process.stderr.write(
|
|
21
|
+
'Usage: itcase-config-husky-hooks <hook-name> [args...]\n',
|
|
22
|
+
)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const hookScript = path.join(import.meta.dirname, hookName)
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(hookScript)) {
|
|
29
|
+
process.stderr.write(
|
|
30
|
+
`❌ Husky hook '${hookName}' not found in @itcase/config.\n`,
|
|
31
|
+
)
|
|
32
|
+
process.stderr.write(` Expected: ${hookScript}\n`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveBash() {
|
|
37
|
+
if (process.platform !== 'win32') {
|
|
38
|
+
return 'bash'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const candidates = [
|
|
42
|
+
process.env.PROGRAMFILES &&
|
|
43
|
+
path.join(process.env.PROGRAMFILES, 'Git', 'bin', 'bash.exe'),
|
|
44
|
+
process.env['PROGRAMFILES(X86)'] &&
|
|
45
|
+
path.join(process.env['PROGRAMFILES(X86)'], 'Git', 'bin', 'bash.exe'),
|
|
46
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
47
|
+
].filter(Boolean)
|
|
48
|
+
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (fs.existsSync(candidate)) {
|
|
51
|
+
return candidate
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return 'bash'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const result = spawnSync(resolveBash(), [hookScript, ...hookArgs], {
|
|
59
|
+
env: { ...process.env, HUSKY_DIR: process.env.HUSKY_DIR || '' },
|
|
60
|
+
stdio: 'inherit',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
if (result.error) {
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`❌ Failed to run bash for husky hook '${hookName}': ${result.error.message}\n`,
|
|
66
|
+
)
|
|
67
|
+
process.exit(1)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
process.exit(result.status ?? 1)
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
PACKAGE_HOOKS="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
4
|
+
|
|
5
|
+
# Импортируем утилиты для настройки TTY
|
|
6
|
+
if [ -f "${PACKAGE_HOOKS}/setup_tty.sh" ]; then
|
|
7
|
+
# shellcheck disable=SC1091
|
|
8
|
+
. "${PACKAGE_HOOKS}/setup_tty.sh"
|
|
9
|
+
# Настраиваем TTY для интерактивного ввода
|
|
10
|
+
setup_tty
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Опциональный параметр - хеш коммита для обновления (если не указан, обновляем HEAD)
|
|
14
|
+
TARGET_COMMIT="${1:-HEAD}"
|
|
15
|
+
|
|
16
|
+
clear_stale_merge_state() {
|
|
17
|
+
local git_dir="$1"
|
|
18
|
+
rm -f "${git_dir}/MERGE_HEAD" \
|
|
19
|
+
"${git_dir}/MERGE_MSG" \
|
|
20
|
+
"${git_dir}/MERGE_MODE" \
|
|
21
|
+
"${git_dir}/MERGE_RR" \
|
|
22
|
+
"${git_dir}/AUTO_MERGE" 2>/dev/null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# release_type: major | minor | patch
|
|
26
|
+
format_release_message() {
|
|
27
|
+
local release_type="$1"
|
|
28
|
+
local commit_message="$2"
|
|
29
|
+
|
|
30
|
+
if echo "$commit_message" | grep -qE "^${release_type}(\([^)]+\))?:"; then
|
|
31
|
+
printf '%s\n' "$commit_message"
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
if [ -n "$RELEASE_BRANCH" ] || echo "$commit_message" | grep -qE '^Merge '; then
|
|
36
|
+
local subject
|
|
37
|
+
subject=$(printf '%s' "$commit_message" | head -n 1)
|
|
38
|
+
printf '%s(other): %s\n' "$release_type" "$subject"
|
|
39
|
+
return 0
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if echo "$commit_message" | grep -qE '^[^:]+:'; then
|
|
43
|
+
local before_colon after_colon
|
|
44
|
+
before_colon=$(echo "$commit_message" | sed 's/:.*$//')
|
|
45
|
+
after_colon=$(echo "$commit_message" | sed 's/^[^:]*: *//')
|
|
46
|
+
printf '%s(%s): %s\n' "$release_type" "$before_colon" "$after_colon"
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
printf '%s(other): %s\n' "$release_type" "$commit_message"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
update_message() {
|
|
54
|
+
local release_type="$1"
|
|
55
|
+
local commit_message
|
|
56
|
+
local git_dir
|
|
57
|
+
local new_message
|
|
58
|
+
|
|
59
|
+
commit_message=$(git log --format=%B -n 1 "$TARGET_COMMIT" 2>/dev/null) || return 1
|
|
60
|
+
[ -z "$commit_message" ] && return 1
|
|
61
|
+
|
|
62
|
+
if echo "$commit_message" | grep -qE "^${release_type}(\([^)]+\))?:"; then
|
|
63
|
+
return 0
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
git_dir=$(git rev-parse --git-dir 2>/dev/null) || return 1
|
|
67
|
+
new_message=$(format_release_message "$release_type" "$commit_message") || return 1
|
|
68
|
+
|
|
69
|
+
# Если обновляем не HEAD, используем rebase
|
|
70
|
+
if [ "$TARGET_COMMIT" != "HEAD" ]; then
|
|
71
|
+
# Проверяем, не находится ли репозиторий в процессе rebase (не проверяем MERGE_HEAD, т.к. в post-merge merge уже завершен)
|
|
72
|
+
if [ -f "${git_dir}/rebase-merge" ] || [ -f "${git_dir}/rebase-apply" ]; then
|
|
73
|
+
echo "Error: Repository is in the middle of a rebase. Cannot update commit message."
|
|
74
|
+
return 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
local commit_hash parent temp_sequence_script temp_editor_script message_file short_hash
|
|
78
|
+
|
|
79
|
+
commit_hash=$(git rev-parse "$TARGET_COMMIT" 2>/dev/null) || return 1
|
|
80
|
+
parent=$(git rev-parse "${commit_hash}^" 2>/dev/null) || return 1
|
|
81
|
+
|
|
82
|
+
message_file=$(mktemp)
|
|
83
|
+
printf '%s' "$new_message" > "$message_file"
|
|
84
|
+
|
|
85
|
+
temp_sequence_script=$(mktemp)
|
|
86
|
+
# Используем полный хеш для надежности
|
|
87
|
+
short_hash=$(git rev-parse --short "$commit_hash" 2>/dev/null || echo "${commit_hash:0:7}")
|
|
88
|
+
echo "sed -i.bak '1s/^pick ${short_hash}/reword ${short_hash}/'" > "$temp_sequence_script"
|
|
89
|
+
chmod +x "$temp_sequence_script"
|
|
90
|
+
|
|
91
|
+
# Создаем временный скрипт для редактирования сообщения коммита
|
|
92
|
+
temp_editor_script=$(mktemp)
|
|
93
|
+
cat > "$temp_editor_script" <<EOF
|
|
94
|
+
#!/usr/bin/env bash
|
|
95
|
+
# GIT_EDITOR получает путь к файлу с сообщением коммита как аргумент
|
|
96
|
+
cp "$message_file" "\$1"
|
|
97
|
+
EOF
|
|
98
|
+
chmod +x "$temp_editor_script"
|
|
99
|
+
|
|
100
|
+
export GIT_SEQUENCE_EDITOR="$temp_sequence_script"
|
|
101
|
+
export GIT_EDITOR="$temp_editor_script"
|
|
102
|
+
|
|
103
|
+
if ! GIT_WORKFLOW=1 git rebase -i "$parent" >/dev/null 2>&1; then
|
|
104
|
+
git rebase --abort >/dev/null 2>&1
|
|
105
|
+
rm -f "$temp_sequence_script" "$temp_editor_script" "$message_file"
|
|
106
|
+
return 1
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
rm -f "$temp_sequence_script" "$temp_editor_script" "$message_file"
|
|
110
|
+
return 0
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
if [ -f "${git_dir}/rebase-merge" ] || [ -f "${git_dir}/rebase-apply" ]; then
|
|
114
|
+
return 1
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
# post-merge: merge-коммит уже создан, MERGE_HEAD ещё не удалён git'ом
|
|
118
|
+
if [ -n "$RELEASE_BRANCH" ] && [ -f "${git_dir}/MERGE_HEAD" ]; then
|
|
119
|
+
clear_stale_merge_state "$git_dir"
|
|
120
|
+
elif [ -f "${git_dir}/MERGE_HEAD" ]; then
|
|
121
|
+
return 1
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
git rev-parse --verify HEAD >/dev/null 2>&1 || return 1
|
|
125
|
+
|
|
126
|
+
git diff --cached --quiet 2>&1 || git reset HEAD >/dev/null 2>&1
|
|
127
|
+
|
|
128
|
+
if ! GIT_EDITOR=true GIT_WORKFLOW=1 git commit --amend --no-verify -m "$new_message" >/dev/null 2>&1; then
|
|
129
|
+
echo "Error: Could not amend commit"
|
|
130
|
+
return 1
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
return 0
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
PS3='Select release type: '
|
|
137
|
+
|
|
138
|
+
MAJOR='⭐️ MAJOR version when you make incompatible API changes'
|
|
139
|
+
MINOR='🐞 MINOR version when you add functionality in a backwards compatible manner'
|
|
140
|
+
PATCH='✅ PATCH version when you make backwards compatible bug fixes'
|
|
141
|
+
CANCEL='Cancel'
|
|
142
|
+
|
|
143
|
+
options=("${MAJOR}" "${MINOR}" "${PATCH}" "${CANCEL}")
|
|
144
|
+
select opt in "${options[@]}"
|
|
145
|
+
do
|
|
146
|
+
case $opt in
|
|
147
|
+
$MAJOR)
|
|
148
|
+
update_message 'major' && exit 0 || exit 1
|
|
149
|
+
;;
|
|
150
|
+
$MINOR)
|
|
151
|
+
update_message 'minor' && exit 0 || exit 1
|
|
152
|
+
;;
|
|
153
|
+
$PATCH)
|
|
154
|
+
update_message 'patch' && exit 0 || exit 1
|
|
155
|
+
;;
|
|
156
|
+
$CANCEL)
|
|
157
|
+
exit 1
|
|
158
|
+
;;
|
|
159
|
+
*) echo "invalid option $REPLY";;
|
|
160
|
+
esac
|
|
161
|
+
done
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
# Утилиты для настройки TTY в хуках husky
|
|
4
|
+
|
|
5
|
+
# Проверяет существование команды
|
|
6
|
+
command_exists() {
|
|
7
|
+
command -v "$1" >/dev/null 2>&1
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
# Настраивает TTY для интерактивного ввода
|
|
11
|
+
# Автоматически выходит из скрипта, если не интерактивный режим
|
|
12
|
+
setup_tty() {
|
|
13
|
+
if ! test -t 1; then
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
if command_exists winpty && test -t 1; then
|
|
18
|
+
exec < /dev/tty
|
|
19
|
+
elif test -t 1 && [ -c /dev/tty ] 2>/dev/null; then
|
|
20
|
+
exec < /dev/tty
|
|
21
|
+
fi
|
|
22
|
+
}
|
package/lint-staged/index.js
CHANGED
|
@@ -10,7 +10,7 @@ export default {
|
|
|
10
10
|
// Проверяем, есть ли package.json в корне репозитория
|
|
11
11
|
const rootPackageJson = resolve(process.cwd(), 'package.json')
|
|
12
12
|
if (existsSync(rootPackageJson) && files.includes(rootPackageJson)) {
|
|
13
|
-
return
|
|
13
|
+
return 'npm install --package-lock-only'
|
|
14
14
|
}
|
|
15
15
|
return 'true' // Пропускаем, если не найден
|
|
16
16
|
},
|
package/lint-staged-ts/index.js
CHANGED
|
@@ -18,7 +18,7 @@ export default {
|
|
|
18
18
|
// Проверяем, есть ли package.json в корне репозитория
|
|
19
19
|
const rootPackageJson = resolve(process.cwd(), 'package.json')
|
|
20
20
|
if (existsSync(rootPackageJson) && files.includes(rootPackageJson)) {
|
|
21
|
-
return
|
|
21
|
+
return 'npm install --package-lock-only'
|
|
22
22
|
}
|
|
23
23
|
return 'true' // Пропускаем, если не найден
|
|
24
24
|
},
|
package/package.json
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@itcase/config",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.65",
|
|
4
4
|
"author": "ITCase",
|
|
5
5
|
"description": "ITCase Config",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": ">=22.0.0"
|
|
8
8
|
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"itcase-config-husky-hooks": "./husky-hooks/run.js"
|
|
11
|
+
},
|
|
9
12
|
"scripts": {
|
|
10
13
|
"prepare": "husky",
|
|
14
|
+
"postinstall": "node ./husky-hooks/install-launcher.js",
|
|
11
15
|
"semantic-release": "semantic-release"
|
|
12
16
|
},
|
|
13
17
|
"type": "module",
|
|
@@ -19,6 +23,7 @@
|
|
|
19
23
|
},
|
|
20
24
|
"files": [
|
|
21
25
|
"commitlint",
|
|
26
|
+
"husky-hooks",
|
|
22
27
|
"lint-staged",
|
|
23
28
|
"lint-staged-ts",
|
|
24
29
|
"semantic-release",
|
|
@@ -31,15 +36,15 @@
|
|
|
31
36
|
"registry": "https://registry.npmjs.org/"
|
|
32
37
|
},
|
|
33
38
|
"dependencies": {
|
|
34
|
-
"@commitlint/cli": "^21.2.
|
|
35
|
-
"@commitlint/config-conventional": "^21.2.
|
|
36
|
-
"@commitlint/cz-commitlint": "^21.2.
|
|
39
|
+
"@commitlint/cli": "^21.2.2",
|
|
40
|
+
"@commitlint/config-conventional": "^21.2.2",
|
|
41
|
+
"@commitlint/cz-commitlint": "^21.2.2",
|
|
37
42
|
"@lehoczky/postcss-fluid": "^1.0.3",
|
|
38
|
-
"@lerna-lite/cli": "^5.
|
|
39
|
-
"@lerna-lite/exec": "^5.
|
|
40
|
-
"@lerna-lite/list": "^5.
|
|
41
|
-
"@lerna-lite/run": "^5.
|
|
42
|
-
"@lerna-lite/watch": "^5.
|
|
43
|
+
"@lerna-lite/cli": "^5.5.0",
|
|
44
|
+
"@lerna-lite/exec": "^5.5.0",
|
|
45
|
+
"@lerna-lite/list": "^5.5.0",
|
|
46
|
+
"@lerna-lite/run": "^5.5.0",
|
|
47
|
+
"@lerna-lite/watch": "^5.5.0",
|
|
43
48
|
"@rollup/plugin-alias": "^6.0.0",
|
|
44
49
|
"@rollup/plugin-babel": "^7.1.0",
|
|
45
50
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
@@ -55,9 +60,9 @@
|
|
|
55
60
|
"autoprefixer": "^10.5.4",
|
|
56
61
|
"chokidar-cli": "^3.0.0",
|
|
57
62
|
"commitizen": "^4.3.2",
|
|
58
|
-
"conventional-changelog-conventionalcommits": "^10.
|
|
59
|
-
"cssnano": "^8.0.
|
|
60
|
-
"cssnano-preset-default": "^8.0.
|
|
63
|
+
"conventional-changelog-conventionalcommits": "^10.3.0",
|
|
64
|
+
"cssnano": "^8.0.5",
|
|
65
|
+
"cssnano-preset-default": "^8.0.5",
|
|
61
66
|
"inquirer": "^14.0.2",
|
|
62
67
|
"postcss": "^8.5.26",
|
|
63
68
|
"postcss-aspect-ratio-polyfill": "^2.0.0",
|
|
@@ -67,13 +72,13 @@
|
|
|
67
72
|
"postcss-combine-duplicated-selectors": "^10.0.3",
|
|
68
73
|
"postcss-convert-unit": "^0.1.3",
|
|
69
74
|
"postcss-dark-theme-class": "^2.0.0",
|
|
70
|
-
"postcss-discard-duplicates": "^8.0.
|
|
75
|
+
"postcss-discard-duplicates": "^8.0.3",
|
|
71
76
|
"postcss-each": "^1.1.0",
|
|
72
77
|
"postcss-easings": "^4.0.0",
|
|
73
78
|
"postcss-extend-rule": "^4.0.0",
|
|
74
79
|
"postcss-flexbugs-fixes": "^5.0.2",
|
|
75
80
|
"postcss-hexrgba": "^2.1.0",
|
|
76
|
-
"postcss-import": "^16.
|
|
81
|
+
"postcss-import": "^16.2.0",
|
|
77
82
|
"postcss-import-ext-glob": "^2.1.1",
|
|
78
83
|
"postcss-loader": "^8.2.1",
|
|
79
84
|
"postcss-merge-at-rules": "^2.0.1",
|
|
@@ -97,7 +102,7 @@
|
|
|
97
102
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
98
103
|
"rollup-preserve-directives": "^1.1.3",
|
|
99
104
|
"semantic-release": "^25.0.9",
|
|
100
|
-
"semantic-release-lerna": "^3.6.
|
|
105
|
+
"semantic-release-lerna": "^3.6.1"
|
|
101
106
|
},
|
|
102
107
|
"devDependencies": {
|
|
103
108
|
"@itcase/lint": "^1.1.121",
|
|
@@ -106,7 +111,7 @@
|
|
|
106
111
|
"inquirer": "^14.0.2",
|
|
107
112
|
"lint-staged": "^17.3.0",
|
|
108
113
|
"prettier": "^3.9.6",
|
|
109
|
-
"storybook": "^10.5.
|
|
114
|
+
"storybook": "^10.5.8",
|
|
110
115
|
"stylelint": "^17.14.1",
|
|
111
116
|
"typescript": "^6.0.2"
|
|
112
117
|
}
|