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

Configure Feed

Select the types of activity you want to include in your feed.

feat: production setup (#378)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Daniel Roe <daniel@roe.dev>

authored by

Joaquín Sánchez
autofix-ci[bot]
Daniel Roe
and committed by
GitHub
77b9103a d206a7a2

+331 -33
+1 -1
.gitattributes
··· 1 - * text eol=lf 1 + * text=auto eol=lf
+1
app/app.vue
··· 68 68 69 69 <template> 70 70 <div class="min-h-screen flex flex-col bg-bg text-fg"> 71 + <NuxtPwaAssets /> 71 72 <a href="#main-content" class="skip-link font-mono">{{ $t('common.skip_link') }}</a> 72 73 73 74 <AppHeader :show-logo="!isHomepage" />
+1 -9
app/assets/main.css
··· 541 541 appearance: none; 542 542 } 543 543 544 - /* View transition for logo (hero -> header) */ 545 - .hero-logo, 546 - .header-logo { 547 - view-transition-name: site-logo; 548 - } 549 - 550 544 /* Disable the default fade transition on page navigation */ 551 545 ::view-transition-old(root), 552 546 ::view-transition-new(root) { ··· 555 549 556 550 /* Customize the view transition animations for specific elements */ 557 551 ::view-transition-old(search-box), 558 - ::view-transition-new(search-box), 559 - ::view-transition-old(site-logo), 560 - ::view-transition-new(site-logo) { 552 + ::view-transition-new(search-box) { 561 553 animation-duration: 0.3s; 562 554 animation-timing-function: cubic-bezier(0.22, 1, 0.36, 1); 563 555 }
+9 -1
app/components/AppFooter.vue
··· 1 + <script setup lang="ts"> 2 + const route = useRoute() 3 + const isHome = computed(() => route.name === 'index') 4 + </script> 5 + 1 6 <template> 2 7 <footer class="border-t border-border mt-auto"> 3 8 <div class="container py-3 sm:py-8 flex flex-col gap-2 sm:gap-4 text-fg-subtle text-sm"> 4 9 <div 5 10 class="flex flex-col sm:flex-row items-center sm:items-baseline justify-between gap-2 sm:gap-4" 6 11 > 7 - <p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p> 12 + <div> 13 + <p class="font-mono text-balance m-0 hidden sm:block">{{ $t('tagline') }}</p> 14 + <BuildEnvironment v-if="!isHome" footer /> 15 + </div> 8 16 <div class="flex items-center gap-3 sm:gap-6"> 9 17 <NuxtLink 10 18 to="/about"
+18 -8
app/components/AppHeader.vue
··· 34 34 > 35 35 <!-- Start: Logo --> 36 36 <div :class="{ 'hidden sm:block': showFullSearch }" class="flex-shrink-0"> 37 - <NuxtLink 38 - v-if="showLogo" 39 - to="/" 40 - :aria-label="$t('header.home')" 41 - class="header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded" 42 - > 43 - <span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx 44 - </NuxtLink> 37 + <div v-if="showLogo"> 38 + <NuxtLink 39 + to="/" 40 + :aria-label="$t('header.home')" 41 + dir="ltr" 42 + class="inline-flex items-center gap-2 header-logo font-mono text-lg font-medium text-fg hover:text-fg transition-colors duration-200 focus-ring rounded" 43 + > 44 + <img 45 + aria-hidden="true" 46 + :alt="$t('alt_logo')" 47 + src="/logo.svg" 48 + width="96" 49 + height="96" 50 + class="w-8 h-8 rounded-lg" 51 + /> 52 + <span>npmx</span> 53 + </NuxtLink> 54 + </div> 45 55 <!-- Spacer when logo is hidden --> 46 56 <span v-else class="w-1" /> 47 57 </div>
+42
app/components/BuildEnvironment.vue
··· 1 + <script setup lang="ts"> 2 + defineProps<{ 3 + footer?: boolean 4 + }>() 5 + 6 + const buildInfo = useAppConfig().buildInfo 7 + </script> 8 + 9 + <template> 10 + <div 11 + class="font-mono text-xs text-fg-muted flex items-center gap-2 motion-safe:animate-fade-in motion-safe:animate-fill-both" 12 + :class="footer ? 'mt-4 justify-start' : 'mb-8 justify-center'" 13 + style="animation-delay: 0.05s" 14 + > 15 + <i18n-t keypath="built_at"> 16 + <NuxtTime :datetime="buildInfo.time" relative /> 17 + </i18n-t> 18 + <span>&middot;</span> 19 + <NuxtLink 20 + v-if="buildInfo.env === 'release'" 21 + external 22 + :href="`https://github.com/npmx-dev/npmx.dev/tag/v${buildInfo.version}`" 23 + target="_blank" 24 + class="hover:text-fg transition-colors" 25 + > 26 + v{{ buildInfo.version }} 27 + </NuxtLink> 28 + <span v-else class="tracking-wider">{{ buildInfo.env }}</span> 29 + 30 + <template v-if="buildInfo.commit && buildInfo.branch !== 'release'"> 31 + <span>&middot;</span> 32 + <NuxtLink 33 + external 34 + :href="`https://github.com/npmx-dev/npmx.dev/commit/${buildInfo.commit}`" 35 + target="_blank" 36 + class="hover:text-fg transition-colors" 37 + > 38 + {{ buildInfo.shortCommit }} 39 + </NuxtLink> 40 + </template> 41 + </div> 42 + </template>
+14 -2
app/pages/index.vue
··· 38 38 <header class="flex-1 flex flex-col items-center justify-center text-center py-20"> 39 39 <!-- Animated title --> 40 40 <h1 41 - class="font-mono text-5xl sm:text-7xl md:text-8xl font-medium tracking-tight mb-4 motion-safe:animate-fade-in motion-safe:animate-fill-both" 41 + dir="ltr" 42 + class="flex items-center justify-center gap-2 header-logo font-mono text-5xl sm:text-7xl md:text-8xl font-medium tracking-tight mb-4 motion-safe:animate-fade-in motion-safe:animate-fill-both" 42 43 > 43 - <span class="text-accent"><span class="-tracking-0.2em">.</span>/</span>npmx 44 + <img 45 + aria-hidden="true" 46 + :alt="$t('alt_logo')" 47 + src="/logo.svg" 48 + width="48" 49 + height="48" 50 + class="w-12 h-12 sm:w-20 sm:h-20 md:w-24 md:h-24 rounded-2xl sm:rounded-3xl" 51 + /> 52 + <span class="pb-4">npmx</span> 44 53 </h1> 45 54 46 55 <p ··· 97 106 </div> 98 107 </form> 99 108 </search> 109 + 110 + <!-- Build info badge --> 111 + <BuildEnvironment class="mt-4" /> 100 112 </header> 101 113 102 114 <!-- Popular packages -->
+86
config/env.ts
··· 1 + import Git from 'simple-git' 2 + import * as process from 'node:process' 3 + 4 + export { version } from '../package.json' 5 + 6 + /** 7 + * Environment variable `PULL_REQUEST` provided by Netlify. 8 + * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#git-metadata} 9 + * 10 + * Environment variable `VERCEL_GIT_PULL_REQUEST_ID` provided by Vercel. 11 + * @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_GIT_PULL_REQUEST_ID} 12 + * 13 + * Whether triggered by a GitHub PR 14 + */ 15 + export const isPR = process.env.PULL_REQUEST === 'true' || !!process.env.VERCEL_GIT_PULL_REQUEST_ID 16 + 17 + /** 18 + * Environment variable `BRANCH` provided by Netlify. 19 + * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#git-metadata} 20 + * 21 + * Environment variable `VERCEL_GIT_COMMIT_REF` provided by Vercel. 22 + * @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_GIT_COMMIT_REF} 23 + * 24 + * Git branch 25 + */ 26 + export const gitBranch = process.env.BRANCH || process.env.VERCEL_GIT_COMMIT_REF 27 + 28 + /** 29 + * Environment variable `CONTEXT` provided by Netlify. 30 + * @see {@link https://docs.netlify.com/build/configure-builds/environment-variables/#build-metadata} 31 + * 32 + * Environment variable `VERCEL_ENV` provided by Vercel. 33 + * @see {@link https://vercel.com/docs/environment-variables/system-environment-variables#VERCEL_ENV} 34 + * 35 + * Whether triggered by PR, `deploy-preview` or `dev`. 36 + */ 37 + export const isPreview = 38 + isPR || 39 + process.env.CONTEXT === 'deploy-preview' || 40 + process.env.CONTEXT === 'dev' || 41 + process.env.VERCEL_ENV === 'preview' || 42 + process.env.VERCEL_ENV === 'development' 43 + 44 + const git = Git() 45 + export async function getGitInfo() { 46 + let branch 47 + try { 48 + branch = gitBranch || (await git.revparse(['--abbrev-ref', 'HEAD'])) 49 + } catch { 50 + branch = 'unknown' 51 + } 52 + 53 + let commit 54 + try { 55 + // Netlify: COMMIT_REF, Vercel: VERCEL_GIT_COMMIT_SHA 56 + commit = 57 + process.env.COMMIT_REF || process.env.VERCEL_GIT_COMMIT_SHA || (await git.revparse(['HEAD'])) 58 + } catch { 59 + commit = 'unknown' 60 + } 61 + 62 + let shortCommit 63 + try { 64 + if (commit && commit !== 'unknown') { 65 + shortCommit = commit.slice(0, 7) 66 + } else { 67 + shortCommit = await git.revparse(['--short=7', 'HEAD']) 68 + } 69 + } catch { 70 + shortCommit = 'unknown' 71 + } 72 + 73 + return { branch, commit, shortCommit } 74 + } 75 + 76 + export async function getEnv(isDevelopment: boolean) { 77 + const { commit, shortCommit, branch } = await getGitInfo() 78 + const env = isDevelopment 79 + ? 'dev' 80 + : isPreview 81 + ? 'preview' 82 + : branch === 'main' 83 + ? 'canary' 84 + : 'release' 85 + return { commit, shortCommit, branch, env } as const 86 + }
+3
i18n/locales/en.json
··· 5 5 "description": "A better browser for the npm registry. Search, browse, and explore packages with a modern interface." 6 6 } 7 7 }, 8 + "version": "Version", 9 + "built_at": "built {0}", 10 + "alt_logo": "npmx logo", 8 11 "tagline": "a better browser for the npm registry", 9 12 "non_affiliation_disclaimer": "not affiliated with npm, Inc.", 10 13 "trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
+3
lunaria/files/en-US.json
··· 5 5 "description": "A better browser for the npm registry. Search, browse, and explore packages with a modern interface." 6 6 } 7 7 }, 8 + "version": "Version", 9 + "built_at": "built {0}", 10 + "alt_logo": "npmx logo", 8 11 "tagline": "a better browser for the npm registry", 9 12 "non_affiliation_disclaimer": "not affiliated with npm, Inc.", 10 13 "trademark_disclaimer": "npm is a registered trademark of npm, Inc. This site is not affiliated with npm, Inc.",
+38
modules/build-env.ts
··· 1 + import type { BuildInfo } from '../shared/types' 2 + import { createResolver, defineNuxtModule } from 'nuxt/kit' 3 + import { isCI } from 'std-env' 4 + import { getEnv, version } from '../config/env' 5 + 6 + const { resolve } = createResolver(import.meta.url) 7 + 8 + export default defineNuxtModule({ 9 + meta: { 10 + name: 'npmx:build-env', 11 + }, 12 + async setup(_options, nuxt) { 13 + const { env, commit, shortCommit, branch } = await getEnv(nuxt.options.dev) 14 + 15 + nuxt.options.appConfig = nuxt.options.appConfig || {} 16 + nuxt.options.appConfig.env = env 17 + nuxt.options.appConfig.buildInfo = { 18 + version, 19 + time: +Date.now(), 20 + commit, 21 + shortCommit, 22 + branch, 23 + env, 24 + } satisfies BuildInfo 25 + 26 + nuxt.options.nitro.publicAssets = nuxt.options.nitro.publicAssets || [] 27 + if (env === 'dev') nuxt.options.nitro.publicAssets.unshift({ dir: resolve('../public-dev') }) 28 + else if (env === 'canary' || env === 'preview' || !isCI) 29 + nuxt.options.nitro.publicAssets.unshift({ dir: resolve('../public-staging') }) 30 + }, 31 + }) 32 + 33 + declare module '@nuxt/schema' { 34 + interface AppConfig { 35 + env: BuildInfo['env'] 36 + buildInfo: BuildInfo 37 + } 38 + }
+1 -1
modules/dev.ts modules/oauth.ts
··· 5 5 6 6 export default defineNuxtModule({ 7 7 meta: { 8 - name: 'dev', 8 + name: 'oauth', 9 9 }, 10 10 setup() { 11 11 const nuxt = useNuxt()
+26 -9
nuxt.config.ts
··· 3 3 4 4 export default defineNuxtConfig({ 5 5 modules: [ 6 - function (_, nuxt) { 7 - if (nuxt.options._prepare) { 8 - nuxt.options.pwa ||= {} 9 - nuxt.options.pwa.pwaAssets ||= {} 10 - nuxt.options.pwa.pwaAssets.disabled = true 11 - } 12 - }, 13 6 // Workaround for Nuxt 4.3.0 regression: https://github.com/nuxt/nuxt/issues/34140 14 7 // shared-imports.d.ts pulls in app composables during type-checking of shared context, 15 8 // but the shared context doesn't have access to auto-import globals. ··· 188 181 }, 189 182 190 183 pwa: { 191 - // Disable service worker - only using for asset generation 184 + // Disable service worker 192 185 disable: true, 193 186 pwaAssets: { 194 - config: true, 187 + config: false, 195 188 }, 196 189 manifest: { 197 190 name: 'npmx', ··· 199 192 description: 'A fast, modern browser for the npm registry', 200 193 theme_color: '#0a0a0a', 201 194 background_color: '#0a0a0a', 195 + icons: [ 196 + { 197 + src: 'pwa-64x64.png', 198 + sizes: '64x64', 199 + type: 'image/png', 200 + }, 201 + { 202 + src: 'pwa-192x192.png', 203 + sizes: '192x192', 204 + type: 'image/png', 205 + }, 206 + { 207 + src: 'pwa-512x512.png', 208 + sizes: '512x512', 209 + type: 'image/png', 210 + purpose: 'any', 211 + }, 212 + { 213 + src: 'maskable-icon-512x512.png', 214 + sizes: '512x512', 215 + type: 'image/png', 216 + purpose: 'maskable', 217 + }, 218 + ], 202 219 }, 203 220 }, 204 221
+5
package.json
··· 3 3 "license": "MIT", 4 4 "private": true, 5 5 "type": "module", 6 + "version": "0.0.0", 6 7 "author": { 7 8 "name": "Daniel Roe", 8 9 "email": "daniel@roe.dev", ··· 21 22 "lint:fix": "vite lint --fix && vite fmt", 22 23 "generate": "nuxt generate", 23 24 "npmx-connector": "pnpm --filter npmx-connector dev", 25 + "generate-pwa-icons": "pwa-assets-generator", 24 26 "preview": "nuxt preview", 25 27 "postinstall": "nuxt prepare && simple-git-hooks && pnpm generate:lexicons", 26 28 "generate:lexicons": "lex build --lexicons lexicons --out shared/types/lexicons --clear", ··· 63 65 "sanitize-html": "2.17.0", 64 66 "semver": "7.7.3", 65 67 "shiki": "3.21.0", 68 + "simple-git": "3.30.0", 66 69 "ufo": "1.6.3", 67 70 "valibot": "1.2.0", 68 71 "validate-npm-package-name": "7.0.2", ··· 78 81 "@npm/types": "2.1.0", 79 82 "@nuxt/test-utils": "https://pkg.pr.new/@nuxt/test-utils@1499a48", 80 83 "@playwright/test": "1.58.0", 84 + "@types/node": "24.10.9", 81 85 "@types/sanitize-html": "2.16.0", 82 86 "@types/semver": "7.7.1", 83 87 "@types/validate-npm-package-name": "4.0.2", ··· 100 104 "typescript": "5.9.3", 101 105 "unocss": "66.6.0", 102 106 "unplugin-vue-router": "0.19.2", 107 + "vite-plugin-pwa": "1.2.0", 103 108 "vite-plus": "0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab", 104 109 "vitest": "npm:@voidzero-dev/vite-plus-test@0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab", 105 110 "vitest-environment-nuxt": "1.0.1",
+9
pnpm-lock.yaml
··· 107 107 shiki: 108 108 specifier: 3.21.0 109 109 version: 3.21.0 110 + simple-git: 111 + specifier: 3.30.0 112 + version: 3.30.0 110 113 ufo: 111 114 specifier: 1.6.3 112 115 version: 1.6.3 ··· 147 150 '@playwright/test': 148 151 specifier: 1.58.0 149 152 version: 1.58.0 153 + '@types/node': 154 + specifier: 24.10.9 155 + version: 24.10.9 150 156 '@types/sanitize-html': 151 157 specifier: 2.16.0 152 158 version: 2.16.0 ··· 213 219 unplugin-vue-router: 214 220 specifier: 0.19.2 215 221 version: 0.19.2(@vue/compiler-sfc@3.5.27)(vue-router@4.6.4(vue@3.5.27(typescript@5.9.3)))(vue@3.5.27(typescript@5.9.3)) 222 + vite-plugin-pwa: 223 + specifier: 1.2.0 224 + version: 1.2.0(@vite-pwa/assets-generator@1.0.2)(vite@7.3.1(@types/node@24.10.9)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(yaml@2.8.2))(workbox-build@7.4.0)(workbox-window@7.4.0) 216 225 vite-plus: 217 226 specifier: 0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab 218 227 version: 0.0.0-833c515fa25cef20905a7f9affb156dfa6f151ab(@types/node@24.10.9)(esbuild@0.27.2)(happy-dom@20.4.0)(jiti@2.6.1)(terser@5.46.0)(typescript@5.9.3)(yaml@2.8.2)
public-dev/apple-touch-icon-180x180.png

This is a binary file and will not be displayed.

public-dev/favicon.ico

This is a binary file and will not be displayed.

+12
public-dev/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 2 + <rect width="512" height="512" rx="64" fill="#0a0a0a"/> 3 + <text 4 + x="256" 5 + y="320" 6 + font-family="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" 7 + font-size="280" 8 + font-weight="500" 9 + text-anchor="middle" 10 + fill="#91BA4D" 11 + ><tspan fill="#20461A">.</tspan>/</text> 12 + </svg>
+13
public-dev/logo.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 2 + <rect width="512" height="512" rx="64" fill="#0a0a0a"/> 3 + <rect x="110" y="310" width="60" height="60" fill="#20461A"/> 4 + <text 5 + x="320" 6 + y="370" 7 + font-family="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" 8 + font-size="420" 9 + font-weight="500" 10 + text-anchor="middle" 11 + fill="#91BA4D" 12 + ><tspan>/</tspan></text> 13 + </svg>
public-dev/maskable-icon-512x512.png

This is a binary file and will not be displayed.

public-dev/pwa-192x192.png

This is a binary file and will not be displayed.

public-dev/pwa-512x512.png

This is a binary file and will not be displayed.

public-dev/pwa-64x64.png

This is a binary file and will not be displayed.

public-staging/apple-touch-icon-180x180.png

This is a binary file and will not be displayed.

public-staging/favicon.ico

This is a binary file and will not be displayed.

+12
public-staging/favicon.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 2 + <rect width="512" height="512" rx="64" fill="#0a0a0a"/> 3 + <text 4 + x="256" 5 + y="320" 6 + font-family="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" 7 + font-size="280" 8 + font-weight="500" 9 + text-anchor="middle" 10 + fill="#5AB1CC" 11 + ><tspan fill="#1E4E5E">.</tspan>/</text> 12 + </svg>
+13
public-staging/logo.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 2 + <rect width="512" height="512" rx="64" fill="#0a0a0a"/> 3 + <rect x="110" y="310" width="60" height="60" fill="#1E4E5E"/> 4 + <text 5 + x="320" 6 + y="370" 7 + font-family="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" 8 + font-size="420" 9 + font-weight="500" 10 + text-anchor="middle" 11 + fill="#5AB1CC" 12 + ><tspan>/</tspan></text> 13 + </svg>
public-staging/maskable-icon-512x512.png

This is a binary file and will not be displayed.

public-staging/pwa-192x192.png

This is a binary file and will not be displayed.

public-staging/pwa-512x512.png

This is a binary file and will not be displayed.

public-staging/pwa-64x64.png

This is a binary file and will not be displayed.

public/apple-touch-icon-180x180.png

This is a binary file and will not be displayed.

public/favicon.ico

This is a binary file and will not be displayed.

+13
public/logo.svg
··· 1 + <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"> 2 + <rect width="512" height="512" rx="64" fill="#0a0a0a"/> 3 + <rect x="110" y="310" width="60" height="60" fill="#525252"/> 4 + <text 5 + x="320" 6 + y="370" 7 + font-family="ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace" 8 + font-size="420" 9 + font-weight="500" 10 + text-anchor="middle" 11 + fill="#fafafa" 12 + ><tspan>/</tspan></text> 13 + </svg>
public/maskable-icon-512x512.png

This is a binary file and will not be displayed.

public/pwa-192x192.png

This is a binary file and will not be displayed.

public/pwa-512x512.png

This is a binary file and will not be displayed.

public/pwa-64x64.png

This is a binary file and will not be displayed.

+1 -1
pwa-assets.config.ts
··· 2 2 3 3 export default defineConfig({ 4 4 preset: minimal2023Preset, 5 - images: ['public/favicon.svg'], 5 + images: ['public/logo.svg', 'public-dev/logo.svg', 'public-staging/logo.svg'], 6 6 })
+8
shared/types/env.ts
··· 1 + export interface BuildInfo { 2 + version: string 3 + commit: string 4 + shortCommit: string 5 + time: number 6 + branch: string 7 + env: 'preview' | 'canary' | 'dev' | 'release' 8 + }
+1
shared/types/index.ts
··· 3 3 export * from './dependency-analysis' 4 4 export * from './readme' 5 5 export * from './docs' 6 + export * from './env' 6 7 export * from './deno-doc' 7 8 export * from './i18n-status'
+1 -1
tests/url-compatibility.spec.ts
··· 8 8 // Should show package name 9 9 await expect(page.locator('h1')).toContainText('vue') 10 10 // Should have version badge 11 - await expect(page.locator('text=/v\\d+\\.\\d+/')).toBeVisible() 11 + await expect(page.locator('main header').locator('text=/v\\d+\\.\\d+/')).toBeVisible() 12 12 }) 13 13 14 14 test('/package/@nuxt/kit → scoped package page', async ({ page, goto }) => {