[READ-ONLY] a fast, modern browser for the npm registry

chore: add `eslint-plugin-regexp` (#1123)

authored by

btea and committed by
GitHub
f13279ed 8bcb1714

+146 -20
+67 -2
.oxlintrc.json
··· 1 1 { 2 2 "$schema": "https://unpkg.com/oxlint/configuration_schema.json", 3 3 "plugins": ["unicorn", "typescript", "oxc", "vue", "vitest"], 4 - "jsPlugins": ["@e18e/eslint-plugin"], 4 + "jsPlugins": ["@e18e/eslint-plugin", "eslint-plugin-regexp"], 5 5 "categories": { 6 6 "correctness": "error", 7 7 "suspicious": "warn", ··· 17 17 "e18e/prefer-timer-args": "error", 18 18 "e18e/prefer-date-now": "error", 19 19 "e18e/prefer-regex-test": "error", 20 - "e18e/prefer-array-some": "error" 20 + "e18e/prefer-array-some": "error", 21 + // RegExp - Possible Errors (most critical) 22 + "regexp/no-contradiction-with-assertion": "error", 23 + "regexp/no-dupe-disjunctions": "error", 24 + "regexp/no-empty-alternative": "error", 25 + "regexp/no-empty-capturing-group": "error", 26 + "regexp/no-empty-character-class": "error", 27 + "regexp/no-empty-group": "error", 28 + "regexp/no-empty-lookarounds-assertion": "error", 29 + "regexp/no-escape-backspace": "error", 30 + "regexp/no-invalid-regexp": "error", 31 + "regexp/no-lazy-ends": "error", 32 + "regexp/no-misleading-capturing-group": "error", 33 + "regexp/no-misleading-unicode-character": "error", 34 + "regexp/no-missing-g-flag": "error", 35 + "regexp/no-optional-assertion": "error", 36 + "regexp/no-potentially-useless-backreference": "error", 37 + "regexp/no-super-linear-backtracking": "error", 38 + "regexp/no-useless-assertions": "error", 39 + "regexp/no-useless-backreference": "error", 40 + "regexp/no-useless-dollar-replacements": "error", 41 + "regexp/strict": "error", 42 + // RegExp - Best Practices 43 + "regexp/confusing-quantifier": "warn", 44 + "regexp/control-character-escape": "error", 45 + "regexp/negation": "error", 46 + "regexp/no-dupe-characters-character-class": "error", 47 + "regexp/no-empty-string-literal": "error", 48 + "regexp/no-extra-lookaround-assertions": "error", 49 + "regexp/no-invisible-character": "error", 50 + "regexp/no-legacy-features": "error", 51 + "regexp/no-non-standard-flag": "error", 52 + "regexp/no-obscure-range": "error", 53 + "regexp/no-octal": "error", 54 + "regexp/no-standalone-backslash": "error", 55 + "regexp/no-trivially-nested-assertion": "error", 56 + "regexp/no-trivially-nested-quantifier": "error", 57 + "regexp/no-unused-capturing-group": "warn", 58 + "regexp/no-useless-character-class": "error", 59 + "regexp/no-useless-flag": "error", 60 + "regexp/no-useless-lazy": "error", 61 + "regexp/no-useless-quantifier": "error", 62 + "regexp/no-useless-range": "error", 63 + "regexp/no-useless-set-operand": "error", 64 + "regexp/no-useless-string-literal": "error", 65 + "regexp/no-useless-two-nums-quantifier": "error", 66 + "regexp/no-zero-quantifier": "error", 67 + "regexp/optimal-lookaround-quantifier": "warn", 68 + "regexp/optimal-quantifier-concatenation": "error", 69 + "regexp/prefer-predefined-assertion": "error", 70 + "regexp/prefer-range": "error", 71 + "regexp/prefer-set-operation": "error", 72 + "regexp/simplify-set-operations": "error", 73 + "regexp/use-ignore-case": "error", 74 + // RegExp - Stylistic Issues (less critical, focused on consistency) 75 + "regexp/match-any": "warn", 76 + "regexp/no-useless-escape": "warn", 77 + "regexp/no-useless-non-capturing-group": "warn", 78 + "regexp/prefer-character-class": "warn", 79 + "regexp/prefer-d": "warn", 80 + "regexp/prefer-plus-quantifier": "warn", 81 + "regexp/prefer-question-quantifier": "warn", 82 + "regexp/prefer-star-quantifier": "warn", 83 + "regexp/prefer-unicode-codepoint-escapes": "warn", 84 + "regexp/prefer-w": "warn", 85 + "regexp/sort-flags": "warn" 21 86 }, 22 87 "overrides": [ 23 88 {
+1 -1
app/composables/useStructuredFilters.ts
··· 42 42 43 43 // Regex to match operators: name:value, desc:value, description:value, kw:value, keyword:value 44 44 // Value continues until whitespace or next operator 45 - const operatorRegex = /\b(name|desc|description|kw|keyword):([^\s]+)/gi 45 + const operatorRegex = /\b(name|desc|description|kw|keyword):(\S+)/gi 46 46 47 47 let remaining = input 48 48 let match
+1 -1
app/pages/search.vue
··· 303 303 // Must start with alphanumeric 304 304 if (!/^[a-z0-9]/i.test(name)) return false 305 305 // Can contain alphanumeric, hyphen, underscore 306 - return /^[a-z0-9_-]+$/i.test(name) 306 + return /^[\w-]+$/.test(name) 307 307 } 308 308 309 309 /** Validated user/org suggestion */
+3 -4
app/utils/formatters.ts
··· 31 31 32 32 const fmt = (n: number) => { 33 33 if (decimals <= 0) return Math.round(n).toString() 34 - return n 35 - .toFixed(decimals) 36 - .replace(/\.0+$/, '') 37 - .replace(/(\.\d*?)0+$/, '$1') 34 + const fixed = n.toFixed(decimals) 35 + // Remove trailing zeros after decimal point 36 + return fixed.includes('.') ? fixed.replace(/0+$/, '').replace(/\.$/, '') : fixed 38 37 } 39 38 40 39 const join = (suffix: string, n: number) => `${sign}${fmt(n)}${space ? ' ' : ''}${suffix}`
+1 -1
app/utils/npm/outdated-dependencies.ts
··· 20 20 */ 21 21 export function constraintIncludesPrerelease(constraint: string): boolean { 22 22 return ( 23 - /-(alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || 23 + /-(?:alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || 24 24 /-\d/.test(constraint) 25 25 ) 26 26 }
+1 -1
cli/src/schemas.ts
··· 3 3 4 4 // Validation pattern for npm usernames/org names 5 5 // These follow similar rules: lowercase alphanumeric with hyphens, can't start/end with hyphen 6 - const NPM_USERNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i 6 + const NPM_USERNAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i 7 7 8 8 // ============================================================================ 9 9 // Base Schemas
+1
knip.ts
··· 42 42 43 43 /** Oxlint plugins don't get picked up yet */ 44 44 '@e18e/eslint-plugin', 45 + 'eslint-plugin-regexp', 45 46 ], 46 47 ignoreUnresolved: ['#components', '#oauth/config'], 47 48 },
+1
package.json
··· 116 116 "@vitest/coverage-v8": "4.0.18", 117 117 "@vue/test-utils": "2.4.6", 118 118 "axe-core": "4.11.1", 119 + "eslint-plugin-regexp": "3.0.0", 119 120 "fast-check": "4.5.3", 120 121 "h3": "1.15.5", 121 122 "knip": "5.83.0",
+59
pnpm-lock.yaml
··· 240 240 axe-core: 241 241 specifier: 4.11.1 242 242 version: 4.11.1 243 + eslint-plugin-regexp: 244 + specifier: 3.0.0 245 + version: 3.0.0(eslint@9.39.2(jiti@2.6.1)) 243 246 fast-check: 244 247 specifier: 4.5.3 245 248 version: 4.5.3 ··· 5038 5041 resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} 5039 5042 engines: {node: '>= 6'} 5040 5043 5044 + comment-parser@1.4.5: 5045 + resolution: {integrity: sha512-aRDkn3uyIlCFfk5NUA+VdwMmMsh8JGhc4hapfV4yxymHGQ3BVskMQfoXGpCo5IoBuQ9tS5iiVKhCpTcB4pW4qw==} 5046 + engines: {node: '>= 12.0.0'} 5047 + 5041 5048 common-tags@1.8.2: 5042 5049 resolution: {integrity: sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA==} 5043 5050 engines: {node: '>=4.0.0'} ··· 5589 5596 5590 5597 eslint-plugin-depend@1.4.0: 5591 5598 resolution: {integrity: sha512-MQs+m4nHSfgAO9bJDsBzqw0ofK/AOA0vfeY/6ahofqcUMLeM6/D1sTYs21fOhc17kNU/gn58YCtj20XaAssh2A==} 5599 + 5600 + eslint-plugin-regexp@3.0.0: 5601 + resolution: {integrity: sha512-iW7hgAV8NOG6E2dz+VeKpq67YLQ9jaajOKYpoOSic2/q8y9BMdXBKkSR9gcMtbqEhNQzdW41E3wWzvhp8ExYwQ==} 5602 + engines: {node: ^20.19.0 || ^22.13.0 || >=24} 5603 + peerDependencies: 5604 + eslint: '>=9.38.0' 5592 5605 5593 5606 eslint-scope@5.1.1: 5594 5607 resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} ··· 6587 6600 resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} 6588 6601 hasBin: true 6589 6602 6603 + jsdoc-type-pratt-parser@7.1.1: 6604 + resolution: {integrity: sha512-/2uqY7x6bsrpi3i9LVU6J89352C0rpMk0as8trXxCtvd4kPk1ke/Eyif6wqfSLvoNJqcDG9Vk4UsXgygzCt2xA==} 6605 + engines: {node: '>=20.0.0'} 6606 + 6590 6607 jsesc@3.1.0: 6591 6608 resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} 6592 6609 engines: {node: '>=6'} ··· 8039 8056 resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} 8040 8057 engines: {node: '>=4'} 8041 8058 8059 + refa@0.12.1: 8060 + resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} 8061 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 8062 + 8042 8063 reflect.getprototypeof@1.0.10: 8043 8064 resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} 8044 8065 engines: {node: '>= 0.4'} ··· 8058 8079 8059 8080 regex@6.1.0: 8060 8081 resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} 8082 + 8083 + regexp-ast-analysis@0.7.1: 8084 + resolution: {integrity: sha512-sZuz1dYW/ZsfG17WSAG7eS85r5a0dDsvg+7BiiYR5o6lKCAtUrEwdmRmaGF6rwVj3LcmAeYkOWKEPlbPzN3Y3A==} 8085 + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} 8061 8086 8062 8087 regexp-tree@0.1.27: 8063 8088 resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} ··· 8270 8295 schema-utils@4.3.3: 8271 8296 resolution: {integrity: sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==} 8272 8297 engines: {node: '>= 10.13.0'} 8298 + 8299 + scslre@0.3.0: 8300 + resolution: {integrity: sha512-3A6sD0WYP7+QrjbfNA2FN3FsOaGGFoekCVgTyypy53gPxhbkCIjtO6YWgdrfM+n/8sI8JeXZOIxsHjMTNxQ4nQ==} 8301 + engines: {node: ^14.0.0 || >=16.0.0} 8273 8302 8274 8303 scule@1.3.0: 8275 8304 resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} ··· 15016 15045 15017 15046 commander@6.2.1: {} 15018 15047 15048 + comment-parser@1.4.5: {} 15049 + 15019 15050 common-tags@1.8.2: {} 15020 15051 15021 15052 commondir@1.0.1: {} ··· 15709 15740 empathic: 2.0.0 15710 15741 module-replacements: 2.11.0 15711 15742 semver: 7.7.3 15743 + 15744 + eslint-plugin-regexp@3.0.0(eslint@9.39.2(jiti@2.6.1)): 15745 + dependencies: 15746 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.2(jiti@2.6.1)) 15747 + '@eslint-community/regexpp': 4.12.2 15748 + comment-parser: 1.4.5 15749 + eslint: 9.39.2(jiti@2.6.1) 15750 + jsdoc-type-pratt-parser: 7.1.1 15751 + refa: 0.12.1 15752 + regexp-ast-analysis: 0.7.1 15753 + scslre: 0.3.0 15712 15754 15713 15755 eslint-scope@5.1.1: 15714 15756 dependencies: ··· 16948 16990 js-yaml@4.1.1: 16949 16991 dependencies: 16950 16992 argparse: 2.0.1 16993 + 16994 + jsdoc-type-pratt-parser@7.1.1: {} 16951 16995 16952 16996 jsesc@3.1.0: {} 16953 16997 ··· 19205 19249 dependencies: 19206 19250 redis-errors: 1.2.0 19207 19251 19252 + refa@0.12.1: 19253 + dependencies: 19254 + '@eslint-community/regexpp': 4.12.2 19255 + 19208 19256 reflect.getprototypeof@1.0.10: 19209 19257 dependencies: 19210 19258 call-bind: 1.0.8 ··· 19231 19279 regex@6.1.0: 19232 19280 dependencies: 19233 19281 regex-utilities: 2.3.0 19282 + 19283 + regexp-ast-analysis@0.7.1: 19284 + dependencies: 19285 + '@eslint-community/regexpp': 4.12.2 19286 + refa: 0.12.1 19234 19287 19235 19288 regexp-tree@0.1.27: {} 19236 19289 ··· 19579 19632 ajv: 8.17.1 19580 19633 ajv-formats: 2.1.1(ajv@8.17.1) 19581 19634 ajv-keywords: 5.1.0(ajv@8.17.1) 19635 + 19636 + scslre@0.3.0: 19637 + dependencies: 19638 + '@eslint-community/regexpp': 4.12.2 19639 + refa: 0.12.1 19640 + regexp-ast-analysis: 0.7.1 19582 19641 19583 19642 scule@1.3.0: {} 19584 19643
+1 -1
server/api/registry/org/[org]/packages.get.ts
··· 3 3 const NPM_REGISTRY = 'https://registry.npmjs.org' 4 4 5 5 // Validation pattern for npm org names (alphanumeric with hyphens) 6 - const NPM_ORG_NAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i 6 + const NPM_ORG_NAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i 7 7 8 8 function validateOrgName(name: string): void { 9 9 if (!name || name.length > 50 || !NPM_ORG_NAME_RE.test(name)) {
+1 -1
server/api/registry/readme/[...pkg].get.ts
··· 18 18 ] 19 19 20 20 /** Matches standard README filenames (case-insensitive, for checking registry metadata) */ 21 - const standardReadmePattern = /^readme(\.md|\.markdown)?$/i 21 + const standardReadmePattern = /^readme(?:\.md|\.markdown)?$/i 22 22 23 23 /** 24 24 * Fetch README from jsdelivr CDN for a specific package version.
+3 -2
server/utils/docs/text.ts
··· 1 + /* oxlint-disable regexp/no-super-linear-backtracking */ 1 2 /** 2 3 * Text Processing Utilities 3 4 * ··· 54 55 * Create a URL-safe HTML anchor ID for a symbol. 55 56 */ 56 57 export function createSymbolId(kind: string, name: string): string { 57 - return `${kind}-${name}`.replace(/[^a-zA-Z0-9-]/g, '_') 58 + return `${kind}-${name}`.replace(/[^a-z0-9-]/gi, '_') 58 59 } 59 60 60 61 /** ··· 126 127 result = result 127 128 .replace(/`([^`]+)`/g, '<code class="docs-inline-code">$1</code>') 128 129 .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>') 129 - .replace(/\n\n+/g, '<br><br>') 130 + .replace(/\n{2,}/g, '<br><br>') 130 131 .replace(/\n/g, '<br>') 131 132 132 133 // Highlight and restore code blocks
+1 -1
server/utils/npm.ts
··· 45 45 function constraintIncludesPrerelease(constraint: string): boolean { 46 46 // Look for prerelease identifiers in the constraint 47 47 return ( 48 - /-(alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || 48 + /-(?:alpha|beta|rc|next|canary|dev|preview|pre|experimental)/i.test(constraint) || 49 49 /-\d/.test(constraint) 50 50 ) // e.g., -0, -1 51 51 }
+1 -1
server/utils/shiki.ts
··· 76 76 defaultColor: 'dark', 77 77 }) 78 78 // Remove inline style from <pre> tag so CSS can control appearance 79 - html = html.replace(/<pre([^>]*)\s+style="[^"]*"/, '<pre$1') 79 + html = html.replace(/<pre([^>]*) style="[^"]*"/, '<pre$1') 80 80 // Shiki doesn't encode > in text content (e.g., arrow functions =>) 81 81 // We need to encode them for HTML validation 82 82 return escapeRawGt(html)
+1 -1
shared/schemas/package.ts
··· 22 22 export const VersionSchema = v.pipe( 23 23 v.string(), 24 24 v.nonEmpty('Version is required'), 25 - v.regex(/^[a-z0-9._+-]+$/i, 'Invalid version format'), 25 + v.regex(/^[\w.+-]+$/, 'Invalid version format'), 26 26 ) 27 27 28 28 /**
+1 -1
shared/utils/git-providers.ts
··· 299 299 const normalized = raw.replace(/^git\+/, '') 300 300 301 301 // Handle ssh:// and git:// URLs by converting to https:// 302 - if (/^(ssh|git):\/\//i.test(normalized)) { 302 + if (/^(?:ssh|git):\/\//i.test(normalized)) { 303 303 try { 304 304 const url = new URL(normalized) 305 305 const path = url.pathname.replace(/^\/*/, '')
+1 -1
shared/utils/npm.ts
··· 2 2 import { createError } from 'h3' 3 3 import validatePackageName from 'validate-npm-package-name' 4 4 5 - const NPM_USERNAME_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/i 5 + const NPM_USERNAME_RE = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/i 6 6 const NPM_USERNAME_MAX_LENGTH = 50 7 7 8 8 /**
+1 -1
shared/utils/parse-basic-frontmatter.ts
··· 1 1 export function parseBasicFrontmatter(fileContent: string): Record<string, unknown> { 2 - const match = fileContent.match(/^---\s*\n([\s\S]*?)\n---\s*(?:\n|$)/) 2 + const match = fileContent.match(/^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/) 3 3 if (!match?.[1]) return {} 4 4 5 5 return match[1].split('\n').reduce<Record<string, unknown>>((acc, line) => {