diff --git a/app/srv/ws/sync/actions/code_edit.ts b/app/srv/ws/sync/actions/code_edit.ts index 2d28a5eb..9486fe56 100644 --- a/app/srv/ws/sync/actions/code_edit.ts +++ b/app/srv/ws/sync/actions/code_edit.ts @@ -1,13 +1,75 @@ +import { Doc } from "yjs"; +import { MContent } from "../../../../web/src/utils/types/general"; +import { MItem } from "../../../../web/src/utils/types/item"; +import { MRoot } from "../../../../web/src/utils/types/root"; import { SAction } from "../actions"; +import { docs } from "../entity/docs"; +import { gunzipAsync } from "../entity/zlib"; import { SyncConnection } from "../type"; - +import { transform } from "esbuild"; +const decoder = new TextDecoder(); export const code_edit: SAction["code"]["edit"] = async function ( this: SyncConnection, arg ) { - if (arg.type === 'adv') { - const { item_id, mode, comp_id, page_id } = arg; + if (arg.type === "adv") { + const { item_id, mode, comp_id, page_id, value } = arg; + const src = decoder.decode(await gunzipAsync(value)); + + let root = undefined as undefined | MRoot | MItem; + let doc = undefined as undefined | Doc; + if (page_id) { + const ref = docs.page[page_id]; + if (ref) { + root = ref.doc.getMap("map").get("root"); + doc = ref.doc as Doc; + } + } + + if (root) { + const mitem = findId(root, item_id); + + if (mitem) { + const adv = mitem.get("adv"); + + if (adv) { + doc?.transact(async () => { + adv.set(mode, src); + + if (mode === "js") { + const res = await transform(`render(${src})`, { + jsx: "transform", + format: "cjs", + loader: "tsx", + minify: true, + sourcemap: "inline", + }); + adv.set("jsBuilt", res.code); + } + }); + } + } + } } return false; }; + +const findId = (mitem: MContent | MRoot, id: string) => { + if ((mitem as MItem).get("id") === id) { + return mitem as MItem; + } + + const childs = (mitem as MItem).get("childs"); + if (childs) { + let found: null | MItem = null; + childs.forEach((child) => { + const f = findId(child, id); + if (f) { + found = f; + } + }); + + if (found) return found; + } +}; diff --git a/app/web/src/nova/ed/panel/popup/script/monaco.tsx b/app/web/src/nova/ed/panel/popup/script/monaco.tsx index a7520643..18e771db 100644 --- a/app/web/src/nova/ed/panel/popup/script/monaco.tsx +++ b/app/web/src/nova/ed/panel/popup/script/monaco.tsx @@ -153,15 +153,24 @@ export const ScriptMonaco = () => { { css: "scss", js: "typescript", html: "html" }[p.ui.popup.script.mode] } onChange={(val) => { + clearTimeout(scriptEdit.timeout); scriptEdit.timeout = setTimeout(() => { const meta = getMetaById(p, active.item_id); const type = p.ui.popup.script.mode; if (meta && meta.mitem) { + let arg = {} as any; + if (active.comp_id) { + arg.comp_id = active.comp_id; + } else { + arg.page_id = p.page.cur.id; + } + p.sync.code.edit({ type: "adv", mode: type, item_id: active.item_id, value: compress(encode.encode(val || "")), + ...arg, }); } }, 1000); diff --git a/app/web/src/nova/view/render/meta/meta.tsx b/app/web/src/nova/view/render/meta/meta.tsx index fc798ca5..5958d126 100644 --- a/app/web/src/nova/view/render/meta/meta.tsx +++ b/app/web/src/nova/view/render/meta/meta.tsx @@ -2,8 +2,9 @@ import { FC, useState } from "react"; import { useGlobal } from "web-utils"; import { ViewGlobal } from "../../logic/global"; import { ViewMetaRender } from "./render"; -import { ViewMetaScript } from "./script"; +import { ViewBoundedScript, ViewMetaScript } from "./script"; import { compPropVal } from "./script/comp-propval"; +import { ErrorBox } from "./script/error-box"; export const ViewMeta: FC<{ id: string; scopeIndex?: Record }> = ({ id, @@ -30,8 +31,15 @@ export const ViewMeta: FC<{ id: string; scopeIndex?: Record }> = ({ } if (item.adv) { - if (item.adv.js && item.adv.jsBuilt) { - return ; + if (item.adv.js && item.adv.jsBuilt && typeof item.adv.js === "string") { + return ( + + ); } } diff --git a/app/web/src/nova/view/render/meta/script.tsx b/app/web/src/nova/view/render/meta/script.tsx index c92f1d1a..8f7bc034 100644 --- a/app/web/src/nova/view/render/meta/script.tsx +++ b/app/web/src/nova/view/render/meta/script.tsx @@ -12,9 +12,39 @@ import { createLocal } from "./script/create-local"; import { createPassProp } from "./script/create-pass-prop"; import { ErrorBox } from "./script/error-box"; import { mergeScopeUpwards } from "./script/merge-upward"; +import { useLocal } from "web-utils"; +const renderLimit = {} as Record< + string, + Record +>; + +export const ViewBoundedScript: FC<{ + v: VG; + item: IItem | IText | ISection; + scopeIndex?: Record; + js: string; +}> = ({ item, v, scopeIndex, js }) => { + const local = useLocal({ js: "" }); + + useEffect(() => { + if (local.js !== js) { + local.js = js; + local.render(); + } + }, [js]); + + if (local.js !== js) { + return null; + } + + return ( + + + + ); +}; -const renderLimit = {} as Record>; export const ViewMetaScript: FC<{ v: VG; item: IItem | IText | ISection; @@ -30,142 +60,129 @@ export const ViewMetaScript: FC<{ }); if (!renderLimit[v.current.page_id]) { - renderLimit[v.current.page_id] = {} + renderLimit[v.current.page_id] = {}; } if (!renderLimit[v.current.page_id][item.id]) { renderLimit[v.current.page_id][item.id] = { ts: Date.now(), count: 1, - cache: null - } + cache: null, + }; } if (renderLimit[v.current.page_id][item.id].ts - Date.now() < 100) { - renderLimit[v.current.page_id][item.id].count++ + renderLimit[v.current.page_id][item.id].count++; } else { renderLimit[v.current.page_id][item.id].ts = Date.now(); renderLimit[v.current.page_id][item.id].count = 1; } if (renderLimit[v.current.page_id][item.id].count > 100) { - - let js = ''; - if (typeof item.adv?.js === 'string') { + let js = ""; + if (typeof item.adv?.js === "string") { js = item.adv.js; } - console.warn(`Maximum render limit (100 render in 100ms) reached in item [${item.name}]:\n${js.length > 30 ? js.substring(0, 30) + '...' : js}`) + console.warn( + `Maximum render limit (100 render in 100ms) reached in item [${ + item.name + }]:\n${js.length > 30 ? js.substring(0, 30) + "..." : js}` + ); return renderLimit[v.current.page_id][item.id].cache; } - const children = ; let args = {}; if (js && meta) { - try { - if (!meta.memoize) { - meta.memoize = {}; - } - const memoizeKey = hash_sum(scopeIndex) || "default"; - if (!meta.memoize[memoizeKey]) { - meta.memoize[memoizeKey] = { - Local: createLocal(v, item.id, scopeIndex), - PassProp: createPassProp(v, item.id, scopeIndex), - }; - } + if (!meta.memoize) { + meta.memoize = {}; + } + const memoizeKey = hash_sum(scopeIndex) || "default"; + if (!meta.memoize[memoizeKey]) { + meta.memoize[memoizeKey] = { + Local: createLocal(v, item.id, scopeIndex), + PassProp: createPassProp(v, item.id, scopeIndex), + }; + } - const _js = item.adv?.js; - if (typeof _js === "string") { - const navs = extractNavigate(_js || ""); - if (navs.length > 0) { - navs.map((nav) => preload(v, nav)); + const _js = item.adv?.js; + if (typeof _js === "string") { + const navs = extractNavigate(_js || ""); + if (navs.length > 0) { + navs.map((nav) => preload(v, nav)); + } + } + + if (v.script.api_url) { + if (!v.script.db) v.script.db = createDB(v.script.api_url); + if (!v.script.api) v.script.api = createAPI(v.script.api_url); + } + + const finalScope = mergeScopeUpwards(v, item.id, scopeIndex); + for (const [k, v] of Object.entries(finalScope)) { + if (v && typeof v === "object") { + const t: { + _jsx: true; + Comp: FC<{ parent_id: string; scopeIndex?: Record }>; + } = v as any; + if (t._jsx && t.Comp) { + finalScope[k] = ( + <> + + + ); } } + } + const output = { jsx: null as any }; - if (v.script.api_url) { - if (!v.script.db) v.script.db = createDB(v.script.api_url); - if (!v.script.api) v.script.api = createAPI(v.script.api_url); - } - - const finalScope = mergeScopeUpwards(v, item.id, scopeIndex); - for (const [k, v] of Object.entries(finalScope)) { - if (v && typeof v === "object") { - const t: { - _jsx: true; - Comp: FC<{ parent_id: string; scopeIndex?: Record }>; - } = v as any; - if (t._jsx && t.Comp) { - finalScope[k] = ( - <> - - - ); - } - } - } - const output = { jsx: null as any }; - - args = { - ...w.exports, - ...finalScope, - ...meta.memoize[memoizeKey], - db: v.script.db, - api: v.script.api, - children, - props: { - className, - onPointerOver: v.view.hover - ? (e: any) => { + args = { + ...w.exports, + ...finalScope, + ...meta.memoize[memoizeKey], + db: v.script.db, + api: v.script.api, + children, + props: { + className, + onPointerOver: v.view.hover + ? (e: any) => { e.stopPropagation(); e.preventDefault(); v.view.hover?.set(meta); } - : undefined, - onClick: v.view.active - ? (e: any) => { + : undefined, + onClick: v.view.active + ? (e: any) => { e.stopPropagation(); e.preventDefault(); v.view.active?.set(meta); } - : undefined, - }, - useEffect: useEffect, - render: (jsx: ReactNode) => { - output.jsx = ( - - {jsx} - - ); - renderLimit[v.current.page_id][item.id].cache = output.jsx; - }, - }; + : undefined, + }, + useEffect: useEffect, + render: (jsx: ReactNode) => { + output.jsx = jsx; + renderLimit[v.current.page_id][item.id].cache = output.jsx; + }, + }; - // execute - const fn = new Function(...Object.keys(args), js); - const res = fn(...Object.values(args)); - if (res instanceof Promise) { - res.catch((e: any) => { - console.warn(e); - console.warn( - ( - `ERROR in ${item.type} [${item.name}]:\n ` + - ((item.adv?.js || "") as any) - ).trim() - ); - console.warn(`Available var:`, args, `\n\n`); - }); - } - return output.jsx; - } catch (e) { - console.warn(e); - console.warn( - ( - `ERROR in ${item.type} [${item.name}]:\n ` + - ((item.adv?.js || "") as any) - ).trim() - ); - console.warn(`Available var:`, args, `\n\n`); + // execute + const fn = new Function(...Object.keys(args), js); + const res = fn(...Object.values(args)); + if (res instanceof Promise) { + res.catch((e: any) => { + console.warn(e); + console.warn( + ( + `ERROR in ${item.type} [${item.name}]:\n ` + + ((item.adv?.js || "") as any) + ).trim() + ); + console.warn(`Available var:`, args, `\n\n`); + }); } + return output.jsx; } }; diff --git a/app/web/src/nova/view/render/meta/script/error-box.tsx b/app/web/src/nova/view/render/meta/script/error-box.tsx index 96bbc609..66b76105 100644 --- a/app/web/src/nova/view/render/meta/script/error-box.tsx +++ b/app/web/src/nova/view/render/meta/script/error-box.tsx @@ -4,10 +4,20 @@ import { EdMeta } from "../../../../ed/logic/ed-global"; import { ViewGlobal } from "../../../logic/global"; export const ErrorBox = withErrorBoundary( - ({ children, meta, id }: { children: any; meta?: EdMeta; id?: string }) => { + ({ + children, + meta, + id, + silent, + }: { + children: any; + meta?: EdMeta; + id?: string; + silent?: boolean; + }) => { const local = useLocal({ retrying: false }); const [error, resetError] = useErrorBoundary((error, errorInfo) => { - console.warn(error); + if (silent !== true) console.warn(error); }); let _meta = meta;