which-nuxt
which-nuxt 0.4.1 reads what any visitor can fetch from a site: the page, public endpoints and /_nuxt/ chunks. It reports the Nuxt and Vue versions, 20 Nuxt modules and the host, then checks the versions against GitHub advisories.
Setup
- ESM only, Node 22 or newer.
require('which-nuxt')throwsERR_PACKAGE_PATH_NOT_EXPORTED. - The
cacheoption takes anunstorageinstance. Installunstoragein your project; under pnpm, the copy insidewhich-nuxtcannot be imported. - CLI:
npx which-nuxt https://example.com. Thewhichnuxtbinary is the same command.
Automatic behaviour
- By default,
detectNuxt(url)sends up to 21 requests to the target: the page, 16 endpoint probes in parallel (/_payload.json,/robots.txt,/sitemap.xml, moduledebug.jsonroutes and more), then up to 4/_nuxt/chunks one at a time.probeEndpoints: falseandscanJs: false(CLI--no-endpoints,--no-js) remove them. Hosting reads the page headers and sends nothing, unlessdomainAge: true(CLI--age) adds one RDAP lookup. checkAdvisories()sends one GitHub API request per package with an exact version or an inferred version range. Module versions read fromdebug.jsonroutes count too, so one scan can cost 5 or more requests.timeoutondetectNuxt(default8000ms) limits each request, not the whole scan.checkAdvisoriestakes its owntimeout(default10000ms).- Each request sends
User-Agent: which-nuxt/<version> (+https://github.com/harlan-zw/which-nuxt).userAgentondetectNuxtreplaces it for the target and RDAP.checkAdvisoriestakes its ownuserAgent. detectNuxtruns every module preset unless you passmoduleDetectors.moduleDetectors: falsekeeps Nuxt and Vue detection only. The README lists the preset arrays. Import a single preset, such asnuxtSitemapModuleDetector, fromwhich-nuxt/modules,which-nuxt/modules/officialorwhich-nuxt/modules/community.- Passing a preset stops
detectNuxtfrom loading the other presets at runtime. A bundler still emits every preset as a lazy chunk, becausedetectNuxtkeeps a dynamic import for its default. For a smaller bundle, usewhich-nuxt/scannerswith explicit presets. - For a batch scan, or a site you do not own, keep the default User-Agent so the owner can identify you. Set
probeEndpoints: falseunless you need the endpoint signals; the probes include/__nuxt_content/content/sql_dump.txtand module debug routes. - Nuxt SEO (
@nuxtjs/seo) is inferred when 3 of its child modules are found. OnlydetectNuxtruns this inference.
Common tasks
Gate CI on advisories
The CLI exits 0 after every scan. That includes "Nuxt not detected", a failed fetch and matched advisories. Gate on the result instead.
import process from 'node:process'
import { checkAdvisories, detectNuxt } from 'which-nuxt'
const detected = await detectNuxt('https://example.com')
if (!detected.isNuxt)
throw new Error(`Nuxt not detected. ${detected.errors.join('; ')}`)
if (detected.incomplete)
throw new Error(`Scan incomplete: ${detected.errors.join('; ')}`)
const advisories = await checkAdvisories(detected, { token: process.env.GITHUB_TOKEN })
if (advisories.incomplete)
throw new Error(`Advisory check failed: ${advisories.errors.join('; ')}`)
const unchecked = advisories.packages.filter(pkg => pkg.skipped).map(pkg => pkg.name)
if (unchecked.length > 0)
console.warn(`No exact version, not checked: ${unchecked.join(', ')}`)
const serious = advisories.matches.filter(match => ['high', 'critical'].includes(match.severity.toLowerCase()))
for (const match of serious)
console.error(`${match.packageName}@${match.version ?? match.versionRange}: ${match.severity} ${match.ghsaId} ${match.url}`)
if (serious.length > 0)
process.exitCode = 1The gate also fails on a matchType: 'possible' match, which comes from an inferred version range. Filter on match.matchType === 'exact' to fail only on exact versions. It fails on an incomplete scan, because a timed out chunk can hide the Nuxt version.
From a shell, run which-nuxt https://example.com --json and apply the same checks to { detected, advisories }. advisories is null with --no-advisories. Never parse the human output: under CI it changes from a box to plain [info] lines.
Scan HTML you already fetched
Pass the page as { html, url, headers } and turn off both network steps. Without them, a url makes detectNuxt send up to 20 requests to that site.
import { detectNuxt } from 'which-nuxt'
const response = await fetch('https://example.com/')
const detected = await detectNuxt(
{
html: await response.text(),
url: response.url,
headers: Object.fromEntries(response.headers),
},
{ scanJs: false, probeEndpoints: false },
)
console.log(detected.isNuxt, detected.modules.map(module => module.packageName), detected.hosting?.provider)scanHtml and scanJs from which-nuxt/scanners never fetch. They return signals, packages and modules, with no isNuxt verdict.
Cache results
import type { DetectCacheArtifact } from 'which-nuxt'
import { createStorage } from 'unstorage'
import memoryDriver from 'unstorage/drivers/memory'
import { detectNuxt } from 'which-nuxt'
const cache = createStorage<DetectCacheArtifact>({ driver: memoryDriver() })
const ONE_HOUR = 60 * 60 * 1000
let result = await detectNuxt('https://example.com', { cache, cacheMaxAge: ONE_HOUR })
if (result.incomplete)
result = await detectNuxt('https://example.com', { cache, cacheMaxAge: ONE_HOUR, cacheBust: true })
console.log(result.cache)Traps
- A target without
http://orhttps://is scanned as HTML.detectNuxt('nuxt.com')andwhich-nuxt nuxt.comreport "Nuxt not detected" with no error and no request. detectNuxtdoes not throw on a network failure. An unreachable site givesisNuxt: false,incomplete: true,errors: ['fetch failed']. Readerrorsbefore you report "not Nuxt".{ html, url }input anddetectNuxt(html, { url })still probe that URL. See Scan HTML you already fetched.- Header keys must be lowercase.
headers: { 'CF-Ray': 'abc' }giveshosting.provider: null;{ 'cf-ray': 'abc' }givesCloudflare. scanHtmlandscanJsfind no modules withoutmoduleDetectors. UnlikedetectNuxt, they do not default to every preset.moduleDetectorstakes an array. Pass[nuxtSitemapModuleDetector], never the preset alone.cacheMaxAgeis milliseconds.cacheMaxAge: 3600expires after 3.6 seconds.- An incomplete result is cached. A timed out chunk is replayed on every hit until a
cacheBust: truescan. - The README cache example fails strict type checking: plain
createStorage()returnsStorage<StorageValue>. UsecreateStorage<DetectCacheArtifact>(). - The CLI prints
✔ No advisories matched exact detected package versions.even when every GitHub request failed. The failure goes to stderr:WARN GitHub advisory request failed for nuxt: 401. - No match can mean no check. A package with neither an exact version nor a version range is skipped:
skipped: true,reason: 'No exact valid semver version detected.'. The Vue version can be missing, so Vue then goes unchecked. - Without a token, GitHub allows 60 API requests an hour. Batch scans run out fast, and each failure lands in
advisories.errorsasGitHub advisory request failed for nuxt: 403. The CLI readsGITHUB_TOKENor--github-token; the library reads onlytoken. PreferGITHUB_TOKEN: a--github-tokenvalue shows in shell history andpsoutput. --timeoutand--max-js-requeststake positive integers.--timeout 1.5exits1with a stack trace.
Config
The README lists the CLI flags. The DetectOptions and AdvisoryOptions types hold the library options, such as maxJsBytes and includePackages. Each function takes its own fetch to route its requests through your client.