@archpublicwebsite/eslint-config 1.0.22 → 1.0.30

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
+ ## Last Month Updates (May-Jun 2026)
6
+
7
+ Here are the most important recent changes:
8
+
9
+ - The installer can now create key setup files automatically when they are missing.
10
+ - The `pre-commit` hook is now more resilient when project config is incomplete.
11
+ - The security scanner is more accurate and produces fewer false positives.
12
+ - A strict account/signature validation mode is available via `REQUIRE_VERIFIED_ACCOUNT=1`.
13
+
14
+ When `REQUIRE_VERIFIED_ACCOUNT=1` is enabled:
15
+
16
+ - `pre-commit` checks `user.name`, `user.email`, and `commit.gpgsign=true`.
17
+ - `pre-push` rejects commits whose signature status is not `G`.
18
+
19
+ Quick start:
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
+ Enable strict verification only when needed:
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",
@@ -96,6 +131,43 @@ The setup merges ESLint-related settings into `.vscode/settings.json`:
96
131
 
97
132
  Existing settings are preserved. Only the ESLint-related keys are added or updated.
98
133
 
134
+ ## Troubleshooting (Nuxt)
135
+
136
+ If you see errors like:
137
+
138
+ - `Cannot find module '~/composables'`
139
+ - `Cannot find name 'useRoute'`
140
+ - `Cannot find name 'useRuntimeConfig'`
141
+ - `'<script setup ...>' expected` in `.vue` files
142
+
143
+ Use this checklist:
144
+
145
+ 1. Ensure root `tsconfig.json` extends Nuxt types:
146
+
147
+ ```json
148
+ {
149
+ "extends": "./.nuxt/tsconfig.json"
150
+ }
151
+ ```
152
+
153
+ 2. Generate Nuxt type files:
154
+
155
+ ```bash
156
+ npx nuxi prepare
157
+ ```
158
+
159
+ 3. Confirm VS Code uses Volar for Vue files (disable Vetur).
160
+ 4. Re-run setup script:
161
+
162
+ ```bash
163
+ node node_modules/@archpublicwebsite/eslint-config/tools/setup/install.mjs
164
+ ```
165
+
166
+ Note:
167
+
168
+ - The setup now writes workspace settings that disable Vetur validators to prevent false Vue/TS errors.
169
+ - After running setup, restart VS Code (`Developer: Reload Window`) so the language server state is refreshed.
170
+
99
171
  ## Public API
100
172
 
101
173
  The package exports:
@@ -1,5 +1,7 @@
1
1
  const config = {
2
- '*.{js,mjs,cjs,ts,tsx,vue}': ['eslint --fix', 'prettier --write'],
2
+ // Keep JS/TS/Vue formatting and lint fixes in one engine (ESLint)
3
+ // so save behavior and pre-commit output stay consistent.
4
+ '*.{js,mjs,cjs,ts,tsx,vue}': ['eslint --fix'],
3
5
  '*.{json,md,yml,yaml,css,scss,html}': ['prettier --write'],
4
6
  }
5
7
 
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.30",
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')
@@ -15,11 +15,96 @@ function runIfExists(scriptPath, command) {
15
15
  run(command, { stdio: 'inherit' })
16
16
  }
17
17
 
18
+ function quoteShellArg(value) {
19
+ const escaped = String(value).replaceAll("'", '\'"\'"\'')
20
+ return `'${escaped}'`
21
+ }
22
+
23
+ function getStagedLintTargets() {
24
+ const output = runSafe('git diff --cached --name-only --diff-filter=ACMR')
25
+ if (!output) {
26
+ return []
27
+ }
28
+
29
+ return output
30
+ .split('\n')
31
+ .map(line => line.trim())
32
+ .filter(Boolean)
33
+ // Keep this check focused on app/source code files that should match
34
+ // editor save behavior. Tooling files are validated by lint-staged itself.
35
+ .filter(filePath => /\.(js|ts|tsx|vue)$/.test(filePath))
36
+ }
37
+
38
+ function ensureEslintFlatConfig() {
39
+ const hasFlatConfig =
40
+ existsSync('./eslint.config.mjs') || existsSync('./eslint.config.js') || existsSync('./eslint.config.cjs')
41
+
42
+ if (hasFlatConfig) {
43
+ return
44
+ }
45
+
46
+ const installerPath = join(__dirname, '..', 'setup', 'install.mjs')
47
+ console.log('\nMissing eslint.config.* detected. Running setup installer to auto-create required config...')
48
+ run(`node "${installerPath}"`, { stdio: 'inherit' })
49
+
50
+ const configCreated =
51
+ existsSync('./eslint.config.mjs') || existsSync('./eslint.config.js') || existsSync('./eslint.config.cjs')
52
+
53
+ if (!configCreated) {
54
+ console.error('\nCOMMIT FAILED: eslint.config.* is still missing after setup.')
55
+ console.error('Run: node node_modules/@archpublicwebsite/eslint-config/tools/setup/install.mjs')
56
+ process.exit(1)
57
+ }
58
+ }
59
+
60
+ function verifySaveEquivalentLintState() {
61
+ const lintTargets = getStagedLintTargets()
62
+ if (lintTargets.length === 0) {
63
+ return
64
+ }
65
+
66
+ const targetArgs = lintTargets.map(quoteShellArg).join(' ')
67
+ console.log('\nVerifying staged code with ESLint check...')
68
+
69
+ try {
70
+ run(`pnpm exec eslint --no-error-on-unmatched-pattern ${targetArgs}`, { stdio: 'inherit' })
71
+ } catch {
72
+ console.error('\nCOMMIT FAILED: staged code still has ESLint issues after auto-fix.')
73
+ console.error('Fix the remaining issues, stage again, then commit.')
74
+ process.exit(1)
75
+ }
76
+ }
77
+
18
78
  if (!hasCommand('pnpm')) {
19
79
  console.error('\nCOMMIT FAILED: pnpm is required but not found.\n')
20
80
  process.exit(1)
21
81
  }
