From a3e0746d484422f15b13591372a8856c8126cba9 Mon Sep 17 00:00:00 2001 From: Taras Mankovski Date: Sun, 15 Dec 2024 17:40:27 -0500 Subject: [PATCH] Showing a condensed version of JSR link --- www/assets/images/jsr-logo.svg | 8 + www/components/package/cicle-score.tsx | 32 ++++ www/components/package/cross.tsx | 4 + www/components/package/icons.tsx | 38 +++++ www/components/score-card.tsx | 128 +++++++++++++++ www/components/score.tsx | 7 - www/hooks/use-jsr-client.ts | 89 +++-------- www/hooks/use-package.tsx | 213 ++++++++++++++----------- www/main.ts | 8 +- www/routes/package.tsx | 9 +- 10 files changed, 364 insertions(+), 172 deletions(-) create mode 100644 www/assets/images/jsr-logo.svg create mode 100644 www/components/package/cicle-score.tsx create mode 100644 www/components/package/cross.tsx create mode 100644 www/components/package/icons.tsx create mode 100644 www/components/score-card.tsx delete mode 100644 www/components/score.tsx diff --git a/www/assets/images/jsr-logo.svg b/www/assets/images/jsr-logo.svg new file mode 100644 index 0000000..6587522 --- /dev/null +++ b/www/assets/images/jsr-logo.svg @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/www/components/package/cicle-score.tsx b/www/components/package/cicle-score.tsx new file mode 100644 index 0000000..4999547 --- /dev/null +++ b/www/components/package/cicle-score.tsx @@ -0,0 +1,32 @@ +import { PackageDetailsResult } from "../../hooks/use-jsr-client.ts"; + +export function CircleScore({ details }: { details: PackageDetailsResult }) { + return ( + <> +

+ JSR Logo + Score +

+
+ + {details.score}% + +
+ + ); +} + +/** @src https://github.com/jsr-io/jsr/blob/34603e996f56eb38e811619f8aebc6e5c4ad9fa7/frontend/utils/score_ring_color.ts */ +export function getScoreBgColorClass(score: number): string { + if (score >= 90) { + return "bg-green-500"; + } else if (score >= 60) { + return "bg-yellow-500"; + } + return "bg-red-500"; +} diff --git a/www/components/package/cross.tsx b/www/components/package/cross.tsx new file mode 100644 index 0000000..b3c51f0 --- /dev/null +++ b/www/components/package/cross.tsx @@ -0,0 +1,4 @@ + \ No newline at end of file diff --git a/www/components/package/icons.tsx b/www/components/package/icons.tsx new file mode 100644 index 0000000..dc792e3 --- /dev/null +++ b/www/components/package/icons.tsx @@ -0,0 +1,38 @@ +// @ts-nocheck Property 'svg' does not exist on type 'JSX.IntrinsicElements'. + +export function Check() { + return ( + + ); +} + +export function Cross() { + return ( + + ); +} \ No newline at end of file diff --git a/www/components/score-card.tsx b/www/components/score-card.tsx new file mode 100644 index 0000000..c20fb78 --- /dev/null +++ b/www/components/score-card.tsx @@ -0,0 +1,128 @@ +import { Package, usePackage } from "../hooks/use-package.tsx"; +import { + PackageDetailsResult, + type PackageScoreResult, +} from "../hooks/use-jsr-client.ts"; +import { Check, Cross } from "./package/icons.tsx"; + +export function ScoreCard() { + return function* () { + const pkg = yield* usePackage(); + const [details, score] = yield* pkg.jsrPackageDetails(); + + return ( +
+ {details.success && details.data ? ( + <> + + Available on + JSR Logo + + + {score.success && score.data ? ( + + ) : ( + <> + )} + + ) : ( + <> + )} +
+ ); + }; +} + +function ScoreDescription({ + score, + pkg, +}: { + score: PackageScoreResult; + pkg: Package; +}) { + const { + percentageDocumentedSymbols: _percentageDocumentedSymbols, + total: _total, + ...flags + } = score; + + const SCORE_MAP = { + hasReadme: "Has a readme or module doc", + hasReadmeExamples: "Has examples in the readme or module doc", + allEntrypointsDocs: "Has module docs in all entrypoints", + allFastCheck: ( + <> + No{" "} + + slow types + {" "} + are used + + ), + hasProvenance: "Has provenance", + hasDescription: ( + <> + Has a{" "} + + description + + + ), + atLeastOneRuntimeCompatible: "At least one runtime is marked as compatible", + multipleRuntimesCompatible: + "At least two runtimes are marked as compatible", + }; + + return ( +
+ + The JSR score is a measure of the overall quality of a package, expand + for more detail. + + +
+ ); +} + +/** @src https://github.com/jsr-io/jsr/blob/34603e996f56eb38e811619f8aebc6e5c4ad9fa7/frontend/utils/score_ring_color.ts */ +export function getScoreTextColorClass(score: number): string { + if (score >= 90) { + return "text-green-600"; + } else if (score >= 60) { + return "text-yellow-700"; + } + return "text-red-500"; +} diff --git a/www/components/score.tsx b/www/components/score.tsx deleted file mode 100644 index 6d7f33a..0000000 --- a/www/components/score.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { Package } from "../hooks/use-package.tsx"; - -export function Score(pkg: Package) { - return function*() { - - } -} \ No newline at end of file diff --git a/www/hooks/use-jsr-client.ts b/www/hooks/use-jsr-client.ts index b28b866..f4f037d 100644 --- a/www/hooks/use-jsr-client.ts +++ b/www/hooks/use-jsr-client.ts @@ -10,13 +10,13 @@ const PackageScore = z.object({ hasReadme: z.boolean(), hasReadmeExamples: z.boolean(), allEntrypointsDocs: z.boolean(), - percentageDocumentedSymbols: z.number().min(0).max(1), allFastCheck: z.boolean(), hasProvenance: z.boolean(), hasDescription: z.boolean(), atLeastOneRuntimeCompatible: z.boolean(), multipleRuntimesCompatible: z.boolean(), - total: z.number().min(0).max(1), + percentageDocumentedSymbols: z.number().min(0).max(1), + total: z.number(), }); const PackageDetails = z.object({ @@ -24,45 +24,40 @@ const PackageDetails = z.object({ name: z.string(), description: z.string(), runtimeCompat: z.object({ - browser: z.boolean(), - deno: z.boolean(), - node: z.boolean(), - workerd: z.boolean(), - bun: z.boolean(), + browser: z.boolean().optional(), + deno: z.boolean().optional(), + node: z.boolean().optional(), + bun: z.boolean().optional(), + workerd: z.boolean().optional() }), - createdAt: z.string().datetime({ precision: 3 }), - updatedAt: z.string().datetime({ precision: 3 }), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), githubRepository: z.object({ + id: z.number(), owner: z.string(), name: z.string(), + createdAt: z.string().datetime(), + updatedAt: z.string().datetime(), }), - score: z.number().min(0).max(1), + score: z.number().min(0).max(100), }); -export type PackageScoreType = z.infer; -export type PackageDetailsType = z.infer; +export type PackageScoreResult = z.infer; +export type PackageDetailsResult = z.infer; export interface JSRClient { getPackageScore: ( params: GetPackageDetailsParams, - ) => Operation; + ) => Operation>; getPackageDetails: ( params: GetPackageDetailsParams, - ) => Operation; + ) => Operation>; } const JSRClientContext = createContext("jsr-client"); export function* initJSRClient({ token }: { token: string }) { - let client: JSRClient | undefined; - if (token === "example") { - console.info( - `JSR Client Context is using the example token; will return example data`, - ); - client = createExampleJSRClient(); - } else { - client = createJSRClient(token); - } + let client = createJSRClient(token); return yield* JSRClientContext.set(client); } @@ -86,7 +81,8 @@ function createJSRClient(token: string): JSRClient { ); if (response.ok) { - return PackageScore.parse(yield* call(() => response.json())); + const json = yield* call(() => response.json()); + return yield* call(() => PackageScore.safeParseAsync(json)); } throw new Error(`${response.status}: ${response.statusText}`); @@ -104,50 +100,11 @@ function createJSRClient(token: string): JSRClient { ); if (response.ok) { - return PackageDetails.parse(yield* call(() => response.json())); + const json = yield* call(() => response.json()); + return yield* call(() => PackageDetails.safeParseAsync(json)); } throw new Error(`${response.status}: ${response.statusText}`); }, }; -} - -function createExampleJSRClient(): JSRClient { - return { - *getPackageScore() { - return { - hasReadme: true, - hasReadmeExamples: true, - allEntrypointsDocs: true, - percentageDocumentedSymbols: 1, - allFastCheck: true, - hasProvenance: true, - hasDescription: true, - atLeastOneRuntimeCompatible: true, - multipleRuntimesCompatible: true, - total: 1, - }; - }, - *getPackageDetails() { - return { - scope: "effection-contrib", - name: "websocket", - description: "Use the WebSocket API as an Effection resource.", - runtimeCompat: { - browser: true, - deno: true, - node: true, - workerd: true, - bun: true, - }, - createdAt: "2024-12-15T02:18:26.624Z", - updatedAt: "2024-12-15T02:18:26.624Z", - githubRepository: { - owner: "thefrontside", - name: "effection-contribs", - }, - score: 1, - }; - }, - }; -} +} \ No newline at end of file diff --git a/www/hooks/use-package.tsx b/www/hooks/use-package.tsx index 88970b9..7d7d274 100644 --- a/www/hooks/use-package.tsx +++ b/www/hooks/use-package.tsx @@ -9,8 +9,8 @@ import { useMDX } from "./use-mdx.tsx"; import { useDescriptionParse } from "./use-description-parse.tsx"; import { REPOSITORY_DEFAULT_BRANCH_URL } from "../config.ts"; import { - PackageDetailsType, - PackageScoreType, + PackageDetailsResult, + PackageScoreResult, useJSRClient, } from "./use-jsr-client.ts"; @@ -86,7 +86,12 @@ export interface Package { /** * JSR Score */ - jsrPackageDetails: () => Operation<[PackageDetailsType, PackageScoreType]>; + jsrPackageDetails: () => Operation< + [ + z.SafeParseReturnType, + z.SafeParseReturnType, + ] + >; /** * Generated docs */ @@ -127,7 +132,9 @@ export function* usePackage(): Operation { return yield* PackageContext; } -export function* readPackageConfig(workspace: string): Operation { +export function* readPackageConfig( + workspace: string, +): Operation { const workspacePath = resolve(Deno.cwd(), workspace); const config: { private?: boolean } = yield* call( @@ -150,102 +157,118 @@ export function* readPackageConfig(workspace: string): Operation workspace, workspacePath, readme, - } + }; } function* createPackage(config: PackageConfig) { - let mod = yield* useMDX(config.readme); - - const content = mod.default({}); - - let file: VFile = yield* useDescriptionParse(config.readme); - - const exports = typeof config.exports === "string" - ? { - [DEFAULT_MODULE_KEY]: config.exports, - } - : config.exports; - - const [, scope, name] = config.name.match(/@(.*)\/(.*)/) ?? []; - - if (!scope) throw new Error(`Expected a scope but got ${scope}`); - if (!name) throw new Error(`Expected a package name but got ${name}`); - - const entrypoints: Record = {}; - for (const key of Object.keys(exports)) { - entrypoints[key] = new URL(join(config.workspacePath, exports[key]), "file://"); + let mod = yield* useMDX(config.readme); + + const content = mod.default({}); + + let file: VFile = yield* useDescriptionParse(config.readme); + + const exports = typeof config.exports === "string" + ? { + [DEFAULT_MODULE_KEY]: config.exports, } - - let docs: Package["docs"] = {}; - for (const key of Object.keys(entrypoints)) { - const docNodes = yield* useDenoDoc(String(entrypoints[key])); - docs[key] = yield* all(docNodes.map(function* (node) { - if (node.jsDoc && node.jsDoc.doc) { - try { - const mod = yield* useMDX(node.jsDoc.doc); - return { - id: exportHash(key, node), - ...node, - MDXDoc: () => mod.default({}), - }; - } catch (e) { - console.error( - `Could not parse doc string for ${node.name} at ${node.location}`, - e, - ); - } + : config.exports; + + const [, scope, name] = config.name.match(/@(.*)\/(.*)/) ?? []; + + if (!scope) throw new Error(`Expected a scope but got ${scope}`); + if (!name) throw new Error(`Expected a package name but got ${name}`); + + const entrypoints: Record = {}; + for (const key of Object.keys(exports)) { + entrypoints[key] = new URL( + join(config.workspacePath, exports[key]), + "file://", + ); + } + + let docs: Package["docs"] = {}; + for (const key of Object.keys(entrypoints)) { + const docNodes = yield* useDenoDoc(String(entrypoints[key])); + docs[key] = yield* all(docNodes.map(function* (node) { + if (node.jsDoc && node.jsDoc.doc) { + try { + const mod = yield* useMDX(node.jsDoc.doc); + return { + id: exportHash(key, node), + ...node, + MDXDoc: () => mod.default({}), + }; + } catch (e) { + console.error( + `Could not parse doc string for ${node.name} at ${node.location}`, + e, + ); } - return { - id: exportHash(key, node), - ...node, - }; - })); - } - - return { - workspace: config.workspace.replace("./", ""), - jsr: new URL(`./${config.name}`, "https://jsr.io/"), - jsrBadge: new URL(`./${config.name}`, "https://jsr.io/badges/"), - npm: new URL(`./${config.name}`, "https://www.npmjs.com/package/"), - bundleSizeBadge: new URL( - `./${config.name}/${config.version}`, - "https://img.shields.io/bundlephobia/minzip/", - ), - npmVersionBadge: new URL( - `./${config.name}`, - "https://img.shields.io/npm/v/", - ), - bundlephobia: new URL( - `./${config.name}/${config.version}`, - "https://bundlephobia.com/package/", - ), - dependencyCountBadge: new URL( - `./${config.name}`, - "https://badgen.net/bundlephobia/dependency-count/", - ), - treeShakingSupportBadge: new URL( - `./${config.name}`, - "https://badgen.net/bundlephobia/tree-shaking/", - ), - path: config.workspacePath, - packageName: config.name, - scope, - source: new URL(config.workspace, REPOSITORY_DEFAULT_BRANCH_URL), - name, - exports, - readme: config.readme, - docs, - version: config.version, - jsrPackageDetails: function* getJSRPackageDetails() { - const client = yield* useJSRClient(); - return yield* all([ - client.getPackageDetails({ scope, package: name }), - client.getPackageScore({ scope, package: name }), - ]); - }, - MDXContent: () => content, - MDXDescription: () => <>{file.data?.meta?.description}, - }; + } + return { + id: exportHash(key, node), + ...node, + }; + })); + } + + return { + workspace: config.workspace.replace("./", ""), + jsr: new URL(`./${config.name}/`, "https://jsr.io/"), + jsrBadge: new URL(`./${config.name}`, "https://jsr.io/badges/"), + npm: new URL(`./${config.name}`, "https://www.npmjs.com/package/"), + bundleSizeBadge: new URL( + `./${config.name}/${config.version}`, + "https://img.shields.io/bundlephobia/minzip/", + ), + npmVersionBadge: new URL( + `./${config.name}`, + "https://img.shields.io/npm/v/", + ), + bundlephobia: new URL( + `./${config.name}/${config.version}`, + "https://bundlephobia.com/package/", + ), + dependencyCountBadge: new URL( + `./${config.name}`, + "https://badgen.net/bundlephobia/dependency-count/", + ), + treeShakingSupportBadge: new URL( + `./${config.name}`, + "https://badgen.net/bundlephobia/tree-shaking/", + ), + path: config.workspacePath, + packageName: config.name, + scope, + source: new URL(config.workspace, REPOSITORY_DEFAULT_BRANCH_URL), + name, + exports, + readme: config.readme, + docs, + version: config.version, + *jsrPackageDetails(): Operation<[ + z.SafeParseReturnType, + z.SafeParseReturnType, + ]> { + const client = yield* useJSRClient(); + const [details, score] = yield* all([ + client.getPackageDetails({ scope, package: name }), + client.getPackageScore({ scope, package: name }), + ]); + + if (!details.success) { + console.info(`JSR package details response failed validation`, details.error.format()) + } + + if (!score.success) { + console.info(`JSR score response failed validation`, score.error.format()) + } + + return [details, score]; + }, + MDXContent: () => content, + MDXDescription: () => <>{file.data?.meta?.description}, + }; } function exportHash(exportName: string, doc: DocNode): string { diff --git a/www/main.ts b/www/main.ts index 82a73bf..fe7b99b 100644 --- a/www/main.ts +++ b/www/main.ts @@ -17,8 +17,14 @@ import { initJSRClient } from "./hooks/use-jsr-client.ts"; if (import.meta.main) { await main(function* () { yield* initDenoDeploy(); + + const token = Deno.env.get("JSR_API") ?? "" + if (token === "") { + console.log("Missing JSR API token; expect score card not to load."); + } + yield* initJSRClient({ - token: Deno.env.get("JSR_API") ?? "example", + token, }); let revolution = createRevolution({ diff --git a/www/routes/package.tsx b/www/routes/package.tsx index f9e5407..4c71feb 100644 --- a/www/routes/package.tsx +++ b/www/routes/package.tsx @@ -7,6 +7,7 @@ import { useMarkdown } from "../hooks/use-markdown.tsx"; import { PackageHeader } from "../components/package/header.tsx"; import { PackageExports } from "../components/package/exports.tsx"; import { readPackages } from "../hooks/read-packages.ts"; +import { ScoreCard } from "../components/score-card.tsx"; export function packageRoute(): SitemapRoute { return { @@ -51,21 +52,23 @@ export function packageRoute(): SitemapRoute { ); - } catch { + } catch (e) { + console.error(e); const AppHTML = yield* useAppHtml({ title: `${params.workspace}`, - description: `Not found`, + description: `Failed to load ${params.workspace} due to error.`, pageTitle: `${params.workspace} not found`, }); return (

- {params.workspace} not found + Failed to load {params.workspace} due to error.

);