From a1c516674812f9cff3b63b291e99fbf17932012d Mon Sep 17 00:00:00 2001 From: Rizky Date: Thu, 26 Oct 2023 05:46:54 +0700 Subject: [PATCH] fix monaco --- app/web/src/render/ed/ed-base.tsx | 17 +- app/web/src/render/ed/logic/ed-global.ts | 4 + app/web/src/render/ed/logic/ed-init.ts | 2 +- .../render/ed/panel/script/monaco/init.tsx | 102 ++++++++++++ .../render/ed/panel/script/monaco/monaco.tsx | 98 +++++++++++ app/web/src/render/ed/panel/script/site.tsx | 36 ++++ .../ed/panel/script/site/script-site.tsx | 3 - app/web/src/render/ed/panel/top/left/js.tsx | 12 +- .../src/render/editor/elements/e-render.tsx | 6 +- app/web/src/render/editor/logic/init.ts | 10 +- .../src/render/editor/panel/e-main-editor.tsx | 2 + .../src/render/editor/panel/script/monaco.ts | 0 .../panel/script/monaco/monaco-custom.tsx | 7 +- .../editor/panel/script/monaco/monaco-el.tsx | 16 +- .../editor/panel/script/script-custom.tsx | 14 +- .../editor/panel/script/script-element.tsx | 60 +------ .../editor/panel/side/props/CPInstance.tsx | 7 +- .../editor/panel/tree/item/right-click.tsx | 6 +- .../panel => utils}/script/esbuild/fs.ts | 0 .../panel => utils}/script/esbuild/ipc.ts | 0 .../panel => utils}/script/esbuild/output.ts | 0 .../script/esbuild/sourcemap.ts | 0 .../script/esbuild/versions.ts | 0 .../script/esbuild/worker.d.ts | 0 .../panel => utils}/script/esbuild/worker.ts | 0 app/web/src/utils/script/jscript.ts | 85 ++++++++++ app/web/src/utils/script/mount.tsx | 152 +++++++++++++++++ app/web/src/utils/script/types/base.ts | 74 +++++++++ app/web/src/utils/script/types/prop.tsx | 114 +++++++++++++ .../src/utils/script/types/type-stringify.ts | 14 ++ app/web/src/utils/script/typings.ts | 155 ++++++++++++++++++ 31 files changed, 877 insertions(+), 119 deletions(-) create mode 100644 app/web/src/render/ed/panel/script/monaco/init.tsx create mode 100644 app/web/src/render/ed/panel/script/monaco/monaco.tsx create mode 100644 app/web/src/render/ed/panel/script/site.tsx delete mode 100644 app/web/src/render/ed/panel/script/site/script-site.tsx delete mode 100644 app/web/src/render/editor/panel/script/monaco.ts rename app/web/src/{render/editor/panel => utils}/script/esbuild/fs.ts (100%) rename app/web/src/{render/editor/panel => utils}/script/esbuild/ipc.ts (100%) rename app/web/src/{render/editor/panel => utils}/script/esbuild/output.ts (100%) rename app/web/src/{render/editor/panel => utils}/script/esbuild/sourcemap.ts (100%) rename app/web/src/{render/editor/panel => utils}/script/esbuild/versions.ts (100%) rename app/web/src/{render/editor/panel => utils}/script/esbuild/worker.d.ts (100%) rename app/web/src/{render/editor/panel => utils}/script/esbuild/worker.ts (100%) create mode 100644 app/web/src/utils/script/jscript.ts create mode 100644 app/web/src/utils/script/mount.tsx create mode 100644 app/web/src/utils/script/types/base.ts create mode 100644 app/web/src/utils/script/types/prop.tsx create mode 100644 app/web/src/utils/script/types/type-stringify.ts create mode 100644 app/web/src/utils/script/typings.ts diff --git a/app/web/src/render/ed/ed-base.tsx b/app/web/src/render/ed/ed-base.tsx index 41718aad..01db8f15 100644 --- a/app/web/src/render/ed/ed-base.tsx +++ b/app/web/src/render/ed/ed-base.tsx @@ -9,7 +9,8 @@ import { EdMain } from "./panel/main/main"; import { EdPane } from "./panel/main/pane-resize"; import { EdPopCompGroup } from "./panel/popup/comp-group"; import { EdPopSite } from "./panel/popup/site"; -import { jscript } from "../editor/panel/script/script-element"; +import { EdScriptInit } from "./panel/script/monaco/init"; +import { EdScriptSite } from "./panel/script/site"; export const EdBase = () => { const p = useGlobal(EDGlobal, "EDITOR"); @@ -33,7 +34,6 @@ export const EdBase = () => { ); } - const Editor = jscript.editor; return (
@@ -42,20 +42,11 @@ export const EdBase = () => {
- <> - {Editor && !jscript.ready && ( -
- { - jscript.ready = true; - p.render(); - }} - /> -
- )} + + ); diff --git a/app/web/src/render/ed/logic/ed-global.ts b/app/web/src/render/ed/logic/ed-global.ts index 69831523..b4ceeb95 100644 --- a/app/web/src/render/ed/logic/ed-global.ts +++ b/app/web/src/render/ed/logic/ed-global.ts @@ -69,6 +69,7 @@ export const EDGlobal = { | "ready", sync: null as unknown as Awaited>, site: EmptySite, + script: { siteTypes: {} as Record }, page: { cur: EmptyPage, doc: null as null | DPage, @@ -86,6 +87,9 @@ export const EDGlobal = { group: {} as Record>>, }, ui: { + script: { + site: false, + }, layout: { left: parseInt(localStorage.getItem("prasi-layout-left") || "250"), right: parseInt(localStorage.getItem("prasi-layout-right") || "250"), diff --git a/app/web/src/render/ed/logic/ed-init.ts b/app/web/src/render/ed/logic/ed-init.ts index 099d97cc..e417d89f 100644 --- a/app/web/src/render/ed/logic/ed-init.ts +++ b/app/web/src/render/ed/logic/ed-init.ts @@ -1,6 +1,6 @@ import init from "wasm-gzip"; import { PG } from "./ed-global"; -import { jscript } from "../../editor/panel/script/script-element"; +import { jscript } from "../../../utils/script/jscript"; export const edInit = async (p: PG) => { await init(); diff --git a/app/web/src/render/ed/panel/script/monaco/init.tsx b/app/web/src/render/ed/panel/script/monaco/init.tsx new file mode 100644 index 00000000..05f2e23b --- /dev/null +++ b/app/web/src/render/ed/panel/script/monaco/init.tsx @@ -0,0 +1,102 @@ +import { ReactNode } from "react"; +import { useLocal } from "web-utils"; +import { jscript } from "../../../../../utils/script/jscript"; +import { Loading } from "../../../../../utils/ui/loading"; + +export const EdScriptInit = () => { + const Editor = jscript.editor; + const local = useLocal({ editorLoaded: false }, () => {}); + + jscript.events.editorLoaded = () => { + local.editorLoaded = true; + local.render(); + }; + + return ( + <> + {Editor && local.editorLoaded && ( +
+ { + jscript.events.pendingDone(); + }} + /> +
+ )} + + ); +}; + +export const EdMonacoWrap = ({ + children, +}: { + children: (Editor: Exclude) => ReactNode; +}) => { + const local = useLocal({}); + + if (jscript.pending && (!jscript.editor || !jscript.build)) { + jscript.pending.then(() => { + local.render(); + }); + } + + return ( +
+ {!jscript.editor || !jscript.build ? ( + + ) : ( + children(jscript.editor) + )} +
+ ); +}; diff --git a/app/web/src/render/ed/panel/script/monaco/monaco.tsx b/app/web/src/render/ed/panel/script/monaco/monaco.tsx new file mode 100644 index 00000000..b3e77ab5 --- /dev/null +++ b/app/web/src/render/ed/panel/script/monaco/monaco.tsx @@ -0,0 +1,98 @@ +import { jsMount } from "../../../../../utils/script/mount"; +import { monacoTypings } from "../../../../../utils/script/typings"; +import { Modal } from "../../../../../utils/ui/modal"; +import { EdMonacoWrap } from "./init"; +import type { Editor } from "@monaco-editor/react"; + +const monacoState = {} as Record; +export const EdMonaco = (arg: { + id?: string; + type: "js" | "html" | "css"; + filename: string; + monaco: Parameters[0]; + onClose: () => void; + prop?: { + val: Record; + types: Record; + }; +}) => { + const filename = arg.filename; + const m = arg.monaco; + const prop = { ...arg.monaco }; + + prop.options = { + minimap: { enabled: false }, + wordWrap: "wordWrapColumn", + autoClosingBrackets: "always", + tabSize: 2, + autoIndent: "full", + formatOnPaste: true, + formatOnType: true, + useTabStops: true, + }; + if (arg.type === "html") { + prop.language = "html"; + } + if (arg.type === "css") { + prop.language = "scss"; + } + if (arg.type === "js") { + prop.language = "typescript"; + prop.onMount = async (editor, monaco) => { + const value = editor.getValue(); + monaco.editor.getModels().forEach((model) => { + if (model.uri.toString().startsWith("inmemory://model")) { + model.dispose(); + } + }); + + let model = monaco.editor.createModel( + value, + "typescript", + monaco.Uri.parse(`ts:${filename}`) + ); + editor.setModel(model); + + if (arg.id) { + if (!monacoState[arg.id]) { + editor.trigger("fold", "editor.foldAllMarkerRegions", null); + } else { + editor.restoreViewState(monacoState[arg.id]); + } + } + + await jsMount(editor, monaco); + + if (arg.prop) + await monacoTypings( + { + script: { + siteTypes: {}, + }, + site: { + api_url: "", + }, + site_dts: "", + }, + monaco, + { + values: arg.prop.val, + types: arg.prop.types, + } + ); + + if (m.onMount) m.onMount(editor, monaco); + }; + } + + return ( + { + arg.onClose(); + }} + > + {(Editor) => } + + ); +}; diff --git a/app/web/src/render/ed/panel/script/site.tsx b/app/web/src/render/ed/panel/script/site.tsx new file mode 100644 index 00000000..4d9280b2 --- /dev/null +++ b/app/web/src/render/ed/panel/script/site.tsx @@ -0,0 +1,36 @@ +import { useGlobal } from "web-utils"; +import { EdMonaco } from "./monaco/monaco"; +import { EDGlobal } from "../../logic/ed-global"; + +export const EdScriptSite = () => { + const p = useGlobal(EDGlobal, "EDITOR"); + + if (!p.ui.script.site) { + return null; + } + return ( + { + console.log(v); + }, + }} + prop={{ + val: {}, + types: { + exports: "any", + types: "any", + render: "()=>void", + }, + }} + onClose={() => { + p.ui.script.site = false; + p.render(); + }} + /> + ); +}; diff --git a/app/web/src/render/ed/panel/script/site/script-site.tsx b/app/web/src/render/ed/panel/script/site/script-site.tsx deleted file mode 100644 index 02450ae1..00000000 --- a/app/web/src/render/ed/panel/script/site/script-site.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export const EdScriptSite = () => { - -}; diff --git a/app/web/src/render/ed/panel/top/left/js.tsx b/app/web/src/render/ed/panel/top/left/js.tsx index e1f763f2..3d9b5f92 100644 --- a/app/web/src/render/ed/panel/top/left/js.tsx +++ b/app/web/src/render/ed/panel/top/left/js.tsx @@ -1,8 +1,18 @@ +import { useGlobal } from "web-utils"; import { TopBtn } from "../top-btn"; +import { EDGlobal } from "../../../logic/ed-global"; export const EdSiteJS = () => { + const p = useGlobal(EDGlobal, "EDITOR"); return ( - + { + p.ui.script.site = true; + p.render(); + }} + > { - p.render(); - }); + jscript.init(p.render); return null; } jscript diff --git a/app/web/src/render/editor/logic/init.ts b/app/web/src/render/editor/logic/init.ts index 86fab91f..792cb036 100644 --- a/app/web/src/render/editor/logic/init.ts +++ b/app/web/src/render/editor/logic/init.ts @@ -7,10 +7,10 @@ import { } from "../../../utils/script/init-api"; import { LSite } from "../../live/logic/global"; import { validateLayout } from "../../live/logic/layout"; -import { jscript } from "../panel/script/script-element"; import importModule from "../tools/dynamic-import"; import { PG } from "./global"; import { devLoader } from "../../live/dev-loader"; +import { jscript } from "../../../utils/script/jscript"; export const w = window as unknown as { basepath: string; @@ -46,6 +46,10 @@ export const initEditor = async (p: PG, site_id: string) => { return ""; }; + if (!jscript.pending) { + jscript.init(p.render); + } + if (!p.item) return; p.item.active = localStorage.getItem("prasi-item-active-id") || ""; @@ -147,10 +151,6 @@ export const initEditor = async (p: PG, site_id: string) => { p.status = "ready"; p.render(); - - if (!jscript.build) { - jscript.init(); - } }; export const execSiteJS = (p: PG) => { diff --git a/app/web/src/render/editor/panel/e-main-editor.tsx b/app/web/src/render/editor/panel/e-main-editor.tsx index a4ab8414..995ed5df 100644 --- a/app/web/src/render/editor/panel/e-main-editor.tsx +++ b/app/web/src/render/editor/panel/e-main-editor.tsx @@ -5,6 +5,7 @@ import { mobileCSS } from "../elements/e-page"; import { editorStyle } from "../elements/style"; import { EditorGlobal } from "../logic/global"; import { Toolbar } from "./toolbar/Toolbar"; +import { EdScriptInit } from "../../ed/panel/script/monaco/init"; const ETree = lazy(async () => ({ default: (await import("./tree/tree")).ETree, @@ -61,6 +62,7 @@ export const EMainEditor = () => { {p.status === "ready" && ( }> + {p.manager.site && } {p.manager.page && } {p.manager.comp && } diff --git a/app/web/src/render/editor/panel/script/monaco.ts b/app/web/src/render/editor/panel/script/monaco.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/app/web/src/render/editor/panel/script/monaco/monaco-custom.tsx b/app/web/src/render/editor/panel/script/monaco/monaco-custom.tsx index 0d0f67dc..55dbcea5 100644 --- a/app/web/src/render/editor/panel/script/monaco/monaco-custom.tsx +++ b/app/web/src/render/editor/panel/script/monaco/monaco-custom.tsx @@ -5,17 +5,12 @@ import type { } from "@monaco-editor/react"; import { FC } from "react"; import { useGlobal, useLocal } from "web-utils"; +import { FBuild } from "../../../../../utils/script/jscript"; import { EditorGlobal } from "../../../logic/global"; import { jsMount } from "./mount"; import { monacoTypings } from "./typings"; export type MonacoEditor = Parameters[0]; -export type FBuild = ( - entryFileName: string, - src: string, - files?: Record -) => Promise; - export const customMonacoState: Record = {}; export const ScriptMonacoCustom: FC<{ diff --git a/app/web/src/render/editor/panel/script/monaco/monaco-el.tsx b/app/web/src/render/editor/panel/script/monaco/monaco-el.tsx index 88e1cdd5..c4fda0dd 100644 --- a/app/web/src/render/editor/panel/script/monaco/monaco-el.tsx +++ b/app/web/src/render/editor/panel/script/monaco/monaco-el.tsx @@ -7,18 +7,19 @@ import strDelta from "textdiff-create"; import { useGlobal, useLocal } from "web-utils"; import * as Y from "yjs"; import { TypedMap } from "yjs-types"; +import { FBuild } from "../../../../../utils/script/jscript"; import { FMCompDef, FNAdv } from "../../../../../utils/types/meta-fn"; +import { Button } from "../../../../../utils/ui/form/Button"; import { Loading } from "../../../../../utils/ui/loading"; +import { Popover } from "../../../../../utils/ui/popover"; import { EditorGlobal } from "../../../logic/global"; import { mergeScopeUpwards } from "../../../logic/tree-scope"; import { newMap } from "../../../tools/yjs-tools"; +import { MonacoElHistory } from "./monaco-el-history"; +import { MonacoElSnippet } from "./monaco-el-snippet"; import { jsMount } from "./mount"; import { MonacoScopeBar } from "./scope-bar"; import { monacoTypings } from "./typings"; -import { MonacoElSnippet } from "./monaco-el-snippet"; -import { Button } from "../../../../../utils/ui/form/Button"; -import { Popover } from "../../../../../utils/ui/popover"; -import { MonacoElHistory } from "./monaco-el-history"; export type MonacoEditor = Parameters[0]; export const DefaultScript = { @@ -42,13 +43,6 @@ const w = window as unknown as { }; }; -export type FBuild = ( - entryFileName: string, - src: string, - files?: Record, - verbose?: boolean -) => Promise; - const monacoViewState = {} as Record; export const ScriptMonacoElement: FC<{ diff --git a/app/web/src/render/editor/panel/script/script-custom.tsx b/app/web/src/render/editor/panel/script/script-custom.tsx index cbf166bf..34ad089d 100644 --- a/app/web/src/render/editor/panel/script/script-custom.tsx +++ b/app/web/src/render/editor/panel/script/script-custom.tsx @@ -5,7 +5,7 @@ import { Loading } from "../../../../utils/ui/loading"; import { EditorGlobal } from "../../logic/global"; import { ScriptMonacoCustom } from "./monaco/monaco-custom"; import { MonacoEditor } from "./monaco/typings"; -import { jscript } from "./script-element"; +import { jscript } from "../../../../utils/script/jscript"; export const EScriptCustom: FC<{ monaco_id: string; @@ -28,16 +28,8 @@ export const EScriptCustom: FC<{ }) => { const p = useGlobal(EditorGlobal); - if (!jscript.editor) { - Promise.all([ - import("@monaco-editor/react").then((e) => { - jscript.editor = e.Editor; - e.loader.config({ paths: { vs: "/min/vs" } }); - }), - jscript.init(), - ]).then(() => { - p.render(); - }); + if (!jscript.editor && jscript.pending) { + jscript.pending.then(() => p.render()); } return ( diff --git a/app/web/src/render/editor/panel/script/script-element.tsx b/app/web/src/render/editor/panel/script/script-element.tsx index 83e7b4db..0868a0d1 100644 --- a/app/web/src/render/editor/panel/script/script-element.tsx +++ b/app/web/src/render/editor/panel/script/script-element.tsx @@ -1,70 +1,18 @@ -import type { Editor as MonacoEditor } from "@monaco-editor/react"; -import type { BuildOptions } from "esbuild-wasm"; import { FC } from "react"; import { useGlobal } from "web-utils"; import * as Y from "yjs"; +import { jscript } from "../../../../utils/script/jscript"; import { Loading } from "../../../../utils/ui/loading"; import { Modal } from "../../../../utils/ui/modal"; import { EditorGlobal } from "../../logic/global"; import { rebuildTree } from "../../logic/tree-logic"; -import { initJS } from "./monaco/init"; -import { DefaultScript, FBuild, ScriptMonacoElement } from "./monaco/monaco-el"; - -export const jscript = { - editor: null as typeof MonacoEditor | null, - build: null as null | FBuild, - _init: false as false | Promise, - ready: false, - _editor: false, - async init(render: () => void) { - if (this._init) await this._init; - if (!this._init) { - this._init = new Promise(async (resolve) => { - const { sendIPC } = await import("./esbuild/ipc"); - await initJS(); - - if (!this._editor) { - this._editor = true; - const e = await import("@monaco-editor/react"); - jscript.editor = e.Editor; - e.loader.config({ paths: { vs: "/min/vs" } }); - } - - this.build = async (entry, src, files, verbose?: boolean) => { - const options: BuildOptions = { - entryPoints: [entry], - jsx: "transform", - bundle: true, - format: "cjs", - minify: true, - }; - const res = await sendIPC({ - command_: "build", - input_: { ...files, [entry]: src }, - options_: options, - }); - - if (verbose && res.stderr_) { - console.log(res.stderr_); - } - if (res.outputFiles_) return res.outputFiles_[0].text; - - return ""; - }; - - await this.build("el.tsx", `return ""`); - render(); - resolve(); - }); - } - }, -}; +import { DefaultScript, ScriptMonacoElement } from "./monaco/monaco-el"; export const EScriptElement: FC<{}> = ({}) => { const p = useGlobal(EditorGlobal, "EDITOR"); - if (!jscript.editor) { - jscript.init(p.render); + if (!jscript.editor && jscript.pending) { + jscript.pending.then(() => p.render()); } if (!p.script.active) { diff --git a/app/web/src/render/editor/panel/side/props/CPInstance.tsx b/app/web/src/render/editor/panel/side/props/CPInstance.tsx index f8096c94..fdc5bc22 100644 --- a/app/web/src/render/editor/panel/side/props/CPInstance.tsx +++ b/app/web/src/render/editor/panel/side/props/CPInstance.tsx @@ -11,12 +11,12 @@ import { editComp, loadComponent } from "../../../logic/comp"; import { EditorGlobal, PG } from "../../../logic/global"; import { rebuildTree } from "../../../logic/tree-logic"; import { newMap } from "../../../tools/yjs-tools"; -import { jscript } from "../../script/script-element"; import { CPJsx } from "./CPJsx"; import { CPOption } from "./CPOption"; import { CPText } from "./CPText"; import { mergeScopeUpwards } from "../../../logic/tree-scope"; import { treePropEval } from "../../../logic/tree-prop"; +import { jscript } from "../../../../../utils/script/jscript"; export const CPInstance: FC<{ mitem: MItem }> = ({ mitem }) => { const p = useGlobal(EditorGlobal, "EDITOR"); @@ -65,10 +65,7 @@ export const CPInstance: FC<{ mitem: MItem }> = ({ mitem }) => { } if (!jscript.build) { - jscript.init().then(() => { - local.status = "ready"; - local.render(); - }); + jscript.init(p.render); } else { local.status = "ready"; local.render(); diff --git a/app/web/src/render/editor/panel/tree/item/right-click.tsx b/app/web/src/render/editor/panel/tree/item/right-click.tsx index 8e5d3889..2dbc729c 100644 --- a/app/web/src/render/editor/panel/tree/item/right-click.tsx +++ b/app/web/src/render/editor/panel/tree/item/right-click.tsx @@ -15,9 +15,9 @@ import { EditorGlobal, NodeMeta } from "../../../logic/global"; import { fillID } from "../../../tools/fill-id"; import { flatTree } from "../../../tools/flat-tree"; import { newMap } from "../../../tools/yjs-tools"; -import { jscript } from "../../script/script-element"; import { detachComp } from "./action/detach"; import { rebuildTree } from "../../../logic/tree-logic"; +import { jscript } from "../../../../../utils/script/jscript"; export const ETreeRightClick: FC<{ node: NodeModel; @@ -304,7 +304,7 @@ export const ETreeRightClick: FC<{ label="Detach" onClick={async () => { if (!jscript.build) { - await jscript.init(); + await jscript.init(p.render); } if (jscript.build && p.treeMeta[item.id]) { detachComp( @@ -344,7 +344,7 @@ export const ETreeRightClick: FC<{ comp_id: rootComp ? rootComp.id : undefined, group_id, }) - .then(async (e) => { + .then(async (e: any) => { if (e) { await loadComponent(p, e.id); delete p.compLoading[item.id]; diff --git a/app/web/src/render/editor/panel/script/esbuild/fs.ts b/app/web/src/utils/script/esbuild/fs.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/fs.ts rename to app/web/src/utils/script/esbuild/fs.ts diff --git a/app/web/src/render/editor/panel/script/esbuild/ipc.ts b/app/web/src/utils/script/esbuild/ipc.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/ipc.ts rename to app/web/src/utils/script/esbuild/ipc.ts diff --git a/app/web/src/render/editor/panel/script/esbuild/output.ts b/app/web/src/utils/script/esbuild/output.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/output.ts rename to app/web/src/utils/script/esbuild/output.ts diff --git a/app/web/src/render/editor/panel/script/esbuild/sourcemap.ts b/app/web/src/utils/script/esbuild/sourcemap.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/sourcemap.ts rename to app/web/src/utils/script/esbuild/sourcemap.ts diff --git a/app/web/src/render/editor/panel/script/esbuild/versions.ts b/app/web/src/utils/script/esbuild/versions.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/versions.ts rename to app/web/src/utils/script/esbuild/versions.ts diff --git a/app/web/src/render/editor/panel/script/esbuild/worker.d.ts b/app/web/src/utils/script/esbuild/worker.d.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/worker.d.ts rename to app/web/src/utils/script/esbuild/worker.d.ts diff --git a/app/web/src/render/editor/panel/script/esbuild/worker.ts b/app/web/src/utils/script/esbuild/worker.ts similarity index 100% rename from app/web/src/render/editor/panel/script/esbuild/worker.ts rename to app/web/src/utils/script/esbuild/worker.ts diff --git a/app/web/src/utils/script/jscript.ts b/app/web/src/utils/script/jscript.ts new file mode 100644 index 00000000..961cb5ea --- /dev/null +++ b/app/web/src/utils/script/jscript.ts @@ -0,0 +1,85 @@ +import type { Editor as MonacoEditor } from "@monaco-editor/react"; +import type { BuildOptions } from "esbuild-wasm"; +import type Prettier from "prettier/standalone"; +import type estree from "prettier/plugins/estree"; +import type ts from "prettier/plugins/typescript"; +export type FBuild = ( + entryFileName: string, + src: string, + files?: Record, + verbose?: boolean +) => Promise; + +export const initJS = async () => { + const { tryToSetCurrentVersion } = await import("./esbuild/versions"); + await tryToSetCurrentVersion("latest"); +}; + +export const jscript = { + editor: null as typeof MonacoEditor | null, + build: null as null | FBuild, + pending: null as null | Promise, + events: { + editorLoaded: () => {}, + esbuildLoaded: () => {}, + prettierLoaded: () => {}, + pendingDone: () => {}, + }, + prettier: { + standalone: null as null | typeof Prettier, + estree: null as null | typeof estree, + ts: null as null | typeof ts, + }, + async init(render: () => void) { + if (this.pending) { + await this.pending; + render(); + } + if (!this.pending) { + this.pending = new Promise(async (resolve) => { + this.events.pendingDone = resolve; + + const { sendIPC } = await import("./esbuild/ipc"); + await initJS(); + this.events.esbuildLoaded(); + + this.prettier.standalone = ( + await import("prettier/standalone") + ).default; + this.prettier.estree = await import("prettier/plugins/estree"); + this.prettier.ts = await import("prettier/plugins/typescript"); + this.events.prettierLoaded(); + + const e = await import("@monaco-editor/react"); + jscript.editor = e.Editor; + e.loader.config({ paths: { vs: "/min/vs" } }); + this.events.editorLoaded(); + + this.build = async (entry, src, files, verbose?: boolean) => { + const options: BuildOptions = { + entryPoints: [entry], + jsx: "transform", + bundle: true, + format: "cjs", + minify: true, + }; + const res = await sendIPC({ + command_: "build", + input_: { ...files, [entry]: src }, + options_: options, + }); + + if (verbose && res.stderr_) { + console.log(res.stderr_); + } + if (res.outputFiles_) return res.outputFiles_[0].text; + + return ""; + }; + + await this.build("el.tsx", `return ""`); + render(); + }); + } + }, +}; diff --git a/app/web/src/utils/script/mount.tsx b/app/web/src/utils/script/mount.tsx new file mode 100644 index 00000000..98db069b --- /dev/null +++ b/app/web/src/utils/script/mount.tsx @@ -0,0 +1,152 @@ +import type { OnMount } from "@monaco-editor/react"; +import trim from "lodash.trim"; +import { + MonacoJsxSyntaxHighlight, + getWorker, +} from "monaco-jsx-syntax-highlight-v2"; +import { jscript } from "./jscript"; + +export type MonacoEditor = Parameters[0]; +type Monaco = Parameters[1]; +type CompilerOptions = Parameters< + Parameters[1]["languages"]["typescript"]["typescriptDefaults"]["setCompilerOptions"] +>[0]; + +export const jsMount = async (editor: MonacoEditor, monaco: Monaco) => { + const m = monaco as any; + if (!m.customJSMounted) { + m.customJSMounted = true; + } else { + return; + } + + const compilerOptions: CompilerOptions = { + jsx: monaco.languages.typescript.JsxEmit.React, + jsxFactory: "React.createElement", + jsxFragmentFactory: "React.Fragment", + target: monaco.languages.typescript.ScriptTarget.ES2015, + allowNonTsExtensions: true, + lib: ["esnext", "dom"], + module: monaco.languages.typescript.ModuleKind.ESNext, + esModuleInterop: true, + moduleResolution: monaco.languages.typescript.ModuleResolutionKind.NodeJs, + }; + + const jsxHgController = new MonacoJsxSyntaxHighlight(getWorker(), monaco); + const { highlighter } = jsxHgController.highlighterBuilder({ + editor: editor, + }); + highlighter(); + editor.onDidChangeModelContent(() => { + highlighter(); + }); + + monaco.languages.registerDocumentFormattingEditProvider("typescript", { + async provideDocumentFormattingEdits(model, options, token) { + const prettier = jscript.prettier.standalone; + const prettier_ts = jscript.prettier.ts; + const prettier_estree = jscript.prettier.estree; + + if (prettier && prettier_estree && prettier_ts) { + const text = trim( + await prettier.format(model.getValue(), { + parser: "typescript", + plugins: [prettier_ts, prettier_estree], + }), + "; \n" + ); + + return [ + { + range: model.getFullModelRange(), + text, + }, + ]; + } + }, + }); + + monaco.languages.registerCompletionItemProvider("typescript", { + provideCompletionItems: (model, position) => { + const word = model.getWordUntilPosition(position); + return { + suggestions: [ + { + label: "log", + kind: monaco.languages.CompletionItemKind.Snippet, + documentation: "Add Console.log", + insertText: `console.log($1)`, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range: { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }, + }, + { + label: "sfy", + kind: monaco.languages.CompletionItemKind.Snippet, + documentation: "Add JSON.stringify", + insertText: `JSON.stringify($1)`, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range: { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }, + }, + ], + }; + }, + }); + + monaco.languages.registerCompletionItemProvider("typescript", { + triggerCharacters: [">"], + provideCompletionItems: (model, position) => { + const codePre: string = model.getValueInRange({ + startLineNumber: position.lineNumber, + startColumn: 1, + endLineNumber: position.lineNumber, + endColumn: position.column, + }); + + const tag = codePre.match(/.*<(\w+)>$/)?.[1]; + + if (!tag) { + return; + } + + const word = model.getWordUntilPosition(position); + + return { + suggestions: [ + { + label: ``, + kind: monaco.languages.CompletionItemKind.EnumMember, + insertText: `$1`, + insertTextRules: + monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet, + range: { + startLineNumber: position.lineNumber, + endLineNumber: position.lineNumber, + startColumn: word.startColumn, + endColumn: word.endColumn, + }, + }, + ], + }; + }, + }); + + monaco.languages.typescript.typescriptDefaults.setCompilerOptions( + compilerOptions + ); + + setTimeout(() => { + editor.getAction("editor.action.formatDocument")?.run(); + }, 100); +}; diff --git a/app/web/src/utils/script/types/base.ts b/app/web/src/utils/script/types/base.ts new file mode 100644 index 00000000..77d39f08 --- /dev/null +++ b/app/web/src/utils/script/types/base.ts @@ -0,0 +1,74 @@ +export const baseTypings = ` +type FC = React.FC; +const Fragment = React.Fragment; +const ReactNode = React.ReactNode; +const useCallback = React.useCallback; +const useMemo = React.useMemo; +const ReactElement = React.ReactElement; +const isValidElement = React.isValidElement; +const useEffect = React.useEffect; +const useState = React.useState; + +const pathname: string; +const isEditor: boolean; +const isLayout: boolean; +const isMobile: boolean; +const isDesktop: boolean; +const preload: (pathname: string) => void; +const apiHeaders: Record; +const navigate: (url:string) => void; +const params: any; +const cx = (...classNames: any[]) => string; +const css = ( + tag: CSSAttribute | TemplateStringsArray | string, + ...props: Array +) => string; + +const props: { + className: string; + onPointerDown?: () => void; + onPointerMove?: () => void; + onPointerLeave?: () => void; +}; +const children: ReactNode; + +const PassProp: FC & {children: React.ReactNode; }>; +const PassChild: FC<{name: string}>; +const Preload: FC<{url: string[]}>; +const apiurl: string; +const pageid: string; +type ITEM = { + id: string + name: string; + type: 'item' | 'text'; + adv?: { + js?: string; + jsBuilt?: string; + css?: string; + html?: string; + }, + text: string, + html: string, + component?: { id:string, props: Record}, + childs: ITEM[] +} +const newElement: (gen?: (item: ITEM) => ITEM | ITEM[]) => React.ReactNode; +const Local: >(arg: { + name: string; + value: T; + children: ((local: T & { render: () => void }) => any); + deps?: any[]; + effect?: ( + local: T & { render: () => void } + ) => void | (() => void) | Promise void)>; + hook?: ( + local: T & { render: () => void } + ) => void | (() => void) | Promise void)>; + cache?: boolean; +}) => ReactNode; + +`; diff --git a/app/web/src/utils/script/types/prop.tsx b/app/web/src/utils/script/types/prop.tsx new file mode 100644 index 00000000..7d27e999 --- /dev/null +++ b/app/web/src/utils/script/types/prop.tsx @@ -0,0 +1,114 @@ +import trim from "lodash.trim"; +import { isValidElement } from "react"; + +export const extractProp = (prop: { + values: Record; + types: Record; +}) => { + const propTypes: string[] = []; + const props: Record = {}; + + if (prop) { + if (prop.values) { + for (const [k, v] of Object.entries(prop.values)) { + if (!props[k]) { + props[k] = {}; + } + + if (typeof v === "function") { + if (isFunctionalComponent(v)) { + props[k].type = "React.FC"; + } else if (isClassComponent(v)) { + props[k].type = "React.Component"; + } else { + props[k].type = "any"; + } + } else if (v) { + if (typeof v === "object" && v._jsx) { + props[k].type = "React.ReactElement;"; + } else if (!!v.render && typeof v.$$typeof === "symbol") { + props[k].type = "React.FC & {ref?:any}>"; + } else { + props[k].val = v; + } + } + } + } + } + + if (prop.types) { + for (const [k, v] of Object.entries(prop.types)) { + if (!props[k]) { + props[k] = {}; + } + props[k].type = v; + } + } + + for (const [k, v] of Object.entries(props)) { + if (v.type) { + propTypes.push(`const ${k}: ${trim(v.type, "; \n")};`); + } else if (v.val) { + if (typeof v.val === "object" && isValidElement(v.val)) { + propTypes.push(`const ${k}: ReactElement;`); + } else { + try { + let val = v.val; + + if (typeof val === "object") { + if (typeof val.render === "function") { + val = { ...val, render: () => {} }; + } + + propTypes.push(`const ${k}: ${recurseTypes(val)};`); + } else { + propTypes.push(`const ${k}: string;`); + } + } catch (e) {} + } + } + } + + return propTypes; +}; + +function recurseTypes(object: any) { + const result: string[] = []; + if (typeof object === "object") { + if (object === null) return "null"; + if (Array.isArray(object)) { + return `any[]`; + } + + for (const [k, v] of Object.entries(object)) { + result.push( + `${k}: ${typeof v === "object" && v ? recurseTypes(v) : typeof v}` + ); + } + + return `{ + ${result.join(";\n ")} +}`; + } + return typeof object; +} + +function isFunctionalComponent(Component: any) { + return ( + typeof Component === "function" && // can be various things + !( + ( + Component.prototype && // native arrows don't have prototypes + Component.prototype.isReactComponent + ) // special property + ) + ); +} + +function isClassComponent(Component: any) { + return !!( + typeof Component === "function" && + Component.prototype && + Component.prototype.isReactComponent + ); +} diff --git a/app/web/src/utils/script/types/type-stringify.ts b/app/web/src/utils/script/types/type-stringify.ts new file mode 100644 index 00000000..7b94fb93 --- /dev/null +++ b/app/web/src/utils/script/types/type-stringify.ts @@ -0,0 +1,14 @@ +export const typeStringify = function (this: any, key: string, value: any) { + if (typeof value === "function") { + return `___FFF||any||FFF___`; + } + return value; +}; + +export const typeReviver = (key: any, value: any) => { + if (typeof key === "string" && key.indexOf("function ") === 0) { + let functionTemplate = `(${value})`; + return eval(functionTemplate); + } + return value; +}; diff --git a/app/web/src/utils/script/typings.ts b/app/web/src/utils/script/typings.ts new file mode 100644 index 00000000..1a44728c --- /dev/null +++ b/app/web/src/utils/script/typings.ts @@ -0,0 +1,155 @@ +import type { OnMount } from "@monaco-editor/react"; +import { w } from "../types/general"; +import { baseTypings } from "./types/base"; +import { extractProp } from "./types/prop"; +export type MonacoEditor = Parameters[0]; +type Monaco = Parameters[1]; + +const map = new WeakMap(); + +export const monacoTypings = async ( + p: { + site_dts: string; + site: { api_url: string }; + script: { siteTypes: Record }; + }, + monaco: Monaco, + prop: { values: Record; types: Record } +) => { + if (!map.has(prop.values)) { + map.set(prop.values, true); + } else { + return; + } + + if (w.prasiApi[p.site.api_url] && w.prasiApi[p.site.api_url].prismaTypes) { + const prisma = w.prasiApi[p.site.api_url].prismaTypes; + + register( + monaco, + `\ +declare module "ts:runtime/index" { + ${prisma["runtime/index.d.ts"]} +}`, + `ts:runtime/index.d.ts` + ); + + register( + monaco, + `\ +declare module "ts:runtime/library" { + ${prisma["runtime/library.d.ts"]} +}`, + `ts:runtime/library.d.ts` + ); + + register( + monaco, + `\ +declare module "ts:prisma" { + ${prisma["prisma.d.ts"].replace( + `import * as runtime from './runtime/library';`, + `import * as runtime from 'ts:runtime/library';` + )} +}`, + `ts:prisma.d.ts` + ); + + register(monaco, w.prasiApi[p.site.api_url].apiTypes, "ts:api.d.ts"); + } + + monaco.languages.typescript.typescriptDefaults.setExtraLibs([ + { + filePath: "react.d.ts", + content: await loadText( + "https://cdn.jsdelivr.net/npm/@types/react@18.2.0/index.d.ts" + ), + }, + { + filePath: "jsx-runtime.d.ts", + content: await loadText( + "https://cdn.jsdelivr.net/npm/@types/react@18.2.0/jsx-runtime.d.ts" + ), + }, + { + filePath: "site.d.ts", + content: p.site_dts.replaceAll("export declare const", "declare const"), + }, + ]); + + const propText = extractProp({ + values: prop.values, + types: { ...prop.types, ...p.script.siteTypes }, + }); + + const apiTypes = w.prasiApi[p.site.api_url] + ? w.prasiApi[p.site.api_url].apiTypes + : ""; + + let apiPath = "app/gen/srv/api/srv"; + if (apiTypes && apiTypes.includes(`export * as srv from "gen/srv/api/srv"`)) { + apiPath = "gen/srv/api/srv"; + } + + register( + monaco, + `\ +import React from 'react'; +import prisma from 'ts:prisma'; + +${iftext( + apiTypes, + `\ +import "./api" +import type * as SRVAPI from "${apiPath}";` +)} + +declare global {; + const db: prisma.PrismaClient; + + ${baseTypings} + + const moko: {nama: string}; + ${propText.join("\n")} + + ${iftext( + apiTypes, + ` + type Api = typeof SRVAPI; + type ApiName = keyof Api; + const api: { [k in ApiName]: Awaited["_"]["api"] }; + ` + )} +} + + `, + "ts:global.d.ts" + ); +}; + +const loadText = async (url: string) => { + try { + const res = await fetch(url); + return await res.text(); + } catch (e) { + return ""; + } +}; + +export const iftext = (condition: any, text: string) => { + if (condition) { + return text; + } + return ""; +}; + +export const register = (monaco: Monaco, source: string, uri: string) => { + const model = monaco.editor.getModels().find((e) => { + return e.uri.toString() === uri; + }); + if (model) { + model.setValue(source); + } else { + monaco.editor.createModel(source, "typescript", monaco.Uri.parse(uri)); + } +};