@archpublicwebsite/eslint-config 1.0.5
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 +145 -0
- package/eslint.config.mjs +83 -0
- package/package.json +58 -0
- package/tools/git-hooks/commit-msg.mjs +7 -0
- package/tools/git-hooks/generate-commit-message.mjs +127 -0
- package/tools/git-hooks/post-commit.mjs +15 -0
- package/tools/git-hooks/pre-commit.mjs +10 -0
- package/tools/git-hooks/prepare-commit-msg.mjs +23 -0
- package/tools/git-hooks/shared.mjs +71 -0
- package/tools/git-hooks/verify-commit-message.mjs +33 -0
- package/tools/setup/install.mjs +123 -0
package/README.md
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# eslint-config
|
|
2
|
+
|
|
3
|
+
Reusable ESLint + Git hooks toolkit for Nuxt/Vue projects.
|
|
4
|
+
|
|
5
|
+
## Publish
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd packages/eslint-config
|
|
9
|
+
pnpm version patch
|
|
10
|
+
npm publish --access public
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Quick checks before publish:
|
|
14
|
+
|
|
15
|
+
- Ensure `.npmrc` points to public npm registry.
|
|
16
|
+
- Run `npm pack --dry-run` and verify only `eslint.config.mjs`, `tools/`, and `README.md` are included.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pnpm add -Dw eslint-config
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Required dependencies (consumer project)
|
|
25
|
+
|
|
26
|
+
This toolkit expects these dev dependencies to exist in the project that installs it:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pnpm add -D lint-staged prettier turbo
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `lint-staged` is used by the generated `pre-commit` hook.
|
|
33
|
+
- `prettier` is used by the `lint:fix` flow.
|
|
34
|
+
- `turbo` is used by the required `lint:check` / `lint:fix` scripts.
|
|
35
|
+
|
|
36
|
+
On install, this package automatically sets up in your project root:
|
|
37
|
+
|
|
38
|
+
- `.hooks/pre-commit`
|
|
39
|
+
- `.hooks/prepare-commit-msg`
|
|
40
|
+
- `.hooks/commit-msg`
|
|
41
|
+
- `.hooks/post-commit`
|
|
42
|
+
- `eslint.config.mjs` (if not present)
|
|
43
|
+
- `.prettierrc` plugin entry for `prettier-plugin-tailwindcss`
|
|
44
|
+
- `git config core.hooksPath .hooks` (when in a git repo)
|
|
45
|
+
|
|
46
|
+
## What this package provides
|
|
47
|
+
|
|
48
|
+
- `eslint.config.mjs` builder via `createArchipelagoConfig`
|
|
49
|
+
- Reusable git hook handlers in `tools/git-hooks/`
|
|
50
|
+
- `pre-commit.mjs`
|
|
51
|
+
- `prepare-commit-msg.mjs`
|
|
52
|
+
- `commit-msg.mjs`
|
|
53
|
+
- `post-commit.mjs`
|
|
54
|
+
- `generate-commit-message.mjs`
|
|
55
|
+
- `verify-commit-message.mjs`
|
|
56
|
+
- `tools/setup/install.mjs` automatic project bootstrap
|
|
57
|
+
|
|
58
|
+
## ESLint usage
|
|
59
|
+
|
|
60
|
+
Auto-generated `eslint.config.mjs` uses this package directly:
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
import { createArchipelagoConfig } from 'eslint-config'
|
|
64
|
+
|
|
65
|
+
export default createArchipelagoConfig()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Override rules
|
|
69
|
+
|
|
70
|
+
You can override any rule in your root `eslint.config.mjs`:
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
import { createArchipelagoConfig } from 'eslint-config'
|
|
74
|
+
|
|
75
|
+
export default createArchipelagoConfig({
|
|
76
|
+
name: 'project/overrides',
|
|
77
|
+
rules: {
|
|
78
|
+
'no-console': 'off',
|
|
79
|
+
'vue/max-attributes-per-line': 'off',
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Manual setup (optional)
|
|
85
|
+
|
|
86
|
+
Automatic setup is the default. If needed, you can still run setup manually:
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node node_modules/eslint-config/tools/setup/install.mjs
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Hook wrappers reference
|
|
93
|
+
|
|
94
|
+
Root `.hooks` scripts should delegate to installed package path:
|
|
95
|
+
|
|
96
|
+
- `.hooks/pre-commit`
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
#!/usr/bin/env bash
|
|
100
|
+
set -euo pipefail
|
|
101
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
102
|
+
node node_modules/eslint-config/tools/git-hooks/pre-commit.mjs
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
- `.hooks/prepare-commit-msg`
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
#!/usr/bin/env bash
|
|
109
|
+
set -euo pipefail
|
|
110
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
111
|
+
node node_modules/eslint-config/tools/git-hooks/prepare-commit-msg.mjs "$@"
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
- `.hooks/commit-msg`
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
#!/usr/bin/env bash
|
|
118
|
+
set -euo pipefail
|
|
119
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
120
|
+
node node_modules/eslint-config/tools/git-hooks/commit-msg.mjs "$1"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
- `.hooks/post-commit`
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
#!/usr/bin/env bash
|
|
127
|
+
set -euo pipefail
|
|
128
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
129
|
+
node node_modules/eslint-config/tools/git-hooks/post-commit.mjs
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Required root scripts
|
|
133
|
+
|
|
134
|
+
```json
|
|
135
|
+
{
|
|
136
|
+
"scripts": {
|
|
137
|
+
"lint": "pnpm lint:fix",
|
|
138
|
+
"lint:check": "turbo run lint",
|
|
139
|
+
"lint:fix": "((pnpm format || true) && turbo run lint --continue=always -- --fix) || true"
|
|
140
|
+
},
|
|
141
|
+
"lint-staged": {
|
|
142
|
+
"*.{js,ts,tsx,vue}": ["eslint --fix"]
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import antfu from '@antfu/eslint-config'
|
|
2
|
+
import perfectionistNatural from 'eslint-plugin-perfectionist'
|
|
3
|
+
import tailwind from 'eslint-plugin-tailwindcss'
|
|
4
|
+
|
|
5
|
+
export function createArchipelagoConfig(...overrides) {
|
|
6
|
+
return antfu(
|
|
7
|
+
{
|
|
8
|
+
formatters: true,
|
|
9
|
+
stylistic: true,
|
|
10
|
+
vue: true,
|
|
11
|
+
typescript: true,
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: 'archipelago/ignores',
|
|
15
|
+
ignores: [
|
|
16
|
+
'**/node_modules/**',
|
|
17
|
+
'**/.nuxt/**',
|
|
18
|
+
'**/.output/**',
|
|
19
|
+
'**/.turbo/**',
|
|
20
|
+
'**/dist/**',
|
|
21
|
+
'**/coverage/**',
|
|
22
|
+
'**/.next/**',
|
|
23
|
+
'**/public/**',
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
...tailwind.configs['flat/recommended'],
|
|
27
|
+
{
|
|
28
|
+
name: 'archipelago/perfectionist',
|
|
29
|
+
plugins: {
|
|
30
|
+
perfectionistNatural,
|
|
31
|
+
},
|
|
32
|
+
rules: {
|
|
33
|
+
'import/order': 'off',
|
|
34
|
+
'sort-imports': 'off',
|
|
35
|
+
'perfectionist/sort-imports': ['warn', {
|
|
36
|
+
type: 'alphabetical',
|
|
37
|
+
}],
|
|
38
|
+
'perfectionist/sort-exports': ['warn', {
|
|
39
|
+
type: 'alphabetical',
|
|
40
|
+
}],
|
|
41
|
+
'perfectionist/sort-named-imports': ['warn', {
|
|
42
|
+
type: 'alphabetical',
|
|
43
|
+
}],
|
|
44
|
+
'perfectionist/sort-named-exports': ['warn', {
|
|
45
|
+
type: 'alphabetical',
|
|
46
|
+
}],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
name: 'archipelago/rules',
|
|
51
|
+
rules: {
|
|
52
|
+
'comma-dangle': ['warn', 'always-multiline'],
|
|
53
|
+
'no-console': 'warn',
|
|
54
|
+
'max-statements-per-line': 'warn',
|
|
55
|
+
'tailwindcss/no-custom-classname': 'off',
|
|
56
|
+
'vue/multi-word-component-names': 'off',
|
|
57
|
+
'vue/no-required-prop-with-default': 'off',
|
|
58
|
+
'vue/html-self-closing': 'off',
|
|
59
|
+
'vue/html-closing-bracket-spacing': 'off',
|
|
60
|
+
'vue/no-multiple-template-root': 'off',
|
|
61
|
+
'vue/max-attributes-per-line': ['error', {
|
|
62
|
+
singleline: 3,
|
|
63
|
+
multiline: 1,
|
|
64
|
+
}],
|
|
65
|
+
'vue/max-len': ['warn', {
|
|
66
|
+
code: 120,
|
|
67
|
+
ignoreComments: true,
|
|
68
|
+
ignoreUrls: true,
|
|
69
|
+
ignoreStrings: true,
|
|
70
|
+
ignoreTemplateLiterals: true,
|
|
71
|
+
ignoreRegExpLiterals: true,
|
|
72
|
+
ignoreHTMLAttributeValues: true,
|
|
73
|
+
ignoreHTMLTextContents: true,
|
|
74
|
+
}],
|
|
75
|
+
'object-curly-newline': ['warn', {
|
|
76
|
+
ImportDeclaration: { multiline: true, minProperties: 3 },
|
|
77
|
+
}],
|
|
78
|
+
'format/prettier': 'warn',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
...overrides,
|
|
82
|
+
)
|
|
83
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@archpublicwebsite/eslint-config",
|
|
3
|
+
"version": "1.0.5",
|
|
4
|
+
"author": "Archipelago International",
|
|
5
|
+
"description": "Reusable ESLint flat config and git-hook toolkit for Archipelago projects",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./eslint.config.mjs",
|
|
8
|
+
"module": "./eslint.config.mjs",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./eslint.config.mjs",
|
|
12
|
+
"default": "./eslint.config.mjs"
|
|
13
|
+
},
|
|
14
|
+
"./tools/*": "./tools/*"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/ArchipelagoInternational/archi-ui.git",
|
|
19
|
+
"directory": "packages/eslint-config"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"eslint.config.mjs",
|
|
23
|
+
"tools",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"eslint",
|
|
28
|
+
"eslint-config",
|
|
29
|
+
"flat-config",
|
|
30
|
+
"vue",
|
|
31
|
+
"git-hooks"
|
|
32
|
+
],
|
|
33
|
+
"private": false,
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"postinstall": "node ./tools/setup/install.mjs",
|
|
39
|
+
"setup": "node ./tools/setup/install.mjs",
|
|
40
|
+
"prepublishOnly": "npm pack --dry-run",
|
|
41
|
+
"version:patch": "node ../../scripts/bump-version.mjs patch",
|
|
42
|
+
"version:minor": "node ../../scripts/bump-version.mjs minor",
|
|
43
|
+
"version:major": "node ../../scripts/bump-version.mjs major"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@antfu/eslint-config": "^4.12.0",
|
|
48
|
+
"@unocss/eslint-config": "^66.5.3",
|
|
49
|
+
"eslint": "^9.0.0",
|
|
50
|
+
"eslint-plugin-format": "^1.0.1",
|
|
51
|
+
"eslint-plugin-perfectionist": "^4.10.1",
|
|
52
|
+
"eslint-plugin-tailwindcss": "^3.17.3",
|
|
53
|
+
"prettier-plugin-tailwindcss": "^0.6.14"
|
|
54
|
+
},
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { getStagedDiffSnippet, getStagedNameStatus } from './shared.mjs'
|
|
2
|
+
|
|
3
|
+
function inferType(paths, addedCount, source) {
|
|
4
|
+
const isDocsOnly = paths.every(path => path.startsWith('docs/') || path.endsWith('.md'))
|
|
5
|
+
if (isDocsOnly)
|
|
6
|
+
return 'docs'
|
|
7
|
+
|
|
8
|
+
const isTestOnly = paths.every(path => /(__tests__|\.test\.|\.spec\.)/.test(path))
|
|
9
|
+
if (isTestOnly)
|
|
10
|
+
return 'test'
|
|
11
|
+
|
|
12
|
+
const isInfraOnly = paths.every(path =>
|
|
13
|
+
path.startsWith('.hooks/')
|
|
14
|
+
|| path.startsWith('scripts/')
|
|
15
|
+
|| path.endsWith('package.json')
|
|
16
|
+
|| path.endsWith('pnpm-lock.yaml')
|
|
17
|
+
|| path.endsWith('pnpm-workspace.yaml'),
|
|
18
|
+
)
|
|
19
|
+
if (isInfraOnly)
|
|
20
|
+
return 'chore'
|
|
21
|
+
|
|
22
|
+
if (/(fix|bug|broken|regression|error)/.test(source))
|
|
23
|
+
return 'fix'
|
|
24
|
+
|
|
25
|
+
if (/(button|modal|offer|gallery|room|feature|ui)/.test(source))
|
|
26
|
+
return 'feat'
|
|
27
|
+
|
|
28
|
+
return addedCount > 0 ? 'chore' : 'chore'
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function inferVerb(addedCount, modifiedCount) {
|
|
32
|
+
if (addedCount > 0 && modifiedCount === 0)
|
|
33
|
+
return 'add'
|
|
34
|
+
|
|
35
|
+
if (addedCount === 0 && modifiedCount > 0)
|
|
36
|
+
return 'update'
|
|
37
|
+
|
|
38
|
+
return 'update'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function inferAreas(paths) {
|
|
42
|
+
const mapArea = (path) => {
|
|
43
|
+
if (path.startsWith('packages/ui/'))
|
|
44
|
+
return 'ui'
|
|
45
|
+
if (path.startsWith('apps/nuxt/'))
|
|
46
|
+
return 'nuxt'
|
|
47
|
+
if (path.startsWith('docs/'))
|
|
48
|
+
return 'docs'
|
|
49
|
+
if (path.startsWith('scripts/'))
|
|
50
|
+
return 'scripts'
|
|
51
|
+
|
|
52
|
+
const [first] = path.split('/')
|
|
53
|
+
return first || 'repo'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return [...new Set(paths.map(mapArea))]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function inferSubject(paths, source) {
|
|
60
|
+
const pathsSource = paths.join(' ').toLowerCase()
|
|
61
|
+
const fullSource = `${pathsSource} ${source}`
|
|
62
|
+
|
|
63
|
+
const topicRules = [
|
|
64
|
+
{ test: /(offers?).*(modal)|(modal).*(offers?)/, label: 'offers modal' },
|
|
65
|
+
{ test: /(prepare-commit-msg|commit-msg|verifycommit|generatecommitmessage)/, label: 'commit message flow' },
|
|
66
|
+
{ test: /(\.hooks\/|pre-commit|post-commit|lint-staged)/, label: 'git hooks' },
|
|
67
|
+
{ test: /buttons?/, label: 'buttons' },
|
|
68
|
+
{ test: /modal/, label: 'modal behavior' },
|
|
69
|
+
{ test: /lint/, label: 'lint workflow' },
|
|
70
|
+
{ test: /tailwind|scss|css/, label: 'styles' },
|
|
71
|
+
{ test: /gallery/, label: 'gallery section' },
|
|
72
|
+
{ test: /offers?/, label: 'offers section' },
|
|
73
|
+
{ test: /rooms?/, label: 'rooms section' },
|
|
74
|
+
{ test: /contact[\s-]?us/, label: 'contact us section' },
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
const hit = topicRules.find(rule => rule.test.test(fullSource))
|
|
78
|
+
return hit ? hit.label : ''
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function makeDescription(verb, fileCount, areas, subject) {
|
|
82
|
+
if (subject)
|
|
83
|
+
return `${verb} ${subject}`
|
|
84
|
+
|
|
85
|
+
const filesPart = `${fileCount} file${fileCount > 1 ? 's' : ''}`
|
|
86
|
+
|
|
87
|
+
if (areas.length === 0)
|
|
88
|
+
return `${verb} ${filesPart}`
|
|
89
|
+
|
|
90
|
+
if (areas.length === 1)
|
|
91
|
+
return `${verb} ${filesPart} in ${areas[0]}`
|
|
92
|
+
|
|
93
|
+
if (areas.length === 2)
|
|
94
|
+
return `${verb} ${filesPart} in ${areas[0]} and ${areas[1]}`
|
|
95
|
+
|
|
96
|
+
return `${verb} ${filesPart} across ${areas[0]}, ${areas[1]}, and more`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function normalizeDescription(description) {
|
|
100
|
+
const maxLength = 50
|
|
101
|
+
if (description.length <= maxLength)
|
|
102
|
+
return description
|
|
103
|
+
|
|
104
|
+
return description.slice(0, maxLength).trim().replace(/[.,;:-]$/, '')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function generateCommitMessage() {
|
|
108
|
+
const changed = getStagedNameStatus()
|
|
109
|
+
if (changed.length === 0)
|
|
110
|
+
return 'chore: update repository files'
|
|
111
|
+
|
|
112
|
+
const source = getStagedDiffSnippet()
|
|
113
|
+
const paths = changed.map(item => item.path)
|
|
114
|
+
const addedCount = changed.filter(item => item.status === 'A').length
|
|
115
|
+
const modifiedCount = changed.filter(item => item.status === 'M' || item.status === 'R').length
|
|
116
|
+
|
|
117
|
+
const type = inferType(paths, addedCount, source)
|
|
118
|
+
const verb = inferVerb(addedCount, modifiedCount)
|
|
119
|
+
const areas = inferAreas(paths)
|
|
120
|
+
const subject = inferSubject(paths, source)
|
|
121
|
+
const description = normalizeDescription(makeDescription(verb, changed.length, areas, subject))
|
|
122
|
+
|
|
123
|
+
return `${type}: ${description}`
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (import.meta.url === `file://${process.argv[1]}`)
|
|
127
|
+
console.log(generateCommitMessage())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { getCommittedCodeFiles, runSafe } from './shared.mjs'
|
|
3
|
+
|
|
4
|
+
const committedFiles = getCommittedCodeFiles().filter(file => existsSync(file))
|
|
5
|
+
|
|
6
|
+
if (committedFiles.length === 0)
|
|
7
|
+
process.exit(0)
|
|
8
|
+
|
|
9
|
+
console.log(`\nRunning post-commit lint auto-fix on ${committedFiles.length} file(s)...`)
|
|
10
|
+
const quotedFiles = committedFiles.map(file => `'${file.replace(/'/g, "'\\''")}'`).join(' ')
|
|
11
|
+
runSafe(`pnpm exec eslint --fix ${quotedFiles}`)
|
|
12
|
+
|
|
13
|
+
const changedAfterFix = runSafe(`git diff -- ${quotedFiles}`)
|
|
14
|
+
if (changedAfterFix)
|
|
15
|
+
console.log('\nLINT FIXES APPLIED. Review and commit follow-up changes if needed.\n')
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { hasCommand, run } from './shared.mjs'
|
|
2
|
+
|
|
3
|
+
if (!hasCommand('pnpm')) {
|
|
4
|
+
console.error('\nCOMMIT FAILED: pnpm is required but not found.\n')
|
|
5
|
+
process.exit(1)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
console.log('\nRunning lint-staged (auto-fix staged files)...')
|
|
9
|
+
run('pnpm lint-staged', { stdio: 'inherit' })
|
|
10
|
+
console.log('\nCOMMIT CHECKS PASSED\n')
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { generateCommitMessage } from './generate-commit-message.mjs'
|
|
3
|
+
|
|
4
|
+
const messageFile = process.argv[2]
|
|
5
|
+
const source = process.argv[3] || ''
|
|
6
|
+
|
|
7
|
+
if (!messageFile)
|
|
8
|
+
process.exit(0)
|
|
9
|
+
|
|
10
|
+
if (['merge', 'squash', 'commit', 'message'].includes(source))
|
|
11
|
+
process.exit(0)
|
|
12
|
+
|
|
13
|
+
const currentMessage = readFileSync(messageFile, 'utf-8')
|
|
14
|
+
const hasMessage = currentMessage
|
|
15
|
+
.split('\n')
|
|
16
|
+
.some(line => line.trim() && !line.trim().startsWith('#'))
|
|
17
|
+
|
|
18
|
+
if (hasMessage)
|
|
19
|
+
process.exit(0)
|
|
20
|
+
|
|
21
|
+
const autoMessage = generateCommitMessage()
|
|
22
|
+
if (autoMessage)
|
|
23
|
+
writeFileSync(messageFile, `${autoMessage}\n`, 'utf-8')
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
export function run(command, options = {}) {
|
|
4
|
+
const output = execSync(command, {
|
|
5
|
+
encoding: 'utf8',
|
|
6
|
+
stdio: 'pipe',
|
|
7
|
+
...options,
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
if (output == null) {
|
|
11
|
+
return ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (typeof output === 'string') {
|
|
15
|
+
return output.trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return String(output).trim()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function runSafe(command) {
|
|
22
|
+
try {
|
|
23
|
+
return run(command)
|
|
24
|
+
} catch {
|
|
25
|
+
return ''
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getRepoRoot() {
|
|
30
|
+
return run('git rev-parse --show-toplevel')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function hasCommand(name) {
|
|
34
|
+
try {
|
|
35
|
+
run(`command -v ${name}`)
|
|
36
|
+
return true
|
|
37
|
+
} catch {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function getStagedNameStatus() {
|
|
43
|
+
const output = runSafe('git diff --cached --name-status --diff-filter=ACMR')
|
|
44
|
+
if (!output)
|
|
45
|
+
return []
|
|
46
|
+
|
|
47
|
+
return output
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((line) => {
|
|
50
|
+
const [status = '', ...rest] = line.split(/\s+/)
|
|
51
|
+
return {
|
|
52
|
+
status,
|
|
53
|
+
path: rest.join(' '),
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
.filter(item => item.path)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getStagedDiffSnippet() {
|
|
60
|
+
return runSafe('git diff --cached -- .').slice(0, 15000).toLowerCase()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getCommittedCodeFiles() {
|
|
64
|
+
const output = runSafe('git diff-tree --no-commit-id --name-only -r HEAD')
|
|
65
|
+
if (!output)
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
return output
|
|
69
|
+
.split('\n')
|
|
70
|
+
.filter(file => /\.(js|jsx|ts|tsx|vue)$/.test(file))
|
|
71
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
|
|
3
|
+
export function isValidCommitMessage(message) {
|
|
4
|
+
const releaseRE = /^v\d/
|
|
5
|
+
const commitRE = /^(revert: )?(feat|fix|docs|refactor|perf|test|build|ci|chore|style|types|workflow|release|deps)(\([a-z0-9-]+\))?: \S.{0,49}$/
|
|
6
|
+
return releaseRE.test(message) || commitRE.test(message)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function verifyCommitMessageFile(messageFilePath) {
|
|
10
|
+
const message = readFileSync(messageFilePath, 'utf-8').trim()
|
|
11
|
+
if (isValidCommitMessage(message))
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
const examples = [
|
|
15
|
+
"feat: add 'comments' option",
|
|
16
|
+
'fix: handle events on blur (close #28)',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
console.error('\nERROR: invalid commit message format.\n')
|
|
20
|
+
console.error('Required format: <type>(optional-scope): <description>')
|
|
21
|
+
console.error('Description max length: 50 characters\n')
|
|
22
|
+
console.error('Examples:')
|
|
23
|
+
examples.forEach(example => console.error(` - ${example}`))
|
|
24
|
+
console.error('\nSee .github/commit-convention.md for details.\n')
|
|
25
|
+
process.exit(1)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
29
|
+
const messageFilePath = process.argv[2]
|
|
30
|
+
if (!messageFilePath)
|
|
31
|
+
process.exit(0)
|
|
32
|
+
verifyCommitMessageFile(messageFilePath)
|
|
33
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
function getProjectRoot() {
|
|
6
|
+
const root = process.env.INIT_CWD || process.cwd()
|
|
7
|
+
if (!existsSync(join(root, 'package.json')))
|
|
8
|
+
return null
|
|
9
|
+
return root
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ensureDir(path) {
|
|
13
|
+
if (!existsSync(path))
|
|
14
|
+
mkdirSync(path, { recursive: true })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function writeIfMissing(path, content) {
|
|
18
|
+
if (existsSync(path))
|
|
19
|
+
return false
|
|
20
|
+
writeFileSync(path, content, 'utf8')
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function ensurePrettierConfig(projectRoot) {
|
|
25
|
+
const prettierPath = join(projectRoot, '.prettierrc')
|
|
26
|
+
const defaults = {
|
|
27
|
+
plugins: ['prettier-plugin-tailwindcss'],
|
|
28
|
+
singleQuote: true,
|
|
29
|
+
printWidth: 120,
|
|
30
|
+
semi: false,
|
|
31
|
+
tabWidth: 2,
|
|
32
|
+
trailingComma: 'es5',
|
|
33
|
+
arrowParens: 'avoid',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!existsSync(prettierPath)) {
|
|
37
|
+
writeFileSync(prettierPath, `${JSON.stringify(defaults, null, 2)}\n`, 'utf8')
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const current = JSON.parse(readFileSync(prettierPath, 'utf8'))
|
|
43
|
+
const plugins = Array.isArray(current.plugins) ? current.plugins : []
|
|
44
|
+
const deduped = [...new Set([...plugins, 'prettier-plugin-tailwindcss'])]
|
|
45
|
+
current.plugins = deduped
|
|
46
|
+
if (typeof current.singleQuote !== 'boolean')
|
|
47
|
+
current.singleQuote = true
|
|
48
|
+
if (typeof current.semi !== 'boolean')
|
|
49
|
+
current.semi = false
|
|
50
|
+
writeFileSync(prettierPath, `${JSON.stringify(current, null, 2)}\n`, 'utf8')
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Keep existing file untouched if it is not JSON.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ensureEslintConfig(projectRoot) {
|
|
58
|
+
const eslintConfigPath = join(projectRoot, 'eslint.config.mjs')
|
|
59
|
+
const content = `import { createArchipelagoConfig } from 'eslint-config'\n\nexport default createArchipelagoConfig({\n name: 'project/overrides',\n rules: {\n // Add your project overrides here\n },\n})\n`
|
|
60
|
+
writeIfMissing(eslintConfigPath, content)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureHooks(projectRoot) {
|
|
64
|
+
const hooksDir = join(projectRoot, '.hooks')
|
|
65
|
+
ensureDir(hooksDir)
|
|
66
|
+
|
|
67
|
+
const hooks = {
|
|
68
|
+
'pre-commit': `#!/usr/bin/env bash
|
|
69
|
+
set -euo pipefail
|
|
70
|
+
|
|
71
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
72
|
+
node node_modules/eslint-config/tools/git-hooks/pre-commit.mjs
|
|
73
|
+
`,
|
|
74
|
+
'prepare-commit-msg': `#!/usr/bin/env bash
|
|
75
|
+
set -euo pipefail
|
|
76
|
+
|
|
77
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
78
|
+
node node_modules/eslint-config/tools/git-hooks/prepare-commit-msg.mjs "$@"
|
|
79
|
+
`,
|
|
80
|
+
'commit-msg': `#!/usr/bin/env bash
|
|
81
|
+
set -euo pipefail
|
|
82
|
+
|
|
83
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
84
|
+
node node_modules/eslint-config/tools/git-hooks/commit-msg.mjs "$1"
|
|
85
|
+
`,
|
|
86
|
+
'post-commit': `#!/usr/bin/env bash
|
|
87
|
+
set -euo pipefail
|
|
88
|
+
|
|
89
|
+
cd "$(git rev-parse --show-toplevel)"
|
|
90
|
+
node node_modules/eslint-config/tools/git-hooks/post-commit.mjs
|
|
91
|
+
`,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Object.entries(hooks).forEach(([name, content]) => {
|
|
95
|
+
const path = join(hooksDir, name)
|
|
96
|
+
if (writeIfMissing(path, content))
|
|
97
|
+
chmodSync(path, 0o755)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function ensureHooksPath(projectRoot) {
|
|
102
|
+
if (!existsSync(join(projectRoot, '.git')))
|
|
103
|
+
return
|
|
104
|
+
try {
|
|
105
|
+
execSync('git config core.hooksPath .hooks', { cwd: projectRoot, stdio: 'ignore' })
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Ignore setup failures in non-git contexts.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function main() {
|
|
113
|
+
const projectRoot = getProjectRoot()
|
|
114
|
+
if (!projectRoot)
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
ensureHooks(projectRoot)
|
|
118
|
+
ensureHooksPath(projectRoot)
|
|
119
|
+
ensureEslintConfig(projectRoot)
|
|
120
|
+
ensurePrettierConfig(projectRoot)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
main()
|