fix(core): use mcfly config in eventHandler

This commit is contained in:
Ayo 2023-10-21 15:56:36 +02:00
parent 9523e751f6
commit fb8192f367
7 changed files with 34 additions and 289 deletions

View file

@ -3,4 +3,4 @@
*/ */
import McFly from "@mcflyjs/core"; import McFly from "@mcflyjs/core";
import config from "../mcfly.config"; import config from "../mcfly.config";
export default McFly(config, useStorage); export default McFly(config, useStorage());

7
package-lock.json generated
View file

@ -3979,13 +3979,14 @@
}, },
"packages/core": { "packages/core": {
"name": "@mcflyjs/core", "name": "@mcflyjs/core",
"version": "0.0.1", "version": "0.1.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esprima": "^4.0.1", "esprima": "^4.0.1",
"h3": "^1.8.2", "h3": "^1.8.2",
"nitropack": "^2.7.0", "nitropack": "^2.7.0",
"ultrahtml": "^1.5.2" "ultrahtml": "^1.5.2",
"unstorage": "^1.9.0"
} }
}, },
"packages/create-mcfly": { "packages/create-mcfly": {
@ -4002,7 +4003,7 @@
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@mcflyjs/config": "^0.0.1", "@mcflyjs/config": "^0.0.1",
"@mcflyjs/core": "^0.0.1", "@mcflyjs/core": "^0.1.0",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"nitropack": "latest", "nitropack": "latest",
"ultrahtml": "^1.5.2" "ultrahtml": "^1.5.2"

View file

@ -3,7 +3,8 @@
"scripts": { "scripts": {
"start": "npm start -w @mcflyjs/landing-page", "start": "npm start -w @mcflyjs/landing-page",
"build": "npm run build -w @mcflyjs/landing-page", "build": "npm run build -w @mcflyjs/landing-page",
"build:preview": "npm run build:preview -w @mcflyjs/landing-page" "build:preview": "npm run build:preview -w @mcflyjs/landing-page",
"template:basic": "npm run dev -w @templates/basic"
}, },
"workspaces": [ "workspaces": [
"packages/config", "packages/config",

View file

@ -1,14 +1,14 @@
import { eventHandler } from "h3"; import { eventHandler } from "h3";
import { Storage as NitroStorage } from "unstorage";
import { ELEMENT_NODE, parse, render, renderSync, walkSync } from "ultrahtml"; import { ELEMENT_NODE, parse, render, renderSync, walkSync } from "ultrahtml";
import { parseScript } from "esprima"; import { parseScript } from "esprima";
export default (config: Function, useStorage) => { export default (config: Function, storage: NitroStorage) => {
const { componentType } = config();
return eventHandler(async (event) => { return eventHandler(async (event) => {
const { path } = event; const { path } = event;
let html = await getHtml(path, useStorage); const { components: componentType } = config();
let html = await getHtml(path, storage);
// transforms
const transforms = [doSetUp, deleteServerScripts]; const transforms = [doSetUp, deleteServerScripts];
if (html) { if (html) {
for (const transform of transforms) { for (const transform of transforms) {
@ -16,24 +16,24 @@ export default (config: Function, useStorage) => {
} }
} }
html = await useFragments(html.toString(), useStorage); html = await useFragments(html.toString(), storage);
if (!!componentType && !!html) { if (!!componentType && !!html) {
html = await insertRegistry(html.toString(), componentType, useStorage); html = await insertRegistry(html.toString(), componentType, storage);
} }
return html ?? new Response("Not found", { status: 404 }); return html ?? new Response("Not found", { status: 404 });
}); });
}; };
const getHtml = async (path: string, useStorage) => { const getHtml = async (path: string, storage) => {
const rawPath = path[path.length - 1] === "/" ? path.slice(0, -1) : path; const rawPath = path[path.length - 1] === "/" ? path.slice(0, -1) : path;
const filename = rawPath === "" ? "/index.html" : `${rawPath}.html`; const filename = rawPath === "" ? "/index.html" : `${rawPath}.html`;
const fallback = getPath(rawPath + "/index.html"); const fallback = getPath(rawPath + "/index.html");
const filePath = getPath(filename); const filePath = getPath(filename);
let html = await useStorage().getItem(filePath); let html = await storage.getItem(filePath);
if (!html) html = await useStorage().getItem(fallback); if (!html) html = await storage.getItem(fallback);
if (!html) html = await useStorage().getItem(getPath("/404.html")); if (!html) html = await storage.getItem(getPath("/404.html"));
return html; return html;
}; };
@ -45,10 +45,10 @@ function getPath(filename: string) {
async function insertRegistry( async function insertRegistry(
html: string, html: string,
type: "js" | "ts", type: "js" | "ts",
useStorage storage: NitroStorage
): Promise<string> { ): Promise<string> {
const ast = parse(html); const ast = parse(html);
const componentFiles = await getFiles(type, useStorage); const componentFiles = await getFiles(type, storage);
const availableComponents = componentFiles.map((key) => const availableComponents = componentFiles.map((key) =>
key.replace(`.${type}`, "") key.replace(`.${type}`, "")
); );
@ -68,7 +68,7 @@ async function insertRegistry(
const registryScript = await buildRegistry( const registryScript = await buildRegistry(
usedCustomElements, usedCustomElements,
type, type,
useStorage storage
); );
walkSync(ast, (node) => { walkSync(ast, (node) => {
if (node.type === ELEMENT_NODE && node.name === "head") { if (node.type === ELEMENT_NODE && node.name === "head") {
@ -83,16 +83,14 @@ async function insertRegistry(
async function buildRegistry( async function buildRegistry(
usedCustomElements: string[], usedCustomElements: string[],
type: "js" | "ts", type: "js" | "ts",
useStorage storage: NitroStorage
) { ) {
let registryScript = `<script type='module'>`; let registryScript = `<script type='module'>`;
let isBaseClassImported = false; let isBaseClassImported = false;
let classesImported = []; let classesImported = [];
for (const name of usedCustomElements) { for (const name of usedCustomElements) {
const content = await useStorage().getItem( const content = await storage.getItem(`assets:components:${name}.${type}`);
`assets:components:${name}.${type}`
);
const evalStore = eval( const evalStore = eval(
`class WebComponent {}; class HTMLElement {}; (${content.toString()})` `class WebComponent {}; class HTMLElement {}; (${content.toString()})`
); );
@ -216,8 +214,8 @@ function removeComments(script: string) {
return script; return script;
} }
async function useFragments(html: string, useStorage) { async function useFragments(html: string, storage: NitroStorage) {
const fragmentFiles = await getFiles("html", useStorage); const fragmentFiles = await getFiles("html", storage);
const availableFragments = fragmentFiles.reduce((acc, key) => { const availableFragments = fragmentFiles.reduce((acc, key) => {
return { return {
@ -228,7 +226,7 @@ async function useFragments(html: string, useStorage) {
const ast = parse(html); const ast = parse(html);
for (const key in availableFragments) { for (const key in availableFragments) {
let text: string = await useStorage().getItem( let text: string = await storage.getItem(
"assets:components:" + key + ".html" "assets:components:" + key + ".html"
); );
availableFragments[key] = text.replace(/\n/g, "").replace(/\s+/g, " "); availableFragments[key] = text.replace(/\n/g, "").replace(/\s+/g, " ");
@ -261,8 +259,8 @@ function replaceSlots(fragmentNode, node) {
}); });
} }
async function getFiles(type: string, useStorage) { async function getFiles(type: string, storage: NitroStorage) {
return (await useStorage().getKeys("assets:components")) return (await storage.getKeys("assets:components"))
.map((key) => key.replace("assets:components:", "")) .map((key) => key.replace("assets:components:", ""))
.filter((key) => key.includes(type)); .filter((key) => key.includes(type));
} }

View file

@ -1,6 +1,6 @@
{ {
"name": "@mcflyjs/core", "name": "@mcflyjs/core",
"version": "0.0.1", "version": "0.1.0",
"description": "McFly core package", "description": "McFly core package",
"main": "index.ts", "main": "index.ts",
"files": [ "files": [
@ -25,6 +25,7 @@
"esprima": "^4.0.1", "esprima": "^4.0.1",
"h3": "^1.8.2", "h3": "^1.8.2",
"nitropack": "^2.7.0", "nitropack": "^2.7.0",
"ultrahtml": "^1.5.2" "ultrahtml": "^1.5.2",
"unstorage": "^1.9.0"
} }
} }

View file

@ -9,9 +9,9 @@
}, },
"dependencies": { "dependencies": {
"@mcflyjs/config": "^0.0.1", "@mcflyjs/config": "^0.0.1",
"@mcflyjs/core": "^0.0.1", "@mcflyjs/core": "^0.1.0",
"nitropack": "latest",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"nitropack": "latest",
"ultrahtml": "^1.5.2" "ultrahtml": "^1.5.2"
}, },
"name": "@templates/basic", "name": "@templates/basic",

View file

@ -1,262 +1,6 @@
/** /**
* McFly SSR logic * McFly SSR logic
*/ */
import McFly from "@mcflyjs/core";
import { ELEMENT_NODE, parse, render, renderSync, walkSync } from "ultrahtml";
import { parseScript } from "esprima";
import config from "../mcfly.config"; import config from "../mcfly.config";
export default McFly(config, useStorage());
const { components: componentType } = config();
export default eventHandler(async (event) => {
const { path } = event;
let html = await getHtml(path);
// transforms
const transforms = [doSetUp, deleteServerScripts];
if (html) {
for (const transform of transforms) {
html = transform(html.toString());
}
}
html = await useFragments(html.toString());
if (!!componentType && !!html) {
html = await insertRegistry(html.toString(), componentType);
}
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}`;
}
async function insertRegistry(
html: string,
type: "js" | "ts"
): Promise<string> {
const ast = parse(html);
const componentFiles = await getFiles(type);
const availableComponents = componentFiles.map((key) =>
key.replace(`.${type}`, "")
);
const usedCustomElements = [];
walkSync(ast, (node) => {
const usedElement = availableComponents.find((name) => name === node.name);
if (node.type === ELEMENT_NODE && !!usedElement) {
usedCustomElements.push(usedElement);
}
});
// insert registry script to head
if (usedCustomElements.length > 0) {
const registryScript = await buildRegistry(usedCustomElements, type);
walkSync(ast, (node) => {
if (node.type === ELEMENT_NODE && node.name === "head") {
node.children.push(parse(registryScript));
}
});
}
return render(ast);
}
async function buildRegistry(usedCustomElements: string[], type: "js" | "ts") {
let registryScript = `<script type='module'>`;
let isBaseClassImported = false;
let classesImported = [];
for (const name of usedCustomElements) {
const content = await useStorage().getItem(
`assets:components:${name}.${type}`
);
const evalStore = eval(
`class WebComponent {}; class HTMLElement {}; (${content.toString()})`
);
const className = new evalStore().constructor.name;
if (!classesImported.includes(className)) {
if (
!isBaseClassImported &&
content.toString().includes("extends WebComponent")
) {
const baseClassImport = `import { WebComponent } from "https://unpkg.com/web-component-base@1.9.1/WebComponent.mjs";`;
registryScript += baseClassImport;
isBaseClassImported = true;
}
registryScript += content;
registryScript += `customElements.define("${name}", ${className});`;
classesImported.push(className);
}
}
registryScript += "</script>";
return registryScript;
}
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);
const script = cleanScript(scripts);
serverScripts.push(script);
}
});
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 = `(function(){}.constructor)(\`${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);
}
function cleanScript(scripts: string[]): string {
let script = scripts.map((s) => s.trim()).join(" ");
script = removeComments(script);
return script.replace(/\n/g, "").replace(/\s+/g, " ");
}
function isComment(node) {
return (
node.type === "Line" ||
node.type === "Block" ||
node.type === "BlockComment" ||
node.type === "LineComment"
);
}
function removeComments(script: string) {
const entries = [];
parseScript(script, { comment: true }, function (node, meta) {
if (isComment(node)) {
entries.push({
start: meta.start.offset,
end: meta.end.offset,
});
}
});
entries
.sort((a, b) => {
return b.end - a.end;
})
.forEach((n) => {
script = script.slice(0, n.start) + script.slice(n.end);
});
return script;
}
async function useFragments(html: string) {
const fragmentFiles = await getFiles("html");
const availableFragments = fragmentFiles.reduce((acc, key) => {
return {
...acc,
[key.replace(".html", "")]: "",
};
}, {});
const ast = parse(html);
for (const key in availableFragments) {
let text: string = await useStorage().getItem(
"assets:components:" + key + ".html"
);
availableFragments[key] = text.replace(/\n/g, "").replace(/\s+/g, " ");
}
walkSync(ast, (node) => {
const selector = Object.keys(availableFragments).find(
(name) => name === node.name
);
if (node.type === ELEMENT_NODE && !!selector) {
const index = node.parent.children.indexOf(node);
const fragmentNode = parse(availableFragments[selector]);
replaceSlots(fragmentNode, node);
node.parent.children[index] = fragmentNode;
}
});
return render(ast);
}
function replaceSlots(fragmentNode, node) {
walkSync(fragmentNode, (n) => {
if (n.type === ELEMENT_NODE && n.name === "slot") {
const index = n.parent.children.indexOf(n);
n.parent.children.splice(index, 1, ...node.children);
}
});
}
async function getFiles(type: string) {
return (await useStorage().getKeys("assets:components"))
.map((key) => key.replace("assets:components:", ""))
.filter((key) => key.includes(type));
}