22
82
 
83
+ function enforceVerifiedAccountIfEnabled() {
84
+ if (process.env.REQUIRE_VERIFIED_ACCOUNT !== '1') {
85
+ return
86
+ }
87
+
88
+ const userEmail = runSafe('git config --get user.email')
89
+ const userName = runSafe('git config --get user.name')
90
+ const commitSign = runSafe('git config --get commit.gpgsign').toLowerCase()
91
+
92
+ if (!userEmail || !userName) {
93
+ console.error('\nCOMMIT FAILED: REQUIRE_VERIFIED_ACCOUNT=1 but git user identity is incomplete.\n')
94
+ console.error('Set both user.name and user.email first.')
95
+ process.exit(1)
96
+ }
97
+
98
+ if (commitSign !== 'true') {
99
+ console.error('\nCOMMIT FAILED: REQUIRE_VERIFIED_ACCOUNT=1 but commit signing is disabled.\n')
100
+ console.error('Run: git config commit.gpgsign true')
101
+ process.exit(1)
102
+ }
103
+ }
104
+
105
+ // ── 0. Optional strict verified-account gate ─────────────────────────────────
106
+ enforceVerifiedAccountIfEnabled()
107
+
23
108
  // ── 1. Security scan — blocks on critical/high findings ───────────────────────
24
109
  run(`node "${SCAN_SCRIPT}"`, { stdio: 'inherit' })
25
110
 
@@ -33,6 +118,8 @@ if (process.env.SKIP_GLOBAL_SCAN === '1') {
33
118
  }
34
119
 
35
120
  // ── 3. Lint-staged — auto-fix & format staged files ───────────────────────────
121
+ ensureEslintFlatConfig()
122
+
36
123
  console.log('\nRunning lint-staged (auto-fix staged files)...')
37
124
  if (existsSync('./lint-staged.config.mjs')) {
38
125
  run('pnpm exec lint-staged --config lint-staged.config.mjs', { stdio: 'inherit' })
@@ -41,4 +128,7 @@ if (existsSync('./lint-staged.config.mjs')) {
41
128
  run('pnpm exec lint-staged', { stdio: 'inherit' })
42
129
  }
43
130
 
131
+ // ── 4. Save-equivalent verification (same lint result as editor save) ────────
132
+ verifySaveEquivalentLintState()
133
+
44
134
  console.log('\nCOMMIT CHECKS PASSED\n')
@@ -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
@@ -27,6 +27,11 @@ function writeIfMissing(filePath, content) {
27
27
  return true
28
28
  }
29
29
 
30
+ function isNuxtProject(projectRoot) {
31
+ const nuxtConfigNames = ['nuxt.config.ts', 'nuxt.config.js', 'nuxt.config.mjs', 'nuxt.config.cjs']
32
+ return nuxtConfigNames.some(fileName => existsSync(join(projectRoot, fileName)))
33
+ }
34
+
30
35
  // ─── .editorconfig ──────────────────────────────────────────────────────────
31
36
 
32
37
  function ensureEditorConfig(projectRoot) {
@@ -137,6 +142,79 @@ export default config
137
142
  }
138
143
  }
139
144
 
