This commit is contained in:
Rizky 2023-10-14 13:40:33 +07:00
parent b58490700b
commit de88c0adcf
21 changed files with 614 additions and 51 deletions

44
app/srv/exports.d.ts vendored
View File

@ -0,0 +1,44 @@
declare module "exports" {
export const _web: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<any>;
};
export const _upload: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<any>;
};
export const _prasi: {
name: string;
url: string;
path: string;
args: any[];
handler: Promise<any>;
};
export const _file: {
name: string;
url: string;
path: string;
args: any[];
handler: Promise<any>;
};
export const _api_frm: {
name: string;
url: string;
path: string;
args: any[];
handler: Promise<any>;
};
export const _dbs: {
name: string;
url: string;
path: string;
args: string[];
handler: Promise<any>;
};
}

View File

@ -0,0 +1,42 @@
export const _web = {
name: "_web",
url: "/_web/:id/**",
path: "app/srv/api/_web.ts",
args: ["id","_"],
handler: import("./api/_web")
}
export const _upload = {
name: "_upload",
url: "/_upload",
path: "app/srv/api/_upload.ts",
args: ["body"],
handler: import("./api/_upload")
}
export const _prasi = {
name: "_prasi",
url: "/_prasi/**",
path: "app/srv/api/_prasi.ts",
args: [],
handler: import("./api/_prasi")
}
export const _file = {
name: "_file",
url: "/_file/**",
path: "app/srv/api/_file.ts",
args: [],
handler: import("./api/_file")
}
export const _api_frm = {
name: "_api_frm",
url: "/_api_frm",
path: "app/srv/api/_api_frm.ts",
args: [],
handler: import("./api/_api_frm")
}
export const _dbs = {
name: "_dbs",
url: "/_dbs/:dbName/:action",
path: "app/srv/api/_dbs.ts",
args: ["dbName","action"],
handler: import("./api/_dbs")
}

View File

