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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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" } }); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** @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 }); |