diff --git a/modules/runtime/server/cache.ts b/modules/runtime/server/cache.ts index 9e35b46b1a..800e17a007 100644 --- a/modules/runtime/server/cache.ts +++ b/modules/runtime/server/cache.ts @@ -28,6 +28,7 @@ const FIXTURE_PATHS = { esmTypes: 'esm-sh:types', githubContributors: 'github:contributors.json', githubContributorsStats: 'github:contributors-stats.json', + jsdelivr: 'jsdelivr', } as const type FixtureType = keyof typeof FIXTURE_PATHS @@ -101,12 +102,8 @@ function parseScopedPackageWithVersion(input: string): { name: string; version?: } function getMockForUrl(url: string): MockResult | null { - let urlObj: URL - try { - urlObj = new URL(url) - } catch { - return null - } + const urlObj = URL.parse(url) + if (!urlObj) return null const { host, pathname, searchParams } = urlObj @@ -163,33 +160,6 @@ function getMockForUrl(url: string): MockResult | null { } } - // jsdelivr CDN - return 404 for README files, etc. - if (host === 'cdn.jsdelivr.net') { - // Return null data which will cause a 404 - README files are optional - return { data: null } - } - - // jsdelivr data API - return mock file listing - if (host === 'data.jsdelivr.com') { - const packageMatch = decodeURIComponent(pathname).match(/^\/v1\/packages\/npm\/(.+)$/) - if (packageMatch?.[1]) { - const pkgWithVersion = packageMatch[1] - const parsed = parseScopedPackageWithVersion(pkgWithVersion) - return { - data: { - type: 'npm', - name: parsed.name, - version: parsed.version || 'latest', - files: [ - { name: 'package.json', hash: 'abc123', size: 1000 }, - { name: 'index.js', hash: 'def456', size: 500 }, - { name: 'README.md', hash: 'ghi789', size: 2000 }, - ], - }, - } - } - } - // Gravatar API - return 404 (avatars not needed in tests) if (host === 'www.gravatar.com') { return { data: null } @@ -385,12 +355,8 @@ async function handleFastNpmMeta( url: string, storage: ReturnType, ): Promise { - let urlObj: URL - try { - urlObj = new URL(url) - } catch { - return null - } + const urlObj = URL.parse(url) + if (!urlObj) return null const { host, pathname, searchParams } = urlObj @@ -430,12 +396,8 @@ async function handleGitHubApi( url: string, storage: ReturnType, ): Promise { - let urlObj: URL - try { - urlObj = new URL(url) - } catch { - return null - } + const urlObj = URL.parse(url) + if (!urlObj) return null const { host, pathname } = urlObj @@ -486,12 +448,8 @@ interface FixtureMatchWithVersion extends FixtureMatch { } function matchUrlToFixture(url: string): FixtureMatchWithVersion | null { - let urlObj: URL - try { - urlObj = new URL(url) - } catch { - return null - } + const urlObj = URL.parse(url) + if (!urlObj) return null const { host, pathname, searchParams } = urlObj @@ -571,6 +529,42 @@ function logUnmockedRequest(type: string, detail: string, url: string): void { ) } +async function handleJsdelivrDataApi( + url: string, + storage: ReturnType, +): Promise { + const urlObj = URL.parse(url) + if (!urlObj) return null + + if (urlObj.host !== 'data.jsdelivr.com') return null + + const packageMatch = decodeURIComponent(urlObj.pathname).match(/^\/v1\/packages\/npm\/(.+)$/) + if (!packageMatch?.[1]) return null + + const parsed = parseScopedPackageWithVersion(packageMatch[1]) + + // Try per-package fixture first + const fixturePath = getFixturePath('jsdelivr', parsed.name) + const fixture = await storage.getItem(fixturePath) + if (fixture) { + return { data: fixture } + } + + // Fall back to generic stub (no declaration files) + return { + data: { + type: 'npm', + name: parsed.name, + version: parsed.version || 'latest', + files: [ + { name: 'package.json', hash: 'abc123', size: 1000 }, + { name: 'index.js', hash: 'def456', size: 500 }, + { name: 'README.md', hash: 'ghi789', size: 2000 }, + ], + }, + } +} + /** * Shared fixture-backed fetch implementation. * This is used by both cachedFetch and the global $fetch override. @@ -593,6 +587,12 @@ async function fetchFromFixtures( return { data: fastNpmMetaResult.data as T, isStale: false, cachedAt: Date.now() } } + const jsdelivrResult = await handleJsdelivrDataApi(url, storage) + if (jsdelivrResult) { + if (VERBOSE) process.stdout.write(`[test-fixtures] jsDelivr Data API: ${url}\n`) + return { data: jsdelivrResult.data as T, isStale: false, cachedAt: Date.now() } + } + // Check for GitHub API const githubResult = await handleGitHubApi(url, storage) if (githubResult) { diff --git a/server/api/registry/analysis/[...pkg].get.ts b/server/api/registry/analysis/[...pkg].get.ts index 940bc25925..3c23102dfd 100644 --- a/server/api/registry/analysis/[...pkg].get.ts +++ b/server/api/registry/analysis/[...pkg].get.ts @@ -3,15 +3,9 @@ import { PackageRouteParamsSchema } from '#shared/schemas/package' import type { PackageAnalysis, ExtendedPackageJson, - TypesPackageInfo, CreatePackageInfo, } from '#shared/utils/package-analysis' -import { - analyzePackage, - getTypesPackageName, - getCreatePackageName, - hasBuiltInTypes, -} from '#shared/utils/package-analysis' +import { analyzePackage, getCreatePackageName } from '#shared/utils/package-analysis' import { getDevDependencySuggestion, type DevDependencySuggestion, @@ -23,13 +17,8 @@ import { } from '#shared/utils/constants' import { parseRepoUrl } from '#shared/utils/git-providers' import { encodePackageName } from '#shared/utils/npm' -import { flattenFileTree } from '#server/utils/import-resolver' -import { getPackageFileTree } from '#server/utils/file-tree' -import { getLatestVersion, getLatestVersionBatch } from 'fast-npm-meta' - -interface AnalysisPackageJson extends ExtendedPackageJson { - readme?: string -} +import { fetchPackageWithTypesAndFiles } from '#server/utils/file-tree' +import { getLatestVersionBatch } from 'fast-npm-meta' export default defineCachedEventHandler( async event => { @@ -44,38 +33,7 @@ export default defineCachedEventHandler( packageName: decodeURIComponent(rawPackageName), version: rawVersion, }) - - // Fetch package data - const encodedName = encodePackageName(packageName) - const versionSuffix = version ? `/${version}` : '/latest' - const pkg = await $fetch( - `${NPM_REGISTRY}/${encodedName}${versionSuffix}`, - ) - - let typesPackage: TypesPackageInfo | undefined - let files: Set | undefined - - // Only check for @types and files when the package doesn't ship its own types - if (!hasBuiltInTypes(pkg)) { - const typesPkgName = getTypesPackageName(packageName) - const resolvedVersion = pkg.version ?? version ?? 'latest' - - // Fetch @types info and file tree in parallel — they are independent - const [typesResult, fileTreeResult] = await Promise.allSettled([ - fetchTypesPackageInfo(typesPkgName), - getPackageFileTree(packageName, resolvedVersion), - ]) - - if (typesResult.status === 'fulfilled') { - typesPackage = typesResult.value - } - if (fileTreeResult.status === 'fulfilled') { - files = flattenFileTree(fileTreeResult.value.tree) - } - } - - // Check for associated create-* package (e.g., vite -> create-vite, next -> create-next-app) - // Only show if the packages are actually associated (same maintainers or same org) + const { pkg, typesPackage, files } = await fetchPackageWithTypesAndFiles(packageName, version) const createPackage = await findAssociatedCreatePackage(packageName, pkg) const analysis = analyzePackage(pkg, { typesPackage, @@ -107,21 +65,6 @@ export default defineCachedEventHandler( }, ) -/** - * Fetch @types package info including deprecation status using fast-npm-meta. - * Returns undefined if the package doesn't exist. - */ -async function fetchTypesPackageInfo(packageName: string): Promise { - const result = await getLatestVersion(packageName, { metadata: true, throw: false }) - if ('error' in result) { - return undefined - } - return { - packageName, - deprecated: result.deprecated, - } -} - /** Package metadata needed for association validation */ interface PackageWithMeta { maintainers?: Array<{ name: string }> diff --git a/server/api/registry/badge/[type]/[...pkg].get.ts b/server/api/registry/badge/[type]/[...pkg].get.ts index 5b4033e642..e94a61c37c 100644 --- a/server/api/registry/badge/[type]/[...pkg].get.ts +++ b/server/api/registry/badge/[type]/[...pkg].get.ts @@ -6,6 +6,7 @@ import { PackageRouteParamsSchema } from '#shared/schemas/package' import { CACHE_MAX_AGE_ONE_HOUR, ERROR_NPM_FETCH_FAILED } from '#shared/utils/constants' import { fetchNpmPackage } from '#server/utils/npm' import { assertValidPackageName } from '#shared/utils/npm' +import { fetchPackageWithTypesAndFiles } from '#server/utils/file-tree' import { handleApiError } from '#server/utils/error-handler' const NPM_DOWNLOADS_API = 'https://api.npmjs.org/downloads/point' @@ -372,12 +373,46 @@ const badgeStrategies = { return { label: 'node', value: nodeVersion, color: COLORS.yellow } }, - 'types': async (pkgData: globalThis.Packument) => { - const latest = getLatestVersion(pkgData) - const versionData = latest ? pkgData.versions?.[latest] : undefined - const hasTypes = !!(versionData?.types || versionData?.typings) - const value = hasTypes ? 'included' : 'missing' - const color = hasTypes ? COLORS.blue : COLORS.slate + 'types': async (pkgData: globalThis.Packument, requestedVersion?: string) => { + const targetVersion = requestedVersion ?? getLatestVersion(pkgData) + const versionData = targetVersion ? pkgData.versions?.[targetVersion] : undefined + + if (versionData && hasBuiltInTypes(versionData)) { + return { label: 'types', value: 'included', color: COLORS.blue } + } + + const { pkg, typesPackage, files } = await fetchPackageWithTypesAndFiles( + pkgData.name, + targetVersion, + ) + + const typesStatus = detectTypesStatus(pkg, typesPackage, files) + + let value: string + let color: string + + switch (typesStatus.kind) { + case 'included': + value = 'included' + color = COLORS.blue + break + + case '@types': + value = '@types' + color = COLORS.purple + if (typesStatus.deprecated) { + value += ' (deprecated)' + color = COLORS.red + } + break + + case 'none': + default: + value = 'missing' + color = COLORS.slate + break + } + return { label: 'types', value, color } }, diff --git a/server/utils/file-tree.ts b/server/utils/file-tree.ts index 76bbf259ea..50e21d439c 100644 --- a/server/utils/file-tree.ts +++ b/server/utils/file-tree.ts @@ -1,3 +1,7 @@ +import { getLatestVersion } from 'fast-npm-meta' +import { flattenFileTree } from '#server/utils/import-resolver' +import type { ExtendedPackageJson, TypesPackageInfo } from '#shared/utils/package-analysis' + /** * Fetch the file tree from jsDelivr API. * Returns a nested tree structure of all files in the package. @@ -83,3 +87,62 @@ export async function getPackageFileTree( tree, } } + +/** + * Fetch @types package info including deprecation status using fast-npm-meta. + * Returns undefined if the package doesn't exist. + */ +async function fetchTypesPackageInfo(packageName: string): Promise { + const result = await getLatestVersion(packageName, { metadata: true, throw: false }) + if ('error' in result) { + return undefined + } + return { + packageName, + deprecated: result.deprecated, + } +} + +interface AnalysisPackageJson extends ExtendedPackageJson { + readme?: string +} + +export async function fetchPackageWithTypesAndFiles( + packageName: string, + version?: string, +): Promise<{ + pkg: AnalysisPackageJson + typesPackage?: TypesPackageInfo + files?: Set +}> { + // Fetch main package data + const encodedName = encodePackageName(packageName) + const versionSuffix = version ? `/${version}` : '/latest' + + const pkg = await $fetch(`${NPM_REGISTRY}/${encodedName}${versionSuffix}`) + + let typesPackage: TypesPackageInfo | undefined + let files: Set | undefined + + // Only attempt to fetch @types + file tree when the package doesn't ship its own types + if (!hasBuiltInTypes(pkg)) { + const typesPkgName = getTypesPackageName(packageName) + const resolvedVersion = pkg.version ?? version ?? 'latest' + + // Fetch both in parallel — they're independent + const [typesResult, fileTreeResult] = await Promise.allSettled([ + fetchTypesPackageInfo(typesPkgName), + getPackageFileTree(packageName, resolvedVersion), + ]) + + if (typesResult.status === 'fulfilled') { + typesPackage = typesResult.value + } + + if (fileTreeResult.status === 'fulfilled') { + files = flattenFileTree(fileTreeResult.value.tree) + } + } + + return { pkg, typesPackage, files } +} diff --git a/shared/utils/package-analysis.ts b/shared/utils/package-analysis.ts index 18908c3832..d9c0a39f15 100644 --- a/shared/utils/package-analysis.ts +++ b/shared/utils/package-analysis.ts @@ -324,19 +324,10 @@ export function detectTypesStatus( typesPackageInfo?: TypesPackageInfo, files?: Set, ): TypesStatus { - // Check for built-in types - if (pkg.types || pkg.typings) { + if (hasBuiltInTypes(pkg)) { return { kind: 'included' } } - // Check exports field for types - if (pkg.exports) { - const exportInfo = analyzeExports(pkg.exports) - if (exportInfo.hasTypes) { - return { kind: 'included' } - } - } - // Check for implicit types by deriving expected declaration file paths from // entry points (e.g. .d.mts for .mjs) and checking if they exist in the package if (files && hasImplicitTypesForEntryPoints(pkg, files)) { diff --git a/test/e2e/badge.spec.ts b/test/e2e/badge.spec.ts index 4cf8a33d37..5e57d6cb47 100644 --- a/test/e2e/badge.spec.ts +++ b/test/e2e/badge.spec.ts @@ -56,12 +56,12 @@ test.describe('badge API', () => { }) test('explicit version badge renders successfully', async ({ page, baseURL }) => { - const url = toLocalUrl(baseURL, `/api/registry/badge/${type}/nuxt/v/3.12.0`) + const url = toLocalUrl(baseURL, `/api/registry/badge/${type}/nuxt/v/3.21.0`) const { response, body } = await fetchBadge(page, url) expect(response.status()).toBe(200) if (type === 'version') { - expect(body).toContain('v3.12.0') + expect(body).toContain('v3.21.0') } }) @@ -100,6 +100,25 @@ test.describe('badge API', () => { expect(body).toContain('active') }) + + test('types badge shows @types badge', async ({ page, baseURL }) => { + const url = toLocalUrl(baseURL, '/api/registry/badge/types/is-odd') + const { body } = await fetchBadge(page, url) + + expect(body).toContain('@types') + expect(body).not.toContain('missing') + }) + + test('types badge shows included badge when types not declared explicitly', async ({ + page, + baseURL, + }) => { + const url = toLocalUrl(baseURL, '/api/registry/badge/types/nano-stringify-object') + const { body } = await fetchBadge(page, url) + + expect(body).toContain('included') + expect(body).not.toContain('missing') + }) }) test('custom labelColor parameter is applied to SVG', async ({ page, baseURL }) => { diff --git a/test/fixtures/jsdelivr/nano-stringify-object.json b/test/fixtures/jsdelivr/nano-stringify-object.json new file mode 100644 index 0000000000..88617d6be4 --- /dev/null +++ b/test/fixtures/jsdelivr/nano-stringify-object.json @@ -0,0 +1,48 @@ +{ + "type": "npm", + "name": "nano-stringify-object", + "version": "0.0.0", + "default": null, + "files": [ + { + "type": "directory", + "name": "dist", + "files": [ + { + "type": "file", + "name": "index.d.mts", + "hash": "miLdJwbJG1quJbUdHplSpmv+/7F8rHZM+kVXXRdz9W0=", + "size": 1192 + }, + { + "type": "file", + "name": "index.mjs", + "hash": "jznEg84ts4a5VCzdpfQViYnTBWoREZeolfbHUVtAymA=", + "size": 5731 + } + ] + }, + { + "type": "file", + "name": "LICENSE", + "hash": "GxzKussvQB30SBOW+2S33DnejHW8yBDc3jxk/WDXyzM=", + "size": 1221 + }, + { + "type": "file", + "name": "package.json", + "hash": "1Z382Cx0/ZycKM2TUBZlLWHt/RB/z00gOfwkt8WyPG4=", + "size": 639 + }, + { + "type": "file", + "name": "README.md", + "hash": "47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=", + "size": 0 + } + ], + "links": { + "stats": "https://data.jsdelivr.com/v1/stats/packages/npm/nano-stringify-object@0.0.0", + "entrypoints": "https://data.jsdelivr.com/v1/packages/npm/nano-stringify-object@0.0.0/entrypoints" + } +} diff --git a/test/fixtures/mock-routes.cjs b/test/fixtures/mock-routes.cjs index bd5527246b..41edafcc6f 100644 --- a/test/fixtures/mock-routes.cjs +++ b/test/fixtures/mock-routes.cjs @@ -422,6 +422,12 @@ function matchJsdelivrDataApi(urlString) { const packageMatch = pathname.match(/^\/v1\/packages\/npm\/(.+)$/) if (packageMatch && packageMatch[1]) { const parsed = parseScopedPackage(packageMatch[1]) + + const fixture = readFixture(`jsdelivr/${parsed.name}.json`) + if (fixture) { + return json(fixture) + } + return json({ type: 'npm', name: parsed.name, diff --git a/test/fixtures/npm-registry/packuments/@types/is-odd.json b/test/fixtures/npm-registry/packuments/@types/is-odd.json new file mode 100644 index 0000000000..327bb2817f --- /dev/null +++ b/test/fixtures/npm-registry/packuments/@types/is-odd.json @@ -0,0 +1,341 @@ +{ + "_id": "@types/is-odd", + "_rev": "357-6aea66bbc40695119f00465c0fa53de3", + "name": "@types/is-odd", + "dist-tags": { + "ts2.0": "3.0.0", + "ts2.1": "3.0.0", + "ts2.2": "3.0.0", + "ts2.3": "3.0.0", + "ts2.4": "3.0.0", + "ts2.5": "3.0.0", + "ts2.6": "3.0.0", + "ts2.7": "3.0.0", + "ts2.8": "3.0.0", + "ts2.9": "3.0.0", + "ts3.0": "3.0.0", + "ts3.1": "3.0.0", + "ts3.2": "3.0.0", + "ts3.3": "3.0.0", + "ts3.4": "3.0.0", + "ts3.5": "3.0.0", + "ts3.6": "3.0.0", + "ts3.7": "3.0.0", + "ts3.8": "3.0.1", + "ts3.9": "3.0.1", + "ts4.0": "3.0.1", + "ts4.1": "3.0.1", + "ts4.2": "3.0.1", + "ts4.3": "3.0.1", + "ts4.4": "3.0.1", + "ts5.8": "3.0.4", + "ts5.7": "3.0.4", + "latest": "3.0.4", + "ts4.5": "3.0.4", + "ts4.6": "3.0.4", + "ts4.7": "3.0.4", + "ts4.8": "3.0.4", + "ts4.9": "3.0.4", + "ts5.0": "3.0.4", + "ts5.1": "3.0.4", + "ts5.2": "3.0.4", + "ts5.3": "3.0.4", + "ts5.4": "3.0.4", + "ts5.5": "3.0.4", + "ts5.9": "3.0.4", + "ts5.6": "3.0.4", + "ts6.0": "3.0.4" + }, + "versions": { + "3.0.0": { + "name": "@types/is-odd", + "version": "3.0.0", + "license": "MIT", + "_id": "@types/is-odd@3.0.0", + "maintainers": [ + { + "name": "types", + "email": "ts-npm-types@microsoft.com" + } + ], + "contributors": [ + { + "url": "https://github.com/adamzerella", + "name": " Adam Zerella", + "githubUsername": "adamzerella" + } + ], + "dist": { + "shasum": "861b26fa31e656354bb05d63254091f734c461b5", + "tarball": "https://registry.npmjs.org/@types/is-odd/-/is-odd-3.0.0.tgz", + "fileCount": 4, + "integrity": "sha512-XfoBBgqc9DYRWo0V6clOC9cDy9KYELqgWwyulwCkv8/6Hlnd25XbDXGGnWYqOr3GUYIwqp4/gqm7/RtCZvqOJA==", + "signatures": [ + { + "sig": "MEUCIQDCb3RyeOCBQWvvgk/slsNiLZrw352GCFOXn7BvVT+33gIgfYjvWpa8EI/H67e8ndZkZLz93oN/dRVTLqnnm1d/1bM=", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA" + } + ], + "unpackedSize": 2712, + "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.4\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJccLErCRA9TVsSAnZWagAAQhcP/2Ab+nx4IOo4BGdIySnD\nBONPypFkgSzUa29B81x0SYuFLk9M6lgVhcHLM6s62/HlTDMgT6MHFJdaIHcg\n9xvm/ROaBeKxKcm/iP2yS8OzMZ4GTzNc8t/r1w1GPrUGw5d/3GuEPktSlA3o\nUoVg9tc+QIMog46UGmcXARX6Hv81UGFqHUiV0JgRWQNLvTlUyXFpcpE1esyB\njumZDFuH+E9Bcj/k+DYy+G12lKhMHmTwLOMBN5YI9Cl3dNgnujcFbpKXCf3s\nYBKxjrWblz1l3eT1hk24KRhpOnUm262Y/v1axSeA2l3xxO/feg7MWf9V0fHZ\ndVDa4QBK+w2yt+0rNLJ0I0EqOxk7lMnxLRbbbBRSf4iwiwrzOVAxqwCCG2MW\nRj6ZAXf0/girV4/tXxFZX6maZVWXRiEEKRVWOFuHPAozp6Sdl8dpB8IXjKn6\n4vbGfDqqN2VMLPAfTtevEaZJDM+1/cnpcJSYRFbuzxxG1a9XU75hWNMzOvDZ\n/YhAmtTxNrNNux9DDVtifg0VoIBbIMUZ90/alhaczf0P2yvA5pJ2ktPHumSh\ne0RaiSu70C+ye2MHraEV9/uBoq+U1/ct6XPt1iYE1vQqtc+jf3nBazSDumpA\n2UXV4kmbHCUgg1a/a7xQH7lRgip+Sx776sv3ljRWmb2QUdlzI1gbipsUIZAA\n08K4\r\n=/LEL\r\n-----END PGP SIGNATURE-----\r\n" + }, + "main": "", + "types": "index", + "scripts": {}, + "_npmUser": { + "name": "types", + "email": "ts-npm-types@microsoft.com" + }, + "repository": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "type": "git", + "directory": "types/is-odd" + }, + "description": "TypeScript definitions for is-odd", + "directories": {}, + "dependencies": {}, + "_hasShrinkwrap": false, + "typeScriptVersion": "2.0", + "_npmOperationalInternal": { + "tmp": "tmp/is-odd_3.0.0_1550889259065_0.09125334321631584", + "host": "s3://npm-registry-packages" + }, + "typesPublisherContentHash": "1dc04627cea88eece89c95a0aed7905067a849634ee4f4bc84b23fa135d0dd02" + }, + "3.0.1": { + "name": "@types/is-odd", + "version": "3.0.1", + "license": "MIT", + "_id": "@types/is-odd@3.0.1", + "maintainers": [ + { + "name": "types", + "email": "ts-npm-types@microsoft.com" + } + ], + "contributors": [ + { + "url": "https://github.com/DefinitelyTyped", + "name": "DefinitelyTyped", + "githubUsername": "DefinitelyTyped" + } + ], + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/is-odd", + "dist": { + "shasum": "b43a16362855c86db3c4df59e36b6eb3e44f555b", + "tarball": "https://registry.npmjs.org/@types/is-odd/-/is-odd-3.0.1.tgz", + "fileCount": 4, + "integrity": "sha512-mY+bL1FNKoF6xEN87ayz2Xk1O5L5L4v0KKheja7bWcqTD3je1w0nYzDVJ3W/bTtzkHJuqEb0UAFANtF5cI2wBg==", + "signatures": [ + { + "sig": "MEUCIQCIF54WCx1KAczN/AuO1k/pC1b6pj4o1a6FVbyunJ51BgIgcqi0fG9vJU1hms3h9IUwrrMJVbBMNlH+G4vPbyDsUf4=", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA" + } + ], + "unpackedSize": 3272, + "npm-signature": "-----BEGIN PGP SIGNATURE-----\r\nVersion: OpenPGP.js v3.0.13\r\nComment: https://openpgpjs.org\r\n\r\nwsFcBAEBCAAQBQJhxQ2wCRA9TVsSAnZWagAAPhsP/3HOZ/uL7BMK+rZAXIdE\n0wy77RIsTo/+ZCWlJ3MNPWocIBi+eVfFmRkEIxnD3u9+EMOjZl10Q3rKlX/D\n6Zu4PDIBIUGU0p6WqdOqAMxHIGbehid+DG8RbgdcZw72QDCztPzPEwYDd3lE\nuaANy3qdPDdN5knlN0QvrJuiU9I++bCgEBWIXuiGTddeQOQXbitEBP7D7Q0E\n+PRYWlGXUjb3YiJgrZJfemEUfvntoqgOa1mPsM9crfo4+tzb5q8H4AEUhpd3\nZyYNxRZKsEN1MpA/i8KjwdO4P2zFofFgvmw/umU2+7/ACD86lYwpII/N26VB\nBlWaZXOPKxsX1uHZSmSI2Z1d6uE6PVSNP9R2fq7KuMWOkhnTb/U39uYBlJHE\nW0SCmMKIz/+TQP83sIH7LKBZmp4hft6UxIoL2LkOTStp7ibAw+rCbPA18NPl\nfMdcCQuHoY2bNHYa87/0ecOFke8JKLkI3DY1AwK+6h9WIX9cOV4MJXl/hXhG\nQiL7NIZ25XfWbqBTfrCtHInxd0e8X1hvR7OytDsyQ6ruAmZqWqNWIjhcLSYL\nGS9wahiPysJcRBxvKiYAfhd8wwUmi/ObbczqygNJqNMcoUZLzNd+AHZSnmlv\nYgU6p6lDs4aOeAcG7moR/+0Pe+OpZwnE0ngkQGLisG/YVayO+cBHLorBkQvc\nBTFo\r\n=oVZZ\r\n-----END PGP SIGNATURE-----\r\n" + }, + "main": "", + "types": "index.d.ts", + "scripts": {}, + "_npmUser": { + "name": "types", + "email": "ts-npm-types@microsoft.com" + }, + "repository": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "type": "git", + "directory": "types/is-odd" + }, + "description": "TypeScript definitions for is-odd", + "directories": {}, + "dependencies": {}, + "_hasShrinkwrap": false, + "typeScriptVersion": "3.8", + "_npmOperationalInternal": { + "tmp": "tmp/is-odd_3.0.1_1640304047958_0.02408279906546351", + "host": "s3://npm-registry-packages" + }, + "typesPublisherContentHash": "cfeed0da2d9c42717cef03273f341ce6f4edd16f2adf2bacbf13bc82214bfca7" + }, + "3.0.2": { + "name": "@types/is-odd", + "version": "3.0.2", + "license": "MIT", + "_id": "@types/is-odd@3.0.2", + "maintainers": [ + { + "name": "types", + "email": "ts-npm-types@microsoft.com" + } + ], + "contributors": [ + { + "url": "https://github.com/DefinitelyTyped", + "name": "DefinitelyTyped", + "githubUsername": "DefinitelyTyped" + } + ], + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/is-odd", + "dist": { + "shasum": "064ccdfef0cba1f8597e2ec0eba0802fc6aba743", + "tarball": "https://registry.npmjs.org/@types/is-odd/-/is-odd-3.0.2.tgz", + "fileCount": 5, + "integrity": "sha512-a/mE50FokBkPY86oocj/oorgDaYjT+l2EGSVLwbAUFwjOpFFDlbqHW5I9FS0cISLxmuDRr1J26znAy88UkHUbg==", + "signatures": [ + { + "sig": "MEUCIC7Z/Qq4ZJJjg5ibpVZWQu1VD8LS/arVBl5awUSBOtRqAiEA20t3jhV+JVYMlUF3fwMl8KneYzDO9+HRYTJ9mQ7WXPY=", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA" + } + ], + "unpackedSize": 3276 + }, + "main": "", + "types": "index.d.ts", + "scripts": {}, + "_npmUser": { + "name": "types", + "email": "ts-npm-types@microsoft.com" + }, + "repository": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "type": "git", + "directory": "types/is-odd" + }, + "description": "TypeScript definitions for is-odd", + "directories": {}, + "dependencies": {}, + "_hasShrinkwrap": false, + "typeScriptVersion": "4.5", + "_npmOperationalInternal": { + "tmp": "tmp/is-odd_3.0.2_1695740874873_0.09491697275033473", + "host": "s3://npm-registry-packages" + }, + "typesPublisherContentHash": "f9b12a308df62402e8e0f8a8b33f699ed9f75f869afce468cfeafb9f8714df4e" + }, + "3.0.3": { + "name": "@types/is-odd", + "version": "3.0.3", + "license": "MIT", + "_id": "@types/is-odd@3.0.3", + "maintainers": [ + { + "name": "types", + "email": "ts-npm-types@microsoft.com" + } + ], + "contributors": [], + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/is-odd", + "dist": { + "shasum": "a60696d7d28b6795522cc757f4bbf0d0250bfa59", + "tarball": "https://registry.npmjs.org/@types/is-odd/-/is-odd-3.0.3.tgz", + "fileCount": 5, + "integrity": "sha512-YdKKQ4CTGEfvdO+MWIPj/DLA3RZMJ9yOJOyLmq0nmN2FrKg6WQ5zynhlmjP7vnfYBTHNp/E9BJmBDsvw0/on/A==", + "signatures": [ + { + "sig": "MEQCIGg7zdX3mlxczMdanHMrScYDLSnyDf6TL9b0DLRRkoWVAiBZYASSD5iCM3xRTS767+Gw41jQnDkIeK0rCHuy1SjgIw==", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA" + } + ], + "unpackedSize": 2576 + }, + "main": "", + "types": "index.d.ts", + "scripts": {}, + "_npmUser": { + "name": "types", + "email": "ts-npm-types@microsoft.com" + }, + "repository": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "type": "git", + "directory": "types/is-odd" + }, + "description": "TypeScript definitions for is-odd", + "directories": {}, + "dependencies": {}, + "_hasShrinkwrap": false, + "typeScriptVersion": "4.5", + "_npmOperationalInternal": { + "tmp": "tmp/is-odd_3.0.3_1697606685662_0.7307845211597903", + "host": "s3://npm-registry-packages" + }, + "typesPublisherContentHash": "f0888f66f4772459e29e716f6016cdaacfc7731573b71c46d23ded2554bd7a73" + }, + "3.0.4": { + "name": "@types/is-odd", + "version": "3.0.4", + "license": "MIT", + "_id": "@types/is-odd@3.0.4", + "maintainers": [ + { + "name": "types", + "email": "ts-npm-types@microsoft.com" + } + ], + "contributors": [], + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/is-odd", + "dist": { + "shasum": "281d2e0ff78f04b319e40f83b2c4cfae5377622a", + "tarball": "https://registry.npmjs.org/@types/is-odd/-/is-odd-3.0.4.tgz", + "fileCount": 5, + "integrity": "sha512-BdjXVZXuHVqdXNi9gEbwf/W/k2lIhXzqnmygz4BFRTdpgFpJsn1v7AfB3DaLHj04O9hKiqQvnxu5AeogyptDjQ==", + "signatures": [ + { + "sig": "MEUCIHLi2pVt8NsZM0434wTXqtu7F2kqqL9X7uySIbQ/XW7qAiEAzjn/2YlFKI02nQ2vaBNq0OtctmuDAdly4lY0V4eguIc=", + "keyid": "SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA" + } + ], + "unpackedSize": 2576 + }, + "main": "", + "types": "index.d.ts", + "scripts": {}, + "_npmUser": { + "name": "types", + "email": "ts-npm-types@microsoft.com" + }, + "repository": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "type": "git", + "directory": "types/is-odd" + }, + "description": "TypeScript definitions for is-odd", + "directories": {}, + "dependencies": {}, + "_hasShrinkwrap": false, + "typeScriptVersion": "4.5", + "_npmOperationalInternal": { + "tmp": "tmp/is-odd_3.0.4_1699344348594_0.45442272513797977", + "host": "s3://npm-registry-packages" + }, + "typesPublisherContentHash": "4f85cab04d848065af4c299b2e5770379553a5d4500593c40f39e06931124f91" + } + }, + "time": { + "created": "2019-02-23T02:34:18.884Z", + "modified": "2025-08-03T07:01:33.304Z", + "3.0.0": "2019-02-23T02:34:19.212Z", + "3.0.1": "2021-12-24T00:00:48.118Z", + "3.0.2": "2023-09-26T15:07:55.020Z", + "3.0.3": "2023-10-18T05:24:45.883Z", + "3.0.4": "2023-11-07T08:05:48.848Z" + }, + "license": "MIT", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/is-odd", + "repository": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "type": "git", + "directory": "types/is-odd" + }, + "description": "TypeScript definitions for is-odd", + "contributors": [], + "maintainers": [ + { + "name": "types", + "email": "ts-npm-types@microsoft.com" + } + ], + "readme": "[object Object]", + "readmeFilename": "" +} diff --git a/test/fixtures/npm-registry/packuments/nano-stringify-object.json b/test/fixtures/npm-registry/packuments/nano-stringify-object.json new file mode 100644 index 0000000000..cca55cc149 --- /dev/null +++ b/test/fixtures/npm-registry/packuments/nano-stringify-object.json @@ -0,0 +1,141 @@ +{ + "name": "nano-stringify-object", + "version": "0.0.0", + "license": "MIT", + "author": "gameroman", + "repository": { + "type": "git", + "url": "git+https://github.com/gameroman-npm/nano-stringify-object.git" + }, + "type": "module", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "scripts": { + "format": "oxfmt", + "lint": "oxlint", + "test": "bun test", + "build": "tsdown", + "prepublishOnly": "bun run build" + }, + "devDependencies": { + "oxfmt": "^0.38.0", + "oxlint": "^1.50.0", + "tsdown": "^0.21.2", + "typescript": "^5.9.3" + }, + "_id": "nano-stringify-object", + "_integrity": "sha512-s4XcjhMpc1u7HfHHimUAT0dtNNYFiOunlNPGUv5N9BoX4X1Lwklq4ckdFDPo+50/exciNsf0ggH4s8Sj6CufOA==", + "_nodeVersion": "24.3.0", + "_npmVersion": "10.8.3", + "shasum": "dbb692872dc94d6fd0adc93b66c8e09c67f78260", + "dist": { + "integrity": "sha512-s4XcjhMpc1u7HfHHimUAT0dtNNYFiOunlNPGUv5N9BoX4X1Lwklq4ckdFDPo+50/exciNsf0ggH4s8Sj6CufOA==", + "shasum": "dbb692872dc94d6fd0adc93b66c8e09c67f78260", + "tarball": "https://registry.npmjs.org/nano-stringify-object/-/nano-stringify-object-0.0.0.tgz", + "fileCount": 5, + "unpackedSize": 8783, + "signatures": [ + { + "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U", + "sig": "MEQCIF396neosjaDQP6JgESc3D/CpgE5VZik5CqXY/F68gUnAiAmtw8VnyNnYMNisW4K0S75AJicujUpd0u8g4kQ35t0YA==" + } + ] + }, + "_npmUser": { + "name": "gameroman", + "email": "dev@rman.dev" + }, + "directories": {}, + "maintainers": [ + { + "name": "gameroman", + "email": "dev@rman.dev" + }, + { + "name": "gameroman", + "email": "dev@rman.dev" + } + ], + "_npmOperationalInternal": { + "host": "s3://npm-registry-packages-npm-production", + "tmp": "tmp/nano-stringify-object_0.0.0_1773271904176_0.4735360871124257" + }, + "_hasShrinkwrap": false, + "dist-tags": { + "latest": "0.0.0" + }, + "versions": { + "0.0.0": { + "name": "nano-stringify-object", + "version": "0.0.0", + "license": "MIT", + "author": "gameroman", + "repository": { + "type": "git", + "url": "git+https://github.com/gameroman-npm/nano-stringify-object.git" + }, + "type": "module", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, + "scripts": { + "format": "oxfmt", + "lint": "oxlint", + "test": "bun test", + "build": "tsdown", + "prepublishOnly": "bun run build" + }, + "devDependencies": { + "oxfmt": "^0.38.0", + "oxlint": "^1.50.0", + "tsdown": "^0.21.2", + "typescript": "^5.9.3" + }, + "_id": "nano-stringify-object@0.0.0", + "_integrity": "sha512-s4XcjhMpc1u7HfHHimUAT0dtNNYFiOunlNPGUv5N9BoX4X1Lwklq4ckdFDPo+50/exciNsf0ggH4s8Sj6CufOA==", + "_nodeVersion": "24.3.0", + "_npmVersion": "10.8.3", + "shasum": "dbb692872dc94d6fd0adc93b66c8e09c67f78260", + "dist": { + "integrity": "sha512-s4XcjhMpc1u7HfHHimUAT0dtNNYFiOunlNPGUv5N9BoX4X1Lwklq4ckdFDPo+50/exciNsf0ggH4s8Sj6CufOA==", + "shasum": "dbb692872dc94d6fd0adc93b66c8e09c67f78260", + "tarball": "https://registry.npmjs.org/nano-stringify-object/-/nano-stringify-object-0.0.0.tgz", + "fileCount": 5, + "unpackedSize": 8783, + "signatures": [ + { + "keyid": "SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U", + "sig": "MEQCIF396neosjaDQP6JgESc3D/CpgE5VZik5CqXY/F68gUnAiAmtw8VnyNnYMNisW4K0S75AJicujUpd0u8g4kQ35t0YA==" + } + ] + }, + "_npmUser": { + "name": "gameroman", + "email": "dev@rman.dev" + }, + "directories": {}, + "maintainers": [ + { + "name": "gameroman", + "email": "dev@rman.dev" + } + ], + "_npmOperationalInternal": { + "host": "s3://npm-registry-packages-npm-production", + "tmp": "tmp/nano-stringify-object_0.0.0_1773271904176_0.4735360871124257" + }, + "_hasShrinkwrap": false + } + }, + "time": { + "created": "2026-03-11T23:31:44.175Z", + "0.0.0": "2026-03-11T23:31:44.334Z", + "modified": "2026-03-11T23:31:44.554Z" + }, + "readme": "", + "readmeFilename": "", + "_rev": "1-d7e9fda2cb18e6ea7d359fdc679eea56" +}