From 0f4c1395ac1e3287ef1a9c8e7b81ac50105866db Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Sun, 12 Jul 2026 18:58:22 +0200 Subject: [PATCH 1/6] feat: add deprecation warning --- src/components/App.tsx | 68 +++++++++++++++++++++++++++++++++--------- src/lib/api.ts | 45 ++++++++++++++++++++-------- 2 files changed, 86 insertions(+), 27 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 0b5202e..a34672b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,7 +1,11 @@ // @ts-expect-error semver has no types import semver from "semver"; import { createSignal, For, onMount, Show } from "solid-js"; -import { getBasePackageSize, getSortedDependents } from "#lib/api"; +import { + getBasePackageSize, + getPackageIsDeprecated, + getSortedDependents, +} from "#lib/api"; import { escapeMdTable, formatDownloads, @@ -14,6 +18,7 @@ interface AnalysisResult { version: string; downloads: number; traffic: number; + deprecated: boolean; } type State = @@ -128,9 +133,9 @@ export default function App() { isDev: dev, onProgress, }); - onProgress(99, "Calculating traffic and versions..."); + onProgress(90, "Calculating traffic and versions..."); - const items = sortedDeps + const items: AnalysisResult[] = sortedDeps .filter((d) => { if (!requestedRange) return true; if (d.v === requestedRange) return true; @@ -141,13 +146,30 @@ export default function App() { return false; } }) + .slice(0, limit) .map((d) => ({ name: d.n, version: d.v, downloads: d.d, traffic: d.d * pkgSize, - })) - .slice(0, limit); + deprecated: false, + })); + + onProgress(90, "Checking deprecation status..."); + + const chunkSize = 50; + for (let i = 0; i < items.length; i += chunkSize) { + const chunk = items.slice(i, i + chunkSize); + await Promise.all( + chunk.map(async (item) => { + item.deprecated = await getPackageIsDeprecated(item.name).catch(() => false); + }), + ); + onProgress( + 90 + Math.floor(((i + chunk.length) / items.length) * 9), + "Checking deprecation status...", + ); + } setState( items.length > 0 @@ -181,7 +203,7 @@ export default function App() { const downloadsStr = formatDownloads(pkg.downloads); const trafficStr = formatTraffic(pkg.traffic); const versionStr = pkg.version || "any"; - const pkgLink = `[${pkg.name}](https://npmx.dev/${pkg.name})`; + const pkgLink = `[${pkg.name}](https://npmx.dev/${pkg.name})${pkg.deprecated ? " ⚠️" : ""}`; if (!isDev()) { md += escapeMdTable`| ${indexStr} | ${downloadsStr} | ${trafficStr} | ${versionStr} | ${pkgLink} |\n`; @@ -500,14 +522,32 @@ export default function App() { - - {pkg.name} - +
+ + {pkg.name} + + + + Deprecated + + + +
)} diff --git a/src/lib/api.ts b/src/lib/api.ts index d6b3038..ab8db5e 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -9,12 +9,19 @@ import { import { hash } from "./util"; async function cachedFetch(url: string, options: RequestInit = {}) { - const hashKey = `fetch:${hash(url + (options.body || ""))}`; + const bodyString = options.body ? String(options.body) : ""; + const hashKey = `fetch:${hash(url + bodyString)}`; const cached = localStorage.getItem(hashKey); if (cached) { const { data, expiry }: { data: P; expiry: number } = JSON.parse(cached); - if (Date.now() < expiry) return { data, isCached: true as const }; + if (Date.now() < expiry) { + return { + data, + isCached: true as const, + commit: (_processedData: P, _ttlMinutes?: number) => {}, + }; + } localStorage.removeItem(hashKey); } @@ -146,22 +153,22 @@ async function getPackageIsDeprecated(name: string): Promise { return result.data.deprecated?.deprecated ?? false; } -interface DevDependentsRow { - id: string; - key: string; - value: string & { version: undefined }; +interface DependentValueObject { + version?: string; + name?: string; + deprecated?: { deprecated?: boolean }; } interface DependentsRow { id: string; key: string; - value: { name: string; version: string }; + value: string | DependentValueObject | null; } interface Dependents { total_rows: number; offset: number; - rows: DependentsRow[] | DevDependentsRow[]; + rows: DependentsRow[]; } interface ProcessedDependent { @@ -196,11 +203,23 @@ async function getSortedDependents( const MAX_DEPENDENTS = 3000; const processed: ProcessedDependent[] = data.rows - .map((r) => ({ - n: r.id, - v: r.value?.version?.trim() ?? "", - d: allStats[r.id] ?? 0, - })) + .map((r) => { + let v = ""; + if (typeof r.value === "string") { + v = r.value.trim(); + } else if ( + r.value !== null && + typeof r.value === "object" && + "version" in r.value + ) { + v = String(r.value.version).trim(); + } + return { + n: r.id, + v, + d: allStats[r.id] ?? 0, + }; + }) .sort((a, b) => b.d - a.d) .slice(0, MAX_DEPENDENTS); From 674c16dcacfaa6ab52d64c8c5cd370f8fc0403e8 Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:32:44 +0200 Subject: [PATCH 2/6] style: format --- src/components/App.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index a34672b..1269530 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -162,7 +162,9 @@ export default function App() { const chunk = items.slice(i, i + chunkSize); await Promise.all( chunk.map(async (item) => { - item.deprecated = await getPackageIsDeprecated(item.name).catch(() => false); + item.deprecated = await getPackageIsDeprecated(item.name).catch( + () => false, + ); }), ); onProgress( From 6490c5ef8c3609bee4c8e2bb7fa4de59ce107017 Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Sun, 12 Jul 2026 19:56:43 +0200 Subject: [PATCH 3/6] Update src/components/App.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- src/components/App.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/App.tsx b/src/components/App.tsx index 1269530..cd8af8b 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -539,6 +539,8 @@ export default function App() { fill="none" viewBox="0 0 24 24" stroke="currentColor" + role="img" + aria-label="Deprecated" > Deprecated Date: Sun, 12 Jul 2026 19:59:59 +0200 Subject: [PATCH 4/6] revert: api changes --- src/lib/api.ts | 45 +++++++++++++-------------------------------- 1 file changed, 13 insertions(+), 32 deletions(-) diff --git a/src/lib/api.ts b/src/lib/api.ts index ab8db5e..d6b3038 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -9,19 +9,12 @@ import { import { hash } from "./util"; async function cachedFetch(url: string, options: RequestInit = {}) { - const bodyString = options.body ? String(options.body) : ""; - const hashKey = `fetch:${hash(url + bodyString)}`; + const hashKey = `fetch:${hash(url + (options.body || ""))}`; const cached = localStorage.getItem(hashKey); if (cached) { const { data, expiry }: { data: P; expiry: number } = JSON.parse(cached); - if (Date.now() < expiry) { - return { - data, - isCached: true as const, - commit: (_processedData: P, _ttlMinutes?: number) => {}, - }; - } + if (Date.now() < expiry) return { data, isCached: true as const }; localStorage.removeItem(hashKey); } @@ -153,22 +146,22 @@ async function getPackageIsDeprecated(name: string): Promise { return result.data.deprecated?.deprecated ?? false; } -interface DependentValueObject { - version?: string; - name?: string; - deprecated?: { deprecated?: boolean }; +interface DevDependentsRow { + id: string; + key: string; + value: string & { version: undefined }; } interface DependentsRow { id: string; key: string; - value: string | DependentValueObject | null; + value: { name: string; version: string }; } interface Dependents { total_rows: number; offset: number; - rows: DependentsRow[]; + rows: DependentsRow[] | DevDependentsRow[]; } interface ProcessedDependent { @@ -203,23 +196,11 @@ async function getSortedDependents( const MAX_DEPENDENTS = 3000; const processed: ProcessedDependent[] = data.rows - .map((r) => { - let v = ""; - if (typeof r.value === "string") { - v = r.value.trim(); - } else if ( - r.value !== null && - typeof r.value === "object" && - "version" in r.value - ) { - v = String(r.value.version).trim(); - } - return { - n: r.id, - v, - d: allStats[r.id] ?? 0, - }; - }) + .map((r) => ({ + n: r.id, + v: r.value?.version?.trim() ?? "", + d: allStats[r.id] ?? 0, + })) .sort((a, b) => b.d - a.d) .slice(0, MAX_DEPENDENTS); From 82184dbef38f84d3fa4b0cbb91dc2f399250288b Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Wed, 15 Jul 2026 10:10:33 +0200 Subject: [PATCH 5/6] feat: markdown copy notes column --- src/components/App.tsx | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index cd8af8b..0b205c0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -187,17 +187,26 @@ export default function App() { } } + function getPackageNote(pkg: AnalysisResult): string { + if (pkg.deprecated) return "Deprecated"; + return ""; + } + async function copyAsMarkdown() { const currentState = state(); if (currentState.status !== "results") return; + const hasNotes = currentState.items.some( + (pkg) => getPackageNote(pkg) !== "", + ); + let md = ""; if (!isDev()) { - md += "| # | Downloads/month | Traffic | Version | Package |\n"; - md += "|---|-----------------|---------|---------|---------|\n"; + md += `| # | Downloads/month | Traffic | Version | Package |${hasNotes ? " Notes |" : ""}\n`; + md += `|---|-----------------|---------|---------|---------|${hasNotes ? "-------|" : ""}\n`; } else { - md += "| # | Downloads/month | Package |\n"; - md += "|---|-----------------|---------|\n"; + md += `| # | Downloads/month | Package |${hasNotes ? " Notes |" : ""}\n`; + md += `|---|-----------------|---------|${hasNotes ? "-------|" : ""}\n`; } currentState.items.forEach((pkg, i) => { @@ -205,13 +214,18 @@ export default function App() { const downloadsStr = formatDownloads(pkg.downloads); const trafficStr = formatTraffic(pkg.traffic); const versionStr = pkg.version || "any"; - const pkgLink = `[${pkg.name}](https://npmx.dev/${pkg.name})${pkg.deprecated ? " ⚠️" : ""}`; + const pkgLink = `[${pkg.name}](https://npmx.dev/${pkg.name})`; + const noteStr = getPackageNote(pkg); if (!isDev()) { - md += escapeMdTable`| ${indexStr} | ${downloadsStr} | ${trafficStr} | ${versionStr} | ${pkgLink} |\n`; + md += escapeMdTable`| ${indexStr} | ${downloadsStr} | ${trafficStr} | ${versionStr} | ${pkgLink} |`; } else { - md += escapeMdTable`| ${indexStr} | ${downloadsStr} | ${pkgLink} |\n`; + md += escapeMdTable`| ${indexStr} | ${downloadsStr} | ${pkgLink} |`; + } + if (hasNotes) { + md += escapeMdTable` ${noteStr} |`; } + md += "\n"; }); try { From 2b274d8872a2b86c03398d9114f58c501f11ce00 Mon Sep 17 00:00:00 2001 From: Felix Schneider <99918022+trueberryless@users.noreply.github.com> Date: Fri, 17 Jul 2026 06:59:43 +0200 Subject: [PATCH 6/6] fix: suggestions from coderabbit --- src/components/App.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 0b205c0..7e418db 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -36,6 +36,7 @@ export default function App() { const [error, setError] = createSignal(""); const [state, setState] = createSignal({ status: "initial" }); const [copyBtnLabel, setCopyBtnLabel] = createSignal("Copy as Markdown"); + let activeAnalysisId = 0; const isInitial = () => state().status === "initial"; const showEmpty = () => @@ -106,6 +107,7 @@ export default function App() { const pkg = pkgInput().trim(); if (!pkg) return; + const currentAnalysisId = ++activeAnalysisId; setLoading(true); setError(""); setState({ status: "no-results" }); @@ -159,6 +161,7 @@ export default function App() { const chunkSize = 50; for (let i = 0; i < items.length; i += chunkSize) { + if (currentAnalysisId !== activeAnalysisId) return; const chunk = items.slice(i, i + chunkSize); await Promise.all( chunk.map(async (item) => { @@ -167,12 +170,14 @@ export default function App() { ); }), ); + if (currentAnalysisId !== activeAnalysisId) return; onProgress( 90 + Math.floor(((i + chunk.length) / items.length) * 9), "Checking deprecation status...", ); } + if (currentAnalysisId !== activeAnalysisId) return; setState( items.length > 0 ? { status: "results", items } @@ -180,10 +185,13 @@ export default function App() { ); onProgress(100, "Analysis complete"); } catch (err) { + if (currentAnalysisId !== activeAnalysisId) return; console.error(err); setError((err as Error).message); } finally { - setLoading(false); + if (currentAnalysisId === activeAnalysisId) { + setLoading(false); + } } }