@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 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", "javascriptreact",
86
- "typescript", "typescriptreact",
87
- "vue", "json", "jsonc", "markdown"
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@archpublicwebsite/eslint-config",
3
- "version": "1.0.22",
3
+ "version": "1.0.23",
4
4
  "author": "Archipelago Hotels",
5
5
  "description": "Reusable ESLint flat config and git-hook toolkit for Archipelago projects",
6
6
  "type": "module",
@@ -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 bare = pkgName.startsWith('@') ? (pkgName.split('/')[1] ?? pkgName) : pkgName
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
@@ -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)