wip fix
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { apiContext } from "service-srv";
|
||||
import { g } from "utils/global";
|
||||
|
||||
export const _ = {
|
||||
url: "/_api_frm",
|
||||
async api() {
|
||||
const { req, res } = apiContext(this);
|
||||
|
||||
let allowUrl = req.headers.get("origin") || req.headers.get("referer");
|
||||
|
||||
res.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
|
||||
res.setHeader("Access-Control-Allow-Headers", "content-type");
|
||||
res.setHeader("Access-Control-Allow-Credentials", "true");
|
||||
if (allowUrl) {
|
||||
res.setHeader("Access-Control-Allow-Origin", allowUrl);
|
||||
}
|
||||
|
||||
res.setHeader("content-type", "text/html");
|
||||
res.setHeader("etag", g.frm.etag);
|
||||
res.send(`<script>${g.frm.js}</script>`);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
import { apiContext } from "service-srv";
|
||||
import { execQuery } from "utils/query";
|
||||
|
||||
export const _ = {
|
||||
url: "/_dbs/:dbName/:action",
|
||||
async api(dbName: any, action?: string) {
|
||||
const { req, res } = apiContext(this);
|
||||
|
||||
const body = req.params;
|
||||
|
||||
try {
|
||||
const result = await execQuery(body, db);
|
||||
res.send(result);
|
||||
} catch (e: any) {
|
||||
res.sendStatus(500);
|
||||
res.send(e.message);
|
||||
console.error(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,228 @@
|
||||
import { $ } from "execa";
|
||||
import * as fs from "fs";
|
||||
import { dirAsync, removeAsync } from "fs-jetpack";
|
||||
import { apiContext } from "service-srv";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
import { restartServer } from "utils/restart";
|
||||
import { loadWebCache } from "../server/load-web";
|
||||
export const _ = {
|
||||
url: "/_deploy",
|
||||
async api(
|
||||
action: (
|
||||
| { type: "check" }
|
||||
| { type: "db-update"; url: string }
|
||||
| { type: "db-pull" }
|
||||
| { type: "restart" }
|
||||
| { type: "domain-add"; domain: string }
|
||||
| { type: "domain-del"; domain: string }
|
||||
| { type: "deploy-del"; ts: string }
|
||||
| { type: "deploy"; dlurl: string }
|
||||
| { type: "deploy-status" }
|
||||
| { type: "redeploy"; ts: string }
|
||||
) & {
|
||||
id_site: string;
|
||||
}
|
||||
) {
|
||||
const { res } = apiContext(this);
|
||||
|
||||
if (!g.web[action.id_site]) {
|
||||
g.web[action.id_site] = {
|
||||
current: 0,
|
||||
domains: [],
|
||||
deploying: null,
|
||||
router: null,
|
||||
deploys: [],
|
||||
site_id: action.id_site,
|
||||
cacheKey: 0,
|
||||
cache: null,
|
||||
};
|
||||
}
|
||||
const path = dir(`app/web/${action.id_site}`);
|
||||
await dirAsync(path);
|
||||
|
||||
const web = g.web[action.id_site];
|
||||
|
||||
if (!web.domains) {
|
||||
web.domains = [];
|
||||
}
|
||||
|
||||
switch (action.type) {
|
||||
case "check":
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: web.current,
|
||||
deploys: web.deploys,
|
||||
domains: web.domains,
|
||||
db: {
|
||||
url: g.dburl || "-",
|
||||
},
|
||||
};
|
||||
case "db-update":
|
||||
if (action.url) {
|
||||
g.dburl = action.url;
|
||||
await Bun.write(
|
||||
dir("app/db/.env"),
|
||||
`\
|
||||
DATABASE_URL="${action.url}"
|
||||
`
|
||||
);
|
||||
}
|
||||
return "ok";
|
||||
case "db-pull":
|
||||
{
|
||||
await $({ cwd: dir("app/db") })`bun prisma db pull`;
|
||||
await $({ cwd: dir("app/db") })`bun prisma generate`;
|
||||
res.send("ok");
|
||||
setTimeout(() => {
|
||||
restartServer();
|
||||
}, 300);
|
||||
}
|
||||
break;
|
||||
case "restart":
|
||||
{
|
||||
res.send("ok");
|
||||
setTimeout(() => {
|
||||
restartServer();
|
||||
}, 300);
|
||||
}
|
||||
break;
|
||||
case "domain-add":
|
||||
{
|
||||
web.domains.push(action.domain);
|
||||
await Bun.write(`${path}/domains.json`, JSON.stringify(web.domains));
|
||||
g.domains = null;
|
||||
res.send("ok");
|
||||
}
|
||||
break;
|
||||
case "domain-del":
|
||||
{
|
||||
web.domains = web.domains.filter((e) => e !== action.domain);
|
||||
await Bun.write(`${path}/domains.json`, web.domains);
|
||||
g.domains = null;
|
||||
|
||||
res.send("ok");
|
||||
}
|
||||
break;
|
||||
case "deploy-del":
|
||||
{
|
||||
web.deploys = web.deploys.filter((e) => e !== parseInt(action.ts));
|
||||
try {
|
||||
await removeAsync(`${path}/deploys/${action.ts}`);
|
||||
} catch (e) {}
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: web.current,
|
||||
deploys: web.deploys,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "deploy-status":
|
||||
break;
|
||||
case "deploy":
|
||||
{
|
||||
await fs.promises.mkdir(`${path}/deploys`, { recursive: true });
|
||||
const cur = Date.now();
|
||||
const filePath = `${path}/deploys/${cur}`;
|
||||
web.deploying = {
|
||||
status: "generating",
|
||||
received: 0,
|
||||
total: 0,
|
||||
};
|
||||
if (
|
||||
await downloadFile(action.dlurl, filePath, (rec, total) => {
|
||||
web.deploying = {
|
||||
status: "transfering",
|
||||
received: rec,
|
||||
total: total,
|
||||
};
|
||||
})
|
||||
) {
|
||||
web.deploying.status = "deploying";
|
||||
await fs.promises.writeFile(`${path}/current`, cur.toString());
|
||||
web.current = cur;
|
||||
web.deploys.push(cur);
|
||||
await loadWebCache(web.site_id, web.current);
|
||||
}
|
||||
web.deploying = null;
|
||||
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: web.current,
|
||||
deploys: web.deploys,
|
||||
};
|
||||
}
|
||||
break;
|
||||
case "redeploy":
|
||||
{
|
||||
const cur = parseInt(action.ts);
|
||||
const lastcur = web.current;
|
||||
try {
|
||||
if (web.deploys.find((e) => e === cur)) {
|
||||
web.current = cur;
|
||||
await fs.promises.writeFile(`${path}/current`, cur.toString());
|
||||
await loadWebCache(web.site_id, web.current);
|
||||
}
|
||||
} catch (e) {
|
||||
web.current = lastcur;
|
||||
web.deploys = web.deploys.filter((e) => e !== parseInt(action.ts));
|
||||
await removeAsync(`${path}/deploys/${action.ts}`);
|
||||
}
|
||||
|
||||
return {
|
||||
now: Date.now(),
|
||||
current: web.current,
|
||||
deploys: web.deploys,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const downloadFile = async (
|
||||
url: string,
|
||||
filePath: string,
|
||||
progress?: (rec: number, total: number) => void
|
||||
) => {
|
||||
try {
|
||||
const _url = new URL(url);
|
||||
if (_url.hostname === "localhost") {
|
||||
_url.hostname = "127.0.0.1";
|
||||
}
|
||||
const res = await fetch(_url);
|
||||
if (res.body) {
|
||||
const file = Bun.file(filePath);
|
||||
|
||||
const writer = file.writer();
|
||||
const reader = res.body.getReader();
|
||||
|
||||
// Step 3: read the data
|
||||
let receivedLength = 0; // received that many bytes at the moment
|
||||
let chunks = []; // array of received binary chunks (comprises the body)
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
writer.end();
|
||||
break;
|
||||
}
|
||||
|
||||
chunks.push(value);
|
||||
writer.write(value);
|
||||
receivedLength += value.length;
|
||||
|
||||
if (progress) {
|
||||
progress(
|
||||
receivedLength,
|
||||
parseInt(res.headers.get("content-length") || "0")
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,70 @@
|
||||
import { apiContext } from "service-srv";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
import { generateIndexHtml } from "../server/serve-web";
|
||||
import mime from "mime";
|
||||
|
||||
export const _ = {
|
||||
url: "/_file/**",
|
||||
async api() {
|
||||
const { req } = apiContext(this);
|
||||
const rpath = decodeURIComponent(req.params._);
|
||||
|
||||
let res = new Response("NOT FOUND", { status: 404 });
|
||||
try {
|
||||
if (rpath.startsWith("site")) {
|
||||
if (rpath === "site-html") {
|
||||
res = new Response(generateIndexHtml("[[base_url]]", "[[site_id]]"));
|
||||
}
|
||||
if (rpath === "site-zip") {
|
||||
const path = dir(`app/static/site.zip`);
|
||||
res = new Response(Bun.file(path));
|
||||
}
|
||||
if (rpath === "site-md5") {
|
||||
const path = dir(`app/static/md5`);
|
||||
res = new Response(Bun.file(path));
|
||||
}
|
||||
} else if (rpath.startsWith("current-")) {
|
||||
if (rpath.startsWith("current-md5-")) {
|
||||
const site_id = rpath.substring("current-md5-".length);
|
||||
const path = dir(`app/web/${site_id}/current`);
|
||||
res = new Response(Bun.file(path));
|
||||
} else {
|
||||
const site_id = rpath.substring("current-".length);
|
||||
const path = dir(`app/web/${site_id}/current`);
|
||||
const id = await Bun.file(path).text();
|
||||
if (id) {
|
||||
const path = dir(`app/web/${site_id}/deploys/${id}`);
|
||||
res = new Response(Bun.file(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
res = new Response("NOT FOUND", { status: 404 });
|
||||
}
|
||||
|
||||
const path = dir(`${g.datadir}/upload/${rpath}`);
|
||||
const file = Bun.file(path);
|
||||
|
||||
if (await file.exists()) {
|
||||
res = new Response(file);
|
||||
} else {
|
||||
res = new Response("NOT FOUND", { status: 404 });
|
||||
}
|
||||
|
||||
const arr = path.split("-");
|
||||
const ext = arr.pop();
|
||||
const fname = arr.join("-") + "." + ext;
|
||||
const ctype = mime.getType(fname);
|
||||
if (ctype) {
|
||||
res.headers.set("content-type", ctype);
|
||||
}
|
||||
|
||||
res.headers.set("Access-Control-Allow-Origin", "*");
|
||||
res.headers.set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT");
|
||||
res.headers.set("Access-Control-Allow-Headers", "content-type");
|
||||
res.headers.set("Access-Control-Allow-Credentials", "true");
|
||||
|
||||
return res;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { Database } from "bun:sqlite";
|
||||
import admin from "firebase-admin";
|
||||
import { apiContext } from "service-srv";
|
||||
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
|
||||
export const _ = {
|
||||
url: "/_notif/:action/:token",
|
||||
async api(
|
||||
action: string,
|
||||
data:
|
||||
| { type: "register"; token: string; id: string }
|
||||
| { type: "send"; id: string; body: string; title: string; data?: any }
|
||||
) {
|
||||
const { req } = apiContext(this);
|
||||
if (!g.firebaseInit) {
|
||||
g.firebaseInit = true;
|
||||
|
||||
try {
|
||||
g.firebase = admin.initializeApp({
|
||||
credential: admin.credential.cert(dir("firebase-admin.json")),
|
||||
});
|
||||
g.notif = {
|
||||
db: new Database(dir(`${g.datadir}/notif.sqlite`)),
|
||||
};
|
||||
|
||||
g.notif.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS notif (
|
||||
token TEXT PRIMARY KEY,
|
||||
id TEXT NOT NULL
|
||||
);
|
||||
`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
if (g.firebase) {
|
||||
switch (action) {
|
||||
case "register":
|
||||
{
|
||||
if (data && data.type === "register" && data.id) {
|
||||
if (data.token) {
|
||||
const q = g.notif.db.query(
|
||||
`SELECT * FROM notif WHERE token = '${data.token}'`
|
||||
);
|
||||
const result = q.all();
|
||||
if (result.length > 0) {
|
||||
g.notif.db.exec(
|
||||
`UPDATE notif SET id = '${data.id}' WHERE token = '${data.token}'`
|
||||
);
|
||||
} else {
|
||||
g.notif.db.exec(
|
||||
`INSERT INTO notif VALUES ('${data.token}', '${data.id}')`
|
||||
);
|
||||
}
|
||||
|
||||
return { result: "OK" };
|
||||
} else {
|
||||
return { error: "missing token" };
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
case "send":
|
||||
{
|
||||
if (data && data.type === "send") {
|
||||
const q = g.notif.db.query<{ token: string }, any>(
|
||||
`SELECT * FROM notif WHERE id = '${data.id}'`
|
||||
);
|
||||
let result = q.all();
|
||||
for (const c of result) {
|
||||
try {
|
||||
await g.firebase.messaging().send({
|
||||
notification: { body: data.body, title: data.title },
|
||||
data: data.data,
|
||||
token: c.token,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
result = result.filter((v) => v.token !== c.token);
|
||||
}
|
||||
}
|
||||
|
||||
return { result: "OK", totalDevice: result.length };
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { error: "missing ./firebase-admin.json" };
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,143 @@
|
||||
import { readAsync } from "fs-jetpack";
|
||||
import { apiContext } from "service-srv";
|
||||
import { g } from "utils/global";
|
||||
import { dir } from "utils/dir";
|
||||
|
||||
const cache = {
|
||||
dev: "",
|
||||
prod: "",
|
||||
};
|
||||
|
||||
export const _ = {
|
||||
url: "/_prasi/**",
|
||||
async api() {
|
||||
const { req, res } = apiContext(this);
|
||||
res.setHeader("Access-Control-Allow-Origin", "*");
|
||||
|
||||
const action = {
|
||||
_: () => {
|
||||
res.send({ prasi: "v2" });
|
||||
},
|
||||
"load.json": async () => {
|
||||
res.setHeader("content-type", "application/json");
|
||||
res.send(
|
||||
JSON.stringify({
|
||||
apiEntry: getApiEntry(),
|
||||
apiTypes: (await getApiTypes()) || "",
|
||||
prismaTypes: {
|
||||
"prisma.d.ts": await getPrisma("prisma"),
|
||||
"runtime/index.d.ts": await getPrisma("runtime"),
|
||||
"runtime/library.d.ts": await getPrisma("library"),
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
"load.js": async () => {
|
||||
res.setHeader("content-type", "text/javascript");
|
||||
|
||||
const url = req.query_parameters["url"]
|
||||
? JSON.stringify(req.query_parameters["url"])
|
||||
: "undefined";
|
||||
|
||||
if (!cache.dev) {
|
||||
cache.dev = `\
|
||||
(() => {
|
||||
const baseurl = new URL(location.href);
|
||||
baseurl.pathname = '';
|
||||
const url = ${url} || baseurl.toString();
|
||||
const w = window;
|
||||
if (!w.prasiApi) {
|
||||
w.prasiApi = {};
|
||||
}
|
||||
w.prasiApi[url] = {
|
||||
apiEntry: ${JSON.stringify(getApiEntry())},
|
||||
apiTypes: ${JSON.stringify((await getApiTypes()) || "")},
|
||||
prismaTypes: {
|
||||
"prisma.d.ts": ${await getPrisma("prisma")},
|
||||
"runtime/index.d.ts": ${await getPrisma("runtime")},
|
||||
"runtime/library.d.ts": ${await getPrisma("library")},
|
||||
},
|
||||
};
|
||||
})();`;
|
||||
|
||||
cache.prod = `\
|
||||
(() => {
|
||||
const baseurl = new URL(location.href);
|
||||
baseurl.pathname = '';
|
||||
const url = ${url} || baseurl.toString();
|
||||
const w = window;
|
||||
if (!w.prasiApi) {
|
||||
w.prasiApi = {};
|
||||
}
|
||||
w.prasiApi[url] = {
|
||||
apiEntry: ${JSON.stringify(getApiEntry())},
|
||||
};
|
||||
})();`;
|
||||
}
|
||||
|
||||
if (req.query_parameters["dev"]) {
|
||||
res.send(cache.dev);
|
||||
} else {
|
||||
res.send(cache.prod);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const pathname: keyof typeof action = req.params._.split("/")[0] as any;
|
||||
const run = action[pathname];
|
||||
|
||||
if (run) {
|
||||
await run();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const getApiEntry = () => {
|
||||
const res: any = {};
|
||||
for (const [k, v] of Object.entries(g.api)) {
|
||||
const name = k.substring(0, k.length - 3);
|
||||
res[name] = { ...v, name, path: `app/srv/api/${v.path}` };
|
||||
}
|
||||
|
||||
return res;
|
||||
};
|
||||
|
||||
const getApiTypes = async () => {
|
||||
return (
|
||||
`\
|
||||
declare module "gen/srv/api/entry" {
|
||||
export * as srv from "gen/srv/api/srv";
|
||||
}
|
||||
|
||||
` +
|
||||
((await readAsync(dir("app/srv/exports.d.ts"))) || "")
|
||||
.replace(/\"app\/srv\/api/gi, '"srv/api')
|
||||
.replace(
|
||||
'declare module "app/srv/exports"',
|
||||
'declare module "gen/srv/api/srv"'
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const getPrisma = async (path: string) => {
|
||||
if (path === "prisma")
|
||||
return JSON.stringify(
|
||||
(
|
||||
(await readAsync(dir("node_modules/.prisma/client/index.d.ts"))) || ""
|
||||
).replace(`@prisma/client/runtime/library`, `./runtime/library`)
|
||||
);
|
||||
|
||||
if (path === "runtime")
|
||||
return JSON.stringify(
|
||||
await readAsync(
|
||||
dir("node_modules/@prisma/client/runtime/index-browser.d.ts")
|
||||
)
|
||||
);
|
||||
|
||||
if (path === "library")
|
||||
return JSON.stringify(
|
||||
await readAsync(dir("node_modules/@prisma/client/runtime/library.d.ts"))
|
||||
);
|
||||
|
||||
return JSON.stringify({});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { g } from "utils/global";
|
||||
import { gzipAsync } from "utils/gzip";
|
||||
|
||||
export const _ = {
|
||||
url: "/_proxy/*",
|
||||
async api(arg: {
|
||||
url: string;
|
||||
method: "POST" | "GET";
|
||||
headers: any;
|
||||
body: any;
|
||||
}) {
|
||||
const res = await fetch(
|
||||
arg.url,
|
||||
arg.body
|
||||
? {
|
||||
method: arg.method || "POST",
|
||||
headers: arg.headers,
|
||||
body: arg.body,
|
||||
}
|
||||
: {
|
||||
headers: arg.headers,
|
||||
}
|
||||
);
|
||||
|
||||
let body: any = null;
|
||||
const headers: any = {};
|
||||
res.headers.forEach((v, k) => {
|
||||
headers[k] = v;
|
||||
});
|
||||
|
||||
body = await res.arrayBuffer();
|
||||
|
||||
if (headers["content-encoding"] === "gzip") {
|
||||
body = await gzipAsync(new Uint8Array(body));
|
||||
} else {
|
||||
delete headers["content-encoding"];
|
||||
}
|
||||
|
||||
return new Response(body, { headers });
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import mp from "@surfy/multipart-parser";
|
||||
import { writeAsync } from "fs-jetpack";
|
||||
import { apiContext } from "service-srv";
|
||||
import { dir } from "utils/dir";
|
||||
import { g } from "utils/global";
|
||||
export const _ = {
|
||||
url: "/_upload",
|
||||
async api(body: any) {
|
||||
const { req } = apiContext(this);
|
||||
let url = "";
|
||||
|
||||
const raw = await req.arrayBuffer();
|
||||
const parts = mp(Buffer.from(raw)) as Record<
|
||||
string,
|
||||
{ fileName: string; mime: string; type: string; buffer: Buffer }
|
||||
>;
|
||||
|
||||
for (const [_, part] of Object.entries(parts)) {
|
||||
const d = new Date();
|
||||
const path = `${d.getFullYear()}-${d.getMonth()}/${d.getDate()}/${d.getTime()}-${part.fileName
|
||||
?.replace(/[\W_]+/g, "-")
|
||||
.toLowerCase()}`;
|
||||
|
||||
url = `/_file/${path}`;
|
||||
await writeAsync(dir(`${g.datadir}/upload/${path}`), part.buffer);
|
||||
}
|
||||
|
||||
return url;
|
||||
},
|
||||
};
|
||||
function toArrayBuffer(buffer: Buffer) {
|
||||
return buffer.buffer.slice(
|
||||
buffer.byteOffset,
|
||||
buffer.byteOffset + buffer.byteLength
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import mime from "mime";
|
||||
import { apiContext } from "service-srv";
|
||||
import { g } from "utils/global";
|
||||
import { getApiEntry } from "./_prasi";
|
||||
|
||||
export const _ = {
|
||||
url: "/_web/:id/**",
|
||||
async api(id: string, _: string) {
|
||||
const { req, res } = apiContext(this);
|
||||
|
||||
const web = g.web[id];
|
||||
if (web) {
|
||||
const cache = web.cache;
|
||||
if (cache) {
|
||||
const parts = _.split("/");
|
||||
|
||||
switch (parts[0]) {
|
||||
case "site": {
|
||||
res.setHeader("content-type", "application/json");
|
||||
if (req.query_parameters["prod"]) {
|
||||
return {
|
||||
site: cache.site,
|
||||
pages: cache.pages.map((e) => {
|
||||
return {
|
||||
id: e.id,
|
||||
url: e.url,
|
||||
};
|
||||
}),
|
||||
api: getApiEntry(),
|
||||
};
|
||||
} else {
|
||||
return cache.site;
|
||||
}
|
||||
}
|
||||
case "pages": {
|
||||
res.setHeader("content-type", "application/json");
|
||||
return cache.pages.map((e) => {
|
||||
return {
|
||||
id: e.id,
|
||||
url: e.url,
|
||||
};
|
||||
});
|
||||
}
|
||||
case "page": {
|
||||
res.setHeader("content-type", "application/json");
|
||||
return cache.pages.find((e) => e.id === parts[1]);
|
||||
}
|
||||
case "npm-site": {
|
||||
let path = parts.slice(1).join("/");
|
||||
res.setHeader("content-type", mime.getType(path) || "text/plain");
|
||||
|
||||
if (path === "site.js") {
|
||||
path = "index.js";
|
||||
}
|
||||
return cache.npm.site[path];
|
||||
}
|
||||
case "npm-page": {
|
||||
const page_id = parts[1];
|
||||
if (cache.npm.pages[page_id]) {
|
||||
let path = parts.slice(2).join("/");
|
||||
res.setHeader("content-type", mime.getType(path) || "text/plain");
|
||||
|
||||
if (path === "page.js") {
|
||||
path = "index.js";
|
||||
}
|
||||
return cache.npm.pages[page_id][path];
|
||||
}
|
||||
res.setHeader("content-type", "text/javascript");
|
||||
}
|
||||
case "comp": {
|
||||
res.setHeader("content-type", "application/json");
|
||||
return cache.comps.find((e) => e.id === parts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return req.params;
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { generateAPIFrm } from "./server/api-frm";
|
||||
import { createServer } from "./server/create";
|
||||
import { prepareAPITypes } from "./server/prep-api-ts";
|
||||
import { config } from "./utils/config";
|
||||
import { g } from "./utils/global";
|
||||
import { createLogger } from "./utils/logger";
|
||||
import { loadWeb } from "./server/load-web";
|
||||
import { ensureNotRunning } from "utils/ensure";
|
||||
import { preparePrisma } from "utils/prisma";
|
||||
import { startDevWatcher } from "utils/dev-watcher";
|
||||
|
||||
g.mode = process.argv.includes("dev") ? "dev" : "prod";
|
||||
g.datadir = g.mode === "prod" ? "../data" : ".data";
|
||||
|
||||
await preparePrisma();
|
||||
await createLogger();
|
||||
await ensureNotRunning();
|
||||
|
||||
if (g.db) {
|
||||
await g.db.$connect();
|
||||
}
|
||||
|
||||
await config.init();
|
||||
|
||||
await loadWeb();
|
||||
|
||||
g.log.info(g.mode === "dev" ? "DEVELOPMENT" : "PRODUCTION");
|
||||
if (g.mode === "dev") {
|
||||
await startDevWatcher();
|
||||
}
|
||||
|
||||
await createServer();
|
||||
|
||||
await generateAPIFrm();
|
||||
await prepareAPITypes();
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "pkgs",
|
||||
"dependencies": {
|
||||
"@surfy/multipart-parser": "^1.0.2",
|
||||
"@swc/core": "^1.3.91",
|
||||
"@types/mime": "^3.0.2",
|
||||
"@types/unzipper": "^0.10.7",
|
||||
"execa": "^8.0.1",
|
||||
"fs-jetpack": "^5.1.0",
|
||||
"lmdb": "^2.8.5",
|
||||
"mime": "^3.0.0",
|
||||
"pino": "^8.15.3",
|
||||
"pino-pretty": "^10.2.0",
|
||||
"radash": "^11.0.0",
|
||||
"radix3": "^1.1.0",
|
||||
"typescript": "^5.2.2",
|
||||
"unzipper": "^0.10.14"
|
||||
}
|
||||
}
|
||||
@@ -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>`;
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { spawnSync } from "bun";
|
||||
import { dirAsync, removeAsync } from "fs-jetpack";
|
||||
import { dirname } from "path";
|
||||
import unzipper from "unzipper";
|
||||
import { dir } from "utils/dir";
|
||||
const res = await fetch(
|
||||
`https://github.com/avolut/prasi-api/archive/refs/heads/main.zip`,
|
||||
{ method: "get" }
|
||||
);
|
||||
|
||||
const data = await unzipper.Open.buffer(Buffer.from(await res.arrayBuffer()));
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
await removeAsync(dir("pkgs"));
|
||||
for (const file of data.files) {
|
||||
if (file.type === "File") {
|
||||
const path = file.path.split("/").slice(1).join("/");
|
||||
if (path === "tsconfig.json" || path.startsWith("pkgs")) {
|
||||
promises.push(
|
||||
new Promise<void>(async (done) => {
|
||||
await dirAsync(dirname(dir(path)));
|
||||
await Bun.write(dir(path), await file.buffer());
|
||||
done();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
await Promise.all(promises);
|
||||
|
||||
spawnSync({ cmd: ["bun", "install"], stdout: "inherit", stderr: "inherit" });
|
||||
@@ -0,0 +1,34 @@
|
||||
import { dirAsync, readAsync } from "fs-jetpack";
|
||||
import { dir } from "./dir";
|
||||
import { g } from "./global";
|
||||
|
||||
const _internal = { config: {} as any, writeTimeout: null as any };
|
||||
|
||||
export const config = {
|
||||
init: async () => {
|
||||
await dirAsync(dir(`${g.datadir}/config`));
|
||||
await dirAsync(dir(`${g.datadir}/files`));
|
||||
|
||||
_internal.config =
|
||||
(await readAsync(dir(`${g.datadir}/config/conf.json`), "json")) || {};
|
||||
},
|
||||
get all() {
|
||||
return _internal.config;
|
||||
},
|
||||
get(key: string) {
|
||||
if (key.endsWith("url")) {
|
||||
if (!(_internal.config[key] instanceof URL)) {
|
||||
_internal.config[key] = new URL(_internal.config[key] || "");
|
||||
}
|
||||
}
|
||||
|
||||
return _internal.config[key];
|
||||
},
|
||||
set(key: string, value: any) {
|
||||
_internal.config[key] = value;
|
||||
clearTimeout(_internal.writeTimeout);
|
||||
_internal.writeTimeout = setTimeout(() => {
|
||||
Bun.write(dir(`${g.datadir}/config/conf.json`), _internal.config);
|
||||
}, 100);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { file } from "bun";
|
||||
import { watch } from "fs";
|
||||
import { dir } from "./dir";
|
||||
import { dirAsync } from "fs-jetpack";
|
||||
|
||||
export const startDevWatcher = async () => {
|
||||
await dirAsync(dir(`app/srv/api`));
|
||||
watch(dir(`app/srv/api`), async (event, filename) => {
|
||||
const s = file(dir(`app/srv/api/${filename}`));
|
||||
if (s.size === 0) {
|
||||
await Bun.write(
|
||||
`app/srv/api/${filename}`,
|
||||
`\
|
||||
import { apiContext } from "service-srv";
|
||||
|
||||
export const _ = {
|
||||
url: "/${filename?.substring(0, filename.length - 3)})}",
|
||||
async api() {
|
||||
const { req, res } = apiContext(this);
|
||||
return "This is ${filename}";
|
||||
}
|
||||
}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
import { join } from "path";
|
||||
|
||||
export const dir = (path: string) => {
|
||||
return join(process.cwd(), path);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
import { connect } from "bun";
|
||||
import { g } from "./global";
|
||||
|
||||
export const ensureNotRunning = async () => {
|
||||
await new Promise<void>(async (resolve) => {
|
||||
const checkPort = () => {
|
||||
return new Promise<boolean>(async (done) => {
|
||||
try {
|
||||
const s = await connect({
|
||||
hostname: "0.0.0.0",
|
||||
port: g.port,
|
||||
socket: {
|
||||
open(socket) {},
|
||||
data(socket, data) {},
|
||||
close(socket) {},
|
||||
drain(socket) {},
|
||||
error(socket, error) {},
|
||||
},
|
||||
});
|
||||
s.end();
|
||||
done(false);
|
||||
} catch (e) {
|
||||
done(true);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
if (!(await checkPort())) {
|
||||
g.log.warn(`Port ${process.env.PORT} is used, waiting...`);
|
||||
setInterval(async () => {
|
||||
if (await checkPort()) resolve();
|
||||
}, 500);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,88 @@
|
||||
import { Server } from "bun";
|
||||
import { Logger } from "pino";
|
||||
import { RadixRouter } from "radix3";
|
||||
import { PrismaClient } from "../../app/db/db";
|
||||
|
||||
import admin from "firebase-admin";
|
||||
import { Database } from "bun:sqlite";
|
||||
|
||||
type SingleRoute = {
|
||||
url: string;
|
||||
args: string[];
|
||||
fn: (...arg: any[]) => Promise<any>;
|
||||
path: string;
|
||||
};
|
||||
|
||||
export const g = global as unknown as {
|
||||
db: PrismaClient;
|
||||
dburl: string;
|
||||
datadir: string;
|
||||
mode: "dev" | "prod";
|
||||
server: Server;
|
||||
log: Logger;
|
||||
firebaseInit: boolean,
|
||||
firebase: admin.app.App;
|
||||
notif: {
|
||||
db: Database;
|
||||
};
|
||||
api: Record<string, SingleRoute>;
|
||||
domains: null | Record<string, string>;
|
||||
web: Record<
|
||||
string,
|
||||
{
|
||||
site_id: string;
|
||||
current: number;
|
||||
deploying: null | { status: string; received: number; total: number };
|
||||
deploys: number[];
|
||||
domains: string[];
|
||||
router: null | RadixRouter<{ id: string }>;
|
||||
cacheKey: number;
|
||||
cache: null | {
|
||||
site: {
|
||||
id: string;
|
||||
name: string;
|
||||
favicon: string;
|
||||
domain: string;
|
||||
id_user: string;
|
||||
created_at: Date | null;
|
||||
id_org: string | null;
|
||||
updated_at: Date | null;
|
||||
responsive: string;
|
||||
} | null;
|
||||
pages: {
|
||||
id: string;
|
||||
name: string;
|
||||
url: string;
|
||||
content_tree: any;
|
||||
id_site: string;
|
||||
created_at: Date | null;
|
||||
js_compiled: string | null;
|
||||
js: string | null;
|
||||
updated_at: Date | null;
|
||||
id_folder: string | null;
|
||||
is_deleted: boolean;
|
||||
}[];
|
||||
npm: {
|
||||
site: Record<string, string>;
|
||||
pages: Record<string, Record<string, string>>;
|
||||
};
|
||||
comps: {
|
||||
id: string;
|
||||
name: string;
|
||||
content_tree: any;
|
||||
created_at: Date | null;
|
||||
updated_at: Date | null;
|
||||
type: string;
|
||||
id_component_group: string | null;
|
||||
props: any;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
>;
|
||||
router: RadixRouter<SingleRoute>;
|
||||
port: number;
|
||||
frm: {
|
||||
js: string;
|
||||
etag: string;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import { gzip, gunzip } from "zlib";
|
||||
|
||||
export const gzipAsync = (bin: Uint8Array) => {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
gzip(bin, (err, res) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(res);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const gunzipAsync = (bin: Uint8Array) => {
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
gunzip(bin, (err, result) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { pino } from "pino";
|
||||
import { g } from "./global";
|
||||
|
||||
export const createLogger = async () => {
|
||||
g.log = pino({
|
||||
transport: {
|
||||
target: "pino-pretty",
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { existsAsync } from "fs-jetpack";
|
||||
import { dir } from "./dir";
|
||||
import { $ } from "execa";
|
||||
import { g } from "./global";
|
||||
|
||||
export const preparePrisma = async () => {
|
||||
if (await existsAsync(dir("app/db/.env"))) {
|
||||
if (!(await existsAsync(dir("node_modules/.prisma")))) {
|
||||
await $({ cwd: dir(`app/db`) })`bun prisma generate`;
|
||||
}
|
||||
const { PrismaClient } = await import("../../app/db/db");
|
||||
g.db = new PrismaClient();
|
||||
}
|
||||
|
||||
g.dburl = process.env.DATABASE_URL || "";
|
||||
g.port = parseInt(process.env.PORT || "3000");
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
export type DBArg = {
|
||||
db: string;
|
||||
table: string;
|
||||
action: string;
|
||||
params: any[];
|
||||
};
|
||||
|
||||
export const execQuery = async (args: DBArg, prisma: any) => {
|
||||
const { table, action, params } = args;
|
||||
|
||||
const tableInstance = prisma[table];
|
||||
|
||||
if (tableInstance) {
|
||||
if (action === "query" && table.startsWith("$query")) {
|
||||
try {
|
||||
const q = params.shift();
|
||||
q.sql = true;
|
||||
Object.freeze(q);
|
||||
return await tableInstance.bind(prisma)(q, ...params);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return e;
|
||||
}
|
||||
}
|
||||
|
||||
const method = tableInstance[action];
|
||||
|
||||
if (method) {
|
||||
try {
|
||||
const result = await method(...params);
|
||||
|
||||
if (!result) {
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e: any) {
|
||||
throw new Error(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { $ } from "execa";
|
||||
import { g } from "./global";
|
||||
|
||||
export const restartServer = () => {
|
||||
if (g.mode === "dev") {
|
||||
$`bun ${g.mode}`;
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
export const waitUntil = (
|
||||
condition: number | (() => any),
|
||||
timeout?: number
|
||||
) => {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
if (typeof condition === "function") {
|
||||
let tout = null as any;
|
||||
if (timeout) {
|
||||
tout = setTimeout(resolve, timeout);
|
||||
}
|
||||
if (await condition()) {
|
||||
clearTimeout(tout);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
const c = setInterval(async () => {
|
||||
if (await condition()) {
|
||||
if (tout) clearTimeout(tout);
|
||||
clearInterval(c);
|
||||
resolve();
|
||||
}
|
||||
if (count > 100) {
|
||||
clearInterval(c);
|
||||
}
|
||||
}, 10);
|
||||
} else if (typeof condition === "number") {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, condition);
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user