[READ-ONLY] a fast, modern browser for the npm registry
at main 378 lines 10 kB view raw
1import process from 'node:process' 2import { currentLocales } from './config/i18n' 3import { isCI, provider } from 'std-env' 4 5export default defineNuxtConfig({ 6 modules: [ 7 '@unocss/nuxt', 8 '@nuxtjs/html-validator', 9 '@nuxt/scripts', 10 '@nuxt/a11y', 11 '@nuxt/fonts', 12 'nuxt-og-image', 13 '@nuxt/test-utils', 14 '@vite-pwa/nuxt', 15 '@vueuse/nuxt', 16 '@nuxtjs/i18n', 17 '@nuxtjs/color-mode', 18 ], 19 20 $test: { 21 debug: { 22 hydration: true, 23 }, 24 }, 25 26 colorMode: { 27 preference: 'system', 28 fallback: 'dark', 29 dataValue: 'theme', 30 storageKey: 'npmx-color-mode', 31 }, 32 33 css: ['~/assets/main.css', 'vue-data-ui/style.css'], 34 35 runtimeConfig: { 36 sessionPassword: '', 37 github: { 38 orgToken: '', 39 }, 40 // Upstash Redis for distributed OAuth token refresh locking in production 41 upstash: { 42 redisRestUrl: process.env.UPSTASH_KV_REST_API_URL || process.env.KV_REST_API_URL || '', 43 redisRestToken: process.env.UPSTASH_KV_REST_API_TOKEN || process.env.KV_REST_API_TOKEN || '', 44 }, 45 public: { 46 // Algolia npm-search index (maintained by Algolia & jsDelivr, used by yarnpkg.com et al.) 47 algolia: { 48 appId: 'OFCNCOG2CU', 49 apiKey: 'f54e21fa3a2a0160595bb058179bfb1e', 50 indexName: 'npm-search', 51 }, 52 }, 53 }, 54 55 devtools: { enabled: true }, 56 57 devServer: { 58 // Used with atproto oauth 59 // https://atproto.com/specs/oauth#localhost-client-development 60 host: '127.0.0.1', 61 }, 62 63 app: { 64 head: { 65 htmlAttrs: { lang: 'en-US' }, 66 title: 'npmx', 67 link: [ 68 { 69 rel: 'search', 70 type: 'application/opensearchdescription+xml', 71 title: 'npm', 72 href: '/opensearch.xml', 73 }, 74 ], 75 meta: [{ name: 'twitter:card', content: 'summary_large_image' }], 76 }, 77 }, 78 79 vue: { 80 compilerOptions: { 81 isCustomElement: tag => tag === 'search', 82 }, 83 }, 84 85 site: { 86 url: 'https://npmx.dev', 87 name: 'npmx', 88 description: 'A fast, modern browser for the npm registry', 89 }, 90 91 router: { 92 options: { 93 scrollBehaviorType: 'smooth', 94 }, 95 }, 96 97 routeRules: { 98 // API routes 99 '/api/**': { isr: 60 }, 100 '/api/registry/badge/**': { 101 isr: { 102 expiration: 60 * 60 /* one hour */, 103 passQuery: true, 104 allowQuery: ['color', 'labelColor', 'label', 'name'], 105 }, 106 }, 107 '/api/registry/downloads/**': { 108 isr: { 109 expiration: 60 * 60 /* one hour */, 110 passQuery: true, 111 allowQuery: ['mode', 'filterOldVersions', 'filterThreshold'], 112 }, 113 }, 114 '/api/registry/docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 115 '/api/registry/file/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 116 '/api/registry/provenance/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 117 '/api/registry/files/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 118 '/api/registry/package-meta/**': { isr: 300 }, 119 '/:pkg/.well-known/skills/**': { isr: 3600 }, 120 '/:scope/:pkg/.well-known/skills/**': { isr: 3600 }, 121 '/__og-image__/**': getISRConfig(60), 122 '/_avatar/**': { isr: 3600, proxy: 'https://www.gravatar.com/avatar/**' }, 123 '/opensearch.xml': { isr: true }, 124 '/oauth-client-metadata.json': { prerender: true }, 125 // never cache 126 '/api/auth/**': { isr: false, cache: false }, 127 '/api/social/**': { isr: false, cache: false }, 128 '/api/opensearch/suggestions': { 129 isr: { 130 expiration: 60 * 60 * 24 /* one day */, 131 passQuery: true, 132 allowQuery: ['q'], 133 }, 134 }, 135 // pages 136 '/package/:name': getISRConfig(60, { fallback: 'html' }), 137 '/package/:name/_payload.json': getISRConfig(60, { fallback: 'json' }), 138 '/package/:name/v/:version': getISRConfig(60, { fallback: 'html' }), 139 '/package/:name/v/:version/_payload.json': getISRConfig(60, { fallback: 'json' }), 140 '/package/:org/:name': getISRConfig(60, { fallback: 'html' }), 141 '/package/:org/:name/_payload.json': getISRConfig(60, { fallback: 'json' }), 142 '/package/:org/:name/v/:version': getISRConfig(60, { fallback: 'html' }), 143 '/package/:org/:name/v/:version/_payload.json': getISRConfig(60, { fallback: 'json' }), 144 // infinite cache (versioned - doesn't change) 145 '/package-code/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 146 '/package-docs/**': { isr: true, cache: { maxAge: 365 * 24 * 60 * 60 } }, 147 // static pages 148 '/': { prerender: true }, 149 '/200.html': { prerender: true }, 150 '/about': { prerender: true }, 151 '/accessibility': { prerender: true }, 152 '/privacy': { prerender: true }, 153 '/search': { isr: false, cache: false }, // never cache 154 '/settings': { prerender: true }, 155 '/recharging': { prerender: true }, 156 // proxy for insights 157 '/_v/script.js': { proxy: 'https://npmx.dev/_vercel/insights/script.js' }, 158 '/_v/view': { proxy: 'https://npmx.dev/_vercel/insights/view' }, 159 '/_v/event': { proxy: 'https://npmx.dev/_vercel/insights/event' }, 160 '/_v/session': { proxy: 'https://npmx.dev/_vercel/insights/session' }, 161 // lunaria status.json 162 '/lunaria/status.json': { 163 headers: { 164 'Cache-Control': 'public, max-age=0, must-revalidate', 165 }, 166 }, 167 }, 168 169 experimental: { 170 entryImportMap: false, 171 typescriptPlugin: true, 172 viteEnvironmentApi: true, 173 typedPages: true, 174 }, 175 176 compatibilityDate: '2026-01-31', 177 178 nitro: { 179 externals: { 180 inline: [ 181 'shiki', 182 '@shikijs/langs', 183 '@shikijs/themes', 184 '@shikijs/types', 185 '@shikijs/engine-javascript', 186 '@shikijs/core', 187 ], 188 external: ['@deno/doc'], 189 }, 190 esbuild: { 191 options: { 192 target: 'es2024', 193 }, 194 }, 195 rollupConfig: { 196 output: { 197 paths: { 198 '@deno/doc': '@jsr/deno__doc', 199 }, 200 }, 201 }, 202 // Storage configuration for local development 203 // In production (Vercel), this is overridden by modules/cache.ts 204 storage: { 205 'fetch-cache': { 206 driver: 'fsLite', 207 base: './.cache/fetch', 208 }, 209 'atproto': { 210 driver: 'fsLite', 211 base: './.cache/atproto', 212 }, 213 }, 214 typescript: { 215 tsConfig: { 216 include: ['../test/unit/server/**/*.ts'], 217 }, 218 }, 219 }, 220 221 fonts: { 222 families: [ 223 { 224 name: 'Geist', 225 weights: ['400', '500', '600'], 226 preload: true, 227 global: true, 228 }, 229 { 230 name: 'Geist Mono', 231 weights: ['400', '500'], 232 preload: true, 233 global: true, 234 }, 235 ], 236 }, 237 238 htmlValidator: { 239 enabled: !isCI || (provider !== 'vercel' && !!process.env.VALIDATE_HTML), 240 options: { 241 rules: { 'meta-refresh': 'off' }, 242 }, 243 failOnError: true, 244 }, 245 246 ogImage: { 247 defaults: { 248 component: 'Default', 249 }, 250 fonts: [ 251 { name: 'Geist', weight: 400, path: '/fonts/Geist-Regular.ttf' }, 252 { name: 'Geist', weight: 500, path: '/fonts/Geist-Medium.ttf' }, 253 { name: 'Geist', weight: 600, path: '/fonts/Geist-SemiBold.ttf' }, 254 { name: 'Geist', weight: 700, path: '/fonts/Geist-Bold.ttf' }, 255 { name: 'Geist Mono', weight: 400, path: '/fonts/GeistMono-Regular.ttf' }, 256 { name: 'Geist Mono', weight: 500, path: '/fonts/GeistMono-Medium.ttf' }, 257 { name: 'Geist Mono', weight: 700, path: '/fonts/GeistMono-Bold.ttf' }, 258 ], 259 }, 260 261 pwa: { 262 // Disable service worker 263 disable: true, 264 pwaAssets: { 265 config: false, 266 }, 267 manifest: { 268 name: 'npmx', 269 short_name: 'npmx', 270 description: 'A fast, modern browser for the npm registry', 271 theme_color: '#0a0a0a', 272 background_color: '#0a0a0a', 273 icons: [ 274 { 275 src: 'pwa-64x64.png', 276 sizes: '64x64', 277 type: 'image/png', 278 }, 279 { 280 src: 'pwa-192x192.png', 281 sizes: '192x192', 282 type: 'image/png', 283 }, 284 { 285 src: 'pwa-512x512.png', 286 sizes: '512x512', 287 type: 'image/png', 288 purpose: 'any', 289 }, 290 { 291 src: 'maskable-icon-512x512.png', 292 sizes: '512x512', 293 type: 'image/png', 294 purpose: 'maskable', 295 }, 296 ], 297 }, 298 }, 299 300 typescript: { 301 tsConfig: { 302 compilerOptions: { 303 noUnusedLocals: true, 304 allowImportingTsExtensions: true, 305 paths: { 306 '#cli/*': ['../cli/src/*'], 307 }, 308 }, 309 include: ['../test/unit/app/**/*.ts'], 310 }, 311 sharedTsConfig: { 312 include: ['../test/unit/shared/**/*.ts'], 313 }, 314 nodeTsConfig: { 315 compilerOptions: { 316 allowImportingTsExtensions: true, 317 paths: { 318 '#cli/*': ['../cli/src/*'], 319 '#server/*': ['../server/*'], 320 '#shared/*': ['../shared/*'], 321 }, 322 }, 323 include: ['../*.ts', '../test/e2e/**/*.ts'], 324 }, 325 }, 326 327 vite: { 328 optimizeDeps: { 329 include: [ 330 '@vueuse/core', 331 '@vueuse/integrations/useFocusTrap', 332 '@vueuse/integrations/useFocusTrap/component', 333 'vue-data-ui/vue-ui-sparkline', 334 'vue-data-ui/vue-ui-xy', 335 'virtua/vue', 336 'semver', 337 'validate-npm-package-name', 338 '@atproto/lex', 339 'fast-npm-meta', 340 '@floating-ui/vue', 341 'algoliasearch/lite', 342 ], 343 }, 344 }, 345 346 i18n: { 347 locales: currentLocales, 348 defaultLocale: 'en-US', 349 strategy: 'no_prefix', 350 detectBrowserLanguage: false, 351 langDir: 'locales', 352 }, 353 354 imports: { 355 dirs: ['~/composables', '~/composables/*/*.ts'], 356 }, 357}) 358 359interface ISRConfigOptions { 360 fallback?: 'html' | 'json' 361} 362function getISRConfig(expirationSeconds: number, options: ISRConfigOptions = {}) { 363 if (options.fallback) { 364 return { 365 isr: { 366 expiration: expirationSeconds, 367 fallback: 368 options.fallback === 'html' ? 'spa.prerender-fallback.html' : 'payload-fallback.json', 369 initialHeaders: options.fallback === 'json' ? { 'content-type': 'application/json' } : {}, 370 } as { expiration: number }, 371 } 372 } 373 return { 374 isr: { 375 expiration: expirationSeconds, 376 }, 377 } 378}