Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ Return aggregated statistics about the library dataset, such as total counts of

## GET /api/library

Lookup one or more libraries by npm package name. Endpoint can optionally perform a quick `check` to return existence flag only.
Lookup one or more libraries by npm package name.
Endpoint can optionally perform a quick `check` to return existence flag only, or when `version` is used as `check` value to return the latest version string when package is present in the directory.

- Method: GET
- Path: `/api/library`
Expand All @@ -213,6 +214,7 @@ Lookup one or more libraries by npm package name. Endpoint can optionally perfor
### Notes

- If `check` is `true`, the response includes only information about existence for the specified package(s).
- If `check` is `version`, the response includes only information about existence for the specified package(s), returning the latest version if package exists.
- If `check` is `false` or not provided, the response includes full library data.

### Example
Expand Down Expand Up @@ -311,6 +313,16 @@ Lookup one or more libraries by npm package name. Endpoint can optionally perfor
}
```

- GET `/api/library?name=react&check=version`

Response:

```json
{
"uniwind": "1.5.0"
}
```

---

## GET /api/proxy/npm-stat
Expand Down
98 changes: 64 additions & 34 deletions bun.lock

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions components/Package/DependenciesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ type Props = {
title: string;
data?: Record<string, string | PeerDependencyData> | null;
checkExistence?: boolean;
checkVersion?: boolean;
};

export default function DependenciesSection({ title, data, checkExistence }: Props) {
export default function DependenciesSection({ title, data, checkExistence, checkVersion }: Props) {
const noData = !data || Object.keys(data).length === 0;

const { data: checkData } = useSWR(
checkExistence && !noData
? `/api/library?name=${Object.keys(data).join(',')}&check=true`
? `/api/library?name=${Object.keys(data).join(',')}&check=${checkVersion ? 'version' : 'true'}`
: null,
(url: string) => fetch(url).then(res => res.json()),
{
Expand All @@ -39,7 +40,7 @@ export default function DependenciesSection({ title, data, checkExistence }: Pro
key={`${title.toLocaleLowerCase()}-${name}`}
name={name}
data={depData}
packageExists={checkData?.[name]}
packageVersion={checkData?.[name]}
/>
))}
</CollapsibleSection>
Expand Down
67 changes: 61 additions & 6 deletions components/Package/DependencyRow.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { type ReactNode } from 'react';
import { View } from 'react-native';
import semverClean from 'semver/functions/clean';
import semverDiff from 'semver/functions/diff';
import semverGt from 'semver/functions/gt';

import { A, HoverEffect, Label, P, useLayout } from '~/common/styleguide';
import { Info, Logo } from '~/components/Icons';
Expand All @@ -10,13 +13,16 @@ import tw from '~/util/tailwind';
type Props = {
name: string;
data: string | PeerDependencyData;
packageExists?: boolean;
packageVersion?: string | boolean;
};

export default function DependencyRow({ name, data, packageExists }: Props) {
export default function DependencyRow({ name, data, packageVersion }: Props) {
const { isSmallScreen } = useLayout();
const isDataString = typeof data === 'string';
const versionLabel = getVersionLabel(isDataString ? data : data.version);
const versionLabel = getVersionLabel(
isDataString ? data : data.version,
typeof packageVersion === 'string' ? packageVersion : undefined
);
const hasLongVersion = typeof versionLabel === 'string' && versionLabel.length > 18;

return (
Expand All @@ -27,7 +33,7 @@ export default function DependencyRow({ name, data, packageExists }: Props) {
isSmallScreen && tw`my-px`,
]}>
<span style={tw`flex flex-shrink flex-row items-center gap-x-1.5 overflow-hidden pl-0.5`}>
{packageExists ? (
{packageVersion ? (
<Tooltip
side="left"
trigger={
Expand Down Expand Up @@ -81,7 +87,7 @@ export default function DependencyRow({ name, data, packageExists }: Props) {
);
}

function getVersionLabel(version: string): ReactNode {
function getVersionLabel(version: string, latestVersion?: string): ReactNode {
if (version.startsWith('http')) {
return (
<A href={version} style={tw`leading-tight`}>
Expand All @@ -95,7 +101,56 @@ function getVersionLabel(version: string): ReactNode {
}
return 'patched';
}
return version;

if (latestVersion) {
const cleanVersion = semverClean(version.replace(/^[~^]/, ''));

if (!cleanVersion || semverGt(cleanVersion, latestVersion)) {
return version;
}

const diff = semverDiff(cleanVersion, latestVersion);
switch (diff) {
case 'patch':
return (
<Tooltip
sideOffset={-3}
trigger={
<span style={tw`cursor-pointer text-[#c99319] dark:text-[#dc9a00]`}>{version}</span>
}>
<Label style={tw`font-light text-white`}>
Patch update available: <span style={tw`font-medium`}>{latestVersion}</span>
</Label>
</Tooltip>
);
case 'minor':
return (
<Tooltip
sideOffset={-3}
trigger={<span style={tw`cursor-pointer text-[#ff5900]`}>{version}</span>}>
<Label style={tw`font-light text-white`}>
Minor update available: <span style={tw`font-medium`}>{latestVersion}</span>
</Label>
</Tooltip>
);
case 'major':
return (
<Tooltip
sideOffset={-3}
trigger={
<span style={tw`cursor-pointer text-[#e70a2f] dark:text-[#eb2d39]`}>{version}</span>
}>
<Label style={tw`font-light text-white`}>
Major update available: <span style={tw`font-medium`}>{latestVersion}</span>
</Label>
</Tooltip>
);
default:
return version;
}
} else {
return version;
}
}

