import globalExternals from "@fal-works/esbuild-plugin-global-externals"; import style from "@hyrious/esbuild-plugin-style"; import { $ } from "bun"; import { dir } from "dir"; import { context, formatMessages } from "esbuild"; import { cleanPlugin } from "esbuild-clean-plugin"; import { watch } from "fs"; import { existsAsync } from "fs-jetpack"; import { appendFile } from "node:fs/promises"; import { server } from "../../../editor/code/server-main"; import { conns } from "../../../entity/conn"; import { user } from "../../../entity/user"; import { sendWS } from "../../../sync-handler"; import { SyncType } from "../../../type"; import { code } from "../../code"; const pending = {} as any; export const initFrontEnd = async ( root: string, id_site: string, force?: boolean ) => { let existing = code.internal.frontend[id_site]; if (existing) { if (existing.npm) { if (pending[id_site]) return; pending[id_site] = true; await existing.npm; delete pending[id_site]; delete existing.npm; } else { if (force) { try { existing.watch.close(); await existing.ctx.dispose(); delete code.internal.frontend[id_site]; } catch (e) {} } else { if (existing.ctx) { return; } } } } else { if ( !(await existsAsync(code.path(id_site, "site", "src", "node_modules"))) ) { return; } } try { await isInstalling(id_site); const broadcastLoading = async () => { const client_ids = user.active .findAll({ site_id: id_site }) .map((e) => e.client_id); const now = Date.now(); client_ids.forEach((client_id) => { const ws = conns.get(client_id)?.ws; if (ws) sendWS(ws, { type: SyncType.Event, event: "code_changes", data: { ts: now, mode: "frontend", status: "building" }, }); }); }; code.internal.frontend[id_site] = { ctx: await initBuildCtx({ id_site, root }), timeout: null, rebuilding: false, watch: watch( dir.data(root), { recursive: true, }, async (event, filename) => { const fe = code.internal.frontend[id_site]; const srv = code.internal.server[id_site]; if ( filename?.startsWith("node_modules") || filename?.startsWith("server.ts") || filename?.startsWith("typings") ) return; if ( filename?.endsWith(".tsx") || filename?.endsWith(".ts") || filename?.endsWith(".css") || filename?.endsWith(".html") ) { if (typeof fe !== "undefined" && !fe.rebuilding) { fe.rebuilding = true; clearTimeout(fe.timeout); fe.timeout = setTimeout(async () => { const build_timeout = setTimeout(async () => { console.log( `Build front-end unfinished ${id_site} ${filename}` ); await fe.ctx.dispose(); fe.ctx = await initBuildCtx({ id_site, root }); }, 3000); try { broadcastLoading(); await fe.ctx.rebuild(); clearTimeout(build_timeout); } catch (e: any) { console.error(`Frontend failed rebuild (site: ${id_site})`); console.error(e.messsage); } fe.rebuilding = false; }, 500); } if (typeof srv !== "undefined" && !srv.rebuilding && srv.ctx) { srv.rebuilding = true; try { await srv.ctx.rebuild(); await server.init(id_site); } catch (e) {} srv.rebuilding = false; } } } ), }; const fe = code.internal.frontend[id_site]; fe.rebuilding = true; try { await fe.ctx.rebuild(); } catch (e) { console.log(`Failed to rebuild front-end ${id_site}`); console.error(e); } fe.rebuilding = false; } catch (e: any) { console.error("Error building front end", id_site); delete code.internal.frontend[id_site]; } }; const codeError = async (id_site: string, error: string, append?: boolean) => { const path = code.path(id_site, "site", "src", "index.log"); if (error) console.log(error); if (append) { await appendFile(path, error); return; } await Bun.write(path, error); }; const isInstalling = async (id_site: string) => { const path = code.path(id_site, "site", "src", "index.log"); const file = Bun.file(path); try { const text = await file.text(); if (typeof text === "string" && text.startsWith("Installing dependencies")) return true; } catch (e) {} return false; }; const initBuildCtx = async ({ id_site, root, }: { id_site: string; root: string; }) => { const out_dir_temp = dir.data(`code/${id_site}/site/build-temp`); const out_dir = dir.data(`code/${id_site}/site/build`); const site_filename = "internal.tsx"; const site_tsx = Bun.file(dir.data(root + `/${site_filename}`)); if (!(await site_tsx.exists())) { await Bun.write( site_tsx, `\ import React from "react"; // export const Loading = () => { // return <>; // }; // export const NotFound = () => { // return <>; // }; ` ); } return await context({ absWorkingDir: dir.data(root), entryPoints: ["index.tsx", site_filename], outdir: out_dir_temp, format: "esm", bundle: true, minify: true, treeShaking: true, splitting: true, logLevel: "silent", sourcemap: true, metafile: true, plugins: [ cleanPlugin(), style(), globalExternals({ react: { varName: "window.React", type: "cjs", }, "react-dom": { varName: "window.ReactDOM", type: "cjs", }, }), { name: "prasi", async setup(setup) { try { setup.onEnd((res) => { setTimeout(async () => { const client_ids = user.active .findAll({ site_id: id_site }) .map((e) => e.client_id); if (res.errors.length > 0) { await codeError( id_site, (await formatMessages(res.errors, { kind: "error" })).join( "\n\n" ) ); const now = Date.now(); client_ids.forEach((client_id) => { const ws = conns.get(client_id)?.ws; if (ws) sendWS(ws, { type: SyncType.Event, event: "code_changes", data: { ts: now, mode: "frontend", status: "error" }, }); }); } else { await codeError(id_site, ""); await $`rm -rf ${out_dir}`.quiet(); await $`mv ${out_dir_temp} ${out_dir}`.quiet(); const now = Date.now(); client_ids.forEach((client_id) => { const ws = conns.get(client_id)?.ws; if (ws) sendWS(ws, { type: SyncType.Event, event: "code_changes", data: { ts: now, mode: "frontend", status: "ok" }, }); }); } }); }); } catch (e) { console.log("ERROR"); } }, }, ], }); };