From bf99c229d5842b28c2b6df442e6c019e5a341089 Mon Sep 17 00:00:00 2001 From: Ayo Date: Thu, 12 Oct 2023 20:38:42 +0200 Subject: [PATCH] feat: initial packages --- mcfly.config.ts | 4 +- packages/assets/mcfly-ssr.ts | 119 ++++++++++++++++++ packages/define-config.ts | 7 +- ...tom-elements.ts => register-components.ts} | 13 +- packages/set-up-ssr.ts | 26 ++++ plugins/_mcfly-plugin.ts | 11 +- routes/[...index].ts | 4 + 7 files changed, 168 insertions(+), 16 deletions(-) create mode 100644 packages/assets/mcfly-ssr.ts rename packages/{custom-elements.ts => register-components.ts} (84%) create mode 100644 packages/set-up-ssr.ts diff --git a/mcfly.config.ts b/mcfly.config.ts index a711861..bb242ff 100644 --- a/mcfly.config.ts +++ b/mcfly.config.ts @@ -1,6 +1,6 @@ -import components from "./packages/custom-elements"; +import registerComponents from "./packages/register-components"; import defineConfig from "./packages/define-config"; export default defineConfig({ - integrations: [components()], + onBuild: [registerComponents()], }); diff --git a/packages/assets/mcfly-ssr.ts b/packages/assets/mcfly-ssr.ts new file mode 100644 index 0000000..bdacdec --- /dev/null +++ b/packages/assets/mcfly-ssr.ts @@ -0,0 +1,119 @@ +/** + * McFly SSR logic + */ + +import { ELEMENT_NODE, parse, renderSync, walkSync } from "ultrahtml"; +import { parseScript } from "esprima"; + +export default eventHandler(async (event) => { + const { path } = event; + let html = await getHtml(path); + + // transforms + const transforms = [doSetUp, deleteServerScripts, insertRegistry]; + if (html) { + for (const transform of transforms) { + html = transform(html.toString()); + } + } + + return html ?? new Response("Not found", { status: 404 }); +}); + +const getHtml = async (path: string) => { + const rawPath = path[path.length - 1] === "/" ? path.slice(0, -1) : path; + const filename = rawPath === "" ? "/index.html" : `${rawPath}.html`; + const fallback = getPath(rawPath + "/index.html"); + const filePath = getPath(filename); + let html = await useStorage().getItem(filePath); + if (!html) html = await useStorage().getItem(fallback); + if (!html) html = await useStorage().getItem(getPath("/404.html")); + + return html; +}; + +function getPath(filename: string) { + return `assets/pages${filename}`; +} + +function insertRegistry(html: string): string { + // temporary; use ultrahtml later + const registryScript = + ''; + + const ast = parse(html); + + let hasCustomElements = false; + + walkSync(ast, (node) => { + if (node.type === ELEMENT_NODE && node.name.includes("-")) { + hasCustomElements = true; + } + }); + + // insert registry script to head + if (hasCustomElements) + walkSync(ast, (node) => { + if (node.type === ELEMENT_NODE && node.name === "head") { + node.children.push(parse(registryScript)); + } + }); + + return renderSync(ast); +} + +function doSetUp(html: string) { + const ast = parse(html); + const serverScripts = []; + walkSync(ast, (node) => { + const { attributes } = node; + const attributeKeys = Object.keys(attributes ?? {}); + const isServerScript = attributeKeys.some((key) => key.includes("server:")); + if ( + node.type === ELEMENT_NODE && + node.name === "script" && + isServerScript + ) { + const scripts = node.children.map((child) => child.value); + serverScripts.push(scripts.join(" ")); + } + }); + + const setupMap = {}; + serverScripts.forEach((script: string) => { + const { body } = parseScript(script); + const keys = body + .filter((node) => node.type === "VariableDeclaration") + .map((node) => node.declarations[0].id.name); + const constructor = `new Function(\`${script}; return {${keys.join( + "," + )}}\`);`; + const evalStore = eval(constructor); + Object.assign(setupMap, new evalStore()); + }); + + const regex = /{{(.*?)}}/g; + var match; + + while ((match = regex.exec(html))) { + let [key, value] = match; + value = value.replace(/\s/g, ""); + html = html.replace(key, setupMap[value]); + } + + return html; +} + +function deleteServerScripts(html: string): string { + const ast = parse(html); + walkSync(ast, (node) => { + const { attributes } = node; + const attributeKeys = Object.keys(attributes ?? {}); + const isServerScript = attributeKeys.some((key) => key.includes("server:")); + if (isServerScript) { + node.parent.children.splice(node.parent.children.indexOf(node), 1); + } + }); + + return renderSync(ast); +} diff --git a/packages/define-config.ts b/packages/define-config.ts index 62549e3..7e3754c 100644 --- a/packages/define-config.ts +++ b/packages/define-config.ts @@ -1,6 +1,9 @@ +import { NitroApp } from "nitropack"; +import setUpSsr from "./set-up-ssr"; + export type McFlyConfig = { - integrations?: Array<() => void>; + onBuild?: Array<(event: NitroApp) => void>; }; export default function defineConfig(config: McFlyConfig) { - return () => config; + return () => ({ ...config, onBuild: [...config.onBuild, setUpSsr()] }); } diff --git a/packages/custom-elements.ts b/packages/register-components.ts similarity index 84% rename from packages/custom-elements.ts rename to packages/register-components.ts index fea76b7..476dd13 100644 --- a/packages/custom-elements.ts +++ b/packages/register-components.ts @@ -1,14 +1,13 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs"; -export default function components() { +export default function registerComponents() { return () => { copyComponents(); - registerComponents(); + buildRegistry(); }; } const copyComponents = async () => { - console.log("Copying components to public folder..."); const rawKeys = await useStorage().getKeys("assets:components"); rawKeys.forEach(async (key) => { const cleanKey = key.replace("assets:components:", ""); @@ -18,7 +17,7 @@ const copyComponents = async () => { }); }; -const registerComponents = async () => { +const buildRegistry = async () => { console.log("Building registry of custom elements..."); const rawKeys = await useStorage().getKeys("/assets/components"); const keys = rawKeys.map((key) => key.replace("assets:components:", "")); @@ -37,9 +36,9 @@ const registerComponents = async () => { const customElementsDefine = `Object.keys(registry).forEach((key) => {if(window?.hasOwnProperty("customElements"))customElements.define(key, registry[key]);})`; - if (!existsSync("./public/.output")) { - mkdirSync("./public/.output"); - } + if (!existsSync("./public")) mkdirSync("./public"); + if (!existsSync("./public/.output")) mkdirSync("./public/.output"); + writeFileSync( "./public/.output/registry.js", [...imports, registryObject, customElementsDefine].join(";") diff --git a/packages/set-up-ssr.ts b/packages/set-up-ssr.ts new file mode 100644 index 0000000..275ac77 --- /dev/null +++ b/packages/set-up-ssr.ts @@ -0,0 +1,26 @@ +import { existsSync, promises as fsp } from "node:fs"; + +export default function setUpSsr() { + return async () => { + if (!existsSync("./routes")) { + await fsp.mkdir("./routes"); + } + + if (!existsSync("./routes/[...index].ts")) + try { + fsp.copyFile( + "./packages/assets/mcfly-ssr.ts", + "./routes/[...index].ts" + ); + console.log("SSR set up successfully!"); + } catch (err) { + if (err) { + console.log("Error Found:", err); + } else { + // Get the current filenames + // after the function + console.log("SSR set up successfully!"); + } + } + }; +} diff --git a/plugins/_mcfly-plugin.ts b/plugins/_mcfly-plugin.ts index 46e1c5b..852ad8d 100644 --- a/plugins/_mcfly-plugin.ts +++ b/plugins/_mcfly-plugin.ts @@ -1,9 +1,10 @@ +import { NitroApp } from "nitropack"; import config from "../mcfly.config"; -export default defineNitroPlugin(() => { - const { integrations } = config(); - if (integrations?.length > 0) - integrations.forEach((integration) => { - integration(); +export default defineNitroPlugin((event: NitroApp) => { + const { onBuild } = config(); + if (onBuild?.length > 0) + onBuild.forEach((callBack) => { + callBack(event); }); }); diff --git a/routes/[...index].ts b/routes/[...index].ts index c993bc0..bdacdec 100644 --- a/routes/[...index].ts +++ b/routes/[...index].ts @@ -1,3 +1,7 @@ +/** + * McFly SSR logic + */ + import { ELEMENT_NODE, parse, renderSync, walkSync } from "ultrahtml"; import { parseScript } from "esprima";