Thomas Reggi's Profile Image

@thomasreggi 🌸

Example Meta Framework Where Client + Server Code Self-Split Within the Same File

October 19, 2022 (Syndicated From dev.to)

The below is the beginings of a meta framework that would allow client side code to be rendered via server code by being taking apart from the AST running on the same file.

// meow
import { parse, print } from "https://deno.land/x/swc@0.2.1/mod.ts";
import type { Program, Statement } from "https://esm.sh/@swc/core@1.2.212/types.d.ts";
export class Self {
ast: Program
#renderRoutes: {
[key:string]: any
} = {}
#apiRoutes: {
[key:string]: any
} = {}
#importRoutes: {
[key:string]: any
} = {}
#calledMembers: {
imports: {[key: string]: Statement[]}
islands: {[key: string]: Statement[]}
}
#url?: string
#URL?: URL
#request?: Request
#main?: (props: any) => string
constructor(
public ref: string,
public render: any,
) {
const decoder = new TextDecoder("utf-8")
const data = Deno.readFileSync(ref.replace('file://', ''))
const code = decoder.decode(data)
this.ast = parse(code, {
tsx: true,
target: "es2019",
syntax: "typescript",
comments: false,
});
this.#calledMembers = this.getCalledMembers()
}
set url (url: string) {
this.#url = url
this.#URL = new URL(url)
}
set request (req: Request) {
this.#request = req
this.url = req.url
}
getCalledMembers () {
const imports: {[key: string]: Statement[]} = {}
const islands: {[key: string]: Statement[]} = {}
this.ast.body.forEach((node) => {
if (node.type === 'VariableDeclaration') {
node.declarations.forEach((declaration) => {
if (declaration.type === "VariableDeclarator") {
if (declaration.init?.type == "CallExpression") {
if (declaration.init.callee.type === "MemberExpression") {
if (declaration.init.callee.object.type === "Identifier" && declaration.init.callee.object.value === 'self') {
const name = declaration.init.arguments[0].expression.type === "StringLiteral" && declaration.init.arguments[0].expression.value
if (!name) throw new Error('Invalid name')
const fnValue = declaration.init.arguments[1].expression.type === "FunctionExpression" && declaration.init.arguments[1].expression.body.stmts
const arrowValue = declaration.init.arguments[1].expression.type === "ArrowFunctionExpression" && declaration.init.arguments[1].expression.body.type === 'BlockStatement' && declaration.init.arguments[1].expression.body.stmts
const value = fnValue || arrowValue
if (!value) throw new Error('Invalid value')
if (declaration.init.callee.property.type === "Identifier" && declaration.init.callee.property.value === 'import') {
imports[name] = value
} else if (declaration.init.callee.property.type === "Identifier" && declaration.init.callee.property.value === 'island') {
islands[name] = value
}
}
}
}
}
})
}
})
return { islands, imports }
}
import (name: string, values: () => void) {
const string = print({
type: "Module",
span: this.ast.span,
interpreter: this.ast.interpreter,
body: this.#calledMembers.imports[name]
})
this.#importRoutes[name] = string
return {
name: name,
}
}
island (name: string, values: () => void) {
return {
name: name,
string: print({
type: "Module",
span: this.ast.span,
interpreter: this.ast.interpreter,
body: this.#calledMembers.islands[name]
}),
viaWebComponent: () => '',
}
}
serverEndpoint (name: string, value: (req?: Request) => Response) {
this.#apiRoutes[name] = value
return {
fetch: () => {},
clientFetch: () => {},
fetchWC: () => {}
}
}
serverRoute (name: string, a: any) {
this.#renderRoutes[name] = a
return {
href: name,
Component: a
}
}
main (value: (props: any) => string) {
this.#main = value
}
server (req: Request): Response {
this.request = req
const path = this.#URL?.pathname
const renderRoute = path && this.#renderRoutes[path]
if (path && renderRoute) {
const main = this.render(renderRoute())
const string = this.#main ? this.#main({ main }) : main
return new Response(string, { status: 200, headers: { "content-type": "text/html; charset=utf-8" } });
}
const apiRoute = path && this.#apiRoutes[`/api/${path}`]
if (path && apiRoute) {
return apiRoute(req)
}
const importRoute = path && this.#importRoutes[`/import/${path}.js`]
if (path && importRoute) {
return new Response(importRoute, { status: 200, headers: { "content-type": "text/javascript; charset=utf-8" } });
}
return new Response('not found', { status: 404, headers: { "content-type": "text/html; charset=utf-8" } });
}
}
view raw self.ts hosted with ❤ by GitHub
/** @jsx h */
import { Self } from "./self.ts"
import { Fragment, h } from "https://esm.sh/preact@10.10.0";
import { useState } from 'https://esm.sh/preact@10.10.0/hooks';
import { renderToString } from "https://esm.sh/v92/preact-render-to-string@5.2.0";
import { serve } from "https://deno.land/std@0.159.0/http/server.ts";
const self = new Self(import.meta.url, renderToString)
const helloClient = self.import('hello', () => {
console.log('hello on client')
})
const MyIsland = self.island('MyIsland', () => {
function MyIsland() {
const [count, setCount] = useState(0);
return (
<div>
Counter is at {count}.{" "}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
})
const helloServer = self.serverEndpoint('hello', () => {
console.log('hello on server')
return Response.json({ hello: 'true' })
})
const Home = self.serverRoute('/', () => {
return (
<Fragment>
<div>home</div>
<Hello.Component />
</Fragment>
)
})
const Hello = self.serverRoute('/hello', () => {
return (
<Fragment>
<a href={Goodbye.href}>GoodBye</a>
{MyIsland.viaWebComponent()}
</Fragment>
)
})
const Goodbye = self.serverRoute('/goodbye', () => {
return (
<a href={Hello.href}>Hello</a>
)
})
self.main(({main}) => `
<!DOCTYPE html>
<html>
<body>
${main}
</body>
</html>
`)
await serve((request: Request): Response => {
return self.server(request)
}, { port: 8080 });
view raw usage.tsx hosted with ❤ by GitHub