feat: initial packages @mcflyjs/core, @templates/basic
This commit is contained in:
parent
9726bb1e72
commit
d87a4260e3
18 changed files with 684 additions and 8 deletions
|
@ -1,4 +1,4 @@
|
||||||
import defineConfig from "./packages/define-config";
|
import defineConfig from "@mcflyjs/core/define-config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
components: "js",
|
components: "js",
|
||||||
|
|
28
package-lock.json
generated
28
package-lock.json
generated
|
@ -6,10 +6,12 @@
|
||||||
"": {
|
"": {
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/config",
|
"packages/config",
|
||||||
"packages/create-mcfly"
|
"packages/create-mcfly",
|
||||||
|
"templates/basic",
|
||||||
|
"packages/core"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcflyjs/config": "*",
|
"@mcflyjs/config": "./packages/config",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"nitropack": "latest",
|
"nitropack": "latest",
|
||||||
"ultrahtml": "^1.5.2"
|
"ultrahtml": "^1.5.2"
|
||||||
|
@ -441,6 +443,10 @@
|
||||||
"resolved": "packages/config",
|
"resolved": "packages/config",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@mcflyjs/core": {
|
||||||
|
"resolved": "packages/core",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@netlify/functions": {
|
"node_modules/@netlify/functions": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.3.0.tgz",
|
||||||
|
@ -990,6 +996,10 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@templates/basic": {
|
||||||
|
"resolved": "templates/basic",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@types/estree": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.3.tgz",
|
||||||
|
@ -3955,6 +3965,11 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
},
|
},
|
||||||
|
"packages/core": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"devDependencies": {}
|
||||||
|
},
|
||||||
"packages/create-mcfly": {
|
"packages/create-mcfly": {
|
||||||
"version": "0.0.4",
|
"version": "0.0.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -3962,6 +3977,15 @@
|
||||||
"create-mcfly": "index.js"
|
"create-mcfly": "index.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {}
|
"devDependencies": {}
|
||||||
|
},
|
||||||
|
"templates/basic": {
|
||||||
|
"name": "@templates/basic",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@mcflyjs/config": "^0.0.1",
|
||||||
|
"nitropack": "latest"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,13 +9,15 @@
|
||||||
"build:preview": "npm run build && npm run preview"
|
"build:preview": "npm run build && npm run preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mcflyjs/config": "*",
|
"@mcflyjs/config": "./packages/config",
|
||||||
"esprima": "^4.0.1",
|
"esprima": "^4.0.1",
|
||||||
"nitropack": "latest",
|
"nitropack": "latest",
|
||||||
"ultrahtml": "^1.5.2"
|
"ultrahtml": "^1.5.2"
|
||||||
},
|
},
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"packages/config",
|
"packages/config",
|
||||||
"packages/create-mcfly"
|
"packages/create-mcfly",
|
||||||
|
"templates/basic",
|
||||||
|
"packages/core"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
262
packages/[...index].ts
Normal file
262
packages/[...index].ts
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
/**
|
||||||
|
* McFly SSR logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ELEMENT_NODE, parse, render, renderSync, walkSync } from "ultrahtml";
|
||||||
|
import { parseScript } from "esprima";
|
||||||
|
import config from "../mcfly.config";
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
|
@ -1,6 +1,4 @@
|
||||||
import { NitroConfig } from "nitropack";
|
export default function McFly() {
|
||||||
|
|
||||||
export default function McFly(): NitroConfig {
|
|
||||||
return {
|
return {
|
||||||
devServer: {
|
devServer: {
|
||||||
watch: ["./src/pages", "./src/components"],
|
watch: ["./src/pages", "./src/components"],
|
||||||
|
|
24
packages/core/package.json
Normal file
24
packages/core/package.json
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "@mcflyjs/core",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "McFly core package",
|
||||||
|
"main": "index.js",
|
||||||
|
"devDependencies": {},
|
||||||
|
"files": [
|
||||||
|
"define-config.ts"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/ayoayco/McFly.git",
|
||||||
|
"directory": "packages/core"
|
||||||
|
},
|
||||||
|
"author": "Ayo Ayco",
|
||||||
|
"license": "MIT",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/ayoayco/McFly/issues"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/ayoayco/McFly#readme"
|
||||||
|
}
|
15
templates/basic/.editorconfig
Normal file
15
templates/basic/.editorconfig
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
end_of_line = lf
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
charset = utf-8
|
||||||
|
|
||||||
|
[*.js]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
[{package.json,*.yml,*.cjson}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
3
templates/basic/.eslintignore
Normal file
3
templates/basic/.eslintignore
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
dist
|
||||||
|
.output
|
||||||
|
node-modules
|
5
templates/basic/.eslintrc
Normal file
5
templates/basic/.eslintrc
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"extends": [
|
||||||
|
"@nuxtjs/eslint-config-typescript"
|
||||||
|
]
|
||||||
|
}
|
7
templates/basic/.gitignore
vendored
Normal file
7
templates/basic/.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules
|
||||||
|
*.log*
|
||||||
|
.nitro
|
||||||
|
.cache
|
||||||
|
.output
|
||||||
|
.env
|
||||||
|
dist
|
2
templates/basic/.npmrc
Normal file
2
templates/basic/.npmrc
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
shamefully-hoist=true
|
||||||
|
strict-peer-dependencies=false
|
42
templates/basic/README.md
Normal file
42
templates/basic/README.md
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
# Nitro Minimal Starter
|
||||||
|
|
||||||
|
Look at the [Nitro documentation](https://nitro.unjs.io/) to learn more.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Make sure to install the dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# yarn
|
||||||
|
yarn install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Development Server
|
||||||
|
|
||||||
|
Start the development server on <http://localhost:3000>
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Production
|
||||||
|
|
||||||
|
Build the application for production:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Locally preview production build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run preview
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out the [deployment documentation](https://nitro.unjs.io/deploy) for more information.
|
5
templates/basic/mcfly.config.ts
Normal file
5
templates/basic/mcfly.config.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import defineConfig from "./packages/define-config";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
components: "js",
|
||||||
|
});
|
3
templates/basic/nitro.config.ts
Normal file
3
templates/basic/nitro.config.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import McFly from "@mcflyjs/config";
|
||||||
|
|
||||||
|
export default defineNitroConfig({ ...McFly() });
|
19
templates/basic/package.json
Normal file
19
templates/basic/package.json
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"prepare": "nitropack prepare",
|
||||||
|
"dev": "nitropack dev",
|
||||||
|
"build": "nitropack build",
|
||||||
|
"preview": "node .output/server/index.mjs"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mcflyjs/config": "^0.0.1",
|
||||||
|
"nitropack": "latest"
|
||||||
|
},
|
||||||
|
"name": "@templates/basic",
|
||||||
|
"description": "Look at the [Nitro documentation](https://nitro.unjs.io/) to learn more.",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"main": "index.js",
|
||||||
|
"author": "Ayo Ayco",
|
||||||
|
"license": "MIT"
|
||||||
|
}
|
262
templates/basic/routes/[...index].ts
Normal file
262
templates/basic/routes/[...index].ts
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
/**
|
||||||
|
* McFly SSR logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ELEMENT_NODE, parse, render, renderSync, walkSync } from "ultrahtml";
|
||||||
|
import { parseScript } from "esprima";
|
||||||
|
import config from "../../../mcfly.config";
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
3
templates/basic/tsconfig.json
Normal file
3
templates/basic/tsconfig.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "./.nitro/types/tsconfig.json"
|
||||||
|
}
|
Loading…
Reference in a new issue