145
+ // ─── tsconfig.base.json ──────────────────────────────────────────────────────
146
+ // Shared TypeScript compiler settings used by every package and app.
147
+ // Includes @vue/typescript-plugin so the IDE delegates .vue file handling to
148
+ // Volar instead of the plain TS server (eliminates "Cannot find name 'div'" etc.)
149
+
150
+ function ensureTsConfigBase(projectRoot) {
151
+ const tsconfigBasePath = join(projectRoot, 'tsconfig.base.json')
152
+ const content = `{
153
+ "compilerOptions": {
154
+ "target": "ES2020",
155
+ "module": "ESNext",
156
+ "moduleResolution": "Bundler",
157
+ "lib": ["DOM", "DOM.Iterable", "ES2020"],
158
+ "strict": true,
159
+ "jsx": "preserve",
160
+ "resolveJsonModule": true,
161
+ "skipLibCheck": true,
162
+ "types": ["vite/client"],
163
+ "plugins": [
164
+ {
165
+ "name": "@vue/typescript-plugin",
166
+ "languages": ["vue"]
167
+ }
168
+ ]
169
+ }
170
+ }
171
+ `
172
+ if (writeIfMissing(tsconfigBasePath, content)) log('Created tsconfig.base.json')
173
+ }
174
+
175
+ // ─── tsconfig.json ───────────────────────────────────────────────────────────
176
+ // Root TypeScript project file that extends tsconfig.base.json.
177
+ // Covers src/ and packages/ source; excludes apps/ (each app owns its tsconfig).
178
+ // Add path aliases here as workspace packages grow.
179
+
180
+ function ensureTsConfig(projectRoot) {
181
+ const tsconfigPath = join(projectRoot, 'tsconfig.json')
182
+ const nonNuxtContent = `{
183
+ "extends": "./tsconfig.base.json",
184
+ "compilerOptions": {
185
+ "paths": {}
186
+ },
187
+ "include": [
188
+ "**/*.ts",
189
+ "**/*.tsx",
190
+ "**/*.vue",
191
+ "**/*.d.ts",
192
+ "vite.config.ts"
193
+ ],
194
+ "exclude": [
195
+ "node_modules",
196
+ "**/dist/**",
197
+ "**/.nuxt/**",
198
+ "**/.output/**"
199
+ ]
200
+ }
201
+ `
202
+
203
+ // Nuxt projects must extend .nuxt/tsconfig.json so aliases (`~/`, `#imports`)
204
+ // and auto-imported composables (`useRoute`, `useRuntimeConfig`, etc.) resolve.
205
+ const nuxtContent = `{
206
+ "extends": "./.nuxt/tsconfig.json"
207
+ }
208
+ `
209
+
210
+ const nuxtProject = isNuxtProject(projectRoot)
211
+ const content = nuxtProject ? nuxtContent : nonNuxtContent
212
+
213
+ if (writeIfMissing(tsconfigPath, content)) {
214
+ log(nuxtProject ? 'Created tsconfig.json (Nuxt mode)' : 'Created tsconfig.json')
215
+ }
216
+ }
217
+
140
218
  function ensureCommitlintConfig(projectRoot) {
141
219
  const commitlintConfigPath = join(projectRoot, 'commitlint.config.cjs')
142
220
  const content = `module.exports = require('@archpublicwebsite/eslint-config/commitlint')
@@ -357,6 +435,8 @@ function main() {
357
435
 
358
436
  ensureEditorConfig(projectRoot)
359
437
  ensureEslintUserIgnore(projectRoot)
438
+ ensureTsConfigBase(projectRoot)
439
+ ensureTsConfig(projectRoot)
360
440
  ensureEslintConfig(projectRoot)
361
441
  ensurePrettierConfig(projectRoot)
362
442
  ensurePrettierIgnore(projectRoot)
@@ -20,7 +20,13 @@ const VSCODE_ESLINT_SETTINGS = {
20
20
 
21
21
  // Volar + TS integration for Vue 3 SFC template diagnostics
22
22
  'vue.server.hybridMode': true,
23
- 'volar.takeOverMode.enabled': true,
23
+
24
+ // Defensive guard: if Vetur is installed globally, disable its validators
25
+ // to avoid conflicting diagnostics like "'<script setup ...>' expected".
26
+ 'vetur.validation.template': false,
27
+ 'vetur.validation.script': false,
28
+ 'vetur.validation.style': false,
29
+ 'vetur.experimental.templateInterpolationService': false,
24
30
 
25
31
  // Prevent duplicate/broken diagnostics from built-in TS/JS validators
26
32
  // (vue-tsc + Volar remain the source of truth for Vue type diagnostics).
@@ -36,7 +42,7 @@ const VSCODE_ESLINT_SETTINGS = {
36
42
 
37
43
  // Auto-fix on save via ESLint
38
44
  'editor.codeActionsOnSave': {
39
- 'source.fixAll.eslint': 'explicit',
45
+ 'source.fixAll.eslint': true,
40
46
  'source.organizeImports': 'never',
41
47
  },
42
48
 
@@ -57,7 +63,7 @@ const VSCODE_ESLINT_SETTINGS = {
57
63
  'editor.defaultFormatter': 'dbaeumer.vscode-eslint',
58
64
  },
59
65
  '[vue]': {
60
- 'editor.defaultFormatter': 'Vue.volar',
66
+ 'editor.defaultFormatter': 'dbaeumer.vscode-eslint',
61
67
  },
62
68
  '[json]': {
63
69
  'editor.defaultFormatter': 'dbaeumer.vscode-eslint',