Reface is a embeddable fullstack framework designed for creating Hypermedia-Driven Applications without a build step, based on HTMX.
MIT License
This package is under development and will be frequently updated. The author would appreciate any help, advice, and pull requests! Thank you for your understanding 😊
Reface is a embeddable fullstack framework designed for creating Hypermedia-Driven Applications without a build step, based on HTMX.
In this tool, I strive to combine the power of SSR, SPA, and islands-based architecture while using plain HTML, CSS, and JS. I came up with the idea for this tool as a result of trying to optimize the development of TWA (Telegram Web Apps) using Deno and Deno Deploy. Traditional approaches that push for building and maintaining complex data APIs to support SPAs in React or Vue seem excessively cumbersome, complex, and costly to me.
Currently, Reface is based on Hono, but in the future, support for other libraries may be added.
I believe this approach will be useful if you don't need to separate the frontend and backend into distinct services connected by a data API. In my case, it is perfect for developing Telegram Web Apps for small bots that are monolithic. It can also perform well in developing desktop applications where such a separation is unnecessary. Additionally, since the library integrates well by nature, it can be used to implement web interfaces in tools that initially do not anticipate them.
deno run --allow-all https://raw.githubusercontent.com/vseplet/reface/main/examples/ex1.ts
deno run --allow-all https://raw.githubusercontent.com/vseplet/reface/main/examples/ex2.ts
deno run --allow-all --unstable-kv https://raw.githubusercontent.com/vseplet/reface/main/examples/ex3.ts
deno run --allow-all https://raw.githubusercontent.com/vseplet/reface/main/examples/ex4.ts
First, you need to import all necessary objects and functions. For simplicity, I'll do this directly from JSR:
import { Hono } from "jsr:@hono/[email protected]";
import {
clean,
component,
html,
island,
Reface,
RESPONSE,
} from "jsr:@vseplet/[email protected]";
Here is a simple example of a function that can "call" sh with a certain set of arguments, returning the stdout of the created process after its completion. You can read more about this at tutorials/subprocess.
const sh = async (command: string) => {
const process = new Deno.Command("sh", { args: ["-c", command] });
const { code, stdout, stderr } = await process.output();
return {
code,
out: new TextDecoder().decode(stdout),
err: new TextDecoder().decode(stderr),
};
};
In reface, there are components and islands. The former simply return a Template, while the latter additionally implement a set of API handlers. In the example below, I describe a component that will display text from the process output:
const OutputBlock = component<{
out: string;
err: string;
code: number;
// deno-fmt-ignore
}>((props) =>
html`
<div class="p-1 my-1">
${
props.code
? html`<pre class="text-danger">${props.err}</pre>`
: html`<pre>${props.out}</pre>`
}
</div>`
);
Handling command input is a bit more complex and requires an island. Essentially, it is the same component but with additional RPC descriptions:
const CommandInput = island<{}, { exec: { command: string } }>({
name: "CommandInput",
// deno-fmt-ignore
template:({ rpc }) => html`
<form
class="container p-3"
${rpc.hx.exec()}
hx-target="#output"
hx-swap="afterbegin">
<div class="row align-items-center">
<div class="col-auto">
<label for="command">Command:</label>
</div>
<div class="col">
<input class="form-control" type="text" name="command" />
</div>
<div class="col-auto">
<button class="btn btn-primary" type="submit">Run</button>
</div>
</div>
</form>
`,
rpc: {
exec: async ({ args }) => RESPONSE(OutputBlock(await sh(args.command))),
},
});
In reface, you can describe pages. Any page is essentially a component or an island:
const Entry = component(() =>
html`
<div class="container grid my-3">
<h1>Simple Web Terminal</h1>
<div class="row my-3">${CommandInput({})}</div>
<div class="row my-3">
<div class="container p-3" style="height: 500px; overflow-y: scroll">
<div id="output"></div>
</div>
</div>
`
);
And finally, running all of this. It's important to remember that reface is currently a wrapper around the Hono router:
const app = new Hono().route(
"/",
new Reface({ layout: clean({ htmx: true, jsonEnc: true, bootstrap: true }) })
.page("/", Entry)
.hono(),
);
Deno.serve(app.fetch);
Watch full source code and try:
deno run --allow-all https://raw.githubusercontent.com/vseplet/reface/main/examples/ex4.ts