diff --git a/api/src/bundler/index.ts b/api/src/bundler/index.ts index d8c3cf5a..4231e66f 100644 --- a/api/src/bundler/index.ts +++ b/api/src/bundler/index.ts @@ -7,195 +7,195 @@ 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 = { - // The owner of the repository to bundle. - owner: string; - // The repository to bundle. - repository: string; - // The path to file in the repository to bundle. - path: string; - // An optional ref to use for the content. - ref?: string; - // A list of components which are supported for bundling. - components?: Array; + // The owner of the repository to bundle. + owner: string; + // The repository to bundle. + repository: string; + // The path to file in the repository to bundle. + path: string; + // An optional ref to use for the content. + ref?: string; + // A list of components which are supported for bundling. + components?: Array; }; export class Bundler { - readonly #owner: string; - readonly #repository: string; - readonly #path: string; - readonly #components: Array; - #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; - this.#components = params.components || []; - } - - /** - * 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, - components: this.#components, - }); - - 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 - 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; + readonly #components: Array; + #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; + this.#components = params.components || []; + } + + /** + * 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, + components: this.#components, + }); + + 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 + 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/bundler/mdx.ts b/api/src/bundler/mdx.ts index 06e7b450..465af1a2 100644 --- a/api/src/bundler/mdx.ts +++ b/api/src/bundler/mdx.ts @@ -5,65 +5,65 @@ import { getRehypePlugins, getRemarkPlugins } from "./plugins/index"; import rehypeHeadings, { type HeadingNode } from "./plugins/rehype-headings"; type MdxResponse = { - code: string; - frontmatter: Record; - errors: Message[]; - headings: HeadingNode[]; + code: string; + frontmatter: Record; + errors: Message[]; + headings: HeadingNode[]; }; export function headerDepthToHeaderList(depth: number): string[] { - const list: string[] = []; - if (depth === 0) return list; + const list: string[] = []; + if (depth === 0) return list; - for (let i = 2; i <= depth; i++) { - list.push(`h${i}`); - } + for (let i = 2; i <= depth; i++) { + list.push(`h${i}`); + } - return list; + return list; } export async function parseMdx( - rawText: string, - options: { - headerDepth: number; - components: Array; - } + rawText: string, + options: { + headerDepth: number; + components: Array; + }, ): Promise { - const output = { - headings: [] as HeadingNode[], - frontmatter: {} as { [key: string]: string }, - }; + const output = { + headings: [] as HeadingNode[], + frontmatter: {} as { [key: string]: string }, + }; - const parsed = frontmatter(rawText); - output.frontmatter = parsed.data; + const parsed = frontmatter(rawText); + output.frontmatter = parsed.data; - const vfile = await compile(parsed.content, { - // prevent this error `_jsxDEV is not a function` - // enable next line - // development: process.env.NODE_ENV === 'production', - format: "mdx", - outputFormat: "function-body", - remarkPlugins: getRemarkPlugins({ - components: options.components, - }), - rehypePlugins: [ - ...getRehypePlugins(), - [ - rehypeHeadings, - { - headings: headerDepthToHeaderList(options.headerDepth), - callback: (headings: HeadingNode[]) => { - output.headings = headings; - }, - }, - ], - ], - }); + const vfile = await compile(parsed.content, { + // prevent this error `_jsxDEV is not a function` + // enable next line + // development: process.env.NODE_ENV === 'production', + format: "mdx", + outputFormat: "function-body", + remarkPlugins: getRemarkPlugins({ + components: options.components, + }), + rehypePlugins: [ + ...getRehypePlugins(), + [ + rehypeHeadings, + { + headings: headerDepthToHeaderList(options.headerDepth), + callback: (headings: HeadingNode[]) => { + output.headings = headings; + }, + }, + ], + ], + }); - return { - code: String(vfile), - frontmatter: output.frontmatter, - errors: [], - headings: output.headings, - }; + return { + code: String(vfile), + frontmatter: output.frontmatter, + errors: [], + headings: output.headings, + }; } diff --git a/api/src/bundler/plugins/index.ts b/api/src/bundler/plugins/index.ts index 0ade873f..b6f87ec4 100644 --- a/api/src/bundler/plugins/index.ts +++ b/api/src/bundler/plugins/index.ts @@ -15,37 +15,37 @@ import rehypeCodeBlocks from "./rehype-code-blocks"; import rehypeInlineBadges from "./rehype-inline-badges"; type PluginOptions = { - components?: Array; - codeHike?: boolean; - math?: boolean; + components?: Array; + codeHike?: boolean; + math?: boolean; }; export function getRemarkPlugins(options?: PluginOptions): PluggableList { - const plugins = [ - remarkComponentCheck(options?.components ?? []), - remarkUndeclaredVariables, - remarkGfm, - remarkComment, - ]; - - if (options?.codeHike) { - // plugins.push([remarkCodeHike, { theme: codeHikeTheme, lineNumbers: true }]); - } - - return plugins; + const plugins = [ + remarkComponentCheck(options?.components ?? []), + remarkUndeclaredVariables, + remarkGfm, + remarkComment, + ]; + + if (options?.codeHike) { + // plugins.push([remarkCodeHike, { theme: codeHikeTheme, lineNumbers: true }]); + } + + return plugins; } export function getRehypePlugins(options?: PluginOptions): PluggableList { - const plugins = [ - rehypeCodeBlocks, - rehypeSlug, - rehypeInlineBadges, - rehypeAccessibleEmojis, - ]; - - if (options?.codeHike) { - // plugins.push([]); - } - - return plugins; + const plugins = [ + rehypeCodeBlocks, + rehypeSlug, + rehypeInlineBadges, + rehypeAccessibleEmojis, + ]; + + if (options?.codeHike) { + // plugins.push([]); + } + + return plugins; } diff --git a/api/src/bundler/plugins/remark-component-check.ts b/api/src/bundler/plugins/remark-component-check.ts index c0f59e9b..087f1b15 100644 --- a/api/src/bundler/plugins/remark-component-check.ts +++ b/api/src/bundler/plugins/remark-component-check.ts @@ -8,75 +8,75 @@ import { visit } from "unist-util-visit"; */ interface DeclaredNode extends Node { - value: string; + value: string; } interface UnDeclaredNode extends Node { - name: string; - data?: Data; - value?: string; - attributes?: { - type: string; - name: string; - value: string; - }[]; + name: string; + data?: Data; + value?: string; + attributes?: { + type: string; + name: string; + value: string; + }[]; } export default function remarkComponentCheck( - components: Array + components: Array, ): () => (ast: Node) => void { - return () => { - const keywords = ["var", "let", "const", "function"]; - const withExport = keywords.map( - (k) => new RegExp(`(export)[ \t]+${k}[ \t]`) - ); + return () => { + const keywords = ["var", "let", "const", "function"]; + const withExport = keywords.map( + (k) => new RegExp(`(export)[ \t]+${k}[ \t]`), + ); - const declared: string[] = []; + const declared: string[] = []; - function visitorForDeclared(node: DeclaredNode) { - // Get the kind of export. This is actually stored in the Node, but the following was quicker for typescript: - const exportKeyword = withExport.filter((re) => re.test(node.value))[0]; + function visitorForDeclared(node: DeclaredNode) { + // Get the kind of export. This is actually stored in the Node, but the following was quicker for typescript: + const exportKeyword = withExport.filter((re) => re.test(node.value))[0]; - if (exportKeyword) { - declared.push( - node.value - .replace(exportKeyword, "") - .replace(/^[a-z0-9-_A-Z]*[ \t][a-z0-9-_A-Z]*[ \t]/, "") - .split(" ")[0] - ); - } - } + if (exportKeyword) { + declared.push( + node.value + .replace(exportKeyword, "") + .replace(/^[a-z0-9-_A-Z]*[ \t][a-z0-9-_A-Z]*[ \t]/, "") + .split(" ")[0], + ); + } + } - function visitorForUndeclared(node: UnDeclaredNode) { - // HTML elements are not components (e.g.
) - if (!isUppercase(node.name.charAt(0))) { - return; - } + function visitorForUndeclared(node: UnDeclaredNode) { + // HTML elements are not components (e.g.
) + if (!isUppercase(node.name.charAt(0))) { + return; + } - if (!declared.includes(node.name) && !components.includes(node.name)) { - console.log(components); - // Override all props of the component with our own. - node.attributes = [ - { - type: "mdxJsxAttribute", - name: "name", - value: node.name, - }, - ]; + if (!declared.includes(node.name) && !components.includes(node.name)) { + console.log(components); + // Override all props of the component with our own. + node.attributes = [ + { + type: "mdxJsxAttribute", + name: "name", + value: node.name, + }, + ]; - // Update the name of the component to the internal docs page component - // which renders a warning message. - node.name = "__InvalidComponent__"; - } - } + // Update the name of the component to the internal docs page component + // which renders a warning message. + node.name = "__InvalidComponent__"; + } + } - return async (ast: Node): Promise => { - visit(ast, "mdxjsEsm", visitorForDeclared); - visit(ast, "mdxJsxFlowElement", visitorForUndeclared); - visit(ast, "mdxJsxTextElement", visitorForUndeclared); - }; - }; + return async (ast: Node): Promise => { + visit(ast, "mdxjsEsm", visitorForDeclared); + visit(ast, "mdxJsxFlowElement", visitorForUndeclared); + visit(ast, "mdxJsxTextElement", visitorForUndeclared); + }; + }; } function isUppercase(str: string) { - return str === str.toUpperCase(); + return str === str.toUpperCase(); } diff --git a/api/src/routes/preview.ts b/api/src/routes/preview.ts index 9073d4b8..5e2efeeb 100644 --- a/api/src/routes/preview.ts +++ b/api/src/routes/preview.ts @@ -6,59 +6,59 @@ import { parseConfig } from "../config"; import { badRequest, ok, serverError } from "../res"; const PreviewSchema = z.object({ - markdown: z.string().nullable(), - config: z.object({ - json: z.string().nullable(), - yaml: z.string().nullable(), - }), - components: z.array(z.string()).optional(), + markdown: z.string().nullable(), + config: z.object({ + json: z.string().nullable(), + yaml: z.string().nullable(), + }), + components: z.array(z.string()).optional(), }); export default async function preview( - req: Request, - res: Response + req: Request, + res: Response, ): Promise { - const input = PreviewSchema.safeParse(JSON.parse(req.body)); + const input = PreviewSchema.safeParse(JSON.parse(req.body)); - if (!input.success) { - console.error(input.error); - return badRequest(res, input.error); - } + if (!input.success) { + console.error(input.error); + return badRequest(res, input.error); + } - try { - const config = parseConfig({ - json: input.data.config.json ?? undefined, - yaml: input.data.config.yaml ?? undefined, - }); + try { + const config = parseConfig({ + json: input.data.config.json ?? undefined, + yaml: input.data.config.yaml ?? undefined, + }); - const mdx = await parseMdx(input.data.markdown ?? "", { - headerDepth: config.content?.headerDepth ?? 3, - components: input.data.components ?? [], - }); + const mdx = await parseMdx(input.data.markdown ?? "", { + headerDepth: config.content?.headerDepth ?? 3, + components: input.data.components ?? [], + }); - const output: BundlerOutput = { - source: { - type: "branch", - owner: "owner", - repository: "repository", - ref: "preview", - }, - private: false, - ref: "preview", - stars: 0, - forks: 0, - baseBranch: "preview", - path: "preview", - config, - markdown: input.data.markdown ?? "", - headings: mdx.headings, - frontmatter: mdx.frontmatter, - code: mdx.code, - }; + const output: BundlerOutput = { + source: { + type: "branch", + owner: "owner", + repository: "repository", + ref: "preview", + }, + private: false, + ref: "preview", + stars: 0, + forks: 0, + baseBranch: "preview", + path: "preview", + config, + markdown: input.data.markdown ?? "", + headings: mdx.headings, + frontmatter: mdx.frontmatter, + code: mdx.code, + }; - return ok(res, output); - } catch (e: unknown) { - console.error(e); - return serverError(res, e); - } + return ok(res, output); + } catch (e: unknown) { + console.error(e); + return serverError(res, e); + } } diff --git a/packages/client/package.json b/packages/client/package.json index 68016013..6d11b65b 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,24 +1,19 @@ { - "name": "@docs.page/client", - "version": "2.0.0", - "author": "Invertase (http://invertase.io)", - "license": "Apache-2.0", - "main": "dist/bundle.cjs.js", - "module": "dist/bundle.ems.js", - "types": "dist/index.d.ts", - "scripts": { - "build": "rimraf dist && rollup -c", - "postinstall": "npm run build" - }, - "dependencies": { - "rimraf": "^6.0.1", - "rollup": "^4.18.1", - "rollup-plugin-typescript2": "^0.36.0" - }, - "files": [ - "dist", - "LICENSE", - "README.md", - "rollup.config.js" - ] + "name": "@docs.page/client", + "version": "2.0.0", + "author": "Invertase (http://invertase.io)", + "license": "Apache-2.0", + "main": "dist/bundle.cjs.js", + "module": "dist/bundle.ems.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "rimraf dist && rollup -c", + "postinstall": "npm run build" + }, + "dependencies": { + "rimraf": "^6.0.1", + "rollup": "^4.18.1", + "rollup-plugin-typescript2": "^0.36.0" + }, + "files": ["dist", "LICENSE", "README.md", "rollup.config.js"] } diff --git a/packages/client/src/index.ts b/packages/client/src/index.ts index ebea01f6..028cacf2 100644 --- a/packages/client/src/index.ts +++ b/packages/client/src/index.ts @@ -1,17 +1,17 @@ type CheckArgs = { - // An object of relative file paths (to /docs) and their contents. - files: Record; + // An object of relative file paths (to /docs) and their contents. + files: Record; }; type CheckResult = { - errors: unknown; - warnings: unknown; + errors: unknown; + warnings: unknown; }; export function check(args: CheckArgs): CheckResult { - // TODO - return { - errors: [], - warnings: [], - }; + // TODO + return { + errors: [], + warnings: [], + }; } diff --git a/website/app/api.ts b/website/app/api.ts index 714830e3..3671c0a5 100644 --- a/website/app/api.ts +++ b/website/app/api.ts @@ -1,94 +1,94 @@ import type { - BundleErrorResponse, - BundleResponse, - BundlerOutput, - SidebarGroup, + BundleErrorResponse, + BundleResponse, + BundlerOutput, + SidebarGroup, } from "../../api/src/types"; import { COMPONENTS } from "./components/Content"; export type { - BundleResponse, - BundleErrorResponse, - BundlerOutput, - SidebarGroup, + BundleResponse, + BundleErrorResponse, + BundlerOutput, + SidebarGroup, }; type GetBundleArgs = { - owner: string; - repository: string; - ref?: string; - path?: string; + owner: string; + repository: string; + ref?: string; + path?: string; }; const PRODUCTION = process.env.NODE_ENV === "production"; const API_URL = - process.env.API_URL || - (PRODUCTION ? "https://api.docs.page" : "http://localhost:8080"); + process.env.API_URL || + (PRODUCTION ? "https://api.docs.page" : "http://localhost:8080"); const API_PASSWORD = Buffer.from(`admin:${process.env.API_PASSWORD}`).toString( - "base64" + "base64", ); export async function getBundle(args: GetBundleArgs): Promise { - const params = new URLSearchParams({ - owner: args.owner, - repository: args.repository, - }); + const params = new URLSearchParams({ + owner: args.owner, + repository: args.repository, + }); - if (args.path) params.append("path", args.path); - if (args.ref) params.append("ref", args.ref); + if (args.path) params.append("path", args.path); + if (args.ref) params.append("ref", args.ref); - for (const component of Object.keys(COMPONENTS)) { - params.append("components", component); - } + for (const component of Object.keys(COMPONENTS)) { + params.append("components", component); + } - const response = await fetch(`${API_URL}/bundle?${params.toString()}`, { - headers: new Headers({ - Authorization: `Bearer ${API_PASSWORD}`, - }), - }); + const response = await fetch(`${API_URL}/bundle?${params.toString()}`, { + headers: new Headers({ + Authorization: `Bearer ${API_PASSWORD}`, + }), + }); - const json = await response.json(); + const json = await response.json(); - if (!response.ok) { - throw Response.json(json, { - status: response.status, - }); - } + if (!response.ok) { + throw Response.json(json, { + status: response.status, + }); + } - return json.data as BundlerOutput; + return json.data as BundlerOutput; } type GetPreviewBundleArgs = { - markdown: string; - config: { - json?: string; - yaml?: string; - }; + markdown: string; + config: { + json?: string; + yaml?: string; + }; }; export async function getPreviewBundle( - args: GetPreviewBundleArgs + args: GetPreviewBundleArgs, ): Promise { - const response = await fetch(`${API_URL}/preview`, { - method: "POST", - headers: new Headers({ - Authorization: `Bearer ${API_PASSWORD}`, - }), - body: JSON.stringify({ - markdown: args.markdown, - config: args.config, - components: Object.keys(COMPONENTS), - }), - }); - - const json = await response.json(); - - if (!response.ok) { - throw Response.json(json, { - status: response.status, - }); - } - - return json.data as BundlerOutput; + const response = await fetch(`${API_URL}/preview`, { + method: "POST", + headers: new Headers({ + Authorization: `Bearer ${API_PASSWORD}`, + }), + body: JSON.stringify({ + markdown: args.markdown, + config: args.config, + components: Object.keys(COMPONENTS), + }), + }); + + const json = await response.json(); + + if (!response.ok) { + throw Response.json(json, { + status: response.status, + }); + } + + return json.data as BundlerOutput; } diff --git a/website/app/components/Content.tsx b/website/app/components/Content.tsx index d9a2cfd5..56090a80 100644 --- a/website/app/components/Content.tsx +++ b/website/app/components/Content.tsx @@ -10,6 +10,7 @@ import { CodeBlock } from "./mdx/CodeBlock"; import { CodeGroup } from "./mdx/CodeGroup"; import { Heading } from "./mdx/Heading"; import { Image } from "./mdx/Image"; +import { InvalidComponent } from "./mdx/InvalidComponent"; import { Link } from "./mdx/Link"; import { Section } from "./mdx/Section"; import { Table } from "./mdx/Table"; @@ -19,112 +20,113 @@ import { Video } from "./mdx/Video"; import { Vimeo } from "./mdx/Vimeo"; import { YouTube } from "./mdx/YouTube"; import { Zapp } from "./mdx/Zapp"; -import { InvalidComponent } from "./mdx/InvalidComponent"; export const COMPONENTS = { - Accordion, - AccordionGroup, - CodeGroup, - Image, - Icon, - Info, - Warning, - Error, - Success, - Tabs, - TabItem, - Tweet, - Vimeo, - Video, - YouTube, - X: Tweet, - Zapp, + Accordion, + AccordionGroup, + CodeGroup, + Image, + Icon, + Info, + Warning, + Error, + Success, + Tabs, + TabItem, + Tweet, + Vimeo, + Video, + YouTube, + X: Tweet, + Zapp, }; export function Content() { - const { bundle } = usePageContext(); - const { default: MDX } = runSync( - bundle.code, - // @ts-expect-error - seems to be a bug in the types - { ...runtime } - ); + const { bundle } = usePageContext(); + const { default: MDX } = runSync( + bundle.code, + // @ts-expect-error - seems to be a bug in the types + { ...runtime }, + ); - // Show the page title if the frontmatter or config has it enabled - const showPageTitle = - Boolean(bundle.frontmatter.showPageTitle) || - Boolean(bundle.config.content?.showPageTitle) || - false; + // Show the page title if the frontmatter or config has it enabled + const showPageTitle = + Boolean(bundle.frontmatter.showPageTitle) || + Boolean(bundle.config.content?.showPageTitle) || + false; - const showPageImage = - Boolean(bundle.frontmatter.showPageImage) || - Boolean(bundle.config.content?.showPageImage) || - false; + const showPageImage = + Boolean(bundle.frontmatter.showPageImage) || + Boolean(bundle.config.content?.showPageImage) || + false; - const title = bundle.frontmatter.title; - const description = bundle.frontmatter.description; - const image = bundle.frontmatter.image; + const title = bundle.frontmatter.title; + const description = bundle.frontmatter.description; + const image = bundle.frontmatter.image; - const showMeta = showPageTitle || showPageImage; + const showMeta = showPageTitle || showPageImage; - return ( -
- {showMeta && ( -
- {!!title && ( - <> - - {String(title)} - - {!!description && ( -
-

{String(description)}

-
- )} - - )} - {!!image && } -
- )} -
+ {showMeta && ( +
+ {!!title && ( + <> + + {String(title)} + + {!!description && ( +
+

{String(description)}

+
+ )} + + )} + {!!image && } +
+ )} +
- - ) => ( - - ), - h2: (props: ComponentProps<"h2">) => ( - - ), - h3: (props: ComponentProps<"h3">) => ( - - ), - h4: (props: ComponentProps<"h4">) => ( - - ), - h5: (props: ComponentProps<"h5">) => ( - - ), - h6: (props: ComponentProps<"h6">) => ( - - ), - a: (props: ComponentProps<"a">) => , - table: (props: ComponentProps<"table">) => , - section: (props: ComponentProps<"section">) => ( -
- ), - img: (props: ComponentProps<"img">) => , - pre: (props: ComponentProps<"pre">) => , - /* Custom Components */ - ...COMPONENTS, - __InvalidComponent__: (props: ComponentProps<"div">) => , - }} - /> - - - - ); + > + + ) => ( + + ), + h2: (props: ComponentProps<"h2">) => ( + + ), + h3: (props: ComponentProps<"h3">) => ( + + ), + h4: (props: ComponentProps<"h4">) => ( + + ), + h5: (props: ComponentProps<"h5">) => ( + + ), + h6: (props: ComponentProps<"h6">) => ( + + ), + a: (props: ComponentProps<"a">) => , + table: (props: ComponentProps<"table">) =>
, + section: (props: ComponentProps<"section">) => ( +
+ ), + img: (props: ComponentProps<"img">) => , + pre: (props: ComponentProps<"pre">) => , + /* Custom Components */ + ...COMPONENTS, + __InvalidComponent__: (props: ComponentProps<"div">) => ( + + ), + }} + /> + + + + ); } diff --git a/website/app/components/TableOfContents.tsx b/website/app/components/TableOfContents.tsx index b7e5b95d..dab5e5bf 100644 --- a/website/app/components/TableOfContents.tsx +++ b/website/app/components/TableOfContents.tsx @@ -4,65 +4,65 @@ import { usePageContext, useTabs } from "~/context"; import { cn } from "~/utils"; export function TableOfContents() { - const ctx = usePageContext(); - const hasTabs = useTabs().length > 0; - const headings = ctx.bundle.headings; - const [activeId, setActiveId] = useState(""); + const ctx = usePageContext(); + const hasTabs = useTabs().length > 0; + const headings = ctx.bundle.headings; + const [activeId, setActiveId] = useState(""); - useEffect(() => { - const handleScroll = () => { - const sections = document.querySelectorAll("main div[data-section]"); - let active = ""; + useEffect(() => { + const handleScroll = () => { + const sections = document.querySelectorAll("main div[data-section]"); + let active = ""; - for (const section of sections) { - const span = section.querySelector("span:first-child"); - if (span && span.getBoundingClientRect().y < 1) { - active = section.getAttribute("data-section") || ""; - } - } + for (const section of sections) { + const span = section.querySelector("span:first-child"); + if (span && span.getBoundingClientRect().y < 1) { + active = section.getAttribute("data-section") || ""; + } + } - setActiveId(active); - }; + setActiveId(active); + }; - window.addEventListener("scroll", handleScroll); + window.addEventListener("scroll", handleScroll); - return () => { - window.removeEventListener("scroll", handleScroll); - }; - }, []); + return () => { + window.removeEventListener("scroll", handleScroll); + }; + }, []); - return ( -
-

- - On this page -

- -
- ); + return ( +
+

+ + On this page +

+ +
+ ); } diff --git a/website/app/components/mdx/Callout.tsx b/website/app/components/mdx/Callout.tsx index 1eed5c16..391f2559 100644 --- a/website/app/components/mdx/Callout.tsx +++ b/website/app/components/mdx/Callout.tsx @@ -1,70 +1,70 @@ import { - CircleCheckIcon, - CircleXIcon, - InfoIcon, - TriangleAlertIcon, + CircleCheckIcon, + CircleXIcon, + InfoIcon, + TriangleAlertIcon, } from "lucide-react"; -import type { ComponentProps, ReactElement, PropsWithChildren } from "react"; +import type { ComponentProps, PropsWithChildren, ReactElement } from "react"; import { cn } from "~/utils"; type CalloutProps = ComponentProps<"div"> & { - icon?: ReactElement; + icon?: ReactElement; }; function Callout(props: CalloutProps) { - return ( -
- {props.icon} - {props.children} -
- ); + return ( +
+ {props.icon} + {props.children} +
+ ); } export function Info(props: PropsWithChildren) { - return ( - } - className="border border-sky-500/20 bg-sky-200/10 text-sky-700 dark:border-sky-500/50 dark:bg-sky-500/10 dark:text-white" - > - {props.children} - - ); + return ( + } + className="border border-sky-500/20 bg-sky-200/10 text-sky-700 dark:border-sky-500/50 dark:bg-sky-500/10 dark:text-white" + > + {props.children} + + ); } export function Warning(props: PropsWithChildren) { - return ( - } - className="border border-yellow-500/20 bg-yellow-200/10 text-yellow-700 dark:border-yellow-500/50 dark:bg-yellow-500/10 dark:text-white" - > - {props.children} - - ); + return ( + } + className="border border-yellow-500/20 bg-yellow-200/10 text-yellow-700 dark:border-yellow-500/50 dark:bg-yellow-500/10 dark:text-white" + > + {props.children} + + ); } export function Error(props: PropsWithChildren) { - return ( - } - className="border border-red-500/20 bg-red-200/10 text-red-700 dark:border-red-500/50 dark:bg-red-500/10 dark:text-white" - > - {props.children} - - ); + return ( + } + className="border border-red-500/20 bg-red-200/10 text-red-700 dark:border-red-500/50 dark:bg-red-500/10 dark:text-white" + > + {props.children} + + ); } export function Success(props: PropsWithChildren) { - return ( - } - className="border border-green-500/20 bg-green-200/10 text-green-7s00 dark:border-green-500/50 dark:bg-green-500/10 dark:text-white" - > - {props.children} - - ); + return ( + } + className="border border-green-500/20 bg-green-200/10 text-green-7s00 dark:border-green-500/50 dark:bg-green-500/10 dark:text-white" + > + {props.children} + + ); } diff --git a/website/app/components/mdx/InvalidComponent.tsx b/website/app/components/mdx/InvalidComponent.tsx index 77966651..553ff83f 100644 --- a/website/app/components/mdx/InvalidComponent.tsx +++ b/website/app/components/mdx/InvalidComponent.tsx @@ -2,25 +2,25 @@ import type { ComponentProps } from "react"; import { Error } from "./Callout"; type InvalidComponentProps = ComponentProps<"div"> & { - // The name of the original component, provided by the bundler. - name?: string; + // The name of the original component, provided by the bundler. + name?: string; }; export function InvalidComponent(props: InvalidComponentProps) { - if (!props.name) { - return null; - } + if (!props.name) { + return null; + } - return ( - - Markdown content contains an invalid component declaration called{" "} - - {"<"} - {props.name} - {" />"} - - . To remove this error, either declare the component locally or remove the - declaration. - - ); + return ( + + Markdown content contains an invalid component declaration called{" "} + + {"<"} + {props.name} + {" />"} + + . To remove this error, either declare the component locally or remove the + declaration. + + ); } diff --git a/website/app/routes/preview.$/route.tsx b/website/app/routes/preview.$/route.tsx index f75da48d..86bb6a1a 100644 --- a/website/app/routes/preview.$/route.tsx +++ b/website/app/routes/preview.$/route.tsx @@ -7,126 +7,126 @@ import { getPreviewBundle } from "../../api"; import { PageContext } from "../../context"; import { Toolbar } from "./Toolbar"; import { - FileNotFoundError, - getFile, - queryClient, - useDirectoryHandle, - usePageContent, - useSelectDirectory, + FileNotFoundError, + getFile, + queryClient, + useDirectoryHandle, + usePageContent, + useSelectDirectory, } from "./utils"; import docsearch from "@docsearch/css/dist/style.css?url"; import { ensureLeadingSlash, isExternalLink } from "~/utils"; export const meta: MetaFunction = () => { - return [ - { - tagName: "link", - rel: "stylesheet", - href: docsearch, - }, - ]; + return [ + { + tagName: "link", + rel: "stylesheet", + href: docsearch, + }, + ]; }; export default function PreviewOutlet() { - return ( - - - - ); + return ( + + + + ); } export const action = async (args: ActionFunctionArgs) => { - const json = await args.request.json(); - const bundle = await getPreviewBundle(json).catch((response) => { - args.response = response; - throw args.response; - }); - - // 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) { - const url = isExternalLink(String(redirectTo)) - ? String(redirectTo) - : `/preview${ensureLeadingSlash(String(redirectTo))}`; - - throw redirect(url); - } - - return { - bundle, - }; + const json = await args.request.json(); + const bundle = await getPreviewBundle(json).catch((response) => { + args.response = response; + throw args.response; + }); + + // 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) { + const url = isExternalLink(String(redirectTo)) + ? String(redirectTo) + : `/preview${ensureLeadingSlash(String(redirectTo))}`; + + throw redirect(url); + } + + return { + bundle, + }; }; function Preview() { - const params = useParams(); - const path = params["*"] || ""; - - const fetcher = useFetcher({ key: "bundle" }); - const directory = useDirectoryHandle(); - const selectDirectory = useSelectDirectory(); - const content = usePageContent(path, directory.data); - const bundle = fetcher.data?.bundle; - - useEffect(() => { - if (content.data) { - console.log("Submitting content", content.data); - fetcher.submit(content.data, { - method: "POST", - encType: "application/json", - }); - } - }, [fetcher.submit, content.data]); - - if (directory.isLoading) { - return
Loading...
; - } - - if (directory.error) { - return
Error: {directory.error.message}
; - } - - if (content.isFetched && content.error) { - if (content.error instanceof FileNotFoundError) { - return
File not found...
; - } - - return
Something went wrong...
; - } - - if (directory.data === null) { - return ( - - ); - } - - if (!bundle) { - return
Loading...
; - } - - return ( - - - - - ); + const params = useParams(); + const path = params["*"] || ""; + + const fetcher = useFetcher({ key: "bundle" }); + const directory = useDirectoryHandle(); + const selectDirectory = useSelectDirectory(); + const content = usePageContent(path, directory.data); + const bundle = fetcher.data?.bundle; + + useEffect(() => { + if (content.data) { + console.log("Submitting content", content.data); + fetcher.submit(content.data, { + method: "POST", + encType: "application/json", + }); + } + }, [fetcher.submit, content.data]); + + if (directory.isLoading) { + return
Loading...
; + } + + if (directory.error) { + return
Error: {directory.error.message}
; + } + + if (content.isFetched && content.error) { + if (content.error instanceof FileNotFoundError) { + return
File not found...
; + } + + return
Something went wrong...
; + } + + if (directory.data === null) { + return ( + + ); + } + + if (!bundle) { + return
Loading...
; + } + + return ( + + + + + ); } diff --git a/website/app/routes/preview.$/utils.ts b/website/app/routes/preview.$/utils.ts index 1799d636..ad72916b 100644 --- a/website/app/routes/preview.$/utils.ts +++ b/website/app/routes/preview.$/utils.ts @@ -8,14 +8,14 @@ const DATABASE_VERSION = 2; const REFETCH_INTERVAL = 1000; interface Database extends DBSchema { - handles: { - key: string; - value: FileSystemDirectoryHandle; - }; - files: { - key: string; - value: string; - }; + handles: { + key: string; + value: FileSystemDirectoryHandle; + }; + files: { + key: string; + value: string; + }; } export const queryClient = new QueryClient(); @@ -24,269 +24,269 @@ let _db: IDBPDatabase | undefined; // Opens a database and creates the necessary object stores. async function openDatabase() { - if (_db) { - return _db; - } - - _db ??= await openDB(DATABASE, DATABASE_VERSION, { - upgrade(db) { - db.createObjectStore("handles"); - db.createObjectStore("files"); - }, - }); - - return _db; + if (_db) { + return _db; + } + + _db ??= await openDB(DATABASE, DATABASE_VERSION, { + upgrade(db) { + db.createObjectStore("handles"); + db.createObjectStore("files"); + }, + }); + + return _db; } // Load the directory handle from the database. // Stores all the important entities in the directory in the database. export function useDirectoryHandle() { - return useQuery({ - queryKey: ["directory-handle"], - refetchInterval: REFETCH_INTERVAL, - queryFn: async () => { - const db = await openDatabase(); - - // Get a saved directory handle from the database. - const handle = await db.get("handles", "directory"); - - // If no handle is stored, return null. - if (!handle) { - return null; - } - - // Verify we can access the directory. - const verified = await verifyPermission(handle); - - // Get all the files stored in the database. - const files = await db.getAllKeys("files"); - - // Keep track of all the files we've discovered. - const discoveredFiles: string[] = []; - - // If we can't access the directory, delete the handle & files and return null. - if (!verified) { - await db.delete("handles", "directory"); - await Promise.all(files.map((file) => db.delete("files", file))); - return null; - } - - // Get the contents of a file, and return empty string if it doesn't exist. - async function getFileContent(file: FileSystemFileHandle) { - return file - .getFile() - .then((file) => file.text()) - .catch(() => ""); - } - - // Create handles for each important entity in the directory. - const docsDirectoryHandle = await handle - .getDirectoryHandle("docs") - .catch(() => null); - const yamlConfigHandle = await handle - .getFileHandle("docs.yaml") - .catch(() => null); - const jsonConfigHandle = await handle - .getFileHandle("docs.json") - .catch(() => null); - - // Insert the contents of the files into the database. - if (yamlConfigHandle) { - await db.put( - "files", - await getFileContent(yamlConfigHandle), - "docs.yaml" - ); - - discoveredFiles.push("docs.yaml"); - } - - if (jsonConfigHandle) { - await db.put( - "files", - await getFileContent(jsonConfigHandle), - "docs.json" - ); - - discoveredFiles.push("docs.json"); - } - - // Recursively walk the docs directory and get the contents of each file. - async function walkDirectory( - dir: FileSystemDirectoryHandle, - path: string - ) { - for await (const entry of dir.values()) { - if (entry.kind === "file") { - const file = await entry.getFile(); - const key = path + file.name; - - // Keep track of the file. - discoveredFiles.push(key); - - // Store MDX files as plain text - if (key.endsWith("mdx")) { - await db.put("files", await file.text(), key); - } - // Store other files as blob URLs - else { - const type = mime.getType(file.name) ?? undefined; - const buffer = await file.arrayBuffer(); - const blob = new Blob([buffer], { type }); - await db.put("files", URL.createObjectURL(blob), key); - } - } else { - await walkDirectory( - entry as FileSystemDirectoryHandle, - `${path + entry.name}/` - ); - } - } - } - - if (docsDirectoryHandle) { - // Walk the `docs` directory, if it exists. - await walkDirectory(docsDirectoryHandle, "/"); - - // Delete any files that are no longer in the directory. - await Promise.all( - files.map(async (file) => { - if (!discoveredFiles.includes(file)) { - await db.delete("files", file); - } - }) - ); - } else { - // If the `docs` directory doesn't exist, delete all files. - await Promise.all(files.map((file) => db.delete("files", file))); - } - - return handle; - }, - }); + return useQuery({ + queryKey: ["directory-handle"], + refetchInterval: REFETCH_INTERVAL, + queryFn: async () => { + const db = await openDatabase(); + + // Get a saved directory handle from the database. + const handle = await db.get("handles", "directory"); + + // If no handle is stored, return null. + if (!handle) { + return null; + } + + // Verify we can access the directory. + const verified = await verifyPermission(handle); + + // Get all the files stored in the database. + const files = await db.getAllKeys("files"); + + // Keep track of all the files we've discovered. + const discoveredFiles: string[] = []; + + // If we can't access the directory, delete the handle & files and return null. + if (!verified) { + await db.delete("handles", "directory"); + await Promise.all(files.map((file) => db.delete("files", file))); + return null; + } + + // Get the contents of a file, and return empty string if it doesn't exist. + async function getFileContent(file: FileSystemFileHandle) { + return file + .getFile() + .then((file) => file.text()) + .catch(() => ""); + } + + // Create handles for each important entity in the directory. + const docsDirectoryHandle = await handle + .getDirectoryHandle("docs") + .catch(() => null); + const yamlConfigHandle = await handle + .getFileHandle("docs.yaml") + .catch(() => null); + const jsonConfigHandle = await handle + .getFileHandle("docs.json") + .catch(() => null); + + // Insert the contents of the files into the database. + if (yamlConfigHandle) { + await db.put( + "files", + await getFileContent(yamlConfigHandle), + "docs.yaml", + ); + + discoveredFiles.push("docs.yaml"); + } + + if (jsonConfigHandle) { + await db.put( + "files", + await getFileContent(jsonConfigHandle), + "docs.json", + ); + + discoveredFiles.push("docs.json"); + } + + // Recursively walk the docs directory and get the contents of each file. + async function walkDirectory( + dir: FileSystemDirectoryHandle, + path: string, + ) { + for await (const entry of dir.values()) { + if (entry.kind === "file") { + const file = await entry.getFile(); + const key = path + file.name; + + // Keep track of the file. + discoveredFiles.push(key); + + // Store MDX files as plain text + if (key.endsWith("mdx")) { + await db.put("files", await file.text(), key); + } + // Store other files as blob URLs + else { + const type = mime.getType(file.name) ?? undefined; + const buffer = await file.arrayBuffer(); + const blob = new Blob([buffer], { type }); + await db.put("files", URL.createObjectURL(blob), key); + } + } else { + await walkDirectory( + entry as FileSystemDirectoryHandle, + `${path + entry.name}/`, + ); + } + } + } + + if (docsDirectoryHandle) { + // Walk the `docs` directory, if it exists. + await walkDirectory(docsDirectoryHandle, "/"); + + // Delete any files that are no longer in the directory. + await Promise.all( + files.map(async (file) => { + if (!discoveredFiles.includes(file)) { + await db.delete("files", file); + } + }), + ); + } else { + // If the `docs` directory doesn't exist, delete all files. + await Promise.all(files.map((file) => db.delete("files", file))); + } + + return handle; + }, + }); } // Load all the files from the database. export function useFiles(enabled = true) { - return useQuery({ - queryKey: ["files"], - refetchInterval: REFETCH_INTERVAL, - enabled, - queryFn: async () => { - const db = await openDatabase(); - const keys = await db.getAllKeys("files"); - - const files: Record = {}; - - await Promise.all( - keys.map(async (key) => { - files[key] = await db.get("files", key); - }) - ); - - return files; - }, - }); + return useQuery({ + queryKey: ["files"], + refetchInterval: REFETCH_INTERVAL, + enabled, + queryFn: async () => { + const db = await openDatabase(); + const keys = await db.getAllKeys("files"); + + const files: Record = {}; + + await Promise.all( + keys.map(async (key) => { + files[key] = await db.get("files", key); + }), + ); + + return files; + }, + }); } // Load the current page content from the database. export function usePageContent( - path: string, - directory?: FileSystemDirectoryHandle | null + path: string, + directory?: FileSystemDirectoryHandle | null, ) { - return useQuery({ - enabled: !!directory, - queryKey: ["page-context", directory?.name, path], - refetchInterval: REFETCH_INTERVAL, - retry: false, - queryFn: async () => { - const db = await openDatabase(); - - const filePath = path === "" ? "" : ensureLeadingSlash(path); - - // First check if we even have a file in the database for this path. - const [file1, file2] = await Promise.all([ - // Check for an `index.mdx` file first. - db.get("files", `${filePath}/index.mdx`), - // Then check for a `.mdx` file. - db.get("files", `${filePath}.mdx`), - ]); - - // If neither file exists, return a code...? - if (!file1 && !file2) { - throw new FileNotFoundError(filePath); - } - - // Next get the config file(s) from the database. - const [yamlConfig, jsonConfig] = await Promise.all([ - db.get("files", "docs.yaml"), - db.get("files", "docs.json"), - ]); - - return { - config: { - yaml: yamlConfig ?? null, - json: jsonConfig ?? null, - }, - markdown: file1 ?? file2 ?? null, - }; - }, - }); + return useQuery({ + enabled: !!directory, + queryKey: ["page-context", directory?.name, path], + refetchInterval: REFETCH_INTERVAL, + retry: false, + queryFn: async () => { + const db = await openDatabase(); + + const filePath = path === "" ? "" : ensureLeadingSlash(path); + + // First check if we even have a file in the database for this path. + const [file1, file2] = await Promise.all([ + // Check for an `index.mdx` file first. + db.get("files", `${filePath}/index.mdx`), + // Then check for a `.mdx` file. + db.get("files", `${filePath}.mdx`), + ]); + + // If neither file exists, return a code...? + if (!file1 && !file2) { + throw new FileNotFoundError(filePath); + } + + // Next get the config file(s) from the database. + const [yamlConfig, jsonConfig] = await Promise.all([ + db.get("files", "docs.yaml"), + db.get("files", "docs.json"), + ]); + + return { + config: { + yaml: yamlConfig ?? null, + json: jsonConfig ?? null, + }, + markdown: file1 ?? file2 ?? null, + }; + }, + }); } // Mutation to select a directory and store it in the database. // On success, invalidate the page context query so it will re-fetch. export function useSelectDirectory() { - return useMutation({ - mutationFn: async () => { - const db = await openDatabase(); - const directory = await window.showDirectoryPicker(); - await db.put("handles", directory, "directory"); - }, - onSuccess: () => { - // Invalidate the page context query so it will re-fetch. - queryClient.invalidateQueries({ queryKey: ["page-context"] }); - }, - }); + return useMutation({ + mutationFn: async () => { + const db = await openDatabase(); + const directory = await window.showDirectoryPicker(); + await db.put("handles", directory, "directory"); + }, + onSuccess: () => { + // Invalidate the page context query so it will re-fetch. + queryClient.invalidateQueries({ queryKey: ["page-context"] }); + }, + }); } // Mutation to restart the app by deleting all stored data. export function useRestart() { - return useMutation({ - mutationFn: async () => { - const db = await openDatabase(); - await db.delete("handles", "directory"); - const files = await db.getAllKeys("files"); - await Promise.all(files.map((file) => db.delete("files", file))); - }, - onSuccess: () => { - // Invalidate the page context query so it will re-fetch. - queryClient.invalidateQueries({ queryKey: ["page-context"] }); - }, - }); + return useMutation({ + mutationFn: async () => { + const db = await openDatabase(); + await db.delete("handles", "directory"); + const files = await db.getAllKeys("files"); + await Promise.all(files.map((file) => db.delete("files", file))); + }, + onSuccess: () => { + // Invalidate the page context query so it will re-fetch. + queryClient.invalidateQueries({ queryKey: ["page-context"] }); + }, + }); } // Returns a file from the database. export async function getFile(path: string) { - const db = await openDatabase(); - return db.get("files", path); + const db = await openDatabase(); + return db.get("files", path); } // Helper function to verify permission to access a directory, and request it if needed. async function verifyPermission(directory: FileSystemDirectoryHandle) { - if ((await directory.queryPermission()) === "granted") { - return true; - } + if ((await directory.queryPermission()) === "granted") { + return true; + } - if ((await directory.requestPermission()) === "granted") { - return true; - } + if ((await directory.requestPermission()) === "granted") { + return true; + } - return false; + return false; } export class FileNotFoundError extends Error { - constructor(path: string) { - super(`File not found: ${path}`); - } + constructor(path: string) { + super(`File not found: ${path}`); + } }