tree.ts (3.96 KB)
import { queryOptions } from "@tanstack/react-query";
import { notFound } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { createHighlighter } from "shiki";
import { createJavaScriptRegexEngine } from "shiki/engine/javascript";
import * as z from "zod";
import { getRepoDOStub } from "@/do/repo";
import { getLanguageFromFilename } from "@/lib/utils";

// Create highlighter instance with JavaScript regex engine
const highlighterPromise = createHighlighter({
  themes: ["github-dark-default"],
  langs: [],
  engine: createJavaScriptRegexEngine({
    forgiving: true,
  }),
});

export const getTreeFnSchema = z.object({
  owner: z.string(),
  repo: z.string(),
  ref: z.string().optional(),
  path: z.string().optional(),
});

export const getTreeFn = createServerFn({ method: "GET" })
  .inputValidator(getTreeFnSchema)
  .handler(async ({ data }) => {
    const fullName = `${data.owner}/${data.repo}`;
    const stub = getRepoDOStub(fullName);
    const tree = await stub.getTree({
      ref: data.ref,
      path: data.path,
    });
    return tree;
  });

export const getTreeQueryOptions = (data: z.infer<typeof getTreeFnSchema>) =>
  queryOptions({
    queryKey: ["tree", data.owner, data.repo, data.ref, data.path].filter(
      Boolean
    ),
    queryFn: async () => await getTreeFn({ data }),
  });

export const getBlobFnSchema = z.object({
  owner: z.string(),
  repo: z.string(),
  ref: z.string().optional(),
  filepath: z.string(),
});

export const getBlobFn = createServerFn({ method: "GET" })
  .inputValidator(getBlobFnSchema)
  .handler(async ({ data }) => {
    const fullName = `${data.owner}/${data.repo}`;
    const stub = getRepoDOStub(fullName);
    const blob = await stub.getBlob({
      ref: data.ref,
      filepath: data.filepath,
    });

    if (!blob) {
      throw notFound();
    }

    // WE HAVE TO DO THIS BECAUSE OF CACHE SERIALIZATION ISSUES (TODO: REFACTOR)
    // Handle blob.content which might be a Uint8Array or a plain object from JSON serialization
    let contentBuffer: Buffer;
    if (blob.content instanceof Uint8Array) {
      contentBuffer = Buffer.from(blob.content);
    } else if (typeof blob.content === "object" && blob.content !== null) {
      // Handle JSON-serialized Uint8Array (object with numeric keys)
      contentBuffer = Buffer.from(Object.values(blob.content) as number[]);
    } else {
      contentBuffer = Buffer.from(blob.content as string);
    }

    const contentBase64 = contentBuffer.toString("base64");

    // Check if it's markdown
    const filename = data.filepath.split("/").pop() || data.filepath;
    const isMarkdown = filename.toLowerCase().match(/\.(md|mdx|markdown)$/);

    // Generate syntax-highlighted HTML for non-binary, non-markdown files
    let highlightedHtml: string | null = null;
    if (!blob.isBinary && !isMarkdown) {
      const language = getLanguageFromFilename(filename);
      const highlighter = await highlighterPromise;

      // Decode content for highlighting
      const decoder = new TextDecoder("utf-8");
      const content = decoder.decode(contentBuffer);

      try {
        await highlighter.loadLanguage(language);
      } catch {
        // Language not found, will use no highlighting
        console.warn(`Language "${language}" not found for file "${filename}"`);
      }

      highlightedHtml = highlighter.codeToHtml(content, {
        lang: language,
        theme: "github-dark-default",
        transformers: [
          {
            line(node, line) {
              node.properties["data-line"] = line;
            },
          },
        ],
      });
    }

    return {
      oid: blob.oid,
      content: contentBase64,
      size: blob.size,
      isBinary: blob.isBinary,
      highlightedHtml,
    };
  });

export const getBlobQueryOptions = (data: z.infer<typeof getBlobFnSchema>) =>
  queryOptions({
    queryKey: ["blob", data.owner, data.repo, data.ref, data.filepath].filter(
      Boolean
    ),
    queryFn: async () => await getBlobFn({ data }),
  });