From 3cf99c4a43bcc88e749385f0cc77caa33d46e533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Wed, 17 Dec 2025 13:56:29 +0200 Subject: [PATCH 1/2] Add package version support for getPackageListingDetails Dapper method PackageListing as a concept is version-agnostic, but to ensure we don't serve pages for rejected package versions, they need to be community-scoped, as rejection is tied to PackageListing object. This commit adds the support to Dapper and listingUtils, the actual usage will be done in a separate commit. Since by default a PackageListing shows the latest package version, the version argument remains optional. --- apps/cyberstorm-remix/app/p/listingUtils.ts | 12 ++++++++---- packages/dapper-fake/src/fakers/package.ts | 11 ++++++----- packages/dapper-ts/src/methods/packageListings.ts | 2 ++ packages/dapper/src/types/methods.ts | 3 ++- .../src/get/packageListingDetails.ts | 6 +++++- .../thunderstore-api/src/schemas/requestSchemas.ts | 1 + 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/listingUtils.ts b/apps/cyberstorm-remix/app/p/listingUtils.ts index da17906c2..c3a70a8c4 100644 --- a/apps/cyberstorm-remix/app/p/listingUtils.ts +++ b/apps/cyberstorm-remix/app/p/listingUtils.ts @@ -5,6 +5,7 @@ export interface ListingIdentifiers { communityId: string; namespaceId: string; packageId: string; + packageVersion?: string; } /** @@ -16,12 +17,13 @@ export async function getPublicListing( dapper: DapperTs, ids: ListingIdentifiers ) { - const { communityId, namespaceId, packageId } = ids; + const { communityId, namespaceId, packageId, packageVersion } = ids; try { return await dapper.getPackageListingDetails( communityId, namespaceId, - packageId + packageId, + packageVersion ); } catch (e) { if (isApiError(e) && e.response.status === 404) { @@ -42,13 +44,14 @@ export async function getPrivateListing( dapper: DapperTs, ids: ListingIdentifiers ) { - const { communityId, namespaceId, packageId } = ids; + const { communityId, namespaceId, packageId, packageVersion } = ids; try { return await dapper.getPackageListingDetails( communityId, namespaceId, - packageId + packageId, + packageVersion ); } catch (e) { const is404 = isApiError(e) && e.response?.status === 404; @@ -61,6 +64,7 @@ export async function getPrivateListing( communityId, namespaceId, packageId, + packageVersion, true ); diff --git a/packages/dapper-fake/src/fakers/package.ts b/packages/dapper-fake/src/fakers/package.ts index 9ed6585fb..2dffbac05 100644 --- a/packages/dapper-fake/src/fakers/package.ts +++ b/packages/dapper-fake/src/fakers/package.ts @@ -127,12 +127,13 @@ export const getFakePackagePermissions = async ( export const getFakePackageListingDetails = async ( community: string, namespace: string, - name: string + name: string, + version?: string ) => { const seed = `${community}-${namespace}-${name}`; setSeed(seed); - const version = getVersionNumber(); + const ver = version ?? getVersionNumber(); const dependencies = await getFakeDependencies(community, namespace, name); return { @@ -143,10 +144,10 @@ export const getFakePackageListingDetails = async ( dependant_count: faker.number.int({ min: 0, max: 2000 }), dependencies, dependency_count: dependencies.length, - download_url: `https://thunderstore.io/package/download/${namespace}/${name}/${version}/`, - full_version_name: `${namespace}-${name}-${version}}`, + download_url: `https://thunderstore.io/package/download/${namespace}/${name}/${ver}/`, + full_version_name: `${namespace}-${name}-${ver}`, has_changelog: true, - install_url: `ror2mm://v1/install/thunderstore.io/${namespace}/${name}/${version}/`, + install_url: `ror2mm://v1/install/thunderstore.io/${namespace}/${name}/${ver}/`, latest_version_number: getVersionNumber(), team: { name: faker.word.words(3), diff --git a/packages/dapper-ts/src/methods/packageListings.ts b/packages/dapper-ts/src/methods/packageListings.ts index 3894a91f6..604e21ab5 100644 --- a/packages/dapper-ts/src/methods/packageListings.ts +++ b/packages/dapper-ts/src/methods/packageListings.ts @@ -123,6 +123,7 @@ export async function getPackageListingDetails( communityId: string, namespaceId: string, packageName: string, + version: string | undefined = undefined, useSession = false ) { const data = await fetchPackageListingDetails({ @@ -131,6 +132,7 @@ export async function getPackageListingDetails( community_id: communityId, namespace_id: namespaceId, package_name: packageName, + version_number: version, }, data: {}, queryParams: {}, diff --git a/packages/dapper/src/types/methods.ts b/packages/dapper/src/types/methods.ts index d27a4ce84..62ab6443c 100644 --- a/packages/dapper/src/types/methods.ts +++ b/packages/dapper/src/types/methods.ts @@ -44,7 +44,8 @@ export type GetPackageChangelog = ( export type GetPackageListingDetails = ( community: string, namespace: string, - name: string + name: string, + version?: string ) => Promise; export type GetPackageListings = ( diff --git a/packages/thunderstore-api/src/get/packageListingDetails.ts b/packages/thunderstore-api/src/get/packageListingDetails.ts index 189900891..269091faf 100644 --- a/packages/thunderstore-api/src/get/packageListingDetails.ts +++ b/packages/thunderstore-api/src/get/packageListingDetails.ts @@ -16,7 +16,11 @@ export async function fetchPackageListingDetails( props: ApiEndpointProps ): Promise { const { config, params, useSession } = props; - const path = `${BASE_LISTING_PATH}${params.community_id}/${params.namespace_id}/${params.package_name}/`; + let path = `${BASE_LISTING_PATH}${params.community_id}/${params.namespace_id}/${params.package_name}/`; + + if (params.version_number) { + path = `${path}v/${params.version_number}/`; + } return await apiFetch({ args: { diff --git a/packages/thunderstore-api/src/schemas/requestSchemas.ts b/packages/thunderstore-api/src/schemas/requestSchemas.ts index 491b4fdce..e4ddb568e 100644 --- a/packages/thunderstore-api/src/schemas/requestSchemas.ts +++ b/packages/thunderstore-api/src/schemas/requestSchemas.ts @@ -170,6 +170,7 @@ export const packageListingDetailsRequestParamsSchema = z.object({ community_id: z.string(), namespace_id: z.string(), package_name: z.string(), + version_number: z.string().optional(), }); export type PackageListingDetailsRequestParams = z.infer< From e384759273b6fa1ce71a04722204907cda4c4582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antti=20M=C3=A4ki?= Date: Wed, 17 Dec 2025 16:16:21 +0200 Subject: [PATCH 2/2] Update data fetching for packageVersion.tsx - Use Dapper's getPackageListingDetails method rather than getPackageVersionDetails as the latter may serve rejected content which we don't want to show - SSR loader and clientLoader mimic what their counterparts in packageListing.tsx does, namely loader makes the request unauthenticated and returns undefined if the response is 404, while clientLoader first does an unauthenticated request, and should that fail, another one as authenticated. This is done to improve cache hits - Since both loaders now await the responses for listing, it's never a promise and lot of the Suspense/Await elements become obsolete --- .../cyberstorm-remix/app/p/packageVersion.tsx | 488 ++++++++---------- 1 file changed, 204 insertions(+), 284 deletions(-) diff --git a/apps/cyberstorm-remix/app/p/packageVersion.tsx b/apps/cyberstorm-remix/app/p/packageVersion.tsx index 2502d75cd..26fdd1bed 100644 --- a/apps/cyberstorm-remix/app/p/packageVersion.tsx +++ b/apps/cyberstorm-remix/app/p/packageVersion.tsx @@ -33,7 +33,6 @@ import { type ReactElement, Suspense, useEffect, - useMemo, useRef, useState, } from "react"; @@ -53,68 +52,71 @@ import { getPublicEnvVariables, getSessionTools, } from "cyberstorm/security/publicEnvVariables"; -import { getTeamDetails } from "@thunderstore/dapper-ts/src/methods/team"; -import { isPromise } from "cyberstorm/utils/typeChecks"; -import { getPackageVersionDetails } from "@thunderstore/dapper-ts/src/methods/packageVersion"; +import { getPrivateListing, getPublicListing } from "./listingUtils"; +import { + type PackageListingDetails, + type TeamDetails, +} from "@thunderstore/dapper/types"; export async function loader({ params }: LoaderFunctionArgs) { - if ( - params.communityId && - params.namespaceId && - params.packageId && - params.packageVersion - ) { - const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); - const dapper = new DapperTs(() => { - return { - apiHost: publicEnvVariables.VITE_API_URL, - sessionId: undefined, - }; - }); + const { communityId, namespaceId, packageId, packageVersion } = params; - return { - communityId: params.communityId, - community: await dapper.getCommunity(params.communityId), - version: await dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - params.packageVersion - ), - team: await dapper.getTeamDetails(params.namespaceId), - }; + if (!communityId || !namespaceId || !packageId || !packageVersion) { + throw new Response("Package not found", { status: 404 }); } - throw new Response("Package not found", { status: 404 }); + + const publicEnvVariables = getPublicEnvVariables(["VITE_API_URL"]); + const dapper = new DapperTs(() => ({ + apiHost: publicEnvVariables.VITE_API_URL, + sessionId: undefined, + })); + + const listing = await getPublicListing(dapper, { + communityId, + namespaceId, + packageId, + packageVersion, + }); + + return { + community: await dapper.getCommunity(communityId), + listing, + packageVersion, + team: await dapper.getTeamDetails(namespaceId), + }; } export async function clientLoader({ params }: LoaderFunctionArgs) { - if ( - params.communityId && - params.namespaceId && - params.packageId && - params.packageVersion - ) { - const tools = getSessionTools(); - const dapper = new DapperTs(() => { - return { - apiHost: tools?.getConfig().apiHost, - sessionId: tools?.getConfig().sessionId, - }; - }); + const { communityId, namespaceId, packageId, packageVersion } = params; - return { - communityId: params.communityId, - community: dapper.getCommunity(params.communityId), - version: dapper.getPackageVersionDetails( - params.namespaceId, - params.packageId, - params.packageVersion - ), - team: dapper.getTeamDetails(params.namespaceId), - }; + if (!communityId || !namespaceId || !packageId || !packageVersion) { + throw new Response("Package not found", { status: 404 }); } - throw new Response("Package not found", { status: 404 }); + + const tools = getSessionTools(); + const config = tools.getConfig(); + const dapper = new DapperTs(() => ({ + apiHost: config.apiHost, + sessionId: config.sessionId, + })); + + const listing = await getPrivateListing(dapper, { + communityId, + namespaceId, + packageId, + packageVersion, + }); + + return { + community: dapper.getCommunity(communityId), + listing, + packageVersion, + team: dapper.getTeamDetails(namespaceId), + }; } +clientLoader.hydrate = true; + export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { const oldPath = arg.currentUrl.pathname.split("/"); const newPath = arg.nextUrl.pathname.split("/"); @@ -131,7 +133,7 @@ export function shouldRevalidate(arg: ShouldRevalidateFunctionArgs) { } export default function PackageVersion() { - const { communityId, community, version, team } = useLoaderData< + const { community, listing, packageVersion, team } = useLoaderData< typeof loader | typeof clientLoader >(); @@ -151,48 +153,38 @@ export default function PackageVersion() { // https://react.dev/reference/react/StrictMode // If strict mode is removed from the entry.client.tsx, this should only run once useEffect(() => { - if (!startsHydrated.current && isHydrated) return; - if (isPromise(version)) { - version.then((versionData) => { - setFirstUploaded( - - ); - }); - } else { - setFirstUploaded( - - ); + if (!startsHydrated.current && isHydrated) { + return; } + + if (!listing) { + return; + } + + setFirstUploaded( + + ); }, []); // END: For sidebar meta dates const currentTab = location.pathname.split("/")[8] || "details"; - const versionAndCommunityPromise = useMemo( - () => Promise.all([version, community]), - [] - ); - - const versionAndTeamPromise = useMemo(() => Promise.all([version, team]), []); + if (!listing) { + return
Loading listing...
; + } return ( <> - - {(resolvedValue) => ( + + {(resolvedCommunity) => ( <> - + - + @@ -226,81 +215,59 @@ export default function PackageVersion() {
- - You are viewing a potentially older version of this package. - - } - > - - {(resolvedValue) => ( - - You are viewing a potentially older version of this - package.{" "} + + You are viewing a potentially older version of this package.{" "} + + View Latest Version + + + + + + + + + {listing.namespace} + + {listing.website_url ? ( - View Latest Version + {listing.website_url} + + + - - )} - - - + ) : null} + } > - - {(resolvedValue) => ( - - - - - - {resolvedValue.namespace} - - {resolvedValue.website_url ? ( - - {resolvedValue.website_url} - - - - - ) : null} - - } - > - {formatToDisplayName(resolvedValue.name)} - - )} - - + {formatToDisplayName(listing.name)} +
- - } - > - - {(resolvedValue) => ( - <> - - - Details - - - Required ({resolvedValue.dependency_count}) - - - Versions - - - - )} - - + + + + Details + + + Required ({listing.dependency_count}) + + + Versions + + +
@@ -470,15 +390,15 @@ export default function PackageVersion() { } const Actions = memo(function Actions(props: { - team: Awaited>; - version: Awaited>; + listing: PackageListingDetails; + team: TeamDetails; }) { - const { team, version } = props; + const { listing, team } = props; return (
@@ -506,7 +426,7 @@ const Actions = memo(function Actions(props: { function packageMeta( firstUploaded: ReactElement | undefined, - version: Awaited> + listing: PackageListingDetails ) { return (
@@ -517,13 +437,13 @@ function packageMeta(
Downloads
- {formatInteger(version.download_count)} + {formatInteger(listing.download_count)}
Size
- {formatFileSize(version.size)} + {formatFileSize(listing.size)}
@@ -531,12 +451,12 @@ function packageMeta(
- {version.full_version_name} + {listing.full_version_name} - +