import { Application, Router, isHttpError } from "https://deno.land/x/oak@v12.1.0/mod.ts"; import { searchRepositoryWithTopic, getRepositoryTags, getRepositoryContent, GiteaOption } from "./gitea.ts"; import { ContentsResponse } from "./gitea_api.d.ts"; import { Command } from "https://deno.land/x/cliffy@v0.25.7/mod.ts"; import { load } from "https://deno.land/std@0.191.0/dotenv/mod.ts"; const env = await load(); function getGiteaOptions(): GiteaOption | undefined { const token = env.TOKEN; if (token === undefined) { return undefined } return { token: token }; } const app = new Application(); const router = new Router(); const RelativeTopic = env.tag ?? "denolib"; export interface CompletionList { /** The list (or partial list) of completion items. */ items: string[]; /** If the list is a partial list, and further queries to the endpoint will * change the items, set `isIncomplete` to `true`. */ isIncomplete?: boolean; /** If one of the items in the list should be preselected (the default * suggestion), then set the value of `preselect` to the value of the item. */ preselect?: string; } router.get("/.well-known/deno-import-intellisense.json", (ctx) => { console.log("get /.well-known/deno-import-intellisense.json"); ctx.response.type = "application/json"; ctx.response.body = { "version": 2, "registries": [ { "schema": "/:package([a-z0-9_]*@[a-z0-9_]*)/:version?/:path*", "variables": [ { "key": "package", // "documentation": "/docs/packages/${package}", "url": "/packages/${package}" }, { "key": "version", "url": "/packages/${package}/versions" }, { "key": "path", // "documentation": "/docs/packages/${package}/${{version}}/paths/${path}", "url": "/packages/${package}/${{version}}/paths/${path}" } ] } ] }; }); router.get("/packages/:package", async (ctx) => { const packageName = ctx.params.package; console.log(`searchRepositoryWithTopic: ${packageName}`); const options = getGiteaOptions(); const repositories = await searchRepositoryWithTopic(RelativeTopic, options); const repo_name = repositories.data?.map((repo) => repo.full_name) .filter(x => x !== undefined) .map(x => x?.replace("/", "@")) ?? []; const completionList: CompletionList = { items: repo_name as string[], isIncomplete: true, // TODO: check if there are more than max results preselect: repo_name[0] }; ctx.response.type = "application/json"; ctx.response.body = completionList; }); router.get("/packages/:package/versions", async (ctx) => { const packageName = ctx.params.package; const [owner, repo] = packageName.split("@"); console.log(`getTags: owner: ${owner}, repo: ${repo}`); const options = getGiteaOptions(); const tags = await getRepositoryTags(owner, repo, options); const candidate = ["main", ...tags.map((tag) => tag.name) as string[]] const completionList: CompletionList = { items: candidate, isIncomplete: false, preselect: candidate[0] }; ctx.response.type = "application/json"; ctx.response.body = completionList; }); router.get("/packages/:package/:version/paths/:path*", async (ctx) => { const packageName = ctx.params.package; const version = ctx.params.version; const path = ctx.params.path; const [owner, repo] = packageName.split("@"); console.log(`getFilesEntry: owner: ${owner}, repo: ${repo}, path: ${path}, version: ${version}`); const options = getGiteaOptions(); const entries = await getRepositoryContent(owner, repo, path ?? "", version, options) as ContentsResponse[]; const completionList: CompletionList = { items: entries.map((entry) => entry.name) as string[], isIncomplete: false, preselect: entries[0].name }; ctx.response.type = "application/json"; ctx.response.body = completionList; }); router.get("/:package([a-z0-9_]*@[a-z0-9_]*)/:version?/:path*", async (ctx) => { const packageName = ctx.params.package; const [owner, repo] = packageName.split("@"); const version = ctx.params.version; const path = ctx.params.path; console.log(`getFiles: owner: ${owner}, repo: ${repo}, path: ${path}, version: ${version}`); const options = getGiteaOptions(); const entries = await getRepositoryContent(owner, repo, path ?? "", version, options); if (entries instanceof Array) { ctx.response.type = "application/json"; ctx.response.body = entries; } else { if ("errors" in entries) { ctx.throw(404); } // TODO: check if the file is text file or not (e.g. image) ctx.response.type = "text/plain"; ctx.response.body = atob(entries.content ?? ""); } }); app.use(async (ctx, next) => { try { await next(); } catch (err) { if (isHttpError(err)) { console.log(err); ctx.response.status = err.status; const { message, status, stack } = err; ctx.response.body = { message, status, stack }; } else { throw err; } } }); app.use(router.routes()); app.use(router.allowedMethods()); app.use(async (ctx, next) => { try { await next(); } catch (err) { console.log(err); if (isHttpError(err)) { ctx.response.status = err.status; const { message, status, stack } = err; ctx.response.body = { message, status, stack }; } else { throw err; } } }); //app.use(async (ctx, next) => { // ctx.throw(404); // //ctx.response.body = "Not Found"; // //await next(); //}); app.addEventListener("listen", ({ hostname, port, secure }) => { console.log(`🚀 Listening on: ${secure ? "https://" : "http://"}${hostname ?? "localhost"}:${port}`); }); if (import.meta.main) { const cmd = new Command() .version("0.1.0") .description("Simple Deno import intellisense proxy server for Gitea") .option("-p, --port ", "Port number to listen on", { default: 9999 }) .action(async ({ port }) => { await app.listen({ port: port }); }); await cmd.parse(Deno.args); }