diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2468c4cc9..8320ecf6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,10 +33,14 @@ jobs: - uses: pnpm/action-setup@1e1c8eafbd745f64b1ef30a7d7ed7965034c486c # 1e1c8eafbd745f64b1ef30a7d7ed7965034c486c name: 🟧 Install pnpm - # pnpm cache skipped deliberately as the project is not actually installed here + with: + cache: true + + - name: 📦 Install dependencies (root only, no scripts) + run: pnpm install --filter . --ignore-scripts - name: 🔠 Lint project - run: node scripts/lint.ts + run: pnpm lint types: name: 💪 Type check diff --git a/.oxlintrc.json b/.oxlintrc.json index 7cb23eb90..b0bc5d6ff 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -1,6 +1,7 @@ { "$schema": "https://unpkg.com/oxlint/configuration_schema.json", "plugins": ["unicorn", "typescript", "oxc", "vue", "vitest"], + "jsPlugins": ["@e18e/eslint-plugin"], "categories": { "correctness": "error", "suspicious": "warn", @@ -11,8 +12,27 @@ "no-await-in-loop": "off", "unicorn/no-array-sort": "off", "no-restricted-globals": "error", - "typescript/consistent-type-imports": "error" + "typescript/consistent-type-imports": "error", + "e18e/prefer-array-from-map": "error", + "e18e/prefer-timer-args": "error", + "e18e/prefer-date-now": "error", + "e18e/prefer-regex-test": "error", + "e18e/prefer-array-some": "error" }, + "overrides": [ + { + "files": [ + "server/**/*", + "cli/**/*", + "scripts/**/*", + "modules/**/*", + "app/components/OgImage/*" + ], + "rules": { + "no-console": "off" + } + } + ], "ignorePatterns": [ ".output/**", ".data/**", diff --git a/knip.ts b/knip.ts index 493fc3154..d066b592a 100644 --- a/knip.ts +++ b/knip.ts @@ -39,6 +39,9 @@ const config: KnipConfig = { /** Some components import types from here, but installing it directly could lead to a version mismatch */ 'vue-router', + + /** Oxlint plugins don't get picked up yet */ + '@e18e/eslint-plugin', ], ignoreUnresolved: ['#components', '#oauth/config'], }, diff --git a/package.json b/package.json index 6dce46fbe..a2afadec3 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ "vue-data-ui": "3.14.7" }, "devDependencies": { + "@e18e/eslint-plugin": "0.1.4", "@intlify/core-base": "11.2.8", "@npm/types": "2.1.0", "@playwright/test": "1.58.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e39df21b6..c464f3f31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -201,6 +201,9 @@ importers: specifier: 3.14.7 version: 3.14.7(vue@3.5.27(typescript@5.9.3)) devDependencies: + '@e18e/eslint-plugin': + specifier: 0.1.4 + version: 0.1.4(eslint@9.39.2(jiti@2.6.1)) '@intlify/core-base': specifier: 11.2.8 version: 11.2.8 @@ -1058,6 +1061,11 @@ packages: '@dxup/unimport@0.1.2': resolution: {integrity: sha512-/B8YJGPzaYq1NbsQmwgP8EZqg40NpTw4ZB3suuI0TplbxKHeK94jeaawLmVhCv+YwUnOpiWEz9U6SeThku/8JQ==} + '@e18e/eslint-plugin@0.1.4': + resolution: {integrity: sha512-nN0zo9lIsYJynhFyN+kNVINc9pDMotKjfEr1NcRRXCFKJt89V5S4DL6ZSXN2USJb2XezkMvyLOQMZ0lZTpvFeQ==} + peerDependencies: + eslint: ^9.0.0 + '@emnapi/core@1.8.1': resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} @@ -5565,6 +5573,9 @@ packages: engines: {node: '>=6.0'} hasBin: true + eslint-plugin-depend@1.4.0: + resolution: {integrity: sha512-MQs+m4nHSfgAO9bJDsBzqw0ofK/AOA0vfeY/6ahofqcUMLeM6/D1sTYs21fOhc17kNU/gn58YCtj20XaAssh2A==} + eslint-scope@5.1.1: resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} engines: {node: '>=8.0.0'} @@ -10645,6 +10656,11 @@ snapshots: '@dxup/unimport@0.1.2': {} + '@e18e/eslint-plugin@0.1.4(eslint@9.39.2(jiti@2.6.1))': + dependencies: + eslint: 9.39.2(jiti@2.6.1) + eslint-plugin-depend: 1.4.0 + '@emnapi/core@1.8.1': dependencies: '@emnapi/wasi-threads': 1.1.0 @@ -15639,6 +15655,12 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-plugin-depend@1.4.0: + dependencies: + empathic: 2.0.0 + module-replacements: 2.11.0 + semver: 7.7.3 + eslint-scope@5.1.1: dependencies: esrecurse: 4.3.0 diff --git a/scripts/lint.ts b/scripts/lint.ts deleted file mode 100644 index 3556a29f7..000000000 --- a/scripts/lint.ts +++ /dev/null @@ -1,33 +0,0 @@ -/** - * This script runs oxlint and oxfmt in a CI environment, without the need to install the entire - * project. It reads the required version from pnpm-lock.yaml and executes the linters accordingly. - * It's "stupid by design" so it could work in minimal Node.js environments. - */ - -import { spawnSync } from 'node:child_process' - -function getDependencyVersion(dependencyName: string): string { - const result = spawnSync('npm', ['pkg', 'get', `devDependencies.${dependencyName}`], { - encoding: 'utf8', - }) - - if (result.status) { - throw new Error(`Command failed: pnpm info ${dependencyName} version`) - } - - return JSON.parse(result.stdout) -} - -function runCommand(command: string, args: string[]) { - const result = spawnSync(command, args, { stdio: 'inherit' }) - - if (result.status) { - throw new Error(`Command failed: ${command} ${args.join(' ')}`) - } -} - -const oxlintVersion = getDependencyVersion('oxlint') -const oxfmtVersion = getDependencyVersion('oxfmt') - -runCommand('pnpx', [`oxlint@${oxlintVersion}`]) -runCommand('pnpx', [`oxfmt@${oxfmtVersion}`, '--check']) diff --git a/server/api/atproto/author-profiles.get.ts b/server/api/atproto/author-profiles.get.ts index 6b1f656e2..f0193e1dc 100644 --- a/server/api/atproto/author-profiles.get.ts +++ b/server/api/atproto/author-profiles.get.ts @@ -49,11 +49,7 @@ export default defineCachedEventHandler( if (handles.length === 0) { return { - authors: authors.map(author => ({ - ...author, - avatar: null, - profileUrl: null, - })), + authors: authors.map(author => Object.assign(author, { avatar: null, profileUrl: null })), } } @@ -68,11 +64,14 @@ export default defineCachedEventHandler( } } - const resolvedAuthors: ResolvedAuthor[] = authors.map(author => ({ - ...author, - avatar: author.blueskyHandle ? avatarMap.get(author.blueskyHandle) || null : null, - profileUrl: author.blueskyHandle ? `https://bsky.app/profile/${author.blueskyHandle}` : null, - })) + const resolvedAuthors: ResolvedAuthor[] = authors.map(author => + Object.assign(author, { + avatar: author.blueskyHandle ? avatarMap.get(author.blueskyHandle) || null : null, + profileUrl: author.blueskyHandle + ? `https://bsky.app/profile/${author.blueskyHandle}` + : null, + }), + ) return { authors: resolvedAuthors } }, diff --git a/server/utils/dependency-analysis.ts b/server/utils/dependency-analysis.ts index fcd109c1b..30556436b 100644 --- a/server/utils/dependency-analysis.ts +++ b/server/utils/dependency-analysis.ts @@ -179,7 +179,7 @@ export const analyzeDependencyTree = defineCachedFunction( const resolved = await resolveDependencyTree(name, version, { trackDepth: true }) // Convert to array with query info - const packages: PackageQueryInfo[] = [...resolved.values()].map(pkg => ({ + const packages: PackageQueryInfo[] = Array.from(resolved.values(), pkg => ({ name: pkg.name, version: pkg.version, depth: pkg.depth!, diff --git a/server/utils/readme.ts b/server/utils/readme.ts index 6f66d21e2..a1fcfa36d 100644 --- a/server/utils/readme.ts +++ b/server/utils/readme.ts @@ -264,6 +264,14 @@ function resolveImageUrl(url: string, packageName: string, repoInfo?: Repository return resolved } +// Helper to prefix id attributes with 'user-content-' +function prefixId(tagName: string, attribs: sanitizeHtml.Attributes) { + if (attribs.id && !attribs.id.startsWith('user-content-')) { + attribs.id = `user-content-${attribs.id}` + } + return { tagName, attribs } +} + export async function renderReadmeHtml( content: string, packageName: string, @@ -398,14 +406,6 @@ ${html} const rawHtml = marked.parse(content) as string - // Helper to prefix id attributes with 'user-content-' - const prefixId = (tagName: string, attribs: sanitizeHtml.Attributes) => { - if (attribs.id && !attribs.id.startsWith('user-content-')) { - attribs.id = `user-content-${attribs.id}` - } - return { tagName, attribs } - } - const sanitized = sanitizeHtml(rawHtml, { allowedTags: ALLOWED_TAGS, allowedAttributes: ALLOWED_ATTR, diff --git a/test/e2e/test-utils.ts b/test/e2e/test-utils.ts index 4f774e431..e454b974d 100644 --- a/test/e2e/test-utils.ts +++ b/test/e2e/test-utils.ts @@ -313,7 +313,7 @@ async function handleJsdelivrCdn(route: Route): Promise { const pathname = decodeURIComponent(url.pathname) // README file requests - return 404 (package pages work fine without README) - if (pathname.match(/readme/i)) { + if (/readme/i.test(pathname)) { await route.fulfill({ status: 404, body: 'Not found' }) return true }