@ -15,6 +15,7 @@
"@swc/wasm-web": "1.3.94-nightly-20231014.1",
"algoliasearch": "^4.20.0",
"date-fns": "^2.30.0",
"dbgen": "workspace:*",
"downshift": "^8.2.2",
"esbuild-wasm": "^0.19.4",
"idb-keyval": "^6.2.1",
@ -40,6 +41,7 @@
"prettier": "3.0.3",
"prop-types": "^15.8.1",
"quill-delta": "^5.1.0",
"radix3": "^1.1.0",
"react": "18.2.0",
"react-colorful": "^5.6.1",
"react-dnd": "^16.0.1",
@ -52,11 +54,10 @@
"tinycolor2": "^1.6.0",
"ua-parser-js": "^1.0.36",
"uuid": "9.0.1",
"web-utils": "workspace:*",
"y-pojo": "^0.0.8",
"yjs": "^13.6.8",
"yjs-types": "^0.0.1",
"web-utils": "workspace:*",
"dbgen": "workspace:*"
"yjs-types": "^0.0.1"
},
"devDependencies": {
"@types/lodash.concat": "^4.5.7",

View File

@ -1,9 +1,12 @@
import { useEffect } from "react";
import { page } from "web-utils";
export default page({
url: "*",
url: "**",
component: ({}) => {
navigate("/login");
useEffect(() => {
navigate("/login");
}, []);
return <div>Loading...</div>;
},
});

View File

@ -13,6 +13,7 @@ export default page({
return (
<Live
mode={"dev"}
domain={params.domain}
pathname={`/${params._ === "_" ? "" : params._}`}
loader={defaultLoader}

View File

@ -0,0 +1,8 @@
export const all = {
url: "**",
page: () => import("./page/all"),
};
export const login = {
url: "/login",
page: () => import("./page/auth/login"),
};

View File

@ -1,25 +1,45 @@
import { FC } from "react";
import { useState } from "react";
import { GlobalContext } from "web-utils";
const w = window as unknown as {
prasiContext: any;
rootRender: any;
};
w.prasiContext = {
global: {},
render() {},
};
import { createRouter } from "radix3";
import { FC, Suspense, lazy } from "react";
import { GlobalContext, useLocal } from "web-utils";
import { Loading } from "../utils/ui/loading";
export const Root: FC<{}> = ({}) => {
const [_, render] = useState({});
w.prasiContext.render = () => {
render({});
};
w.rootRender = w.prasiContext.render;
const local = useLocal(
{
router: createRouter<any>({ strictTrailingSlash: true }),
Page: null as any,
},
async () => {
const pages = await import("./pages");
for (const [_, v] of Object.entries(pages)) {
local.router.insert(
v.url,
lazy(async () => {
return { default: (await v.page()).default.component as any };
})
);
}
local.render();
}
);
prasiContext.render = local.render;
const Provider = GlobalContext.Provider as FC<{ value: any; children: any }>;
return <Provider value={w.prasiContext}>Hello mantapun alamuko</Provider>;
const found = local.router.lookup(location.pathname);
if (found) {
local.Page = found;
}
if (!local.Page) {
return <Loading />;
}
return (
<Provider value={prasiContext}>
<Suspense>
<local.Page />
</Suspense>
</Provider>
);
};

View File

@ -1,6 +1,8 @@
import { createRoot } from "react-dom/client";
import "./index.css";
import { defineReact, defineWindow } from "web-utils";
import { Root } from "./base/root";
import "./index.css";
import { createAPI, createDB, reloadDBAPI } from "./utils/script/init-api";
const registerServiceWorker = async () => {
if ("serviceWorker" in navigator) {
@ -21,6 +23,18 @@ const registerServiceWorker = async () => {
registerServiceWorker();
const el = document.getElementById("root");
if (el) {
createRoot(el).render(<Root />);
(async () => {
defineReact();
await defineWindow(false);
const w = window as any;
const base = `${location.protocol}//${location.host}`;
await reloadDBAPI(base);
w.api = createAPI(base);
w.db = createDB(base);
createRoot(el).render(<Root />);
})();
}

View File

@ -1,7 +1,7 @@
import { createStore, get, set } from "idb-keyval";
import trim from "lodash.trim";
import { apiClient, dbClient } from "web-utils";
import { createFrameCors } from "web-utils/src/web/iframe-cors";
import { createFrameCors } from "web-utils";
export const w = window as unknown as {
prasiApi: Record<string, any>;
apiHeaders: any;
@ -16,6 +16,10 @@ export const createAPI = (url: string) => {
w.apiClient = apiClient;
}
if (!w.prasiApi) {
w.prasiApi = {};
}
return w.apiClient(w.prasiApi[url]?.apiEntry, url);
};

BIN
bun.lockb

Binary file not shown.

View File

@ -3,7 +3,7 @@
"module": "src/index.ts",
"type": "module",
"scripts": {
"dev": "bun run --silent --watch ./pkgs/core/index.ts dev",
"dev": "bun clean && bun run --silent --watch ./pkgs/core/index.ts dev",
"clean": "rm -rf app/static && rm -rf app/web/.parcel-cache",
"prod": "bun run --silent ./pkgs/core/index.ts",
"pull": "cd app/db && bun prisma db pull && bun prisma generate",

View File

@ -48,5 +48,5 @@ export const scanApi = async () => {
}
};
await scan(dir(`app/srv/api`));
await scan(dir(`pkgs/api`));
await scan(dir(`pkgs/core/api`));
};

View File

@ -13,25 +13,18 @@ export const createServer = async () => {
async fetch(req) {
const url = new URL(req.url);
if (req.method === "GET") {
try {
const file = Bun.file(dir(`app/static${url.pathname}`));
if (file.type !== "application/octet-stream") {
return new Response(file as any);
}
} catch (e) {}
return new Response(Bun.file(dir(`app/static/index.html`)) as any);
} else {
const api = await serveAPI(url, req);
if (api) {
return api;
}
const api = await serveAPI(url, req);
if (api) {
return api;
}
return new Response(`404 Not Found`, {
status: 404,
statusText: "Not Found",
});
try {
const file = Bun.file(dir(`app/static${url.pathname}`));
if (file.type !== "application/octet-stream") {
return new Response(file as any);
}
} catch (e) {}
return new Response(Bun.file(dir(`app/static/index.html`)) as any);
},
});

View File

@ -3,7 +3,9 @@
"main": "src/export.ts",
"dependencies": {
"@paralleldrive/cuid2": "2.2.0",
"@types/hash-sum": "^1.0.0",
"goober": "^2.1.13",
"hash-sum": "^2.0.0",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@ -0,0 +1,78 @@
import { fetchSendApi } from "./client-frame";
export const apiClient = (
api: Record<string, { url: string; args: any[] }>,
apiUrl: string
) => {
return new Proxy(
{},
{
get: (_, actionName: string) => {
const createFn = (actionName: string) => {
return function (this: { apiUrl: string } | undefined, ...rest: any) {
return new Promise<any>(async (resolve, reject) => {
try {
let _apiURL = apiUrl;
if (typeof this?.apiUrl === "string") {
_apiURL = this.apiUrl;
}
if (!api || !api[actionName]) {
resolve(null);
console.error(
`API ${actionName.toString()} not found, existing API: ${Object.keys(
api
)}`
);
return;
}
let actionUrl = api[actionName].url;
const actionParams = api[actionName].args;
if (actionUrl && actionParams) {
if (rest.length > 0 && actionParams.length > 0) {
for (const [idx, p] of Object.entries(rest)) {
const paramName = actionParams[parseInt(idx)];
if (actionParams && actionParams.includes(paramName)) {
if (
!!p &&
typeof p !== "string" &&
typeof p !== "number"
) {
continue;
}
}
actionUrl = actionUrl.replace(`:${paramName}?`, p + "");
actionUrl = actionUrl.replace(`:${paramName}`, p + "");
}
}
const url = `${_apiURL}${actionUrl}`;
const result = await fetchSendApi(url, rest);
resolve(result);
} else {
console.error(`API Not Found: ${actionName.toString()}`);
}
} catch (e) {
reject(e);
}
});
};
};
if (actionName === "then") {
return new Proxy(
{},
{
get: (_, actionName: string) => {
return createFn(actionName);
},
}
);
}
return createFn(actionName);
},
}
);
};

View File

@ -0,0 +1,131 @@
import { waitUntil } from "web-utils";
import { createFrameCors } from "./client-frame";
import hash_sum from "hash-sum";
export const dbClient = (name: string, dburl?: string) => {
return new Proxy(
{},
{
get(_, table: string) {
if (table === "_tables") {
return () => {
return fetchSendDb(
name,
{
name,
action: "definition",
table: "*",
},
dburl
);
};
}
if (table === "_definition") {
return (table: string) => {
return fetchSendDb(
name,
{
name,
action: "definition",
table,
},
dburl
);
};
}
if (table.startsWith("$")) {
return (...params: any[]) => {
return fetchSendDb(
name,
{
name,
action: "query",
table,
params,
},
dburl
);
};
}
return new Proxy(
{},
{
get(_, action: string) {
return (...params: any[]) => {
if (table === "query") {
table = action;
action = "query";
}
return fetchSendDb(
name,
{
name,
action,
table,
params,
},
dburl
);
};
},
}
);
},
}
);
};
const cachedQueryResult: Record<string, { timestamp: number; result: any }> =
{};
export const fetchSendDb = async (
name: string,
params: any,
dburl?: string
) => {
const w = typeof window === "object" ? window : (globalThis as any);
let url = `/_dbs/${name}`;
let frm: Awaited<ReturnType<typeof createFrameCors>>;
if (params.table) {
url += `/${params.table}`;
}
const _base = dburl || w.serverurl;
if (!w.frmapi) {
w.frmapi = {};
}
if (!w.frmapi[_base]) {
w.frmapi[_base] = await createFrameCors(_base);
}
frm = w.frmapi[_base];
if (!frm) {
await waitUntil(() => {
frm = w.frmapi[_base];
return frm;
});
}
const hsum = hash_sum(params);
const cached = cachedQueryResult[hsum];
if (!cached || (cached && Date.now() - cached.timestamp > 1000)) {
cachedQueryResult[hsum] = {
timestamp: Date.now(),
result: null,
};
const result = await frm.send(url, params, w.apiHeaders);
cachedQueryResult[hsum].result = result;
return result;
}
return cached.result;
};

View File

@ -0,0 +1,208 @@
import { waitUntil } from "web-utils";
import { createId } from "@paralleldrive/cuid2";
const cuid = createId;
(BigInt.prototype as any).toJSON = function (): string {
return `BigInt::` + this.toString();
};
export const createFrameCors = async (url: string, win?: any) => {
let w = window;
if (!!win) {
w = win;
}
const document = w.document;
const id = `__` + url.replace(/\W/g, "");
if (typeof document !== "undefined" && !document.querySelector(`#${id}`)) {
const iframe = document.createElement("iframe");
iframe.style.display = "none";
iframe.id = id;
const _url = new URL(url);
_url.pathname = "/_api_frm";
iframe.src = _url.toString();
await new Promise<void>((resolve, reject) => {
iframe.onload = () => {
if (!iframe.contentDocument) {
setTimeout(() => {
if (!iframe.contentDocument) {
reject(
`Cannot load iframe ${_url.toString()}. content document not found.`
);
}
}, 100);
}
};
const onInit = (e: any) => {
if (e.data === "initialized") {
iframe.setAttribute("loaded", "y");
w.removeEventListener("message", onInit);
resolve();
}
};
w.addEventListener("message", onInit);
document.body.appendChild(iframe);
});
}
const wm = {} as Record<string, any>;
const sendRaw = async (
input: RequestInfo | URL,
init?: RequestInit | undefined
) => {
if (w.document && w.document.querySelector) {
const iframe = w.document.querySelector(`#${id}`) as HTMLIFrameElement;
if (
!iframe ||
!iframe.contentWindow ||
(iframe && iframe.getAttribute("loaded") !== "y")
) {
await waitUntil(
() =>
iframe &&
iframe.contentWindow &&
iframe.getAttribute("loaded") === "y"
);
}
return await new Promise((resolve, reject) => {
if (iframe && iframe.contentWindow) {
const id = cuid();
wm[id] = (e: any) => {
if (id === e.data.id) {
w.removeEventListener("message", wm[id]);
delete wm[id];
if (e.data.error) {
let err = e.data.error;
if (typeof err === "string") {
reject(
err.replace(
/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
""
)
);
}
} else {
resolve(e.data.result);
}
}
};
w.addEventListener("message", wm[id]);
let _input = input;
if (typeof input === "string") {
if (!input.startsWith("http")) {
_input = `${url}${input}`;
}
}
iframe.contentWindow.postMessage({ input: _input, init, id }, "*");
}
});
}
};
return {
sendRaw,
async send(input: string | RequestInfo | URL, data?: any, _headers?: any) {
const uri = input.toString();
const headers = { ..._headers };
let body = data;
let isFile = false;
const formatSingle = async (data: any) => {
if (!(data instanceof w.FormData || data instanceof w.File)) {
headers["content-type"] = "application/json";
} else {
if (data instanceof w.File) {
isFile = true;
let ab = await new Promise<ArrayBuffer | undefined>((resolve) => {
const reader = new FileReader();
reader.addEventListener("load", (e) => {
resolve(e.target?.result as ArrayBuffer);
});
reader.readAsArrayBuffer(data);
});
if (ab) {
data = new File([ab], data.name);
}
}
}
return data;
};
if (Array.isArray(data)) {
body = await Promise.all(data.map((e) => formatSingle(e)));
} else {
body = await formatSingle(data);
}
if (!isFile) {
body = JSON.stringify(body);
}
return await sendRaw(
`${url.endsWith("/") ? url : `${url}/`}${
uri.startsWith("/") ? uri.substring(1) : uri
}`,
data
? {
method: "post",
headers,
body,
}
: {}
);
},
};
};
export const fetchSendApi = async (
_url: string,
params: any,
parentWindow?: any
) => {
let w: any = typeof window === "object" ? window : globalThis;
const win = parentWindow || w;
let url = _url;
let frm: Awaited<ReturnType<typeof createFrameCors>>;
if (!win.frmapi) {
win.frmapi = {};
win.frmapi[w.serverurl] = await createFrameCors(w.serverurl, win);
}
frm = win.frmapi[w.serverurl];
if (url.startsWith("http")) {
const purl = new URL(url);
if (!win.frmapi[purl.host]) {
win.frmapi[purl.host] = await createFrameCors(
`${purl.protocol}//${purl.host}`
);
}
frm = win.frmapi[purl.host];
url = url.substring(`${purl.protocol}//${purl.host}`.length);
}
if (!win.apiHeaders) {
win.apiHeaders = {};
}
if (!frm) {
await waitUntil(() => {
frm = win.frmapi[w.serverurl];
return frm;
});
}
return await frm.send(url, params, win.apiHeaders);
};

View File

@ -6,6 +6,11 @@ export const defineWindow = async (awaitServerUrl = true) => {
if (awaitServerUrl) await waitUntil(() => w.__SRV_URL__);
w.prasiContext = {
global: {},
render() {},
};
const location = window["location"];
const host =
@ -70,11 +75,10 @@ export const defineWindow = async (awaitServerUrl = true) => {
}
history.pushState({}, "", _href);
if (w.rootRes) w.rootRes.pathname = href;
w.pathname = href;
if (w.rootRender) {
w.rootRender();
if (w.prasiContext && w.prasiContext.render) {
w.prasiContext.render();
}
};

View File

@ -7,4 +7,7 @@ export * from "./page";
export * from "./global";
export * from "./define-react";
export * from "./define-window";
export * from './client-api';
export * from './client-frame';
export * from './client-db';
export const React = _React;

View File

@ -1,4 +1,12 @@
import goober from "goober";
declare global {
const navigate: (path: string) => void;
const params: any;
const css: typeof goober.css;
const cx: (...arg: string[]) => string;
const api: any;
const db: any;
const prasiContext: any;
}
export {};

View File

@ -1,6 +1,5 @@
import { useEffect, useRef, useState } from "react";
export const useLocal = <T extends object>(
data: T,
effect?: (arg: {