function extractPatchedVersion(entry: string): string | null {
Expand Down
1 change: 1 addition & 0 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const PACKAGES_TO_OPTIMIZE = [
'react-native-svg',
'react-native-web',
'react-shiki',
'semver',
'shiki/*',
'twrnc',
];
Expand Down
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-native-picker/picker": "^2.11.4",
"@sentry/react": "^10.43.0",
"@sentry/react": "^10.44.0",
"@visx/gradient": "^3.12.0",
"@visx/responsive": "^3.12.0",
"@visx/xychart": "^3.12.0",
"crypto-js": "^4.2.0",
"es-toolkit": "^1.45.1",
"expo": "55.0.6",
"expo": "55.0.7",
"expo-font": "^55.0.4",
"next": "^16.1.6",
"next": "^16.1.7",
"node-emoji": "^2.2.0",
"postcss": "^8.5.8",
"react": "19.2.4",
Expand All @@ -51,18 +51,20 @@
"rehype-sanitize": "^6.0.0",
"remark-emoji": "^5.0.2",
"remark-gfm": "^4.0.1",
"semver": "^7.7.4",
"swr": "^2.4.1",
"tailwindcss": "^3.4.19",
"twrnc": "^4.16.0",
"use-debounce": "^10.1.0"
},
"devDependencies": {
"@expo/next-adapter": "^6.0.0",
"@next/bundle-analyzer": "^16.1.6",
"@next/bundle-analyzer": "^16.1.7",
"@prettier/plugin-oxc": "^0.1.3",
"@types/bun": "^1.3.10",
"@types/crypto-js": "^4.2.2",
"@types/react": "^19.2.14",
"@types/semver": "^7.7.1",
"@vercel/blob": "^0.27.3",
"ajv-cli": "^5.0.0",
"browserslist": "^4.28.1",
Expand Down
25 changes: 14 additions & 11 deletions pages/api/library/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { type NextApiRequest, type NextApiResponse } from 'next';

import data from '~/assets/data.json';
import { type DataAssetType } from '~/types';
import { type DataAssetType, type LibraryType } from '~/types';
import { parseQueryParams } from '~/util/queryParams';

const DATASET = data as DataAssetType;
Expand All @@ -10,8 +10,6 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
const { name, check } = parseQueryParams(req.query);

const packageNames = name ? name.toString().toLowerCase().trim().split(',') : undefined;
const checkOnly = Boolean(check);

const libraries = DATASET.libraries.filter(library =>
packageNames?.includes(library.npmPkg.toLowerCase())
);
Expand All @@ -29,13 +27,18 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {

res.statusCode = 200;
res.json(
Object.fromEntries(
packageNames.map(name => [
name,
checkOnly
? libraries.filter(library => name === library.npmPkg).length >= 1
: libraries.find(library => (name === library.npmPkg ? library : undefined)),
])
)
Object.fromEntries(packageNames.map(name => [name, handleCheck(libraries, name, check)]))
);
}

function handleCheck(libraries: LibraryType[], name: string, check?: string) {
switch (check) {
case 'version':
const lib = libraries.find(library => (name === library.npmPkg ? library : undefined));
return lib ? (lib.npm?.latestRelease ?? 'unknown') : undefined;
case 'true':
return libraries.some(library => name === library.npmPkg);
default:
return libraries.find(library => (name === library.npmPkg ? library : undefined));
}
}
7 changes: 6 additions & 1 deletion scenes/PackageOverviewScene.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,12 @@ export default function PackageOverviewScene({
</ul>
</CollapsibleSection>
)}
<DependenciesSection title="Dependencies" data={dependencies} checkExistence />
<DependenciesSection
title="Dependencies"
data={dependencies}
checkExistence
checkVersion
/>
<DependenciesSection
title="Peer dependencies"
data={mergePeerDependenciesData(registryData)}
Expand Down