From ca5a8fbc8b270e4f9b8129f7b178736db68df192 Mon Sep 17 00:00:00 2001 From: Elliot Hesp Date: Tue, 16 Jul 2024 09:16:36 +0100 Subject: [PATCH] formatting --- api/package.json | 112 +++--- api/src/app.ts | 2 - api/src/bundler/error.ts | 40 +- api/src/bundler/index.ts | 342 ++++++++-------- api/src/octokit.ts | 34 +- api/src/probot.ts | 72 ---- api/src/res.ts | 80 ++-- api/src/routes/webhooks.github.ts | 130 +++--- website/app/ErrorLayout.tsx | 154 ++++---- website/app/api.ts | 2 +- website/app/components/Edit.tsx | 38 +- website/app/components/RefBadge.tsx | 60 +-- website/app/context.ts | 274 ++++++------- website/app/plausible.ts | 50 +-- .../app/routes/$owner.$repository.$/route.tsx | 370 +++++++++--------- 15 files changed, 843 insertions(+), 917 deletions(-) delete mode 100644 api/src/probot.ts diff --git a/api/package.json b/api/package.json index 389bc8f1..378a7f8e 100644 --- a/api/package.json +++ b/api/package.json @@ -1,58 +1,58 @@ { - "type": "module", - "private": true, - "name": "docs-page-api", - "version": "1.0.0", - "main": "src/app.ts", - "author": "Invertase ", - "license": "MIT", - "dependencies": { - "@code-hike/mdx": "0.7.2", - "@mdx-js/mdx": "2.2.1", - "@octokit/graphql": "5.0.0", - "@octokit/webhooks": "^13.2.7", - "@types/express": "^4.17.13", - "@types/morgan": "^1.9.3", - "@types/node": "^18.0.0", - "a2a": "^0.2.0", - "camelcase": "7.0.0", - "dotenv": "16.0.1", - "esbuild": "0.14.47", - "express": "4.18.1", - "express-basic-auth": "^1.2.1", - "gray-matter": "4.0.3", - "hast-util-heading-rank": "2.1.1", - "hast-util-parse-selector": "3.1.0", - "is-badge": "^2.1.0", - "js-yaml": "4.1.0", - "lodash.get": "^4.4.2", - "mdx-bundler": "9.0.1", - "morgan": "1.10.0", - "node-fetch": "3.2.6", - "octokit": "^4.0.2", - "rehype-accessible-emojis": "0.3.2", - "rehype-katex": "6.0.2", - "rehype-slug": "5.0.1", - "remark-comment": "1.0.0", - "remark-gfm": "3.0.1", - "remark-math": "5.1.1", - "remark-parse": "10.0.1", - "shiki": "1.1.7", - "unist-util-visit": "4.1.0", - "zod": "3.22.4", - "zod-to-json-schema": "^3.23.1", - "zod-validation-error": "0.2.2" - }, - "scripts": { - "dev": "bun run src/app.ts --hot", - "start": "bun run src/app.ts" - }, - "devDependencies": { - "@octokit/webhooks-types": "^6.2.4", - "@types/js-yaml": "^4.0.5", - "@types/lodash.get": "^4.4.9", - "rollup": "3.9.1", - "ts-node": "^10.8.1", - "typescript": "5.5.2" - } + "type": "module", + "private": true, + "name": "docs-page-api", + "version": "1.0.0", + "main": "src/app.ts", + "author": "Invertase ", + "license": "MIT", + "dependencies": { + "@code-hike/mdx": "0.7.2", + "@mdx-js/mdx": "2.2.1", + "@octokit/graphql": "5.0.0", + "@octokit/webhooks": "^13.2.7", + "@types/express": "^4.17.13", + "@types/morgan": "^1.9.3", + "@types/node": "^18.0.0", + "a2a": "^0.2.0", + "camelcase": "7.0.0", + "dotenv": "16.0.1", + "esbuild": "0.14.47", + "express": "4.18.1", + "express-basic-auth": "^1.2.1", + "gray-matter": "4.0.3", + "hast-util-heading-rank": "2.1.1", + "hast-util-parse-selector": "3.1.0", + "is-badge": "^2.1.0", + "js-yaml": "4.1.0", + "lodash.get": "^4.4.2", + "mdx-bundler": "9.0.1", + "morgan": "1.10.0", + "node-fetch": "3.2.6", + "octokit": "^4.0.2", + "rehype-accessible-emojis": "0.3.2", + "rehype-katex": "6.0.2", + "rehype-slug": "5.0.1", + "remark-comment": "1.0.0", + "remark-gfm": "3.0.1", + "remark-math": "5.1.1", + "remark-parse": "10.0.1", + "shiki": "1.1.7", + "unist-util-visit": "4.1.0", + "zod": "3.22.4", + "zod-to-json-schema": "^3.23.1", + "zod-validation-error": "0.2.2" + }, + "scripts": { + "dev": "bun run src/app.ts --hot", + "start": "bun run src/app.ts" + }, + "devDependencies": { + "@octokit/webhooks-types": "^6.2.4", + "@types/js-yaml": "^4.0.5", + "@types/lodash.get": "^4.4.9", + "rollup": "3.9.1", + "ts-node": "^10.8.1", + "typescript": "5.5.2" + } } diff --git a/api/src/app.ts b/api/src/app.ts index 683c76a2..88bbea04 100644 --- a/api/src/app.ts +++ b/api/src/app.ts @@ -2,7 +2,6 @@ import { config } from "dotenv"; import express, { Router, text } from "express"; import morgan from "morgan"; -import probot from "./probot"; import { notFound } from "./res"; import bundle from "./routes/bundle"; import preview from "./routes/preview"; @@ -21,7 +20,6 @@ app.use( extended: true, }), ); -app.use(probot); const router = Router(); router.get("/status", (_, res) => res.status(200).send("OK")); diff --git a/api/src/bundler/error.ts b/api/src/bundler/error.ts index 98423ec8..0b7b10f2 100644 --- a/api/src/bundler/error.ts +++ b/api/src/bundler/error.ts @@ -1,23 +1,23 @@ export class BundlerError extends Error { - code: number; - name: string; - source?: string; + code: number; + name: string; + source?: string; - constructor({ - code, - name, - message, - source, - }: { - code: number; - name: string; - message: string; - source?: string; - }) { - super(message); - this.code = code; - this.name = name; - this.message = message; - this.source = source; - } + constructor({ + code, + name, + message, + source, + }: { + code: number; + name: string; + message: string; + source?: string; + }) { + super(message); + this.code = code; + this.name = name; + this.message = message; + this.source = source; + } } diff --git a/api/src/bundler/index.ts b/api/src/bundler/index.ts index 805d87c0..f0eed1c9 100644 --- a/api/src/bundler/index.ts +++ b/api/src/bundler/index.ts @@ -7,187 +7,187 @@ import { parseMdx } from "./mdx"; import type { HeadingNode } from "./plugins/rehype-headings"; export const ERROR_CODES = { - REPO_NOT_FOUND: "REPO_NOT_FOUND", - FILE_NOT_FOUND: "FILE_NOT_FOUND", - BUNDLE_ERROR: "BUNDLE_ERROR", + REPO_NOT_FOUND: "REPO_NOT_FOUND", + FILE_NOT_FOUND: "FILE_NOT_FOUND", + BUNDLE_ERROR: "BUNDLE_ERROR", } as const; type Source = { - type: "PR" | "commit" | "branch"; - owner: string; - repository: string; - ref?: string; + type: "PR" | "commit" | "branch"; + owner: string; + repository: string; + ref?: string; }; export type BundlerOutput = { - source: Source; - ref: string; - stars: number; - forks: number; - private: boolean; - baseBranch: string; - path: string; - config: Config; - markdown: string; - headings: HeadingNode[]; - frontmatter: Record; - code: string; + source: Source; + ref: string; + stars: number; + forks: number; + private: boolean; + baseBranch: string; + path: string; + config: Config; + markdown: string; + headings: HeadingNode[]; + frontmatter: Record; + code: string; }; type CreateBundlerParams = { - owner: string; - repository: string; - path: string; - ref?: string; + owner: string; + repository: string; + path: string; + ref?: string; }; export class Bundler { - readonly #owner: string; - readonly #repository: string; - readonly #path: string; - #ref: string | undefined; - #source?: Source; - #config?: Config; - #markdown?: string; - - constructor(params: CreateBundlerParams) { - this.#owner = params.owner; - this.#repository = params.repository; - this.#path = params.path; - this.#ref = params.ref; - } - - /** - * Gets the source of the bundle. - * - * If the ref is a PR, it will fetch the PR metadata and update the source. - */ - private async getSource(): Promise { - if (this.#ref) { - // If the ref is a PR - if (/^[0-9]*$/.test(this.#ref)) { - const pullRequest = await getPullRequestMetadata( - this.#owner, - this.#repository, - this.#ref - ); - if (pullRequest) { - return { - type: "PR", - ...pullRequest, - }; - } - } - - // If the ref is a commit hash - if (/^[a-fA-F0-9]{40}$/.test(this.#ref)) { - return { - type: "commit", - owner: this.#owner, - repository: this.#repository, - ref: this.#ref, - }; - } - } - - return { - type: "branch", - owner: this.#owner, - repository: this.#repository, - ref: this.#ref, - }; - } - - /** - * Builds the payload with the MDX bundle. - */ - async build(): Promise { - // Get the real source of the request - this.#source = await this.getSource(); - - // Update the ref to the real ref - this.#ref = this.#source.ref; - - const metadata = await getGitHubContents({ - owner: this.#source.owner, - repository: this.#source.repository, - path: this.#path, - ref: this.#ref, - }); - - if (!metadata) { - throw new BundlerError({ - code: 404, - name: ERROR_CODES.REPO_NOT_FOUND, - message: `The repository ${this.#source.owner}/${ - this.#source.repository - } was not found.`, - }); - } - - if (!metadata.md) { - throw new BundlerError({ - code: 404, - name: ERROR_CODES.FILE_NOT_FOUND, - message: `No file was found in the repository matching this path. Ensure a file exists at /docs/${ - this.#path - }.mdx or /docs/${this.#path}/index.mdx.`, - source: `https://github.com/${this.#source.owner}/${ - this.#source.repository - }`, - }); - } - - this.#markdown = metadata.md; - - // If there is no ref (either not provided, or not a PR), use the metadata. - if (!this.#ref) { - this.#ref = metadata.baseBranch; - this.#source.ref = metadata.baseBranch; - } - - // Parse the users config, but fallback if it errors. - try { - this.#config = parseConfig({ - json: metadata.config.configJson, - yaml: metadata.config.configYaml, - }); - } catch { - this.#config = defaultConfig; - } - - try { - // Bundle the markdown file via MDX. - const mdx = await parseMdx(this.#markdown, { - headerDepth: this.#config.content?.headerDepth ?? 3, - }); - - return { - source: this.#source, - ref: this.#ref, - stars: metadata.stars, - forks: metadata.forks, - private: metadata.isPrivate, - baseBranch: metadata.baseBranch, - path: this.#path, - config: this.#config, - markdown: this.#markdown, - headings: mdx.headings, - frontmatter: mdx.frontmatter, - code: replaceMoustacheVariables(this.#config.variables ?? {}, mdx.code), - }; - } catch (e) { - console.error(e); - // @ts-ignore - const message = escapeHtml(e?.message || ""); - throw new BundlerError({ - code: 500, - name: ERROR_CODES.BUNDLE_ERROR, - message: `Something went wrong while bundling the file /${metadata.path}.mdx. Are you sure the MDX is valid?`, - source: `https://github.com/${this.#source.owner}/${ - this.#source.repository - }`, - }); - } - } + readonly #owner: string; + readonly #repository: string; + readonly #path: string; + #ref: string | undefined; + #source?: Source; + #config?: Config; + #markdown?: string; + + constructor(params: CreateBundlerParams) { + this.#owner = params.owner; + this.#repository = params.repository; + this.#path = params.path; + this.#ref = params.ref; + } + + /** + * Gets the source of the bundle. + * + * If the ref is a PR, it will fetch the PR metadata and update the source. + */ + private async getSource(): Promise { + if (this.#ref) { + // If the ref is a PR + if (/^[0-9]*$/.test(this.#ref)) { + const pullRequest = await getPullRequestMetadata( + this.#owner, + this.#repository, + this.#ref, + ); + if (pullRequest) { + return { + type: "PR", + ...pullRequest, + }; + } + } + + // If the ref is a commit hash + if (/^[a-fA-F0-9]{40}$/.test(this.#ref)) { + return { + type: "commit", + owner: this.#owner, + repository: this.#repository, + ref: this.#ref, + }; + } + } + + return { + type: "branch", + owner: this.#owner, + repository: this.#repository, + ref: this.#ref, + }; + } + + /** + * Builds the payload with the MDX bundle. + */ + async build(): Promise { + // Get the real source of the request + this.#source = await this.getSource(); + + // Update the ref to the real ref + this.#ref = this.#source.ref; + + const metadata = await getGitHubContents({ + owner: this.#source.owner, + repository: this.#source.repository, + path: this.#path, + ref: this.#ref, + }); + + if (!metadata) { + throw new BundlerError({ + code: 404, + name: ERROR_CODES.REPO_NOT_FOUND, + message: `The repository ${this.#source.owner}/${ + this.#source.repository + } was not found.`, + }); + } + + if (!metadata.md) { + throw new BundlerError({ + code: 404, + name: ERROR_CODES.FILE_NOT_FOUND, + message: `No file was found in the repository matching this path. Ensure a file exists at /docs/${ + this.#path + }.mdx or /docs/${this.#path}/index.mdx.`, + source: `https://github.com/${this.#source.owner}/${ + this.#source.repository + }`, + }); + } + + this.#markdown = metadata.md; + + // If there is no ref (either not provided, or not a PR), use the metadata. + if (!this.#ref) { + this.#ref = metadata.baseBranch; + this.#source.ref = metadata.baseBranch; + } + + // Parse the users config, but fallback if it errors. + try { + this.#config = parseConfig({ + json: metadata.config.configJson, + yaml: metadata.config.configYaml, + }); + } catch { + this.#config = defaultConfig; + } + + try { + // Bundle the markdown file via MDX. + const mdx = await parseMdx(this.#markdown, { + headerDepth: this.#config.content?.headerDepth ?? 3, + }); + + return { + source: this.#source, + ref: this.#ref, + stars: metadata.stars, + forks: metadata.forks, + private: metadata.isPrivate, + baseBranch: metadata.baseBranch, + path: this.#path, + config: this.#config, + markdown: this.#markdown, + headings: mdx.headings, + frontmatter: mdx.frontmatter, + code: replaceMoustacheVariables(this.#config.variables ?? {}, mdx.code), + }; + } catch (e) { + console.error(e); + // @ts-ignore + const message = escapeHtml(e?.message || ""); + throw new BundlerError({ + code: 500, + name: ERROR_CODES.BUNDLE_ERROR, + message: `Something went wrong while bundling the file /${metadata.path}.mdx. Are you sure the MDX is valid?`, + source: `https://github.com/${this.#source.owner}/${ + this.#source.repository + }`, + }); + } + } } diff --git a/api/src/octokit.ts b/api/src/octokit.ts index 62fe8194..b1a7443d 100644 --- a/api/src/octokit.ts +++ b/api/src/octokit.ts @@ -1,23 +1,23 @@ import { App } from "octokit"; export const app = new App({ - appId: process.env.GITHUB_APP_ID!, - privateKey: process.env.GITHUB_APP_PRIVATE_KEY!, + appId: process.env.GITHUB_APP_ID!, + privateKey: process.env.GITHUB_APP_PRIVATE_KEY!, }); // Type for a getFile response - assumes the repository is available type GetFileResponse = { - repository: { - file?: { - text: string; - }; - }; + repository: { + file?: { + text: string; + }; + }; }; // Queries a repository and extracts a file export async function getDomains(): Promise> { - const response = await app.octokit.graphql( - ` + const response = await app.octokit.graphql( + ` query GetDomains($owner: String!, $repo: String!, $file: String!) { repository(owner: $owner, name: $repo) { file: object(expression: $file) { @@ -28,13 +28,13 @@ export async function getDomains(): Promise> { } } `, - { - owner: "invertase", - repo: "docs.page", - file: "main:domains.json", - } - ); + { + owner: "invertase", + repo: "docs.page", + file: "main:domains.json", + }, + ); - const file = response.repository.file?.text || "[]"; - return JSON.parse(file) as Array<[string, string]>; + const file = response.repository.file?.text || "[]"; + return JSON.parse(file) as Array<[string, string]>; } diff --git a/api/src/probot.ts b/api/src/probot.ts deleted file mode 100644 index 2220df0a..00000000 --- a/api/src/probot.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { type Probot, createNodeMiddleware, createProbot } from "probot"; - -// Create a probot instance for the docs.page app -const probot = createProbot({ - overrides: { - appId: process.env.GITHUB_APP_ID, - privateKey: process.env.GITHUB_APP_PRIVATE_KEY, - }, -}); - -// Queries a repository and extracts a file -const getFile = ` - query GetDomains($owner: String!, $repo: String!, $file: String!) { - repository(owner: $owner, name: $repo) { - file: object(expression: $file) { - ... on Blob { - text - } - } - } - } -`; - -// Type for a getFile response - assumes the repository is available -type GetFileResponse = { - repository: { - file?: { - text: string; - }; - }; -}; - -const app = (app: Probot) => { - app.on("pull_request.opened", async (context) => { - app.log.info(context); - - const pull_request = context.payload.pull_request; - - const { repository } = context.payload; - - // e.g. org/repo - const name = repository.full_name.toLowerCase(); - - // Fetch the domains file from the main repository - const response = await context.octokit.graphql(getFile, { - owner: "invertase", - repo: "docs.page", - file: "main:domains.json", - }); - - const file = response.repository.file?.text || "[]"; - - // Find and set a custom domain, if it exists - const domains = JSON.parse(file) as Array<[string, string]>; - const domain = domains.find(([, repository]) => repository === name)?.[0]; - - const url = domain - ? `${domain}/~${pull_request.number}` - : `docs.page/${name}~${pull_request.number}`; - - const comment = context.issue({ - body: `To view this pull requests documentation preview, visit the following URL:\n\n[${url}](https://${url})\n\nDocumentation is deployed and generated using [docs.page](https://docs.page).`, - }); - - await context.octokit.issues.createComment(comment); - }); -}; - -export default createNodeMiddleware(app, { - probot, - webhooksPath: "/webhooks/github", -}); diff --git a/api/src/res.ts b/api/src/res.ts index 85d87406..87b9c450 100644 --- a/api/src/res.ts +++ b/api/src/res.ts @@ -4,63 +4,63 @@ import { fromZodError } from "zod-validation-error"; import type { BundlerError } from "./bundler/error"; const status = { - 200: "OK", - 400: "BAD_REQUEST", - 404: "NOT_FOUND", - 500: "INTERNAL_SERVER_ERROR", + 200: "OK", + 400: "BAD_REQUEST", + 404: "NOT_FOUND", + 500: "INTERNAL_SERVER_ERROR", } as const; export function ok(res: Response, data: T): Response { - res.status(200); - return res.json({ - code: status[200], - data, - }); + res.status(200); + return res.json({ + code: status[200], + data, + }); } export function bundleError(res: Response, error: BundlerError): Response { - res.status(error.code); - return res.json({ - code: error.name, - error: { - message: error.message, - source: error.source, - }, - }); + res.status(error.code); + return res.json({ + code: error.name, + error: { + message: error.message, + source: error.source, + }, + }); } export function badRequest(res: Response, message: string): Response; export function badRequest(res: Response, error: ZodError): Response; export function badRequest(res: Response, input: string | ZodError): Response { - // Set the HTTP Status code - res.status(400); + // Set the HTTP Status code + res.status(400); - if (typeof input === "string") { - return res.json({ - code: status[400], - error: input, - }); - } + if (typeof input === "string") { + return res.json({ + code: status[400], + error: input, + }); + } - return res.json({ - code: status[400], - error: fromZodError(input).message, - }); + return res.json({ + code: status[400], + error: fromZodError(input).message, + }); } export function notFound(res: Response): Response { - res.status(404); - return res.json({ - code: status[404], - error: "Resource not found.", - }); + res.status(404); + return res.json({ + code: status[404], + error: "Resource not found.", + }); } export function serverError(res: Response, error: unknown): Response { - console.error(error); - res.status(500); - return res.json({ - code: status[500], - error: "Something went wrong.", - }); + console.error(error); + res.status(500); + return res.json({ + code: status[500], + error: "Something went wrong.", + }); } diff --git a/api/src/routes/webhooks.github.ts b/api/src/routes/webhooks.github.ts index 7313a846..179805c1 100644 --- a/api/src/routes/webhooks.github.ts +++ b/api/src/routes/webhooks.github.ts @@ -5,84 +5,84 @@ import { badRequest, ok } from "../res"; import { app, getDomains } from "../octokit"; export default async function githubWebhook( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - // Webhooks are POST requests from GitHub - if (req.method.toUpperCase() !== "POST") { - return badRequest(res, "Invalid method."); - } - - // Get the body of the request. - const body = req.body; - - // Create a new instance of the Webhooks class with the GitHub App secret. - const webhook = new Webhooks({ - secret: process.env.GITHUB_APP_WEBHOOK_SECRET!, - }); - - // Verify the signature of the request. - const verified = await webhook.verify( - body, - String(req.headers["x-hub-signature-256"]) - ); - - if (!verified) { - return badRequest(res, "Invalid signature."); - } - - webhook.on("pull_request.opened", onPullRequestOpened); - - try { - const id = req.headers["x-github-hook-id"] as string; - // biome-ignore lint/suspicious/noExplicitAny: This will be a valid event name from GitHub. - const name = req.headers["x-github-event"] as any; - const payload = JSON.parse(body); - - await webhook.receive({ - id, - name, - payload, - }); - - return ok(res, { message: "OK" }); - } catch (e) { - console.error(e); - return badRequest(res, "Webhook request failed."); - } + // Webhooks are POST requests from GitHub + if (req.method.toUpperCase() !== "POST") { + return badRequest(res, "Invalid method."); + } + + // Get the body of the request. + const body = req.body; + + // Create a new instance of the Webhooks class with the GitHub App secret. + const webhook = new Webhooks({ + secret: process.env.GITHUB_APP_WEBHOOK_SECRET!, + }); + + // Verify the signature of the request. + const verified = await webhook.verify( + body, + String(req.headers["x-hub-signature-256"]), + ); + + if (!verified) { + return badRequest(res, "Invalid signature."); + } + + webhook.on("pull_request.opened", onPullRequestOpened); + + try { + const id = req.headers["x-github-hook-id"] as string; + // biome-ignore lint/suspicious/noExplicitAny: This will be a valid event name from GitHub. + const name = req.headers["x-github-event"] as any; + const payload = JSON.parse(body); + + await webhook.receive({ + id, + name, + payload, + }); + + return ok(res, { message: "OK" }); + } catch (e) { + console.error(e); + return badRequest(res, "Webhook request failed."); + } } async function onPullRequestOpened( - event: EmitterWebhookEvent<"pull_request.opened"> + event: EmitterWebhookEvent<"pull_request.opened">, ) { - const pull_request = event.payload.pull_request; - const { repository } = event.payload; + const pull_request = event.payload.pull_request; + const { repository } = event.payload; - // org/repo - const name = repository.full_name.toLowerCase(); + // org/repo + const name = repository.full_name.toLowerCase(); - // Fetch the domains file from the main repository - const domains = await getDomains(); + // Fetch the domains file from the main repository + const domains = await getDomains(); - // Find a custom domain for the repository, if it exists - const domain = domains.find(([, repository]) => repository === name)?.[0]; + // Find a custom domain for the repository, if it exists + const domain = domains.find(([, repository]) => repository === name)?.[0]; - // Build a domain URL for the comment - const url = domain - ? `${domain}/~${pull_request.number}` - : `docs.page/${name}~${pull_request.number}`; + // Build a domain URL for the comment + const url = domain + ? `${domain}/~${pull_request.number}` + : `docs.page/${name}~${pull_request.number}`; - const comment = `To view this pull requests documentation preview, visit the following URL: + const comment = `To view this pull requests documentation preview, visit the following URL: \n\n\ [${url}](https://${url}) \n\n\ Documentation is deployed and generated using [docs.page](https://docs.page).`; - // Post a comment on the pull request - await app.octokit.rest.issues.createComment({ - owner: repository.owner.login, - repo: repository.name, - issue_number: pull_request.number, - body: comment, - }); + // Post a comment on the pull request + await app.octokit.rest.issues.createComment({ + owner: repository.owner.login, + repo: repository.name, + issue_number: pull_request.number, + body: comment, + }); } diff --git a/website/app/ErrorLayout.tsx b/website/app/ErrorLayout.tsx index 09c7f910..3554811e 100644 --- a/website/app/ErrorLayout.tsx +++ b/website/app/ErrorLayout.tsx @@ -2,93 +2,93 @@ import type { ReactNode } from "react"; import type { BundleErrorResponse } from "./api"; const CODE_MAP: Record = { - NOT_FOUND: "Page not found", - BAD_REQUEST: "Something went wrong", - REPO_NOT_FOUND: "Repository not found", - FILE_NOT_FOUND: "File not found", - BUNDLE_ERROR: "Something went wrong", - INTERNAL_SERVER_ERROR: "Something went wrong", + NOT_FOUND: "Page not found", + BAD_REQUEST: "Something went wrong", + REPO_NOT_FOUND: "Repository not found", + FILE_NOT_FOUND: "File not found", + BUNDLE_ERROR: "Something went wrong", + INTERNAL_SERVER_ERROR: "Something went wrong", }; type Props = - | { - title: string; - description: string; - } - | { - error: BundleErrorResponse; - }; + | { + title: string; + description: string; + } + | { + error: BundleErrorResponse; + }; export function ErrorLayout(props: Props) { - // If an error is thrown from the bundler, we can render - // specific error messages based on the error code. - if ("error" in props) { - return ( - - ) - } - source={ - typeof props.error.error !== "string" - ? props.error.error.source - : undefined - } - /> - ); - } + // If an error is thrown from the bundler, we can render + // specific error messages based on the error code. + if ("error" in props) { + return ( + + ) + } + source={ + typeof props.error.error !== "string" + ? props.error.error.source + : undefined + } + /> + ); + } - // Otherwise it's just a generic error, e.g. 404. - return ; + // Otherwise it's just a generic error, e.g. 404. + return ; } type ViewProps = { - title: string; - description: ReactNode; - source?: string; + title: string; + description: ReactNode; + source?: string; }; function View(props: ViewProps) { - return ( -
-
-

{props.title}

-

{props.description}

- {props.source ? ( - - Go to repository → - - ) : ( - - Back to the homepage → - - )} - -
-
- ); + return ( +
+
+

{props.title}

+

{props.description}

+ {props.source ? ( + + Go to repository → + + ) : ( + + Back to the homepage → + + )} + +
+
+ ); } diff --git a/website/app/api.ts b/website/app/api.ts index 99e376b8..d181ae70 100644 --- a/website/app/api.ts +++ b/website/app/api.ts @@ -75,7 +75,7 @@ export async function getPreviewBundle( }); const json = await response.json(); - + console.log(json); if (!response.ok) { throw Response.json(json, { status: response.status, diff --git a/website/app/components/Edit.tsx b/website/app/components/Edit.tsx index 41cae486..6d0f91c1 100644 --- a/website/app/components/Edit.tsx +++ b/website/app/components/Edit.tsx @@ -2,25 +2,25 @@ import { PencilIcon } from "lucide-react"; import { usePageContext, useSourceUrl } from "~/context"; export function Edit() { - const ctx = usePageContext(); - const url = useSourceUrl(); + const ctx = usePageContext(); + const url = useSourceUrl(); - if (ctx.bundle.private) { - return null; - } + if (ctx.bundle.private) { + return null; + } - return ( - - ); + return ( + + ); } diff --git a/website/app/components/RefBadge.tsx b/website/app/components/RefBadge.tsx index 7769085a..de476cac 100644 --- a/website/app/components/RefBadge.tsx +++ b/website/app/components/RefBadge.tsx @@ -2,37 +2,37 @@ import { usePageContext, useRefUrl } from "~/context"; import { cn } from "~/utils"; export function RefBadge() { - const ctx = usePageContext(); - const url = useRefUrl(); + const ctx = usePageContext(); + const url = useRefUrl(); - // If we're in preview mode or the page doesn't have a ref, don't show the badge. - if (ctx.preview || !ctx.ref) { - return null; - } + // If we're in preview mode or the page doesn't have a ref, don't show the badge. + if (ctx.preview || !ctx.ref) { + return null; + } - const ref = ctx.ref; - const source = ctx.bundle.source; + const ref = ctx.ref; + const source = ctx.bundle.source; - return ( - - {source.type === "branch" && `${ref}`} - {source.type === "commit" && ref.substring(0, 7)} - {source.type === "PR" && `PR #${ref}`} - - ); + return ( + + {source.type === "branch" && `${ref}`} + {source.type === "commit" && ref.substring(0, 7)} + {source.type === "PR" && `PR #${ref}`} + + ); } diff --git a/website/app/context.ts b/website/app/context.ts index c9d7a739..6051d4ff 100644 --- a/website/app/context.ts +++ b/website/app/context.ts @@ -3,30 +3,30 @@ import type { BundlerOutput, SidebarGroup } from "./api"; import { getAssetSrc, getHref, getLocale, isExternalLink } from "./utils"; type BaseContext = { - // The relative path of the current page, e.g. `/contributing`. - path: string; - // The bundle output for the current page. - bundle: BundlerOutput; + // The relative path of the current page, e.g. `/contributing`. + path: string; + // The bundle output for the current page. + bundle: BundlerOutput; }; type PreviewContext = BaseContext & { - // The page is in preview mode. - preview: true; - // Returns a blob URL src for a given path. - getFile: (path: string) => Promise; + // The page is in preview mode. + preview: true; + // Returns a blob URL src for a given path. + getFile: (path: string) => Promise; }; type PageContext = BaseContext & { - // The owner of the repository, e.g. `invertase`. - owner: string; - // The repository name, e.g. `docs.page`. - repository: string; - // The branch or tag of the repository, e.g. `main`. - ref?: string; - // The domain assigned to the repository, e.g. `use.docs.page`. - domain?: string; - // The page is not in preview mode. - preview: false; + // The owner of the repository, e.g. `invertase`. + owner: string; + // The repository name, e.g. `docs.page`. + repository: string; + // The branch or tag of the repository, e.g. `main`. + ref?: string; + // The domain assigned to the repository, e.g. `use.docs.page`. + domain?: string; + // The page is not in preview mode. + preview: false; }; export type Context = PageContext | PreviewContext; @@ -35,32 +35,32 @@ export const PageContext = createContext(undefined); // Returns the current page context. export function usePageContext(): Context { - const context = useContext(PageContext); + const context = useContext(PageContext); - if (!context) { - throw new Error( - "usePageContext must be used within a PageContext.Provider" - ); - } + if (!context) { + throw new Error( + "usePageContext must be used within a PageContext.Provider", + ); + } - return context; + return context; } export function useAssetSrc(path: string) { - const ctx = usePageContext(); - const isPreview = ctx.preview; - const isExternal = isExternalLink(path); + const ctx = usePageContext(); + const isPreview = ctx.preview; + const isExternal = isExternalLink(path); - const [src, setSrc] = useState( - isExternal || !isPreview ? getAssetSrc(ctx, path) : "" - ); + const [src, setSrc] = useState( + isExternal || !isPreview ? getAssetSrc(ctx, path) : "", + ); - useEffect(() => { - if (isExternal || !isPreview) return; - ctx.getFile(path).then((src) => setSrc(src || "")); - }, [ctx, isExternal, isPreview, path]); + useEffect(() => { + if (isExternal || !isPreview) return; + ctx.getFile(path).then((src) => setSrc(src || "")); + }, [ctx, isExternal, isPreview, path]); - return src; + return src; } // Returns the current locale. @@ -69,134 +69,134 @@ export function useAssetSrc(path: string) { // For it to be considered a valid locale, it must be included in the `locales` array of the bundle config, // which is derived from the sidebar configuration. export function useLocale(): string | undefined { - const ctx = usePageContext(); - return getLocale(ctx); + const ctx = usePageContext(); + return getLocale(ctx); } // Returns the tabs for the current page and locale. export function useTabs() { - const context = usePageContext(); - const locale = useLocale(); - const tabs = context.bundle.config.tabs; + const context = usePageContext(); + const locale = useLocale(); + const tabs = context.bundle.config.tabs; - // If no locale is set, return tabs that are not locale-specific. - if (!locale) { - return tabs.filter((tab) => !tab.locale); - } + // If no locale is set, return tabs that are not locale-specific. + if (!locale) { + return tabs.filter((tab) => !tab.locale); + } - // Otherwise, return tabs that match the current locale. - return tabs.filter((tab) => tab.locale === locale); + // Otherwise, return tabs that match the current locale. + return tabs.filter((tab) => tab.locale === locale); } // Returns the sidebar for the current page and locale. export function useSidebar(): SidebarGroup[] { - const ctx = usePageContext(); - const locale = useLocale(); - const activeTab = useActiveTab(); - - let sidebar: SidebarGroup[] = []; - if (locale && !Array.isArray(ctx.bundle.config.sidebar)) { - sidebar = ctx.bundle.config.sidebar[locale]; - } else if (!Array.isArray(ctx.bundle.config.sidebar)) { - sidebar = ctx.bundle.config.sidebar.default || []; - } else { - sidebar = ctx.bundle.config.sidebar; - } - - if (activeTab !== undefined) { - return sidebar.filter((group) => { - return group.tab === activeTab || !group.tab; - }); - } - - return sidebar; + const ctx = usePageContext(); + const locale = useLocale(); + const activeTab = useActiveTab(); + + let sidebar: SidebarGroup[] = []; + if (locale && !Array.isArray(ctx.bundle.config.sidebar)) { + sidebar = ctx.bundle.config.sidebar[locale]; + } else if (!Array.isArray(ctx.bundle.config.sidebar)) { + sidebar = ctx.bundle.config.sidebar.default || []; + } else { + sidebar = ctx.bundle.config.sidebar; + } + + if (activeTab !== undefined) { + return sidebar.filter((group) => { + return group.tab === activeTab || !group.tab; + }); + } + + return sidebar; } // Resolves a path to a full URL. export function useHref(path: string): string { - const ctx = usePageContext(); - return getHref(ctx, path); + const ctx = usePageContext(); + return getHref(ctx, path); } // Returns the active tab for the current page. export function useActiveTab(): string | undefined { - const ctx = usePageContext(); - const tabs = useTabs(); - - if (!tabs.length) { - return; - } - - let closestTab: string | undefined = undefined; - let maxSegments = -1; - - for (const tab of tabs) { - const tabSegments = tab.href.split("/").filter(Boolean); - const pathSegments = ctx.path.split("/").filter(Boolean); - let matchCount = 0; - - // Count matching segments - for (let i = 0; i < tabSegments.length; i++) { - if ( - tabSegments[i] === pathSegments[i] || - tabSegments[i] === "*" || - tabSegments[i].startsWith(":") - ) { - matchCount++; - } else { - break; - } - } - - // Update the closest tab if this tab matches more segments - if (matchCount > maxSegments && matchCount === tabSegments.length) { - closestTab = tab.id; - maxSegments = matchCount; - } - } - - return closestTab; + const ctx = usePageContext(); + const tabs = useTabs(); + + if (!tabs.length) { + return; + } + + let closestTab: string | undefined = undefined; + let maxSegments = -1; + + for (const tab of tabs) { + const tabSegments = tab.href.split("/").filter(Boolean); + const pathSegments = ctx.path.split("/").filter(Boolean); + let matchCount = 0; + + // Count matching segments + for (let i = 0; i < tabSegments.length; i++) { + if ( + tabSegments[i] === pathSegments[i] || + tabSegments[i] === "*" || + tabSegments[i].startsWith(":") + ) { + matchCount++; + } else { + break; + } + } + + // Update the closest tab if this tab matches more segments + if (matchCount > maxSegments && matchCount === tabSegments.length) { + closestTab = tab.id; + maxSegments = matchCount; + } + } + + return closestTab; } // Returns the source URL for the current page. export function useSourceUrl() { - const ctx = usePageContext(); - - if (ctx.preview) { - return "#"; - } - - const source = ctx.bundle.source; - - return [ - "https://github.com/", - ctx.owner, - "/", - ctx.repository, - "/edit/", - source.type === "branch" && source.ref !== "HEAD" - ? source.ref - : ctx.bundle.baseBranch, - "/docs", - ctx.path || "/index", - ".mdx", - ].join(""); + const ctx = usePageContext(); + + if (ctx.preview) { + return "#"; + } + + const source = ctx.bundle.source; + + return [ + "https://github.com/", + ctx.owner, + "/", + ctx.repository, + "/edit/", + source.type === "branch" && source.ref !== "HEAD" + ? source.ref + : ctx.bundle.baseBranch, + "/docs", + ctx.path || "/index", + ".mdx", + ].join(""); } export function useRefUrl() { - const ctx = usePageContext(); + const ctx = usePageContext(); - if (ctx.preview || !ctx.ref) { - return "#"; - } + if (ctx.preview || !ctx.ref) { + return "#"; + } - const source = ctx.bundle.source; - const base = `https://github.com/${ctx.owner}/${ctx.repository}`; + const source = ctx.bundle.source; + const base = `https://github.com/${ctx.owner}/${ctx.repository}`; - if (source.ref === "HEAD") return base; - if (source.type === "branch") return `${base}/tree/${source.ref}`; - if (source.type === "commit") return `${base}/commit/${source.ref}`; - if (source.type === "PR") return `${base}/pull/${source.ref}`; + if (source.ref === "HEAD") return base; + if (source.type === "branch") return `${base}/tree/${source.ref}`; + if (source.type === "commit") return `${base}/commit/${source.ref}`; + if (source.type === "PR") return `${base}/pull/${source.ref}`; - return "#"; + return "#"; } diff --git a/website/app/plausible.ts b/website/app/plausible.ts index 9595c7d0..3d494663 100644 --- a/website/app/plausible.ts +++ b/website/app/plausible.ts @@ -1,30 +1,30 @@ import { getClientIp } from "request-ip"; export async function trackPageRequest( - request: Request, - owner: string, - repository: string + request: Request, + owner: string, + repository: string, ): Promise { - try { - await fetch("https://plausible.io/api/event", { - method: "POST", - headers: new Headers({ - "Content-Type": "application/json", - "User-Agent": request.headers.get("User-Agent") || "", - // @ts-expect-error - request-ip types use none-generic Request - "X-Forwarded-For": getClientIp(request) ?? "", - }), - body: JSON.stringify({ - name: "pageview", - url: request.url, - domain: "docs.page", - props: { - owner: owner.toLowerCase(), - repository: `${owner}/${repository}`.toLowerCase(), - }, - }), - }); - } catch (e) { - console.error("Failed to track page request", e); - } + try { + await fetch("https://plausible.io/api/event", { + method: "POST", + headers: new Headers({ + "Content-Type": "application/json", + "User-Agent": request.headers.get("User-Agent") || "", + // @ts-expect-error - request-ip types use none-generic Request + "X-Forwarded-For": getClientIp(request) ?? "", + }), + body: JSON.stringify({ + name: "pageview", + url: request.url, + domain: "docs.page", + props: { + owner: owner.toLowerCase(), + repository: `${owner}/${repository}`.toLowerCase(), + }, + }), + }); + } catch (e) { + console.error("Failed to track page request", e); + } } diff --git a/website/app/routes/$owner.$repository.$/route.tsx b/website/app/routes/$owner.$repository.$/route.tsx index 334d2672..cc9ff7a0 100644 --- a/website/app/routes/$owner.$repository.$/route.tsx +++ b/website/app/routes/$owner.$repository.$/route.tsx @@ -1,9 +1,9 @@ import type { LoaderFunctionArgs } from "@remix-run/node"; import { redirect, unstable_defineLoader } from "@remix-run/node"; import { - type MetaDescriptor, - type MetaFunction, - useLoaderData, + type MetaDescriptor, + type MetaFunction, + useLoaderData, } from "@remix-run/react"; import { Layout } from "~/Layout"; import { getBundle } from "~/api"; @@ -16,192 +16,192 @@ import domains from "../../../../domains.json"; import { trackPageRequest } from "~/plausible"; export const loader = async (args: LoaderFunctionArgs) => { - const owner = args.params.owner; - const path = args.params["*"] || ""; - let repository = args.params.repository; - let ref: string | undefined; - - if (!owner || !repository) { - throw new Error("Invalid routing scenario."); - } - - // Check if the repo includes a ref (invertase/foo~bar) - if (repository.includes("~")) { - [repository, ref] = repository.split("~"); - } - - const bundle = await getBundle({ - owner, - repository, - path, - ref, - }).catch((response) => { - args.response = response; - throw args.response; - }); - - // Check whether the repository has a domain assigned. - const domain = domains - .find(([, repo]) => repo === `${owner}/${repository}`) - ?.at(0); - - // Check if the user has set a redirect in the frontmatter of this page. - const redirectTo = - typeof bundle.frontmatter.redirect === "string" - ? bundle.frontmatter.redirect - : undefined; - - // Redirect to the specified URL. - if (redirectTo && redirectTo.length > 0) { - if (redirectTo.startsWith("http://") || redirectTo.startsWith("https://")) { - throw redirect(redirectTo); - } - - let url = ""; - if (domain) { - // If there is a domain setup, always redirect to it. - url = `https://${domain}`; - if (ref) url += `/~${ref}`; - url += redirectTo; - } else { - // If no domain, redirect to docs.page. - url = `https://docs.page/${owner}/${repository}`; - if (ref) url += `~${ref}`; - url += redirectTo; - } - - args.response!.status = 301; - args.response!.headers.set("Location", url); - throw args.response; - } - - if (import.meta.env.PROD) { - // Track the page request. - await trackPageRequest(args.request, owner, repository); - - // Set the cache headers - see https://vercel.com/docs/concepts/edge-network/caching - args.response!.headers.set( - "Cache-Control", - "s-maxage=1, stale-while-revalidate=59" - ); - } - - return { - path: ensureLeadingSlash(path), - owner, - repository, - ref, - domain: import.meta.env.PROD ? domain : undefined, - bundle, - preview: false, - } satisfies Context; + const owner = args.params.owner; + const path = args.params["*"] || ""; + let repository = args.params.repository; + let ref: string | undefined; + + if (!owner || !repository) { + throw new Error("Invalid routing scenario."); + } + + // Check if the repo includes a ref (invertase/foo~bar) + if (repository.includes("~")) { + [repository, ref] = repository.split("~"); + } + + const bundle = await getBundle({ + owner, + repository, + path, + ref, + }).catch((response) => { + args.response = response; + throw args.response; + }); + + // Check whether the repository has a domain assigned. + const domain = domains + .find(([, repo]) => repo === `${owner}/${repository}`) + ?.at(0); + + // Check if the user has set a redirect in the frontmatter of this page. + const redirectTo = + typeof bundle.frontmatter.redirect === "string" + ? bundle.frontmatter.redirect + : undefined; + + // Redirect to the specified URL. + if (redirectTo && redirectTo.length > 0) { + if (redirectTo.startsWith("http://") || redirectTo.startsWith("https://")) { + throw redirect(redirectTo); + } + + let url = ""; + if (domain) { + // If there is a domain setup, always redirect to it. + url = `https://${domain}`; + if (ref) url += `/~${ref}`; + url += redirectTo; + } else { + // If no domain, redirect to docs.page. + url = `https://docs.page/${owner}/${repository}`; + if (ref) url += `~${ref}`; + url += redirectTo; + } + + args.response!.status = 301; + args.response!.headers.set("Location", url); + throw args.response; + } + + if (import.meta.env.PROD) { + // Track the page request. + await trackPageRequest(args.request, owner, repository); + + // Set the cache headers - see https://vercel.com/docs/concepts/edge-network/caching + args.response!.headers.set( + "Cache-Control", + "s-maxage=1, stale-while-revalidate=59", + ); + } + + return { + path: ensureLeadingSlash(path), + owner, + repository, + ref, + domain: import.meta.env.PROD ? domain : undefined, + bundle, + preview: false, + } satisfies Context; }; export default function DocsPage() { - const context = useLoaderData(); - - return ( - - - - - ); + const context = useLoaderData(); + + return ( + + + + + ); } export const meta: MetaFunction = ({ data: ctx }) => { - const descriptors: MetaDescriptor[] = []; - - if (!ctx) { - return descriptors; - } - - descriptors.push({ - tagName: "link", - rel: "icon", - href: ctx.bundle.config.favicon - ? getAssetSrc(ctx, ctx.bundle.config.favicon) - : "/favicon.ico", - }); - - // Add noindex meta tag if the frontmatter or config has noindex set to true. - if ( - ctx.bundle.frontmatter.noindex === true || - ctx.bundle.config.seo?.noindex === true - ) { - descriptors.push({ - name: "robots", - content: "noindex", - }); - } - - const title = - ctx.bundle.frontmatter.title || ctx.bundle.config.name || "docs.page"; - const description = - ctx.bundle.frontmatter.description || ctx.bundle.config.description; - - descriptors.push({ - title, - }); - - descriptors.push({ - property: "og:title", - content: title, - }); - - descriptors.push({ - name: "twitter:title", - content: title, - }); - - descriptors.push({ - name: "twitter:card", - content: "summary_large_image", - }); - - if (description) { - descriptors.push({ - name: "description", - content: description, - }); - descriptors.push({ - property: "og:description", - content: description, - }); - descriptors.push({ - name: "twitter:description", - content: description, - }); - } - - if ("domain" in ctx && ctx.domain) { - descriptors.push({ - property: "og:url", - content: `https://${ctx.domain}`, - }); - } - - if (ctx.bundle.config.social?.x) { - descriptors.push({ - name: "twitter:site", - content: `@${ctx.bundle.config.social.x}`, - }); - } - - if (ctx.bundle.config.search?.docsearch) { - // https://docsearch.algolia.com/docs/DocSearch-v3#preconnect - descriptors.push({ - tagName: "link", - rel: "preconnect", - crossOrigin: "true", - href: `https://${ctx.bundle.config.search?.docsearch.appId}-dsn.algolia.net`, - }); - - descriptors.push({ - tagName: "link", - rel: "stylesheet", - href: docsearch, - }); - } - - return descriptors; + const descriptors: MetaDescriptor[] = []; + + if (!ctx) { + return descriptors; + } + + descriptors.push({ + tagName: "link", + rel: "icon", + href: ctx.bundle.config.favicon + ? getAssetSrc(ctx, ctx.bundle.config.favicon) + : "/favicon.ico", + }); + + // Add noindex meta tag if the frontmatter or config has noindex set to true. + if ( + ctx.bundle.frontmatter.noindex === true || + ctx.bundle.config.seo?.noindex === true + ) { + descriptors.push({ + name: "robots", + content: "noindex", + }); + } + + const title = + ctx.bundle.frontmatter.title || ctx.bundle.config.name || "docs.page"; + const description = + ctx.bundle.frontmatter.description || ctx.bundle.config.description; + + descriptors.push({ + title, + }); + + descriptors.push({ + property: "og:title", + content: title, + }); + + descriptors.push({ + name: "twitter:title", + content: title, + }); + + descriptors.push({ + name: "twitter:card", + content: "summary_large_image", + }); + + if (description) { + descriptors.push({ + name: "description", + content: description, + }); + descriptors.push({ + property: "og:description", + content: description, + }); + descriptors.push({ + name: "twitter:description", + content: description, + }); + } + + if ("domain" in ctx && ctx.domain) { + descriptors.push({ + property: "og:url", + content: `https://${ctx.domain}`, + }); + } + + if (ctx.bundle.config.social?.x) { + descriptors.push({ + name: "twitter:site", + content: `@${ctx.bundle.config.social.x}`, + }); + } + + if (ctx.bundle.config.search?.docsearch) { + // https://docsearch.algolia.com/docs/DocSearch-v3#preconnect + descriptors.push({ + tagName: "link", + rel: "preconnect", + crossOrigin: "true", + href: `https://${ctx.bundle.config.search?.docsearch.appId}-dsn.algolia.net`, + }); + + descriptors.push({ + tagName: "link", + rel: "stylesheet", + href: docsearch, + }); + } + + return descriptors; };