From d3e0504bdf3623060686f103bc04a4c4ee8a135b Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Wed, 18 Mar 2026 06:40:22 +0100 Subject: [PATCH 1/4] Reduce knip config --- knip.ts | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/knip.ts b/knip.ts index cd012608b8..61dfe3eece 100644 --- a/knip.ts +++ b/knip.ts @@ -4,24 +4,12 @@ const config: KnipConfig = { workspaces: { '.': { entry: [ - 'app/pages/**/*.vue!', - 'app/components/**/*.vue!', - 'app/components/**/*.d.vue.ts!', - 'app/composables/**/*.ts!', - 'app/middleware/**/*.ts!', - 'app/plugins/**/*.ts!', - 'app/utils/**/*.ts!', - 'server/**/*.ts!', - 'modules/**/*.ts!', - 'config/**/*.ts!', - 'lunaria/**/*.ts!', - 'shared/**/*.ts!', 'i18n/**/*.ts', 'lunaria.config.ts', 'pwa-assets.config.ts', '.lighthouserc.cjs', 'lighthouse-setup.cjs', - 'uno-preset-rtl.ts!', + 'uno-preset-*.ts!', 'scripts/**/*.ts', ], project: [ @@ -45,9 +33,6 @@ const config: KnipConfig = { /** Some components import types from here, but installing it directly could lead to a version mismatch */ 'vue-router', - /** Required by @nuxtjs/i18n at runtime but not directly imported in production code */ - '@intlify/shared', - /** Oxlint plugins don't get picked up yet */ '@e18e/eslint-plugin', 'eslint-plugin-regexp', @@ -55,14 +40,14 @@ const config: KnipConfig = { /** Used in test/e2e/helpers/ which is excluded from knip project scope */ 'h3-next', ], - ignoreUnresolved: ['#components', '#oauth/config'], + ignoreUnresolved: ['#oauth/config'], }, 'cli': { project: ['src/**/*.ts!', '!src/mock-*.ts'], }, 'docs': { entry: ['app/**/*.{ts,vue,css}'], - ignoreDependencies: ['docus', 'better-sqlite3', '@nuxtjs/mdc', 'nuxt!'], + ignoreDependencies: ['docus', 'better-sqlite3', '@nuxtjs/mdc'], }, }, } From 223c37e0f63dbd8bb9d884a3ee615a0da7b7db59 Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Wed, 18 Mar 2026 06:40:22 +0100 Subject: [PATCH 2/4] Remove unused code --- app/components/Compare/ComparisonGrid.vue | 2 +- app/components/Package/ListControls.vue | 2 +- app/components/Select/Field.vue | 2 +- app/components/Tooltip/Announce.vue | 16 -- app/components/UserCombobox.vue | 218 -------------------- app/composables/npm/useAlgoliaSearch.ts | 4 +- app/composables/npm/useNpmSearch.ts | 2 +- app/composables/useVirtualInfiniteScroll.ts | 2 +- app/utils/atproto/likes.ts | 12 +- app/utils/atproto/profile.ts | 2 +- config/env.ts | 12 +- lunaria/components.ts | 111 ++-------- server/utils/diff.ts | 1 - shared/schemas/atproto.ts | 33 --- shared/schemas/package.ts | 12 +- shared/schemas/publicUserSession.ts | 3 - shared/schemas/social.ts | 4 - shared/schemas/user.ts | 5 +- shared/utils/parse-basic-frontmatter.ts | 31 --- 19 files changed, 33 insertions(+), 441 deletions(-) delete mode 100644 app/components/Tooltip/Announce.vue delete mode 100644 app/components/UserCombobox.vue delete mode 100644 server/utils/diff.ts delete mode 100644 shared/schemas/atproto.ts delete mode 100644 shared/utils/parse-basic-frontmatter.ts diff --git a/app/components/Compare/ComparisonGrid.vue b/app/components/Compare/ComparisonGrid.vue index 2bd210b632..325839c49c 100644 --- a/app/components/Compare/ComparisonGrid.vue +++ b/app/components/Compare/ComparisonGrid.vue @@ -1,7 +1,7 @@ - - diff --git a/app/components/UserCombobox.vue b/app/components/UserCombobox.vue deleted file mode 100644 index 5464b65814..0000000000 --- a/app/components/UserCombobox.vue +++ /dev/null @@ -1,218 +0,0 @@ - - - diff --git a/app/composables/npm/useAlgoliaSearch.ts b/app/composables/npm/useAlgoliaSearch.ts index 30f19b13c3..5f8d08ab1a 100644 --- a/app/composables/npm/useAlgoliaSearch.ts +++ b/app/composables/npm/useAlgoliaSearch.ts @@ -107,7 +107,7 @@ function hitToSearchResult(hit: AlgoliaHit): NpmSearchResult { } } -export interface AlgoliaSearchOptions { +interface AlgoliaSearchOptions { size?: number from?: number filters?: string @@ -121,7 +121,7 @@ export interface AlgoliaMultiSearchChecks { checkPackage?: string } -export interface AlgoliaSearchWithSuggestionsResult { +interface AlgoliaSearchWithSuggestionsResult { search: NpmSearchResponse orgExists: boolean userExists: boolean diff --git a/app/composables/npm/useNpmSearch.ts b/app/composables/npm/useNpmSearch.ts index f9e2951f26..e863ca3281 100644 --- a/app/composables/npm/useNpmSearch.ts +++ b/app/composables/npm/useNpmSearch.ts @@ -1,6 +1,6 @@ import { emptySearchResponse, metaToSearchResult } from './search-utils' -export interface NpmSearchOptions { +interface NpmSearchOptions { size?: number from?: number } diff --git a/app/composables/useVirtualInfiniteScroll.ts b/app/composables/useVirtualInfiniteScroll.ts index e90d94f9fc..8b1c05f0ac 100644 --- a/app/composables/useVirtualInfiniteScroll.ts +++ b/app/composables/useVirtualInfiniteScroll.ts @@ -11,7 +11,7 @@ export interface WindowVirtualizerHandle { ) => void } -export interface UseVirtualInfiniteScrollOptions { +interface UseVirtualInfiniteScrollOptions { /** Reference to the WindowVirtualizer component */ listRef: Ref /** Current item count */ diff --git a/app/utils/atproto/likes.ts b/app/utils/atproto/likes.ts index 236abb9adb..c95fd20083 100644 --- a/app/utils/atproto/likes.ts +++ b/app/utils/atproto/likes.ts @@ -2,15 +2,12 @@ import { FetchError } from 'ofetch' import { handleAuthError } from '~/utils/atproto/helpers' import type { PackageLikes } from '#shared/types/social' -export type LikeResult = { success: true; data: PackageLikes } | { success: false; error: Error } +type LikeResult = { success: true; data: PackageLikes } | { success: false; error: Error } /** * Like a package via the API */ -export async function likePackage( - packageName: string, - userHandle?: string | null, -): Promise { +async function likePackage(packageName: string, userHandle?: string | null): Promise { try { const result = await $fetch('/api/social/like', { method: 'POST', @@ -28,10 +25,7 @@ export async function likePackage( /** * Unlike a package via the API */ -export async function unlikePackage( - packageName: string, - userHandle?: string | null, -): Promise { +async function unlikePackage(packageName: string, userHandle?: string | null): Promise { try { const result = await $fetch('/api/social/like', { method: 'DELETE', diff --git a/app/utils/atproto/profile.ts b/app/utils/atproto/profile.ts index 43968ea5b9..0c3ed242a4 100644 --- a/app/utils/atproto/profile.ts +++ b/app/utils/atproto/profile.ts @@ -1,7 +1,7 @@ import { FetchError } from 'ofetch' import { handleAuthError } from './helpers' -export type UpdateProfileResult = { +type UpdateProfileResult = { success: boolean error?: Error } diff --git a/config/env.ts b/config/env.ts index 49daac01ae..d673c5cafd 100644 --- a/config/env.ts +++ b/config/env.ts @@ -6,8 +6,6 @@ import * as process from 'node:process' import { version as packageVersion } from '../package.json' import { getNextVersion } from '../scripts/next-version' -export { packageVersion as version } - /** * Environment variable `PULL_REQUEST` provided by Netlify. * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#git-metadata} @@ -17,7 +15,7 @@ export { packageVersion as version } * * Whether triggered by a GitHub PR */ -export const isPR = process.env.PULL_REQUEST === 'true' || !!process.env.VERCEL_GIT_PULL_REQUEST_ID +const isPR = process.env.PULL_REQUEST === 'true' || !!process.env.VERCEL_GIT_PULL_REQUEST_ID /** * Environment variable `REVIEW_ID` provided by Netlify. @@ -28,7 +26,7 @@ export const isPR = process.env.PULL_REQUEST === 'true' || !!process.env.VERCEL_ * * Pull request number (if in a PR environment) */ -export const prNumber = process.env.REVIEW_ID || process.env.VERCEL_GIT_PULL_REQUEST_ID || null +const prNumber = process.env.REVIEW_ID || process.env.VERCEL_GIT_PULL_REQUEST_ID || null /** * Environment variable `BRANCH` provided by Netlify. @@ -39,7 +37,7 @@ export const prNumber = process.env.REVIEW_ID || process.env.VERCEL_GIT_PULL_REQ * * Git branch */ -export const gitBranch = process.env.BRANCH || process.env.VERCEL_GIT_COMMIT_REF +const gitBranch = process.env.BRANCH || process.env.VERCEL_GIT_COMMIT_REF /** * Whether this is the canary environment (main.npmx.dev). @@ -68,7 +66,7 @@ export const isCanary = * * Whether this is some sort of preview environment. */ -export const isPreview = +const isPreview = isPR || (process.env.CONTEXT && process.env.CONTEXT !== 'production') || process.env.VERCEL_ENV === 'preview' || @@ -118,7 +116,7 @@ export const getProductionUrl = () => : undefined const git = Git() -export async function getGitInfo() { +async function getGitInfo() { let branch try { branch = gitBranch || (await git.revparse(['--abbrev-ref', 'HEAD'])) diff --git a/lunaria/components.ts b/lunaria/components.ts index a8c290727d..4543da6fa0 100644 --- a/lunaria/components.ts +++ b/lunaria/components.ts @@ -49,7 +49,7 @@ export const Page = ( ` } -export const Meta = html` +const Meta = html` npmx - Translation Status @@ -73,11 +73,7 @@ export const Meta = html` ` -export const Body = ( - config: LunariaConfig, - status: LunariaStatus, - lunaria: LunariaInstance, -): string => { +const Body = (config: LunariaConfig, status: LunariaStatus, lunaria: LunariaInstance): string => { return html`
@@ -89,7 +85,7 @@ export const Body = ( ` } -export const StatusByLocale = ( +const StatusByLocale = ( config: LunariaConfig, status: LunariaStatus, lunaria: LunariaInstance, @@ -103,11 +99,7 @@ export const StatusByLocale = ( ` } -export const LocaleDetails = ( - status: LunariaStatus, - locale: Locale, - lunaria: LunariaInstance, -): string => { +const LocaleDetails = (status: LunariaStatus, locale: Locale, lunaria: LunariaInstance): string => { const { label, lang } = locale const missingFiles = status.filter( @@ -173,7 +165,7 @@ export const LocaleDetails = ( ` } -export const OutdatedFiles = ( +const OutdatedFiles = ( outdatedFiles: LunariaStatus, lang: string, lunaria: LunariaInstance, @@ -213,7 +205,7 @@ export const OutdatedFiles = ( ` } -export const StatusByFile = ( +const StatusByFile = ( config: LunariaConfig, status: LunariaStatus, lunaria: LunariaInstance, @@ -237,11 +229,7 @@ export const StatusByFile = ( ` } -export const TableBody = ( - status: LunariaStatus, - locales: Locale[], - lunaria: LunariaInstance, -): string => { +const TableBody = (status: LunariaStatus, locales: Locale[], lunaria: LunariaInstance): string => { const links = lunaria.gitHostingLinks() return html` @@ -261,7 +249,7 @@ export const TableBody = ( ` } -export const TableContentStatus = ( +const TableContentStatus = ( localizations: StatusEntry['localizations'], lang: string, lunaria: LunariaInstance, @@ -289,7 +277,7 @@ export const TableContentStatus = ( return html`${EmojiFileLink(link, status)}` } -export const ContentDetailsLinks = ( +const ContentDetailsLinks = ( fileStatus: StatusEntry, lang: string, lunaria: LunariaInstance, @@ -322,7 +310,7 @@ export const ContentDetailsLinks = ( ` } -export const EmojiFileLink = ( +const EmojiFileLink = ( href: string | null, type: 'missing' | 'outdated' | 'up-to-date', ): string => { @@ -347,15 +335,15 @@ export const EmojiFileLink = ( ` } -export const Link = (href: string, text: string): string => { +const Link = (href: string, text: string): string => { return html`${text}` } -export const CreateFileLink = (href: string, text: string): string => { +const CreateFileLink = (href: string, text: string): string => { return html`${text}` } -export const ProgressBar = ( +const ProgressBar = ( total: number, outdated: number, missing: number, @@ -381,7 +369,7 @@ export const ProgressBar = ( ` } -export const TitleParagraph = html` +const TitleParagraph = html`

If you're interested in helping us translate npmx.dev into one of the languages listed below, you've come to @@ -396,74 +384,3 @@ export const TitleParagraph = html` to learn about our translation process and how you can get involved.

` - -/** - * Build an SVG file showing a summary of each language's translation progress. - */ -export const SvgSummary = (config: LunariaConfig, status: LunariaStatus): string => { - const localeHeight = 56 // Each locale’s summary is 56px high. - const svgHeight = localeHeight * Math.ceil(config.locales.length / 2) - return html` - ${config.locales - .map(locale => SvgLocaleSummary(status, locale)) - .sort((a, b) => b.progress - a.progress) - .map( - ({ svg }, index) => - html`${svg}`, - )} - ` -} - -function SvgLocaleSummary( - status: LunariaStatus, - { label, lang }: Locale, -): { svg: string; progress: number } { - const missingFiles = status.filter( - file => - file.localizations.find(localization => localization.lang === lang)?.status === 'missing', - ) - const outdatedFiles = status.filter(file => { - const localization = file.localizations.find(localizationItem => localizationItem.lang === lang) - if (!localization || localization.status === 'missing') { - return false - } else if (file.type === 'dictionary') { - return 'missingKeys' in localization ? localization.missingKeys.length > 0 : false - } else { - return ( - localization.status === 'outdated' || - ('missingKeys' in localization && localization.missingKeys.length > 0) - ) - } - }) - - const doneLength = status.length - outdatedFiles.length - missingFiles.length - const barWidth = 184 - const doneFraction = doneLength / status.length - const outdatedFraction = outdatedFiles.length / status.length - const doneWidth = (doneFraction * barWidth).toFixed(2) - const outdatedWidth = ((outdatedFraction + doneFraction) * barWidth).toFixed(2) - - return { - progress: doneFraction, - svg: html`${label} (${lang}) - - ${ - missingFiles.length == 0 && outdatedFiles.length == 0 - ? '100% complete, amazing job! 🎉' - : html`${doneLength} done, ${outdatedFiles.length} outdated, ${missingFiles.length} - missing` - } - - - - `, - } -} diff --git a/server/utils/diff.ts b/server/utils/diff.ts deleted file mode 100644 index 45e695a499..0000000000 --- a/server/utils/diff.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../../shared/utils/diff' diff --git a/shared/schemas/atproto.ts b/shared/schemas/atproto.ts deleted file mode 100644 index 34e2ec30da..0000000000 --- a/shared/schemas/atproto.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { boolean, object, optional, pipe, string, url } from 'valibot' -import type { InferOutput } from 'valibot' - -/** - * INFO: Validates AT Protocol createSession response - * Used for authenticating PDS sessions. - */ -export const PDSSessionSchema = object({ - did: string(), - handle: string(), - accessJwt: string(), - refreshJwt: string(), - email: string(), - emailConfirmed: boolean(), -}) - -export type PDSSessionResponse = InferOutput - -export const BlogMetaRequestSchema = object({ - url: pipe(string(), url()), -}) - -export type BlogMetaRequest = InferOutput - -export const BlogMetaResponseSchema = object({ - title: string(), - author: optional(string()), - description: optional(string()), - image: optional(string()), - _meta: optional(object({})), -}) - -export type BlogMetaResponse = InferOutput diff --git a/shared/schemas/package.ts b/shared/schemas/package.ts index 0bd3e5d18e..51be45c33d 100644 --- a/shared/schemas/package.ts +++ b/shared/schemas/package.ts @@ -19,7 +19,7 @@ export const PackageNameSchema = v.pipe( * Enforces a SemVer-like pattern to prevent directory traversal or complex injection attacks * includes: alphanumeric, dots, underscores, dashes, and plus signs (for build metadata) */ -export const VersionSchema = v.pipe( +const VersionSchema = v.pipe( v.string(), v.nonEmpty('Version is required'), v.regex(/^[\w.+-]+$/, 'Invalid version format'), @@ -29,7 +29,7 @@ export const VersionSchema = v.pipe( * * Allows standard subdirectories and extensions but prevents directory traversal */ -export const FilePathSchema = v.pipe( +const FilePathSchema = v.pipe( v.string(), v.nonEmpty('File path is required'), v.check(input => !input.includes('..'), 'Invalid path: directory traversal not allowed'), @@ -88,14 +88,6 @@ export const PackageFileDiffQuerySchema = v.object({ toVersion: VersionSchema, filePath: FilePathSchema, }) - -/** - * Automatically infer types for routes - * Usage - prefer this over manually defining interfaces - */ -export type PackageRouteParams = v.InferOutput -export type PackageVersionQuery = v.InferOutput -export type PackageFileQuery = v.InferOutput /** @public */ export type PackageCompareQuery = v.InferOutput /** @public */ diff --git a/shared/schemas/publicUserSession.ts b/shared/schemas/publicUserSession.ts index c3921af93f..d5f51821ce 100644 --- a/shared/schemas/publicUserSession.ts +++ b/shared/schemas/publicUserSession.ts @@ -1,5 +1,4 @@ import * as v from 'valibot' -import type { InferOutput } from 'valibot' export const PublicUserSessionSchema = v.object({ // Safe to pass to the frontend @@ -9,5 +8,3 @@ export const PublicUserSessionSchema = v.object({ avatar: v.optional(v.pipe(v.string(), v.url())), relogin: v.optional(v.boolean()), }) - -export type PublicUserSession = InferOutput diff --git a/shared/schemas/social.ts b/shared/schemas/social.ts index 1e3305941b..77b010a9e9 100644 --- a/shared/schemas/social.ts +++ b/shared/schemas/social.ts @@ -8,8 +8,6 @@ export const PackageLikeBodySchema = v.object({ packageName: PackageNameSchema, }) -export type PackageLikeBody = v.InferOutput - // TODO: add 'avatar' export const ProfileEditBodySchema = v.object({ displayName: v.pipe(v.string(), v.maxLength(640)), @@ -28,5 +26,3 @@ export const ProfileEditBodySchema = v.object({ ), description: v.optional(v.pipe(v.string(), v.maxLength(2560))), }) - -export type ProfileEditBody = v.InferOutput diff --git a/shared/schemas/user.ts b/shared/schemas/user.ts index 0a4d81e0f7..e7cae61f6b 100644 --- a/shared/schemas/user.ts +++ b/shared/schemas/user.ts @@ -6,7 +6,7 @@ const NPM_USERNAME_MAX_LENGTH = 50 /** * Schema for npm usernames. */ -export const NpmUsernameSchema = v.pipe( +const NpmUsernameSchema = v.pipe( v.string(), v.trim(), v.nonEmpty('Username is required'), @@ -20,6 +20,3 @@ export const NpmUsernameSchema = v.pipe( export const GravatarQuerySchema = v.object({ username: NpmUsernameSchema, }) - -export type NpmUsername = v.InferOutput -export type GravatarQuery = v.InferOutput diff --git a/shared/utils/parse-basic-frontmatter.ts b/shared/utils/parse-basic-frontmatter.ts deleted file mode 100644 index a2c31187f5..0000000000 --- a/shared/utils/parse-basic-frontmatter.ts +++ /dev/null @@ -1,31 +0,0 @@ -export function parseBasicFrontmatter(fileContent: string): Record { - const match = fileContent.match(/^---[ \t]*\n([\s\S]*?)\n---[ \t]*(?:\n|$)/) - if (!match?.[1]) return {} - - return match[1].split('\n').reduce>((acc, line) => { - const idx = line.indexOf(':') - if (idx === -1) return acc - - const key = line.slice(0, idx).trim() - - // Remove surrounding quotes - let value = line - .slice(idx + 1) - .trim() - .replace(/^["']|["']$/g, '') - - // Type coercion (handles 123, 45.6, boolean, arrays) - if (value === 'true') acc[key] = true - else if (value === 'false') acc[key] = false - else if (/^-?\d+$/.test(value)) acc[key] = parseInt(value, 10) - else if (/^-?\d+\.\d+$/.test(value)) acc[key] = parseFloat(value) - else if (value.startsWith('[') && value.endsWith(']')) { - acc[key] = value - .slice(1, -1) - .split(',') - .map(s => s.trim().replace(/^["']|["']$/g, '')) - } else acc[key] = value - - return acc - }, {}) -} From b4a835041d90c5a7898346ce0b081502942a5e24 Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Wed, 18 Mar 2026 06:40:22 +0100 Subject: [PATCH 3/4] Ignore dependencies in knip production mode --- .github/workflows/ci.yml | 2 +- knip.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aabce74779..d3046fca1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -199,7 +199,7 @@ jobs: run: pnpm knip - name: 🧹 Check for unused production code - run: pnpm knip --production + run: pnpm knip --production --exclude dependencies i18n: name: 🌐 i18n validation diff --git a/knip.ts b/knip.ts index 61dfe3eece..a24d572f75 100644 --- a/knip.ts +++ b/knip.ts @@ -23,7 +23,6 @@ const config: KnipConfig = { ignoreDependencies: [ '@iconify-json/*', '@voidzero-dev/vite-plus-core', - 'vite-plus!', 'puppeteer', /** Needs to be explicitly installed, even though it is not imported, to avoid type errors. */ 'unplugin-vue-router', From 41d586a58743994bdb4109d1f44da60a60fc344e Mon Sep 17 00:00:00 2001 From: Lars Kappert Date: Wed, 18 Mar 2026 06:40:22 +0100 Subject: [PATCH 4/4] Restore & ignore some component files --- app/components/Tooltip/Announce.vue | 16 ++ app/components/UserCombobox.vue | 218 ++++++++++++++++++++++++++++ knip.ts | 1 + 3 files changed, 235 insertions(+) create mode 100644 app/components/Tooltip/Announce.vue create mode 100644 app/components/UserCombobox.vue diff --git a/app/components/Tooltip/Announce.vue b/app/components/Tooltip/Announce.vue new file mode 100644 index 0000000000..9134c37274 --- /dev/null +++ b/app/components/Tooltip/Announce.vue @@ -0,0 +1,16 @@ + + + diff --git a/app/components/UserCombobox.vue b/app/components/UserCombobox.vue new file mode 100644 index 0000000000..5464b65814 --- /dev/null +++ b/app/components/UserCombobox.vue @@ -0,0 +1,218 @@ + + + diff --git a/knip.ts b/knip.ts index a24d572f75..a08df176b9 100644 --- a/knip.ts +++ b/knip.ts @@ -40,6 +40,7 @@ const config: KnipConfig = { 'h3-next', ], ignoreUnresolved: ['#oauth/config'], + ignoreFiles: ['app/components/Tooltip/Announce.vue', 'app/components/UserCombobox.vue'], }, 'cli': { project: ['src/**/*.ts!', '!src/mock-*.ts'],