@archpublicwebsite/eslint-config 1.0.22 → 1.0.23
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 +38 -3
- package/package.json +1 -1
- package/tools/git-hooks/pre-commit.mjs +26 -1
- package/tools/git-hooks/pre-push.mjs +74 -0
- package/tools/security/scanner.mjs +22 -1
- package/tools/setup/install.mjs +63 -0
package/README.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
Reusable ESLint flat config and git-hook toolkit for Archipelago projects.
|
|
4
4
|
|
|
5
|
+
## Update 1 Bulan Terakhir (Mei-Jun 2026)
|
|
6
|
+
|
|
7
|
+
Ringkasan perubahan terbaru yang paling penting:
|
|
8
|
+
|
|
9
|
+
- Installer sekarang bisa membuat file setup utama otomatis saat belum ada.
|
|
10
|
+
- Hook `pre-commit` lebih tahan error saat config belum lengkap.
|
|
11
|
+
- Security scanner lebih akurat dan mengurangi false positive.
|
|
12
|
+
- Ada opsi validasi akun/signature ketat lewat `REQUIRE_VERIFIED_ACCOUNT=1`.
|
|
13
|
+
|
|
14
|
+
Jika `REQUIRE_VERIFIED_ACCOUNT=1` aktif:
|
|
15
|
+
|
|
16
|
+
- `pre-commit` akan cek `user.name`, `user.email`, dan `commit.gpgsign=true`.
|
|
17
|
+
- `pre-push` akan menolak commit yang status signature-nya bukan `G`.
|
|
18
|
+
|
|
19
|
+
Cara pakai cepat:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm install
|
|
23
|
+
node node_modules/@archpublicwebsite/eslint-config/tools/setup/install.mjs
|
|
24
|
+
pnpm lint:check
|
|
25
|
+
pnpm typecheck
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Aktifkan strict verification hanya saat dibutuhkan:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
REQUIRE_VERIFIED_ACCOUNT=1 git commit
|
|
32
|
+
REQUIRE_VERIFIED_ACCOUNT=1 git push
|
|
33
|
+
```
|
|
34
|
+
|
|
5
35
|
## What this package includes
|
|
6
36
|
|
|
7
37
|
This package ships a ready-to-use flat config plus setup scripts for local project automation:
|
|
@@ -82,9 +112,14 @@ The setup merges ESLint-related settings into `.vscode/settings.json`:
|
|
|
82
112
|
{
|
|
83
113
|
"eslint.useFlatConfig": true,
|
|
84
114
|
"eslint.validate": [
|
|
85
|
-
"javascript",
|
|
86
|
-
"
|
|
87
|
-
"
|
|
115
|
+
"javascript",
|
|
116
|
+
"javascriptreact",
|
|
117
|
+
"typescript",
|
|
118
|
+
"typescriptreact",
|
|
119
|
+
"vue",
|
|
120
|
+
"json",
|
|
121
|
+
"jsonc",
|
|
122
|
+
"markdown"
|
|
88
123
|
],
|
|
89
124
|
"editor.codeActionsOnSave": {
|
|
90
125
|
"source.fixAll.eslint": "explicit",
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { dirname, join } from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
|
-
import { hasCommand, run } from './shared.mjs'
|
|
4
|
+
import { hasCommand, run, runSafe } from './shared.mjs'
|
|
5
5
|
|
|
6
6
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
7
7
|
const SCAN_SCRIPT = join(__dirname, '..', 'security', 'scan.mjs')
|
|
@@ -20,6 +20,31 @@ if (!hasCommand('pnpm')) {
|
|
|
20
20
|
process.exit(1)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
function enforceVerifiedAccountIfEnabled() {
|
|
24
|
+
if (process.env.REQUIRE_VERIFIED_ACCOUNT !== '1') {
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const userEmail = runSafe('git config --get user.email')
|
|
29
|
+
const userName = runSafe('git config --get user.name')
|
|
30
|
+
const commitSign = runSafe('git config --get commit.gpgsign').toLowerCase()
|
|
31
|
+
|
|
32
|
+
if (!userEmail || !userName) {
|
|
33
|
+
console.error('\nCOMMIT FAILED: REQUIRE_VERIFIED_ACCOUNT=1 but git user identity is incomplete.\n')
|
|
34
|
+
console.error('Set both user.name and user.email first.')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (commitSign !== 'true') {
|
|
39
|
+
console.error('\nCOMMIT FAILED: REQUIRE_VERIFIED_ACCOUNT=1 but commit signing is disabled.\n')
|
|
40
|
+
console.error('Run: git config commit.gpgsign true')
|
|
41
|
+
process.exit(1)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── 0. Optional strict verified-account gate ─────────────────────────────────
|
|
46
|
+
enforceVerifiedAccountIfEnabled()
|
|
47
|
+
|
|
23
48
|
// ── 1. Security scan — blocks on critical/high findings ───────────────────────
|
|
24
49
|
run(`node "${SCAN_SCRIPT}"`, { stdio: 'inherit' })
|
|
25
50
|
|
|
@@ -90,6 +90,76 @@ function collectPushFiles(refs) {
|
|
|
90
90
|
return [...seen]
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Collect commits in push range(s).
|
|
95
|
+
* @param {{ localSha: string, remoteSha: string }[]} refs
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
function collectPushCommits(refs) {
|
|
99
|
+
const seen = new Set()
|
|
100
|
+
for (const { localSha, remoteSha } of refs) {
|
|
101
|
+
const range = remoteSha === ZERO_SHA ? localSha : `${remoteSha}..${localSha}`
|
|
102
|
+
const output = runSafe(`git rev-list ${range}`)
|
|
103
|
+
if (!output) continue
|
|
104
|
+
for (const sha of output.split('\n').filter(Boolean)) {
|
|
105
|
+
seen.add(sha)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...seen]
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Enforce signed + verified commits in push range when REQUIRE_VERIFIED_ACCOUNT=1.
|
|
113
|
+
* Git signature status reference (%G?):
|
|
114
|
+
* G = good signature, N = no signature, B = bad signature, etc.
|
|
115
|
+
* @param {string[]} commits
|
|
116
|
+
*/
|
|
117
|
+
function enforceVerifiedAccountIfEnabled(commits) {
|
|
118
|
+
if (process.env.REQUIRE_VERIFIED_ACCOUNT !== '1') {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const userEmail = runSafe('git config --get user.email')
|
|
123
|
+
const userName = runSafe('git config --get user.name')
|
|
124
|
+
const commitSign = runSafe('git config --get commit.gpgsign').toLowerCase()
|
|
125
|
+
|
|
126
|
+
if (!userEmail || !userName) {
|
|
127
|
+
process.stderr.write(
|
|
128
|
+
`\n${YLW}${BOLD}✖ PUSH BLOCKED${R} — REQUIRE_VERIFIED_ACCOUNT=1 but git user identity is incomplete.\n`
|
|
129
|
+
)
|
|
130
|
+
process.exit(1)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (commitSign !== 'true') {
|
|
134
|
+
process.stderr.write(
|
|
135
|
+
`\n${YLW}${BOLD}✖ PUSH BLOCKED${R} — REQUIRE_VERIFIED_ACCOUNT=1 but commit signing is disabled.\n`
|
|
136
|
+
)
|
|
137
|
+
process.stderr.write(`${DIM}Run: git config commit.gpgsign true${R}\n\n`)
|
|
138
|
+
process.exit(1)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const invalid = []
|
|
142
|
+
for (const sha of commits) {
|
|
143
|
+
const status = runSafe(`git log -1 --format=%G? ${sha}`)
|
|
144
|
+
if (status !== 'G') {
|
|
145
|
+
const subject = runSafe(`git log -1 --format=%s ${sha}`)
|
|
146
|
+
invalid.push({ sha, status: status || '?', subject })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (invalid.length > 0) {
|
|
151
|
+
process.stderr.write(`\n${YLW}${BOLD}✖ PUSH BLOCKED${R} — found unverified commit signature(s).\n`)
|
|
152
|
+
invalid.slice(0, 10).forEach(({ sha, status, subject }) => {
|
|
153
|
+
process.stderr.write(` - ${sha.slice(0, 10)} status=${status} ${subject}\n`)
|
|
154
|
+
})
|
|
155
|
+
if (invalid.length > 10) {
|
|
156
|
+
process.stderr.write(` ...and ${invalid.length - 10} more\n`)
|
|
157
|
+
}
|
|
158
|
+
process.stderr.write(`${DIM}Set REQUIRE_VERIFIED_ACCOUNT=0 to disable this gate.${R}\n\n`)
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
93
163
|
// ─── Scan committed files ──────────────────────────────────────────────────────
|
|
94
164
|
|
|
95
165
|
/**
|
|
@@ -222,6 +292,10 @@ function main() {
|
|
|
222
292
|
|
|
223
293
|
const repoRoot = getRepoRoot()
|
|
224
294
|
const refs = parsePushRefs()
|
|
295
|
+
const commits = refs.length > 0 ? collectPushCommits(refs) : [runSafe('git rev-parse HEAD')].filter(Boolean)
|
|
296
|
+
|
|
297
|
+
// ── 0. Optional strict verified-account gate ───────────────────────────────
|
|
298
|
+
enforceVerifiedAccountIfEnabled(commits)
|
|
225
299
|
|
|
226
300
|
printScanHeader('@archipelago/pre-push', 'full-branch security gate')
|
|
227
301
|
|
|
@@ -160,7 +160,28 @@ export function levenshtein(a, b) {
|
|
|
160
160
|
* @returns {string|null}
|
|
161
161
|
*/
|
|
162
162
|
export function detectTyposquat(pkgName) {
|
|
163
|
-
const
|
|
163
|
+
const trustedScopes = new Set(['@storybook', '@types', '@vitejs', '@vitest', '@nuxt', '@vue', '@typescript-eslint'])
|
|
164
|
+
|
|
165
|
+
if (pkgName.startsWith('@')) {
|
|
166
|
+
const [scope = '', scopedName = ''] = pkgName.split('/')
|
|
167
|
+
// Trusted namespaces publish many package variants (vue3, react-vite, etc.)
|
|
168
|
+
// and should not be treated as typosquat candidates.
|
|
169
|
+
if (trustedScopes.has(scope)) return null
|
|
170
|
+
|
|
171
|
+
const bareScoped = scopedName || pkgName
|
|
172
|
+
if (bareScoped.length < 3) return null
|
|
173
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
174
|
+
if (bareScoped === popular) return null
|
|
175
|
+
// Skip legit version-suffixed variants like vue3/react18.
|
|
176
|
+
if (bareScoped === `${popular}2` || bareScoped === `${popular}3` || bareScoped === `${popular}18`) {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
if (levenshtein(bareScoped.toLowerCase(), popular.toLowerCase()) === 1) return popular
|
|
180
|
+
}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const bare = pkgName
|
|
164
185
|
if (bare.length < 3) return null
|
|
165
186
|
for (const popular of POPULAR_PACKAGES) {
|
|
166
187
|
if (bare === popular) return null
|
package/tools/setup/install.mjs
CHANGED
|
@@ -137,6 +137,67 @@ export default config
|
|
|
137
137
|
}
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
// ─── tsconfig.base.json ──────────────────────────────────────────────────────
|
|
141
|
+
// Shared TypeScript compiler settings used by every package and app.
|
|
142
|
+
// Includes @vue/typescript-plugin so the IDE delegates .vue file handling to
|
|
143
|
+
// Volar instead of the plain TS server (eliminates "Cannot find name 'div'" etc.)
|
|
144
|
+
|
|
145
|
+
function ensureTsConfigBase(projectRoot) {
|
|
146
|
+
const tsconfigBasePath = join(projectRoot, 'tsconfig.base.json')
|
|
147
|
+
const content = `{
|
|
148
|
+
"compilerOptions": {
|
|
149
|
+
"target": "ES2020",
|
|
150
|
+
"module": "ESNext",
|
|
151
|
+
"moduleResolution": "Bundler",
|
|
152
|
+
"lib": ["DOM", "DOM.Iterable", "ES2020"],
|
|
153
|
+
"strict": true,
|
|
154
|
+
"jsx": "preserve",
|
|
155
|
+
"resolveJsonModule": true,
|
|
156
|
+
"skipLibCheck": true,
|
|
157
|
+
"types": ["vite/client"],
|
|
158
|
+
"plugins": [
|
|
159
|
+
{
|
|
160
|
+
"name": "@vue/typescript-plugin",
|
|
161
|
+
"languages": ["vue"]
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
`
|
|
167
|
+
if (writeIfMissing(tsconfigBasePath, content)) log('Created tsconfig.base.json')
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ─── tsconfig.json ───────────────────────────────────────────────────────────
|
|
171
|
+
// Root TypeScript project file that extends tsconfig.base.json.
|
|
172
|
+
// Covers src/ and packages/ source; excludes apps/ (each app owns its tsconfig).
|
|
173
|
+
// Add path aliases here as workspace packages grow.
|
|
174
|
+
|
|
175
|
+
function ensureTsConfig(projectRoot) {
|
|
176
|
+
const tsconfigPath = join(projectRoot, 'tsconfig.json')
|
|
177
|
+
const content = `{
|
|
178
|
+
"extends": "./tsconfig.base.json",
|
|
179
|
+
"compilerOptions": {
|
|
180
|
+
"paths": {}
|
|
181
|
+
},
|
|
182
|
+
"include": [
|
|
183
|
+
"src/**/*.ts",
|
|
184
|
+
"src/**/*.vue",
|
|
185
|
+
"packages/**/*.ts",
|
|
186
|
+
"packages/**/*.vue",
|
|
187
|
+
"vite.config.ts"
|
|
188
|
+
],
|
|
189
|
+
"exclude": [
|
|
190
|
+
"apps/**",
|
|
191
|
+
"node_modules",
|
|
192
|
+
"**/dist/**",
|
|
193
|
+
"**/.nuxt/**",
|
|
194
|
+
"**/.output/**"
|
|
195
|
+
]
|
|
196
|
+
}
|
|
197
|
+
`
|
|
198
|
+
if (writeIfMissing(tsconfigPath, content)) log('Created tsconfig.json')
|
|
199
|
+
}
|
|
200
|
+
|
|
140
201
|
function ensureCommitlintConfig(projectRoot) {
|
|
141
202
|
const commitlintConfigPath = join(projectRoot, 'commitlint.config.cjs')
|
|
142
203
|
const content = `module.exports = require('@archpublicwebsite/eslint-config/commitlint')
|
|
@@ -357,6 +418,8 @@ function main() {
|
|
|
357
418
|
|
|
358
419
|
ensureEditorConfig(projectRoot)
|
|
359
420
|
ensureEslintUserIgnore(projectRoot)
|
|
421
|
+
ensureTsConfigBase(projectRoot)
|
|
422
|
+
ensureTsConfig(projectRoot)
|
|
360
423
|
ensureEslintConfig(projectRoot)
|
|
361
424
|
ensurePrettierConfig(projectRoot)
|
|
362
425
|
ensurePrettierIgnore(projectRoot)
|