From 7497b32195786876f03c36d246247ebdfea8d588 Mon Sep 17 00:00:00 2001 From: Rizky Date: Wed, 7 Feb 2024 16:28:44 +0700 Subject: [PATCH] wip fix --- app/srv/api/deploy.ts | 118 ++++++++++++++++-- app/web/src/nova/deploy/base/base.tsx | 75 ++++++++++++ app/web/src/nova/deploy/base/component.tsx | 54 +++++++++ app/web/src/nova/deploy/base/page.tsx | 9 ++ app/web/src/nova/deploy/base/responsive.tsx | 24 ++++ app/web/src/nova/deploy/base/route.tsx | 82 +++++++++++++ app/web/src/nova/deploy/base/util.ts | 5 + app/web/src/nova/deploy/main.tsx | 2 + app/web/src/nova/deploy/root.tsx | 128 +++++++++++++++++++- app/web/src/nova/ed/logic/tree/build.tsx | 7 +- app/web/src/nova/vi/vi.tsx | 2 +- app/web/src/utils/ui/deadend.tsx | 9 ++ pkgs/core/{utils => }/build-deploy.ts | 3 +- pkgs/core/index.ts | 2 + pkgs/core/utils/dir.ts | 3 +- 15 files changed, 501 insertions(+), 22 deletions(-) create mode 100644 app/web/src/nova/deploy/base/base.tsx create mode 100644 app/web/src/nova/deploy/base/component.tsx create mode 100644 app/web/src/nova/deploy/base/page.tsx create mode 100644 app/web/src/nova/deploy/base/responsive.tsx create mode 100644 app/web/src/nova/deploy/base/route.tsx create mode 100644 app/web/src/nova/deploy/base/util.ts create mode 100644 app/web/src/utils/ui/deadend.tsx rename pkgs/core/{utils => }/build-deploy.ts (83%) diff --git a/app/srv/api/deploy.ts b/app/srv/api/deploy.ts index 8ddc3f01..41bd6d4a 100644 --- a/app/srv/api/deploy.ts +++ b/app/srv/api/deploy.ts @@ -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( + `\ - - - - + + + + -
- +
+ + `, - { headers: { "content-type": "text/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; + 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); } }, }; diff --git a/app/web/src/nova/deploy/base/base.tsx b/app/web/src/nova/deploy/base/base.tsx new file mode 100644 index 00000000..cdbc9208 --- /dev/null +++ b/app/web/src/nova/deploy/base/base.tsx @@ -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, + pending: new Set(), + }, + page: { + id: "", + url: "", + root: null as null | IRoot, + meta: null as null | Record, + cache: {} as Record< + string, + { + id: string; + url: string; + root: IRoot; + meta: Record; + } + >, + }, +}; + +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 + ); + } + } +}; diff --git a/app/web/src/nova/deploy/base/component.tsx b/app/web/src/nova/deploy/base/component.tsx new file mode 100644 index 00000000..d17bd7cf --- /dev/null +++ b/app/web/src/nova/deploy/base/component.tsx @@ -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; + 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); + } + } + } +}; diff --git a/app/web/src/nova/deploy/base/page.tsx b/app/web/src/nova/deploy/base/page.tsx new file mode 100644 index 00000000..db96cacb --- /dev/null +++ b/app/web/src/nova/deploy/base/page.tsx @@ -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; +}; \ No newline at end of file diff --git a/app/web/src/nova/deploy/base/responsive.tsx b/app/web/src/nova/deploy/base/responsive.tsx new file mode 100644 index 00000000..50b80581 --- /dev/null +++ b/app/web/src/nova/deploy/base/responsive.tsx @@ -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; + } + } +}; diff --git a/app/web/src/nova/deploy/base/route.tsx b/app/web/src/nova/deploy/base/route.tsx new file mode 100644 index 00000000..20036dfc --- /dev/null +++ b/app/web/src/nova/deploy/base/route.tsx @@ -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((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, root: IRoot) => { + for (const item of root.childs) { + genMeta( + { + comps: base.comp.list, + meta, + mode: "page", + }, + { item } + ); + } +}; diff --git a/app/web/src/nova/deploy/base/util.ts b/app/web/src/nova/deploy/base/util.ts new file mode 100644 index 00000000..9c1a8c81 --- /dev/null +++ b/app/web/src/nova/deploy/base/util.ts @@ -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(); +} diff --git a/app/web/src/nova/deploy/main.tsx b/app/web/src/nova/deploy/main.tsx index 4f4a18af..7b8b00f9 100644 --- a/app/web/src/nova/deploy/main.tsx +++ b/app/web/src/nova/deploy/main.tsx @@ -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); diff --git a/app/web/src/nova/deploy/root.tsx b/app/web/src/nova/deploy/root.tsx index a84f9e43..6120f8cb 100644 --- a/app/web/src/nova/deploy/root.tsx +++ b/app/web/src/nova/deploy/root.tsx @@ -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 ; + } + // #endregion + + // #region routing + const router = base.route.router; + if (!router) return Failed to create Router; + + const page = router.lookup(base.pathname); + if (!page) return Page Not Found; + + 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 ; + } else { + base.page.root = cache.root; + base.page.meta = cache.meta; + } + // #endregion + + return ( +
+
+ 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 }} + /> +
+
+ ); }; diff --git a/app/web/src/nova/ed/logic/tree/build.tsx b/app/web/src/nova/ed/logic/tree/build.tsx index 81523018..4e2952e5 100644 --- a/app/web/src/nova/ed/logic/tree/build.tsx +++ b/app/web/src/nova/ed/logic/tree/build.tsx @@ -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); diff --git a/app/web/src/nova/vi/vi.tsx b/app/web/src/nova/vi/vi.tsx index f66779ef..040a7f8b 100644 --- a/app/web/src/nova/vi/vi.tsx +++ b/app/web/src/nova/vi/vi.tsx @@ -20,7 +20,7 @@ export const Vi: FC<{ api?: any; db?: any; layout?: VG["layout"]; - script?: { init_local_effect: Record }; + script: { init_local_effect: Record }; visit?: VG["visit"]; render_stat?: "enabled" | "disabled"; on_status_changed?: (status: VG["status"]) => void; diff --git a/app/web/src/utils/ui/deadend.tsx b/app/web/src/utils/ui/deadend.tsx new file mode 100644 index 00000000..d8d95953 --- /dev/null +++ b/app/web/src/utils/ui/deadend.tsx @@ -0,0 +1,9 @@ +import { FC } from "react"; + +export const DeadEnd: FC<{ children: any }> = ({ children }) => { + return ( +
+ {children} +
+ ); +}; diff --git a/pkgs/core/utils/build-deploy.ts b/pkgs/core/build-deploy.ts similarity index 83% rename from pkgs/core/utils/build-deploy.ts rename to pkgs/core/build-deploy.ts index c02925e5..6960d524 100644 --- a/pkgs/core/utils/build-deploy.ts +++ b/pkgs/core/build-deploy.ts @@ -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", diff --git a/pkgs/core/index.ts b/pkgs/core/index.ts index 9724fc35..963aa63d 100644 --- a/pkgs/core/index.ts +++ b/pkgs/core/index.ts @@ -77,6 +77,8 @@ if (!g.parcel) { await parcelBuild(); } +await import("./build-deploy"); + const { createServer } = await import("./server/create"); await createServer(); g.status = "ready"; diff --git a/pkgs/core/utils/dir.ts b/pkgs/core/utils/dir.ts index c9eb33a8..d95046b7 100644 --- a/pkgs/core/utils/dir.ts +++ b/pkgs/core/utils/dir.ts @@ -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); }, };