wip fix
This commit is contained in:
@@ -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;
|
||||
};
|
||||
@@ -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"),
|
||||
};
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
};
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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"> </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>`;
|
||||
};
|
||||
Reference in New Issue
Block a user