179 lines
6.2 KiB
TypeScript
179 lines
6.2 KiB
TypeScript
import { Application, Router, HttpError, 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) => {
|
|
// ctx.throw(404);
|
|
// //ctx.response.status = 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>", "Port number to listen on", { default: 9999 })
|
|
.action(async ({ port }) => {
|
|
await app.listen({ port: port });
|
|
});
|
|
await cmd.parse(Deno.args);
|
|
}
|