This commit is contained in:
Rizky 2024-01-23 17:36:36 +07:00
parent 43745f20df
commit 972fbd6ffd
17 changed files with 309 additions and 300 deletions

View File

@ -1,7 +1,7 @@
import { dir } from "dir";
import { apiContext } from "../../../pkgs/core/server/api/api-ctx";
import { g } from "utils/global";
import { code } from "../ws/sync/editor/code/util";
import { code } from "../ws/sync/editor/code/util-code";
export const _ = {
url: "/nova-load/:mode/:id/**",

View File

@ -65,12 +65,12 @@ export const SyncActions = {
bin: Uint8Array
) => ({}) as { diff: Uint8Array; sv: Uint8Array } | void,
diff_local: async (
mode: "page" | "comp" | "site",
mode: "page" | "comp" | "site" | "code",
id: string,
bin: Uint8Array
) => {},
sv_remote: async (
mode: "page" | "comp" | "site",
mode: "page" | "comp" | "site" | "code",
id: string,
sv: Uint8Array,
diff: Uint8Array
@ -84,7 +84,13 @@ export const SyncActions = {
load: async (id: string, type: "src" | "build") =>
({}) as {
id: string;
snapshot: null | Uint8Array;
snapshot: null | Record<
string,
{
id_doc: number;
bin: Uint8Array;
}
>;
},
edit: async (
arg:

View File

@ -1,5 +1,5 @@
import { SAction } from "../actions";
import { getCode, prepDCode } from "../editor/code/prep-code";
import { prepCodeSnapshot } from "../editor/code/prep-code";
import { SyncConnection } from "../type";
export const code_load: SAction["code"]["load"] = async function (
@ -7,13 +7,9 @@ export const code_load: SAction["code"]["load"] = async function (
site_id,
type
) {
const code = await getCode(site_id, "site");
if (code) {
const prep = await prepDCode(site_id);
if (prep) {
return { id: site_id, snapshot: prep.bin[type] };
}
const snap = await prepCodeSnapshot(site_id, "site");
if (snap && snap.type === "code") {
return { id: site_id, snapshot: snap.build };
}
return { id: site_id, snapshot: null };

View File

@ -3,6 +3,7 @@ import { ESite } from "../../../../web/src/nova/ed/logic/ed-global";
import { SAction } from "../actions";
import { prepCodeSnapshot } from "../editor/code/prep-code";
import { SyncConnection } from "../type";
import { gzipAsync } from "../entity/zlib";
export const site_load: SAction["site"]["load"] = async function (
this: SyncConnection,
@ -27,7 +28,13 @@ export const site_load: SAction["site"]["load"] = async function (
select: { id: true },
});
await prepCodeSnapshot(site_id, "site");
const snap = await prepCodeSnapshot(site_id, "site");
const compressed: any = {};
if (snap) {
for (const [key, value] of Object.entries(snap.build)) {
compressed[key] = { bin: await gzipAsync(value.bin) };
}
}
return {
id: site.id,
@ -38,7 +45,10 @@ export const site_load: SAction["site"]["load"] = async function (
responsive: site.responsive as ESite["responsive"],
js_compiled: site.js_compiled || "",
layout: { id: layout?.id || "", snapshot: null, meta: undefined },
code: { snapshot: null, mode: site.code_mode as "old" | "vsc" },
code: {
snapshot: compressed,
mode: site.code_mode as "old" | "vsc",
},
};
}
}

View File

@ -14,7 +14,11 @@ export const yjs_sv_remote: SAction["yjs"]["sv_remote"] = async function (
console.log(`sv_remote not found`, mode, id);
return;
}
const doc = docs[mode][id].doc;
let doc = null;
if (mode !== "code") doc = docs[mode][id].doc;
else {
doc = docs.code[id].build.site;
}
const diff_local = Y.encodeStateAsUpdate(doc as any, await gunzipAsync(sv));
Y.applyUpdate(doc as any, await gunzipAsync(diff), "local");
return { diff: await gzipAsync(diff_local) };

View File

@ -1,50 +1,78 @@
import { build, context } from "esbuild";
import { Code } from "./watcher";
import { g } from "utils/global";
import { dir } from "dir";
import globalExternals from "@fal-works/esbuild-plugin-global-externals";
import { style } from "@hyrious/esbuild-plugin-style";
import { sendWS } from "../../sync-handler";
import { SyncType } from "../../type";
import { gzipAsync } from "../../entity/zlib";
import { ServerWebSocket } from "bun";
import { WSData } from "../../../../../../pkgs/core/server/create";
import { dir } from "dir";
import { context } from "esbuild";
import { existsAsync, dirAsync, removeAsync, writeAsync } from "fs-jetpack";
import { CodeMode, code } from "./util-code";
import { user } from "../../entity/user";
import { conns } from "../../entity/conn";
import { CodeMode, code } from "./util";
import { docs } from "../../entity/docs";
import { DCode } from "../../../../../web/src/utils/types/root";
import { readDirectoryRecursively } from "../../../../api/site-export";
const encoder = new TextEncoder();
export const codeBuild = async (id_site: any, mode: CodeMode) => {
const src_path = code.path(id_site, mode, "src");
if (!(await existsAsync(src_path))) return;
const build_path = code.path(id_site, mode, "build");
const build_file = dir.path(`${build_path}/index.js`);
await removeAsync(build_path);
await dirAsync(build_path);
const build_file = `${build_path}/index.js`;
await writeAsync(build_file, "");
if (!code.esbuild[id_site]) {
code.esbuild[id_site] = { site: null, ssr: null };
}
code.esbuild[id_site][mode] = await context({
absWorkingDir: src_path,
entryPoints: ["index.tsx"],
bundle: true,
outfile: build_file,
minify: true,
treeShaking: true,
sourcemap: true,
plugins: [
style(),
globalExternals({
react: {
varName: "window.React",
type: "cjs",
if (!code.esbuild[id_site][mode]) {
code.esbuild[id_site][mode] = await context({
absWorkingDir: src_path,
entryPoints: ["index.tsx"],
bundle: true,
outfile: build_file,
minify: true,
treeShaking: true,
format: "cjs",
sourcemap: true,
plugins: [
style(),
globalExternals({
react: {
varName: "window.React",
type: "cjs",
},
"react-dom": {
varName: "window.ReactDOM",
type: "cjs",
},
}),
{
name: "prasi",
setup(setup) {
setup.onEnd((res) => {
const cdoc = docs.code[id_site];
if (cdoc) {
const doc = cdoc.build[mode];
const build_dir = code.path(id_site, mode, "build");
if (doc) {
codeApplyChanges(build_dir, doc);
}
}
});
},
},
"react-dom": {
varName: "window.ReactDOM",
type: "cjs",
},
}),
],
});
],
});
const esbuild = code.esbuild[id_site][mode];
esbuild?.watch();
}
const esbuild = code.esbuild[id_site][mode];
if (esbuild) {
try {
await esbuild.rebuild();
} catch (e) {}
}
const out = Bun.file(build_file);
const src = (await out.text()).replace(
@ -53,3 +81,25 @@ export const codeBuild = async (id_site: any, mode: CodeMode) => {
);
await Bun.write(out, src);
};
const codeApplyChanges = (path: string, doc: DCode) => {
const map = doc.getMap("map");
const files = map.get("files");
const dirs = readDirectoryRecursively(path);
doc.transact(() => {
files?.forEach((v, k) => {
if (!dirs[k]) {
files?.delete(k);
}
});
for (const [k, v] of Object.entries(dirs)) {
if (files) {
files.set(k, v);
}
}
});
return doc;
};

View File

@ -1,75 +0,0 @@
import { spawn } from "bun";
import { sendWS } from "../../sync-handler";
import { SyncType } from "../../type";
import { Code } from "./watcher";
const decoder = new TextDecoder();
export const codePkgInstall = async (id_site: string, mode: string) => {
try {
const proc = spawn({
cmd: ["bun", "i"],
cwd: Code.path(code.id_site, code.id),
stderr: "pipe",
stdout: "pipe",
});
const broadcast = (content: string) => {
activity.site
.room(code.id_site)
.findAll({ site_js: code.name })
.forEach((item, ws) => {
sendWS(ws, {
type: SyncType.Event,
event: "code",
data: {
name: code.name,
id: code.id,
content,
},
});
});
};
(async () => {
for await (const chunk of proc.stdout) {
broadcast(decoder.decode(chunk));
}
})();
(async () => {
for await (const chunk of proc.stderr) {
broadcast(decoder.decode(chunk));
}
})();
await proc.exited;
activity.site
.room(code.id_site)
.findAll({ site_js: code.name })
.forEach((item, ws) => {
sendWS(ws, {
type: SyncType.Event,
event: "code",
data: {
name: code.name,
id: code.id,
event: "code-done",
},
});
});
} catch (e: any) {
activity.site
.room(code.id_site)
.findAll({ site_js: code.name })
.forEach((item, ws) => {
sendWS(ws, {
type: SyncType.Event,
event: "code",
data: {
name: code.name,
id: code.id,
event: "code-done",
content: `ERROR: ${e.message}`,
},
});
});
}
};

View File

@ -1,11 +1,15 @@
import { existsAsync } from "fs-jetpack";
import { Doc } from "yjs";
import { DCode } from "../../../../../web/src/utils/types/root";
import { readDirectoryRecursively } from "../../../../api/site-export";
import { docs } from "../../entity/docs";
import { snapshot } from "../../entity/snapshot";
import { gzipAsync } from "../../entity/zlib";
import { codeBuild } from "./build-code";
import { CodeMode, code } from "./util";
import { CodeMode, code } from "./util-code";
import { user } from "../../entity/user";
import { SyncType } from "../../type";
import { sendWS } from "../../sync-handler";
import { conns } from "../../entity/conn";
export const prepCodeSnapshot = async (id_site: string, mode: CodeMode) => {
await code
@ -17,23 +21,47 @@ export const prepCodeSnapshot = async (id_site: string, mode: CodeMode) => {
)
.await();
let doc = docs.code[id_site];
let dcode = docs.code[id_site];
if (!docs.code[id_site]) {
docs.code[id_site] = {
id: id_site,
build: {},
};
doc = docs.code[id_site];
dcode = docs.code[id_site];
}
if (doc) {
if (!doc.build[mode]) {
if (dcode) {
if (!dcode.build[mode]) {
const build_dir = code.path(id_site, mode, "build");
await codeBuild(id_site, mode);
dcode.build[mode] = codeLoad(id_site, build_dir);
const doc = dcode.build[mode] as Doc;
if (doc) {
doc.on("update", async (e, origin) => {
const bin = Y.encodeStateAsUpdate(doc);
if (!(await existsAsync(build_dir))) {
await codeBuild(id_site, mode);
if (snap && snap.type === "code") {
snap.build[mode].bin = bin;
snapshot.update({
id: id_site,
type: "code",
build: snap.build,
});
}
const sv_local = await gzipAsync(bin);
user.active.findAll({ site_id: id_site }).map((e) => {
const ws = conns.get(e.client_id)?.ws;
if (ws) {
sendWS(ws, {
type: SyncType.Event,
event: "remote_svlocal",
data: { type: "code", sv_local, id: id_site },
});
}
});
});
}
doc.build[mode] = codeLoad(id_site, build_dir);
}
const build: Record<
@ -43,9 +71,10 @@ export const prepCodeSnapshot = async (id_site: string, mode: CodeMode) => {
bin: Uint8Array;
}
> = {};
for (const [k, v] of Object.entries(doc.build)) {
for (const [k, v] of Object.entries(dcode.build)) {
const bin = Y.encodeStateAsUpdate(v as Doc);
build[k] = { bin, id_doc: v.clientID };
build[k] = { bin: bin, id_doc: v.clientID };
}
let snap = await snapshot.getOrCreate({
@ -54,7 +83,9 @@ export const prepCodeSnapshot = async (id_site: string, mode: CodeMode) => {
build,
});
return snap;
if (snap.type === "code") {
return snap;
}
}
};
@ -66,7 +97,7 @@ const codeLoad = (id: string, path: string) => {
const dirs = readDirectoryRecursively(path);
for (const [k, v] of Object.entries(dirs)) {
files.set(k, new Y.Text(v));
files.set(k, v);
}
doc.transact(() => {

View File

@ -1,13 +1,17 @@
import { dir } from "dir";
import { g } from "utils/global";
import { dirAsync, writeAsync } from "fs-jetpack";
import { dirname } from "path";
import { BuildContext } from "esbuild";
import { dirAsync, existsAsync, writeAsync } from "fs-jetpack";
import { dirname } from "path";
import { g } from "utils/global";
export type CodeMode = "site" | "ssr";
export const code = {
path(id_site: string, mode: CodeMode, type: "src" | "build", path?: string) {
return dir.path(`${g.datadir}/code/${id_site}/${mode}/${type}`);
let file_path = "";
if (path) {
file_path = path[0] === "/" ? path : `/${path}`;
}
return dir.path(`${g.datadir}/code/${id_site}/${mode}/${type}${file_path}`);
},
esbuild: {} as Record<string, Record<CodeMode, null | BuildContext>>,
prep(id_site: string, mode: CodeMode) {
@ -17,15 +21,21 @@ export const code = {
});
const promises: Promise<void>[] = [];
return {
path(type: "src" | "build", path?: string) {
return dir.path(`${g.datadir}/code/${id_site}/${mode}/${type}`);
path(type: "src" | "build", path: string) {
return dir.path(
`${g.datadir}/code/${id_site}/${mode}/${type}${
path[0] === "/" ? path : `/${path}`
}`
);
},
new_file(path: string, content: string) {
promises.push(
new Promise(async (done) => {
const full_path = this.path("src", path);
await dirAsync(dirname(full_path));
await writeAsync(full_path, content);
if (!(await existsAsync(full_path))) {
await dirAsync(dirname(full_path));
await writeAsync(full_path, content);
}
done();
})
);

View File

@ -85,8 +85,9 @@ export const snapshot = {
async getOrCreate(data: DocSnapshot) {
const id = `${data.type}-${data.id}`;
let res = this.db.get(id);
if (!res) {
await this.db.put(id, structuredClone(emptySnapshot as DocSnapshot));
if (!res || !res.id) {
await this.db.put(id, structuredClone(data as DocSnapshot));
res = this.db.get(id);
}
return res as DocSnapshot;

View File

@ -5,7 +5,7 @@ import { SAction } from "../../../../../srv/ws/sync/actions";
import { parseJs } from "../../../../../srv/ws/sync/editor/parser/parse-js";
import { clientStartSync } from "../../../utils/sync/ws-client";
import { IItem } from "../../../utils/types/item";
import { DComp, DPage } from "../../../utils/types/root";
import { DCode, DComp, DPage } from "../../../utils/types/root";
import { GenMetaP, IMeta as LogicMeta } from "../../vi/utils/types";
export type IMeta = LogicMeta;
@ -24,7 +24,15 @@ export const EmptySite = {
meta: undefined as void | Record<string, IMeta>,
},
code: {
snapshot: null as null | Uint8Array,
snapshot: {} as
| undefined
| Record<
string,
{
id_doc: number;
bin: Uint8Array;
}
>,
mode: "old" as "old" | "vsc",
},
};
@ -183,7 +191,8 @@ export const EDGlobal = {
>,
group: {} as Record<string, Awaited<ReturnType<SAction["comp"]["group"]>>>,
},
global_prop: [] as string[],
code: {} as Record<string, { doc: null | DCode }>,
global_prop: [] as string[],
ui: {
zoom: localStorage.zoom || "100%",
should_render: false,
@ -211,7 +220,6 @@ export const EDGlobal = {
code: {
init: false,
open: false,
id: "",
name: "site",
log: "",
loading: false,

View File

@ -1,57 +1,59 @@
import { viLoadLegacy } from "../../vi/load/load-legacy";
import { viLoadSnapshot } from "../../vi/load/load-snapshot";
import { ESite, PG } from "./ed-global";
import { reloadPage } from "./ed-route";
export const loadSite = async (p: PG, site: ESite, note: string) => {
const old_layout_id = p.site.layout.id;
const layout_changed = p.site.layout.id !== site.layout.id;
p.site = site;
const old_layout_id = p.site.layout.id;
const layout_changed = p.site.layout.id !== site.layout.id;
p.site = site;
p.mode = "desktop";
if (p.site.responsive === "mobile-only") {
p.mode = "mobile";
} else if (p.site.responsive === "desktop-only") {
p.mode = "desktop";
}
p.mode = "desktop";
if (p.site.responsive === "mobile-only") {
p.mode = "mobile";
} else if (p.site.responsive === "desktop-only") {
p.mode = "desktop";
}
if (layout_changed) {
const old_layout = p.page.list[old_layout_id];
if (layout_changed) {
const old_layout = p.page.list[old_layout_id];
if (old_layout && old_layout.on_update) {
old_layout.doc.off("update", old_layout.on_update);
}
if (old_layout && old_layout.on_update) {
old_layout.doc.off("update", old_layout.on_update);
}
if (!p.script.db && !p.script.api) {
if (p.site.code.mode === "old") {
await viLoadLegacy({
site: {
api_url: p.site.config.api_url,
id: p.site.id,
api: {
get() {
return p.script.api;
},
set(val) {
p.script.api = val;
},
},
db: {
get() {
return p.script.db;
},
set(val) {
p.script.db = val;
},
},
},
render: () => {},
});
} else {
}
}
if (!p.script.db && !p.script.api) {
if (p.site.code.mode === "old") {
await viLoadLegacy({
site: {
api_url: p.site.config.api_url,
id: p.site.id,
api: {
get() {
return p.script.api;
},
set(val) {
p.script.api = val;
},
},
db: {
get() {
return p.script.db;
},
set(val) {
p.script.db = val;
},
},
},
render: () => {},
});
} else {
await viLoadSnapshot(p);
}
}
if (site.layout.id) {
await reloadPage(p, site.layout.id, "load-layout");
}
}
if (site.layout.id) {
await reloadPage(p, site.layout.id, "load-layout");
}
}
};

View File

@ -64,47 +64,11 @@ export const edInitSync = (p: PG) => {
site_id: params.site_id,
page_id: params.page_id,
events: {
code(arg) {
p.ui.popup.code.error = false;
if (arg.event === "code-loading") {
p.ui.popup.code.log = "";
p.ui.popup.code.loading = true;
p.render();
} else if (arg.event === "code-done") {
if (typeof arg.content === "string") {
if (arg.content !== "OK") {
p.ui.popup.code.error = true;
}
p.ui.popup.code.log += arg.content;
}
p.ui.popup.code.loading = false;
if (arg.src) {
const w = window as any;
const module = evalCJS(decoder.decode(decompress(arg.src)));
p.global_prop = Object.keys(module);
if (typeof module === "object") {
for (const [k, v] of Object.entries(module)) {
w[k] = v;
}
}
}
p.render();
} else {
if (typeof arg.content === "string")
p.ui.popup.code.log += arg.content;
p.render();
}
},
activity(arg) {},
opened() {
if (w.offline) {
console.log("reconnected!");
w.offline = false;
p.ui.syncing = true;
p.sync.activity("site", { type: "join", id: params.site_id });
p.render();
} else {
w.offline = false;
@ -158,6 +122,8 @@ export const edInitSync = (p: PG) => {
doc = p.page.doc as Y.Doc;
} else if (data.type === "comp" && p.comp.list[data.id]) {
doc = p.comp.list[data.id].doc;
} else if (data.type === "code") {
doc = p.code.site.doc;
}
if (doc) {
@ -179,7 +145,7 @@ export const edInitSync = (p: PG) => {
Y.applyUpdate(doc, decompress(res.diff), "sv_remote");
if (data.type === "page") {
await treeRebuild(p, { note: "sv_remote" });
} else {
} else if (data.type === "comp") {
const updated = await updateComponentMeta(p, doc, data.id);
if (updated) {
p.comp.list[data.id].meta = updated.meta;

View File

@ -7,11 +7,11 @@ import { Popover } from "../../../../../utils/ui/popover";
import { Tooltip } from "../../../../../utils/ui/tooltip";
import { EDGlobal } from "../../../logic/ed-global";
import {
iconChevronDown,
iconLoading,
iconLog,
iconNewTab,
iconTrash
iconChevronDown,
iconLoading,
iconLog,
iconNewTab,
iconTrash,
} from "./icons";
import { CodeNameItem, CodeNameList, codeName } from "./name-list";
@ -116,7 +116,6 @@ const CodeBody = () => {
onPick={async (e) => {
local.namePicker = false;
p.ui.popup.code.name = e.name;
p.ui.popup.code.id = "";
p.render();
}}
/>
@ -142,41 +141,6 @@ const CodeBody = () => {
></div>
</Popover>
{p.ui.popup.code.name !== "site" &&
p.ui.popup.code.name !== "SSR" && (
<>
<Tooltip
content={"Delete Code Module"}
className="flex items-center border-l relative hover:bg-red-50 cursor-pointer px-2 transition-all text-red-500"
placement="bottom"
onClick={async () => {
if (
prompt(
"Are you sure want to delete this code?\ntype 'yes' to confirm:"
) === "yes"
) {
await db.code.delete({
where: { id: p.ui.popup.code.id },
});
codeName.list = codeName.list.filter(
(e) => e.id !== p.ui.popup.code.id
);
p.ui.popup.code.name = codeName.list[0].name;
p.ui.popup.code.id = codeName.list[0].id;
p.render();
}
}}
>
<div
dangerouslySetInnerHTML={{
__html: iconTrash,
}}
></div>
</Tooltip>
</>
)}
<Tooltip
content="STDOUT Log"
delay={0}
@ -211,9 +175,7 @@ const CodeBody = () => {
placement="bottom"
className={cx("flex items-stretch relative")}
onClick={() => {
window.open(
`${vscode_url}folder=/site/code/${p.site.id}/${p.ui.popup.code.id}`
);
window.open(`${vscode_url}folder=/site/${p.site.id}/site/src`);
}}
>
<div
@ -271,13 +233,13 @@ const CodeBody = () => {
{code_mode === "vsc" ? (
<div className="flex flex-1 relative">
{!p.ui.popup.code.open || !p.ui.popup.code.id ? (
{!p.ui.popup.code.open ? (
<Loading backdrop={false} />
) : (
<>
<iframe
className="flex flex-1 absolute inset-0 w-full h-full z-10"
src={`${vscode_url}folder=/site/code/${p.site.id}/${p.ui.popup.code.id}`}
src={`${vscode_url}folder=/site/${p.site.id}/site/src`}
></iframe>
<div className="flex flex-1 absolute inset-0 z-0 items-center justify-center">
Loading VSCode...

View File

@ -0,0 +1,57 @@
import { decompress } from "wasm-gzip";
import { loadApiProxyDef } from "../../../base/load/api/api-proxy-def";
import { PG } from "../../ed/logic/ed-global";
import { evalCJS } from "../../ed/logic/ed-sync";
import { treeRebuild } from "../../ed/logic/tree/build";
const decoder = new TextDecoder();
export const viLoadSnapshot = async (p: PG) => {
let api_url = p.site.config.api_url;
try {
const apiURL = new URL(api_url);
if (api_url && apiURL.hostname) {
try {
await loadApiProxyDef(api_url, true);
} catch (e) {
console.warn("Failed to load API:", api_url);
}
}
} catch (e) {}
if (p.site.code.snapshot) {
for (const [name, build] of Object.entries(p.site.code.snapshot)) {
const doc = new Y.Doc();
Y.applyUpdate(doc, decompress(build.bin));
p.code[name] = { doc: doc as any };
const code = p.code[name].doc;
if (code) {
const src = code.getMap("map").get("files")?.get("index.js");
applyEnv(p, src);
treeRebuild(p);
p.render();
code.on("update", (e, origin) => {
const src = code.getMap("map").get("files")?.get("index.js");
applyEnv(p, src);
treeRebuild(p);
p.render();
});
}
}
}
};
const applyEnv = (p: PG, src?: string) => {
if (src) {
const w = window as any;
const module = evalCJS(src);
p.global_prop = Object.keys(module);
if (typeof module === "object") {
for (const [k, v] of Object.entries(module)) {
w[k] = v;
}
}
}
};

View File

@ -77,28 +77,9 @@ export const clientStartSync = async (arg: {
site_id?: string;
page_id?: string;
events: {
code: (arg: {
name: string;
id: string;
event: "code-loading" | "code-done";
content?: string;
src?: Uint8Array;
}) => void;
activity: (arg: {
activity: string;
room_id: string;
clients: {
user: {
client_id: string;
user_id: string;
username: string;
};
data: any;
}[];
}) => void;
editor_start: (arg: UserConf) => void;
remote_svlocal: (arg: {
type: "page" | "comp";
type: "page" | "comp" | "code";
id: string;
sv_local: Uint8Array;
}) => void;

View File

@ -28,6 +28,6 @@ export type DComp = TypedDoc<{
export type DCode = TypedDoc<{
map: TypedMap<{
id: string;
files: TypedMap<Record<string, Y.Text>>;
files: TypedMap<Record<string, string>>;
}>;
}>;