This commit is contained in:
Rizky 2024-02-07 16:28:44 +07:00
parent 42c1793880
commit 7497b32195
15 changed files with 501 additions and 22 deletions

View File

@ -1,30 +1,122 @@
import { dir } from "dir";
import { apiContext } from "service-srv";
import { g } from "utils/global";
import { validate } from "uuid";
import { gzipAsync } from "../ws/sync/entity/zlib";
import { code } from "../ws/sync/editor/code/util-code";
export const _ = {
url: "/deploy/**",
url: "/deploy/:site_id/**",
async api() {
const { req, res } = apiContext(this);
const pathname = req.params["*"];
if (pathname === "index.html" || pathname === "_") {
return new Response(
const pathname: string = req.params["*"] || "";
const site_id = req.params.site_id as string;
const index_html = new Response(
`\
<!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="https://prasi.app/index.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title></title>
<link rel="stylesheet" href="https://prasi.app/index.css">
</head>
<body class="flex-col flex-1 w-full min-h-screen flex opacity-0">
<div id="root"></div>
<script src="/deploy/main.js" type="module"></script>
<div id="root"></div>
<script>
window._prasi={basepath: "/deploy/${site_id}",site_id:"${site_id}"}
</script>
<script src="/deploy/${site_id}/main.js" type="module"></script>
</body>
</html>`,
{ headers: { "content-type": "text/html" } }
);
if (!validate(site_id))
return new Response("site not found", { status: 403 });
if (pathname.startsWith("_prasi")) {
const action = pathname.split("/")[1];
switch (action) {
case "code": {
const arr = pathname.split("/").slice(2);
const codepath = arr.join("/");
const build_path = code.path(site_id, "site", "build", codepath);
const file = Bun.file(build_path);
if (!(await file.exists()))
return new Response("Code file not found", { status: 403 });
return new Response(file);
}
case "route": {
const site = await _db.site.findFirst({
where: { id: site_id },
select: {
id: true,
name: true,
domain: true,
responsive: true,
config: true,
},
});
let api_url = "";
if (site && site.config && (site.config as any).api_url) {
api_url = (site.config as any).api_url;
delete (site as any).config;
}
const urls = await _db.page.findMany({
where: {
id_site: site_id,
is_default_layout: false,
is_deleted: false,
},
select: { url: true, id: true },
});
return gzipAsync(
JSON.stringify({ site: { ...site, api_url }, urls }) as any
);
}
case "page": {
const page_id = pathname.split("/").pop() as string;
if (validate(page_id)) {
const page = await _db.page.findFirst({
where: { id: page_id },
select: { content_tree: true },
});
if (page) {
return gzipAsync(JSON.stringify(page.content_tree) as any);
}
}
return null;
}
case "comp": {
const ids = req.params.ids as string[];
const result = {} as Record<string, any>;
if (Array.isArray(ids)) {
const comps = await _db.component.findMany({
where: { id: { in: ids } },
select: { content_tree: true, id: true },
});
for (const comp of comps) {
result[comp.id] = comp.content_tree;
}
}
return gzipAsync(JSON.stringify(result) as any);
}
}
return new Response("action " + action + ": not found");
} else if (pathname === "index.html" || pathname === "_") {
return index_html;
} else {
const res = dir.path(`${g.datadir}/deploy/${pathname}`);
const file = Bun.file(res);
if (!(await file.exists())) {
return index_html;
}
return new Response(file);
}
},
};

View File

@ -0,0 +1,75 @@
import { RadixRouter, createRouter } from "radix3";
import { IRoot } from "../../../utils/types/root";
import { PG } from "../../ed/logic/ed-global";
import { IMeta } from "../../vi/utils/types";
import { IItem } from "../../../utils/types/item";
const w = window as any;
export const base = {
root: null as unknown as URL,
url(...arg: any[]) {
const pathname = arg
.map((e) => (Array.isArray(e) ? e.join("") : e))
.join("");
if (pathname.startsWith("/")) return this.root + pathname;
else return this.root.toString() + "/" + pathname;
},
get pathname() {
return location.pathname.substring(base.root.pathname.length);
},
site: { id: w._prasi?.site_id } as {
id: string;
name: string;
responsive: PG["site"]["responsive"];
domain: string;
api_url: string;
code: {
mode: "new";
};
api: any;
db: any;
},
init_local_effect: {} as any,
mode: "" as "desktop" | "mobile",
route: {
status: "init" as "init" | "loading" | "ready",
router: null as null | RadixRouter<{ id: string; url: string }>,
},
comp: {
list: {} as Record<string, IItem>,
pending: new Set<string>(),
},
page: {
id: "",
url: "",
root: null as null | IRoot,
meta: null as null | Record<string, IMeta>,
cache: {} as Record<
string,
{
id: string;
url: string;
root: IRoot;
meta: Record<string, IMeta>;
}
>,
},
};
export const initBaseConfig = () => {
if (!base.root) {
let url = new URL(location.href);
if (w._prasi.basepath) {
url.pathname = w._prasi.basepath;
}
base.root = new URL(`${url.protocol}//${url.host}${url.pathname}`);
if (base.root.pathname.endsWith("/")) {
base.root.pathname = base.root.pathname.substring(
0,
base.root.length - 1
);
}
}
};

View File

@ -0,0 +1,54 @@
import { IContent } from "../../../utils/types/general";
import { IItem } from "../../../utils/types/item";
import { ISection } from "../../../utils/types/section";
import { base } from "./base";
import { decompressBlob } from "./util";
export const scanComponent = async (items: IContent[]) => {
const comp = base.comp;
for (const item of items) {
if (item && item.type !== "text") {
scanSingle(item);
}
}
if (comp.pending.size > 0) {
try {
const raw = await (
await fetch(base.url`_prasi/comp`, {
method: "POST",
body: JSON.stringify({ ids: [...comp.pending] }),
})
).blob();
const res = JSON.parse(
await (await decompressBlob(raw)).text()
) as Record<string, IItem>;
for (const [id, item] of Object.entries(res)) {
comp.pending.delete(id);
comp.list[id] = item;
}
await scanComponent(Object.values(res));
} catch (e) {}
}
};
const scanSingle = (item: IItem | ISection) => {
const comp = base.comp;
if (item.type === "item") {
const comp_id = item.component?.id;
if (comp_id) {
if (!comp.list[comp_id] && !comp.pending.has(comp_id)) {
comp.pending.add(comp_id);
}
}
}
if (item.childs) {
for (const child of item.childs) {
if (child && child.type !== "text") {
scanSingle(child);
}
}
}
};

View File

@ -0,0 +1,9 @@
import { base } from "./base";
import { IRoot } from "../../../utils/types/root";
import { decompressBlob } from "./util";
export const loadPage = async (page_id: string) => {
const raw = await (await fetch(base.url`_prasi/page/${page_id}`)).blob();
const res = JSON.parse(await (await decompressBlob(raw)).text()) as IRoot;
return res;
};

View File

@ -0,0 +1,24 @@
import { base } from "./base";
import parseUA from "ua-parser-js";
export const detectResponsiveMode = () => {
const p = base;
if (p.site.id) {
if (!p.mode && !!p.site.responsive) {
if (
p.site.responsive !== "mobile-only" &&
p.site.responsive !== "desktop-only"
) {
const parsed = parseUA();
p.mode = parsed.device.type === "mobile" ? "mobile" : "desktop";
} else if (p.site.responsive === "mobile-only") {
p.mode = "mobile";
} else if (p.site.responsive === "desktop-only") {
p.mode = "desktop";
}
}
if (localStorage.getItem("prasi-editor-mode")) {
p.mode = localStorage.getItem("prasi-editor-mode") as any;
}
}
};

View File

@ -0,0 +1,82 @@
import { createRouter } from "radix3";
import { base } from "./base";
import { decompressBlob } from "./util";
import { IMeta } from "../../vi/utils/types";
import { IRoot } from "../../../utils/types/root";
import { genMeta } from "../../vi/meta/meta";
import { IItem } from "../../../utils/types/item";
import { apiProxy } from "../../../base/load/api/api-proxy";
import { dbProxy } from "../../../base/load/db/db-proxy";
export const initBaseRoute = async () => {
const raw = await (await fetch(base.url`_prasi/route`)).blob();
const router = createRouter<{ id: string; url: string }>();
try {
const res = JSON.parse(await (await decompressBlob(raw)).text()) as {
site: any;
urls: {
id: string;
url: string;
}[];
};
if (res && res.site && res.urls) {
base.site = res.site;
base.site.code = { mode: "new" };
await injectSiteScript();
base.site.api = apiProxy(base.site.api_url);
base.site.db = dbProxy(base.site.api_url);
const w = window as any;
w.serverurl = base.site.api_url;
w.db = base.site.db;
w.api = base.site.api;
for (const item of res.urls) {
router.insert(item.url, item);
}
}
} catch (e) {}
return router;
};
const injectSiteScript = () => {
return new Promise<void>((done) => {
const d = document;
const script = d.createElement("script");
script.onload = async () => {
done();
};
const url = base.site.api_url;
if (!localStorage.getItem("api-ts-" + url)) {
localStorage.setItem("api-ts-" + url, Date.now().toString());
}
const ts = localStorage.getItem("api-ts-" + url);
script.src = `${url}/_prasi/load.js?url=${url}&v3&ts=${ts}`;
if (!document.querySelector(`script[src="${script.src}"]`)) {
d.body.appendChild(script);
} else {
done();
}
});
};
export const rebuildMeta = (meta: Record<string, IMeta>, root: IRoot) => {
for (const item of root.childs) {
genMeta(
{
comps: base.comp.list,
meta,
mode: "page",
},
{ item }
);
}
};

View File

@ -0,0 +1,5 @@
export async function decompressBlob(blob: Blob) {
let ds = new DecompressionStream("gzip");
let decompressedStream = blob.stream().pipeThrough(ds);
return await new Response(decompressedStream).blob();
}

View File

@ -1,8 +1,10 @@
import { createRoot } from "react-dom/client";
import { defineReact, defineWindow } from "web-utils";
import { Root } from "./root";
import { initBaseConfig } from "./base/base";
(async () => {
initBaseConfig();
const div = document.getElementById("root");
if (div) {
const root = createRoot(div);

View File

@ -1,7 +1,129 @@
import { useState } from "react";
import { useLocal } from "web-utils";
import { DeadEnd } from "../../utils/ui/deadend";
import { Loading } from "../../utils/ui/loading";
import { base } from "./base/base";
import { loadPage } from "./base/page";
import { detectResponsiveMode } from "./base/responsive";
import { initBaseRoute, rebuildMeta } from "./base/route";
import { scanComponent } from "./base/component";
import { Vi } from "../vi/vi";
import { evalCJS } from "../ed/logic/ed-sync";
const w = window as any;
export const Root = () => {
const [_, render] = useState({});
const local = useLocal({});
return <></>;
// #region init
if (base.route.status !== "ready") {
if (base.route.status === "init") {
base.route.status = "loading";
initBaseRoute().then(async (router) => {
detectResponsiveMode();
base.route.status = "ready";
base.route.router = router;
const site_script = evalCJS(
await (
await fetch(`/deploy/${base.site.id}/_prasi/code/index.js`)
).text()
);
if (site_script) {
for (const [k, v] of Object.entries(site_script)) {
w[k] = v;
}
}
local.render();
});
}
return <Loading note="Loading router" />;
}
// #endregion
// #region routing
const router = base.route.router;
if (!router) return <DeadEnd>Failed to create Router</DeadEnd>;
const page = router.lookup(base.pathname);
if (!page) return <DeadEnd>Page Not Found</DeadEnd>;
w.params = page.params;
base.page.id = page.id;
base.page.url = page.url;
const cache = base.page.cache[page.id];
if (!cache) {
loadPage(page.id)
.then(async (root) => {
const p = {
id: page.id,
url: page.url,
root,
meta: {},
};
await scanComponent(root.childs);
rebuildMeta(p.meta, root);
base.page.cache[p.id] = p;
local.render();
})
.catch(() => {
local.render();
});
return <Loading note="Loading page" />;
} else {
base.page.root = cache.root;
base.page.meta = cache.meta;
}
// #endregion
return (
<div className={cx("relative flex flex-1 items-center justify-center")}>
<div
className={cx(
"absolute flex flex-col items-stretch flex-1 bg-white main-content-preview",
base.mode === "mobile"
? css`
@media (min-width: 768px) {
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
width: 375px;
top: 0px;
overflow-x: hidden;
overflow-y: auto;
bottom: 0px;
}
@media (max-width: 767px) {
left: 0px;
right: 0px;
top: 0px;
bottom: 0px;
overflow-y: auto;
}
`
: "inset-0 overflow-auto",
css`
contain: content;
`
)}
>
<Vi
api_url={base.site.api_url}
entry={Object.values(base.page.root.childs)
.filter((e) => e)
.map((e) => e.id)}
meta={base.page.meta}
mode={base.mode}
page_id={base.page.id}
site_id={base.site.id}
db={base.site.db}
api={base.site.api}
script={{ init_local_effect: base.init_local_effect }}
/>
</div>
</div>
);
};

View File

@ -1,15 +1,14 @@
import { get, set } from "idb-keyval";
import { IContent } from "../../../../utils/types/general";
import { IItem, MItem } from "../../../../utils/types/item";
import { FMCompDef } from "../../../../utils/types/meta-fn";
import { initLoadComp } from "../../../vi/meta/comp/init-comp-load";
import { genMeta } from "../../../vi/meta/meta";
import { nav } from "../../../vi/render/script/extract-nav";
import { loadCompSnapshot, loadComponent } from "../comp/load";
import { loadCompSnapshot } from "../comp/load";
import { IMeta, PG, active } from "../ed-global";
import { assignMitem } from "./assign-mitem";
import { pushTreeNode } from "./build/push-tree";
import { createId } from "@paralleldrive/cuid2";
import { FMCompDef } from "../../../../utils/types/meta-fn";
export const treeCacheBuild = async (p: PG, page_id: string) => {
const page_cache = p.preview.page_cache[page_id];
@ -30,6 +29,8 @@ export const treeCacheBuild = async (p: PG, page_id: string) => {
page_cache.root as unknown as IItem,
{
async load(comp_ids) {
if (!p.sync) return;
const ids = comp_ids.filter((id) => !p.comp.loaded[id]);
const comps = await p.sync.comp.load(ids, true);
let result = Object.entries(comps);

View File

@ -20,7 +20,7 @@ export const Vi: FC<{
api?: any;
db?: any;
layout?: VG["layout"];
script?: { init_local_effect: Record<string, boolean> };
script: { init_local_effect: Record<string, boolean> };
visit?: VG["visit"];
render_stat?: "enabled" | "disabled";
on_status_changed?: (status: VG["status"]) => void;

View File

@ -0,0 +1,9 @@
import { FC } from "react";
export const DeadEnd: FC<{ children: any }> = ({ children }) => {
return (
<div className="flex items-center justify-center w-full h-full fixed inset-0">
{children}
</div>
);
};

View File

@ -1,11 +1,12 @@
import { dir } from "dir";
import { context } from "esbuild";
import { g } from "./utils/global";
const ctx = await context({
bundle: true,
absWorkingDir: dir.path(""),
entryPoints: [dir.path("app/web/src/nova/deploy/main.tsx")],
outdir: dir.path("app/static/deploy"),
outdir: dir.path(`${g.datadir}/deploy`),
splitting: true,
format: "esm",
jsx: "transform",

View File

@ -77,6 +77,8 @@ if (!g.parcel) {
await parcelBuild();
}
await import("./build-deploy");
const { createServer } = await import("./server/create");
await createServer();
g.status = "ready";

View File

@ -2,6 +2,7 @@ import { join } from "path";
export const dir = {
path: (path: string) => {
return join(process.cwd(), path);
const final_path = path.split("/").filter((e) => e !== "..").join('/');
return join(process.cwd(), final_path);
},
};