118 lines
3.6 KiB
TypeScript
118 lines
3.6 KiB
TypeScript
import { defineConfig, type Plugin } from "vite";
|
|
import wasm from "vite-plugin-wasm";
|
|
import topLevelAwait from "vite-plugin-top-level-await";
|
|
|
|
// Normalise a Forge browse/raw URL to its contents API equivalent.
|
|
// Returns { apiUrl, accept } or null if no pattern matched.
|
|
function forgeApiUrl(input: string): { apiUrl: string; accept: string } | null {
|
|
try {
|
|
const u = new URL(input);
|
|
|
|
// GitHub: github.com/{owner}/{repo}/blob/{branch}/{filepath}
|
|
if (u.hostname === "github.com") {
|
|
const m = u.pathname.match(/^\/([^/]+)\/([^/]+)\/blob\/([^/]+)\/(.+)$/);
|
|
if (!m) return null;
|
|
const [, owner, repo, branch, filepath] = m;
|
|
return {
|
|
apiUrl: `https://api.github.com/repos/${owner}/${repo}/contents/${filepath}?ref=${encodeURIComponent(branch)}`,
|
|
accept: "application/vnd.github.v3+json",
|
|
};
|
|
}
|
|
|
|
// Forgejo / Gitea: {forge}/{owner}/{repo}/src/branch/{branch}/{filepath}
|
|
// {forge}/{owner}/{repo}/raw/branch/{branch}/{filepath}
|
|
const m = u.pathname.match(
|
|
/^\/([^/]+)\/([^/]+)\/(?:src|raw)\/branch\/([^/]+)\/(.+)$/,
|
|
);
|
|
if (!m) return null;
|
|
const [, owner, repo, branch, filepath] = m;
|
|
return {
|
|
apiUrl: `${u.origin}/api/v1/repos/${owner}/${repo}/contents/${filepath}?ref=${encodeURIComponent(branch)}`,
|
|
accept: "application/json",
|
|
};
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function forgeProxy(): Plugin {
|
|
async function handler(req: any, res: any) {
|
|
const inputUrl = new URL(req.url, "http://localhost").searchParams.get("url");
|
|
if (!inputUrl) {
|
|
res.statusCode = 400;
|
|
res.end("missing url parameter");
|
|
return;
|
|
}
|
|
|
|
const parsed = forgeApiUrl(inputUrl);
|
|
const apiUrl = parsed?.apiUrl ?? inputUrl;
|
|
const accept = parsed?.accept ?? "application/json";
|
|
|
|
try {
|
|
const upstream = await fetch(apiUrl, {
|
|
headers: { accept },
|
|
});
|
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
|
|
if (!upstream.ok) {
|
|
res.statusCode = upstream.status;
|
|
res.end(`HTTP ${upstream.status}`);
|
|
return;
|
|
}
|
|
|
|
const ct = upstream.headers.get("content-type") || "";
|
|
|
|
// Forgejo contents API — decode base64 content field
|
|
if (ct.includes("application/json")) {
|
|
const json = await upstream.json() as any;
|
|
if (json.encoding === "base64" && typeof json.content === "string") {
|
|
// content may have newlines inserted every 60 chars
|
|
const b64 = json.content.replace(/\s/g, "");
|
|
res.statusCode = 200;
|
|
res.end(Buffer.from(b64, "base64").toString("utf-8"));
|
|
return;
|
|
}
|
|
res.statusCode = 422;
|
|
res.end("Unexpected API response shape");
|
|
return;
|
|
}
|
|
|
|
// Plain text fallthrough (e.g. already a direct raw URL)
|
|
if (ct.includes("text/html")) {
|
|
res.statusCode = 422;
|
|
res.end("Forge returned HTML — check the URL or authentication");
|
|
return;
|
|
}
|
|
|
|
res.statusCode = 200;
|
|
res.end(await upstream.text());
|
|
} catch (e) {
|
|
res.statusCode = 502;
|
|
res.end(String(e));
|
|
}
|
|
}
|
|
|
|
return {
|
|
name: "forge-proxy",
|
|
configureServer(server) {
|
|
server.middlewares.use("/forge-proxy", handler);
|
|
},
|
|
configurePreviewServer(server) {
|
|
server.middlewares.use("/forge-proxy", handler);
|
|
},
|
|
};
|
|
}
|
|
|
|
export default defineConfig({
|
|
plugins: [wasm(), topLevelAwait(), forgeProxy()],
|
|
build: {
|
|
target: "esnext",
|
|
rollupOptions: {
|
|
external: (id) => {
|
|
// MicroPython is served from public/mp/ — never bundled.
|
|
if (id.includes("micropython.mjs")) return true;
|
|
return false;
|
|
},
|
|
},
|
|
},
|
|
});
|