This commit is contained in:
Rizky
2024-01-23 23:03:13 +07:00
commit 1bd21cf6a7
44 changed files with 2920 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import { g } from "../utils/global";
const parseQueryParams = (ctx: any) => {
const pageHref = ctx.req.url;
const searchParams = new URLSearchParams(
pageHref.substring(pageHref.indexOf("?"))
);
const result: any = {};
searchParams.forEach((v, k) => {
result[k] = v;
});
return result as any;
};
export const apiContext = (ctx: any) => {
ctx.req.params = ctx.params;
ctx.req.query_parameters = parseQueryParams(ctx);
return {
req: ctx.req as Request & { params: any; query_parameters: any },
res: {
...ctx.res,
send: (body) => {
ctx.res = createResponse(ctx.res, body);
},
sendStatus: (code: number) => {
ctx.res._status = code;
},
setHeader: (key: string, value: string) => {
ctx.res.headers.append(key, value);
},
} as Response & {
send: (body?: string | object) => void;
setHeader: (key: string, value: string) => void;
sendStatus: (code: number) => void;
},
};
};
export const createResponse = (existingRes: any, body: any) => {
const status =
typeof existingRes._status === "number" ? existingRes._status : undefined;
let res = new Response(
typeof body === "string" ? body : JSON.stringify(body),
status
? {
status,
}
: undefined
);
if (typeof body === "object") {
res.headers.append("content-type", "application/json");
}
const cur = existingRes as Response;
for (const [key, value] of cur.headers.entries()) {
res.headers.append(key, value);
}
return res;
};
+70
View File
@@ -0,0 +1,70 @@
import { transform } from "@swc/core";
import { g } from "../utils/global";
import { createHash } from "crypto";
export const generateAPIFrm = async () => {
const res = await transform(
`
(BigInt.prototype).toJSON = function () {
return "BigInt::" + this.toString();
};
const replacer = (key, value) => {
if (typeof value === "string" && value.startsWith('BigInt::')) {
return BigInt(value.substr(8));
}
return value;
}
window.addEventListener('message', (e) => {
const msg = e.data;
const init = Object.assign({}, msg.init)
let input = msg.input;
let url = msg.input;
if (typeof msg.input === 'string') {
if (!input.startsWith('http')) {
url = new URL(\`\$\{location.origin\}\$\{input\}\`)
} else {
url = new URL(input)
}
}
if (init && init.body && typeof init.body === 'object') {
if (Array.isArray(init.body)) {
const body = new FormData();
body.append("file", init.body[0]);
init.body = body;
}
}
fetch(url.pathname, init)
.then(async (res) => {
if (res) {
const body = await res.text();
if (res.ok) {
try {
parent.postMessage({result: JSON.parse(body, replacer), id: msg.id }, '*')
} catch(e) {
parent.postMessage({result: body, id: msg.id }, '*')
}
} else {
try {
parent.postMessage({error: JSON.parse(body, replacer), id: msg.id }, '*')
} catch(e) {
parent.postMessage({error: body, id: msg.id }, '*')
}
}
}
})
})
parent.postMessage('initialized', '*')`,
{ minify: true }
);
g.frm = {
js: res.code,
etag: createHash("md5").update(res.code).digest("hex"),
};
};
+121
View File
@@ -0,0 +1,121 @@
import { inspectAsync, listAsync } from "fs-jetpack";
import { join } from "path";
import { createRouter } from "radix3";
import { g } from "../utils/global";
import { parseArgs } from "./parse-args";
import { serveAPI } from "./serve-api";
import { serveWeb } from "./serve-web";
import { dir } from "../utils/dir";
import { file } from "bun";
import { trim } from "radash";
export const createServer = async () => {
g.router = createRouter({ strictTrailingSlash: true });
g.api = {};
const scan = async (path: string, root?: string) => {
const apis = await listAsync(path);
if (apis) {
for (const filename of apis) {
const importPath = join(path, filename);
if (filename.endsWith(".ts")) {
try {
const api = await import(importPath);
let args: string[] = await parseArgs(importPath);
const route = {
url: api._.url,
args,
fn: api._.api,
path: importPath.substring((root || path).length + 1),
};
g.api[filename] = route;
g.router.insert(route.url, g.api[filename]);
} catch (e) {
g.log.warn(
`Failed to import app/srv/api${importPath.substring(
(root || path).length
)}`
);
const f = file(importPath);
if (f.size > 0) {
console.error(e);
} else {
g.log.warn(` ➨ file is empty`);
}
}
} else {
const dir = await inspectAsync(importPath);
if (dir?.type === "dir") {
await scan(importPath, path);
}
}
}
}
};
await scan(dir(`app/srv/api`));
await scan(dir(`pkgs/api`));
g.server = Bun.serve({
port: g.port,
async fetch(req) {
const url = new URL(req.url);
const web = await serveWeb(url, req);
let index = ["", ""];
if (web) {
if (Array.isArray(web)) index = web;
else {
return web;
}
}
const api = await serveAPI(url, req);
if (api) {
return api;
}
if (index) {
let status: any = {};
if (!["", "index.html"].includes(trim(url.pathname, " /"))) {
status = {
status: 404,
statusText: "Not Found",
};
}
const [site_id, body] = index;
if (g.web[site_id]) {
const router = g.web[site_id].router;
if (router) {
let found = router.lookup(url.pathname);
if (!found) {
found = router.lookup(url.pathname + "/");
}
if (found) {
status = {};
}
}
}
return new Response(body, {
...status,
headers: {
"content-type": "text/html",
},
});
}
return new Response(`404 Not Found`, {
status: 404,
statusText: "Not Found",
});
},
});
if (process.env.PRASI_MODE === "dev") {
g.log.info(`http://localhost:${g.server.port}`);
} else {
g.log.info(`Started at port: ${g.server.port}`);
}
};
+129
View File
@@ -0,0 +1,129 @@
import { file } from "bun";
import { $ } from "execa";
import {
dirAsync,
existsAsync,
inspectTreeAsync,
readAsync,
removeAsync,
writeAsync,
} from "fs-jetpack";
import { createRouter } from "radix3";
import { gunzipSync } from "zlib";
import { downloadFile } from "../api/_deploy";
import { dir } from "../utils/dir";
import { g } from "../utils/global";
export const loadWeb = async () => {
g.web = {};
await dirAsync(dir(`app/static`));
const siteZip = `${
g.mode === "dev" ? "http://localhost:4550" : "https://prasi.app"
}/site-bundle`;
const zipPath = dir(`app/static/site.zip`);
const md5Path = dir(`app/static/md5`);
if (!(await file(zipPath).exists()) || !(await file(md5Path).exists())) {
const md5 = await fetch(`${siteZip}/md5`);
await writeAsync(md5Path, await md5.text());
await new Promise<void>((r) => setTimeout(r, 1000));
await downloadFile(`${siteZip}/download`, zipPath);
await removeAsync(dir(`app/static/site`));
await $({ cwd: dir(`app/static`) })`unzip site.zip`;
} else {
const md5 = await fetch(`${siteZip}/md5`);
const md5txt = await md5.text();
if (md5txt !== (await readAsync(md5Path))) {
const e = await fetch(`${siteZip}/download`);
await removeAsync(dir(`app/static`));
await dirAsync(dir(`app/static`));
await downloadFile(`${siteZip}/download`, zipPath);
await writeAsync(md5Path, md5txt);
await $({ cwd: dir(`app/static`) })`unzip site.zip`;
}
}
const list = await inspectTreeAsync(dir(`app/web`));
for (const web of list?.children || []) {
if (web.type === "file") continue;
const deploy = web.children?.find((e) => e.name === "deploys");
if (!deploy) {
await dirAsync(dir(`app/web/${web.name}/deploys`));
}
g.web[web.name] = {
current: parseInt(
(await readAsync(dir(`app/web/${web.name}/current`))) || "0"
),
deploys: deploy ? deploy.children.map((e) => parseInt(e.name)) : [],
domains:
(await readAsync(dir(`app/web/${web.name}/domains.json`), "json")) ||
[],
site_id: web.name,
deploying: null,
cacheKey: 0,
router: null,
cache: null,
};
const cur = g.web[web.name];
if (!cur.deploys.includes(cur.current)) {
cur.current = 0;
}
if (cur.current) {
await loadWebCache(cur.site_id, cur.current);
}
}
};
const decoder = new TextDecoder();
export const loadWebCache = async (site_id: string, ts: number | string) => {
const web = g.web[site_id];
if (web) {
const path = dir(`app/web/${site_id}/deploys/${ts}`);
if (await existsAsync(path)) {
const fileContent = await readAsync(path, "buffer");
if (fileContent) {
console.log(
`Loading site ${site_id}: ${humanFileSize(fileContent.byteLength)}`
);
const res = gunzipSync(fileContent);
web.cache = JSON.parse(decoder.decode(res));
web.router = createRouter();
for (const p of web.cache?.pages || []) {
web.router.insert(p.url, p);
}
}
}
}
};
function humanFileSize(bytes: any, si = false, dp = 1) {
const thresh = si ? 1000 : 1024;
if (Math.abs(bytes) < thresh) {
return bytes + " B";
}
const units = si
? ["kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]
: ["KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"];
let u = -1;
const r = 10 ** dp;
do {
bytes /= thresh;
++u;
} while (
Math.round(Math.abs(bytes) * r) / r >= thresh &&
u < units.length - 1
);
return bytes.toFixed(dp) + " " + units[u];
}
+36
View File
@@ -0,0 +1,36 @@
import { parseFile } from "@swc/core";
import { readAsync } from "fs-jetpack";
export const parseArgs = async (path: string) => {
const res = await parseFile(path, { syntax: "typescript" });
const args: string[] = [];
for (const e of res.body) {
if (
e.type === "ExportDeclaration" &&
e.declaration.type === "VariableDeclaration"
) {
const declare = e.declaration.declarations[0];
if (
declare &&
declare.type === "VariableDeclarator" &&
declare.id.type === "Identifier" &&
declare.id.value === "_" &&
declare.init?.type === "ObjectExpression"
) {
for (const prop of declare.init.properties) {
if (prop.type === "MethodProperty") {
for (const param of prop.params) {
if (
param.type === "Parameter" &&
param.pat.type === "Identifier"
) {
args.push(param.pat.value);
}
}
}
}
}
}
}
return args;
};
+55
View File
@@ -0,0 +1,55 @@
import { spawnSync } from "bun";
import { existsAsync, readAsync } from "fs-jetpack";
import { dir } from "../utils/dir";
import { g } from "../utils/global";
export const prepareAPITypes = async () => {
const out: string[] = [];
for (const [k, v] of Object.entries(g.api)) {
const name = k.substring(0, k.length - 3).replace(/\W/gi, "_");
let p = {
path: `"app/srv/api/${v.path}"`,
handler: `"./api/${v.path.substring(0, v.path.length - 3)}"`,
};
if (!(await existsAsync(dir(p.path)))) {
p.path = `"pkgs/api/${v.path}"`;
p.handler = `"../../pkgs/api/${v.path.substring(0, v.path.length - 3)}"`;
}
out.push(`\
export const ${name} = {
name: "${name}",
url: "${v.url}",
path: "app/srv/api/${v.path}",
args: ${JSON.stringify(v.args)},
handler: import(${p.handler})
}`);
}
await Bun.write(dir(`app/srv/exports.ts`), out.join(`\n`));
const targetFile = dir("app/srv/exports.d.ts");
spawnSync(
[
dir("node_modules/.bin/tsc"),
dir("app/srv/exports.ts"),
"--declaration",
"--emitDeclarationOnly",
"--outFile",
targetFile,
],
{
cwd: dir(`node_modules/.bin`),
}
);
let res = await readAsync(targetFile);
if (res) {
res = res.replace('export * from "@prisma/client";', "");
res = res.replace("server: Server;", "");
res = res.replace(`import { PrismaClient } from "app/db/db";`, "");
res = res.replace(`db: PrismaClient;`, "");
await Bun.write(targetFile, res);
}
};
+71
View File
@@ -0,0 +1,71 @@
import { createResponse } from "service-srv";
import { g } from "../utils/global";
export const serveAPI = async (url: URL, req: Request) => {
let found = g.router.lookup(url.pathname);
if (!found?.url) {
if (!url.pathname.endsWith("/")) {
found = g.router.lookup(url.pathname + "/");
}
if (!found?.url) {
found = null;
}
}
if (found) {
const params = { ...found.params };
let args = found.args.map((e) => {
return params[e];
});
if (req.method !== "GET") {
if (!req.headers.get("content-type")?.startsWith("multipart/form-data")) {
try {
const json = await req.json();
if (typeof json === "object") {
if (Array.isArray(json)) {
args = json;
for (let i = 0; i < json.length; i++) {
const val = json[i];
if (found.args[i]) {
params[found.args[i]] = val;
}
}
} else {
for (const [k, v] of Object.entries(json)) {
params[k] = v;
}
for (const [k, v] of Object.entries(params)) {
const idx = found.args.findIndex((arg) => arg === k);
if (idx >= 0) {
args[idx] = v;
}
}
}
}
} catch (e) {}
}
}
const current = {
req,
res: new Response(),
...found,
params,
};
const finalResponse = await current.fn(...args);
if (finalResponse instanceof Response) {
return finalResponse;
}
if (finalResponse) {
return createResponse(current.res, finalResponse);
}
return current.res;
}
};
+104
View File
@@ -0,0 +1,104 @@
import { statSync } from "fs";
import { join } from "path";
import { g } from "../utils/global";
import { dir } from "utils/dir";
const index = {
html: "",
css: {
src: null as any,
encoding: "",
},
isFile: {} as Record<string, boolean>,
};
export const serveWeb = async (url: URL, req: Request) => {
const domain = url.hostname;
let site_id = "";
if (!g.domains) {
g.domains = {};
for (const web of Object.values(g.web)) {
for (const d of web.domains) {
const durl = new URL(d);
g.domains[durl.hostname] = web.site_id;
}
}
}
if (typeof g.domains[domain] === "undefined") {
g.domains[domain] = "";
}
site_id = g.domains[domain];
if (!site_id) {
return false;
}
const base = dir(`app/static/site`);
// const base = `/Users/r/Developer/prasi/.output/app/srv/site`;
let path = join(base, url.pathname);
if (url.pathname === "/site_id") {
return new Response(site_id);
}
if (url.pathname.startsWith("/index.css")) {
if (!index.css.src) {
const res = await fetch("https://prasi.app/index.css");
index.css.src = await res.arrayBuffer();
index.css.encoding = res.headers.get("content-encoding") || "";
}
return new Response(index.css.src, {
headers: {
"content-type": "text/css",
"content-encoding": index.css.encoding,
},
});
}
try {
if (typeof index.isFile[path] === "undefined") {
const s = statSync(path);
if (s.isFile()) {
index.isFile[path] = true;
return new Response(Bun.file(path));
}
} else if (index.isFile[path]) {
return new Response(Bun.file(path));
}
} catch (e) {}
if (!index.html) {
index.html = generateIndexHtml("", site_id);
}
return [site_id, index.html];
};
export const generateIndexHtml = (base_url: string, site_id: string) => {
const base = base_url.endsWith("/")
? base_url.substring(0, base_url.length - 1)
: base_url;
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="${base}/index.css?fresh">
</head>
<body class="flex-col flex-1 w-full min-h-screen flex opacity-0">
<div style="position:absolute;opacity:0.1;top:0;left:0">&nbsp;</div>
<div id="root"></div>
<script src="${base}/site.js" type="module"></script>
<script>window.id_site = "${site_id}";</script>
<script
src="https://js.sentry-cdn.com/a2dfb8b1128f4018b83bdc9c08d18da2.min.js"
crossorigin="anonymous"
></script>
</body>
</html>`;
};