Do you like both bun and nuxt? I do, however, nuxt doesn't work on bun. So I've decided to make my own nuxt-like framework. It won't be the same as nuxt but it will feel similar to nuxt. It won't be near far advanced as nuxt but it will be enough for basic applications, not to mention you can extend it to your liking.
You like Svelte? Follow the blog as well! Rewriting vue to svelte is really simple, instead of
vue
, usesvelte
. It should work the same!
Let's get started!
I am not that good at making names so we will call it bux
.
Before we move on, we should talk about dependencies.
Nuxt has around 600 dependencies. Yikes 😬
We are not going to follow that principle! First, we will need a backend server framework like express.js
. However, express is very slow so elysia
comes to the rescue! Okay, we've got our backend server, now we will need a frontend build tool that works natively on bun. buchta
seems to be the only one for bun so we will go with that.
It is true that Buchta
has official Elysia integration, however, we will modify it to meet our needs.
And last but not least vue
.
Time to code
First, we will create an empty project
mkdir bux && cd bux
bun init
Now, we will install needed dependencies
bun i elysia buchta vue
After dependencies are installed, open up your favorite text editor.
Frontend
What should we do next? First, we should look at nuxt's directory structure
The first directory we will look at is pages
. Let's create a page at /
that will display Hello, World!
. Create a pages
directory and file index.vue
in the directory.
And we will write a basic vue template.
<script setup>
// I need to be here
</script>
<template>
<div>
<h1>Hello, World!</h1>
</div>
</template>
Why also script? I haven't made a check whether it is there or not.
Now, we want to serve it to the user, so open index.ts
and create a basic Elysia
server
import Elysia from "elysia";
const app = new Elysia();
app.listen(3000);
Create file plugin.ts
that will export basic function bux
, like this
import Elysia from "elysia";
export function bux(app: Elysia) {
return app;
}
This is very basic Elysia
plugin which we will plug into Elysia
import Elysia from "elysia";
+ import { bux } from "./plugin";
// ...
- app.listen(3000);
+ app.use(bux);
Why we would want to remove app.listen
? We will call it after everything is done because Elysia
won't register new routes after it has been executed.
Now, we will create a config file bux.config.ts
similar to nuxt.
export default {
port: 3000,
ssr: true,
}
Let's load the config in our plugin using require
+ const config = require(process.cwd() + "/bux.config.ts").default;
return app;
If you want, you can create a type for your config
Before we can continue, we need to patch Buchta
first, I've forgotten to assign a part from Buchta's config.
Which would be
sed -i "s/this.builder.prepare();/this.builder.prepare(this.config?.dirs ?? ['public']);/" node_modules/buchta/src/buchta.ts
Now that Buchta
is patched, we can create a basic setup
Under the config
variable, add
if (existsSync(process.cwd() + "/.buchta/"))
rmSync(process.cwd() + "/.buchta/", { recursive: true });
// this will prevent piling up files from previous builds
const buchta = new Buchta(false, {
port: config.port,
ssr: config.ssr,
rootDir: process.cwd(),
dirs: ["pages"],
plugins: []
});
buchta.earlyHook = earlyHook;
buchta.setup().then(() => {
});
And outside of the plugin function, add
const extraRoutes = new Map<string, Function>();
// This is a hook for files that doesn't have a plugin like pngs
const earlyHook = (build: Buchta) => {
build.on("fileLoad", (data) => {
data.route = "/" + basename(data.path);
const func = async (_: any) => {
return Bun.file(data.path);
}
extraRoutes.set(data.route, func);
})
}
// This function will fix route so elysia won't behave abnormally
const fixRoute = (route: string, append = true) => {
if (!route.endsWith("/") && append) {
route += "/";
}
const matches = route.match(/\[.+?(?=\])./g);
if (matches) {
for (const match of matches) {
route = route.replace(match, match.replace("[", ":").replace("]", ""));
}
}
return route;
}
Now we will add the vue
function call into the plugins
of Buchta's config
Import the vue function from "buchta/plugins/vue"
We've got the build system ready, now we need to serve the build to Elysia
. To do that, we need to fight a little bit against typescript because I forgot to make a getter. In the then
function call add
for (const [route, func] of extraRoutes) {
// @ts-ignore ssh
app.get(route, func);
}
// @ts-ignore I forgot
for (const route of buchta.pages) {
if (route.func) {
app.get(fixRoute(dirname(route.route)), async (_: any) => {
return new Response(await route.func(dirname(route.route), fixRoute(dirname(route.route))),
{ headers: { "Content-Type": "text/html" } });
});
} else {
if (!config.ssr && "html" in route) {
app.get(fixRoute(dirname(route.route)), (_: any) => {
return new Response(route.html, { headers: { "Content-Type": "text/html" } });
});
}
if (!("html" in route)) {
app.get(route.route, () => Bun.file(route.path));
app.get(route.originalRoute, () => Bun.file(route.path));
}
}
}
app.listen(config.port);
Explanation
if (route.func) {
app.get(fixRoute(dirname(route.route)), async (_: any) => {
return new Response(await route.func(dirname(route.route), fixRoute(dirname(route.route))),
{ headers: { "Content-Type": "text/html" } });
});
}
Will execute function
route.func
that will server render the page and return server-rendered HTML.
if (!config.ssr && "html" in route) {
app.get(fixRoute(dirname(route.route)), (_: any) => {
return new Response(route.html, { headers: { "Content-Type": "text/html" } });
});
}
If SSR is disabled, it will send the default CSR html template
if (!("html" in route)) {
app.get(route.route, () => Bun.file(route.path));
app.get(route.originalRoute, () => Bun.file(route.path));
}
This will setup a route for everything else
Now, if you run
bun run index.ts
And open up your web browser at localhost:3000/
You should see
Awesome!
If you want for example route /:test/
Simply go into the pages
directory, and create directory [test]
and index.vue
inside.
This is the first difference to nuxt!
The next directories will simply be public
& assets
.
All you need is to create both directories and add them inside of the dirs
array of Buchta's config.
Into assets
put your own favicon.ico
for example and into public
for example a font from Google Fonts. I used the IBM Plex Sans
font.
So in my case, I just added
<style>
@import url("/IBMPlexSans-Regular.ttf");
body {
margin: 0;
padding: 0;
}
* {
font-family: 'IBM Plex Sans', sans-serif;
}
</style>
We of course would like to make our components, so make a components
directory, and add it to dirs
add counter.vue
in there
<script setup>
import { ref } from "vue";
const count = ref(0);
const increment = () => {
count.value++;
};
</script>
<template>
<div>
<h3>{{ count }}</h3>
<button @click="increment">Increment</button>
</div>
</template>
And open up pages/index.vue
add import
import Counter from "/counter.vue";
And add it under <h1>Hello, World!</h1>
<template>
<div>
<h1>Hello, World!</h1>
+ <Counter />
</div>
</template>
Stop and run the server again, and now when you open localhost:3000/
you should see the header with the component!
For the layouts
directory, do the same as you did with components. However, when you want to use it, you must import it first!
add main.vue
in there
<script setup>
// I am main layout
</script>
<template>
<div>
<slot></slot>
<h3>Footer</h3>
</div>
</template>
<style scoped>
h3 {
color: red;
}
</style>
Import it and replace div
with the import
<script setup>
import Counter from "/counter.vue";
+ import Main from "/main.vue";
</script>
<template>
- <div>
+ <Main>
<h1>Hello, World!</h1>
<Counter />
- </div>
+ </Main>
</template>
After all of that, now your page should look like this
Backend
The last directory will be server
, I'll make it not entirely nuxt-like but it will get its job done. Also, we will be merging middleware
here.
Don't add this directory into
dirs
Just like in nuxt, the route with a specific method is being created as routeName.METHOD.ts
where a list of METHOD
s can be found on elysia's docs
Create directory server
And add file hello.ts
import { Context } from "elysia";
export default (ctx: Context) => {
console.log(ctx.query);
return "Hello, world!";
}
// Very simple middleware (https://elysiajs.com/concept/middleware.html)
export const beforeHandle = (_: any) => {
console.log("Before handle");
}
Information about the Context
can be found here
Now that we have created a basic route, let's connect it to Elysia.
Open the plugin.ts
file and let's make the FS-based router
const fsRouter = (app: Elysia) => {
const files = getFiles(process.cwd() + "/server");
for (const file of files) {
const path = file.replace(process.cwd() + "/server", "");
const count = path.split(".").length - 1;
if (count == 1) {
const route = path.split(".")[0];
const mod = require(file);
const func = mod.default;
app.get(fixRoute("/api" + route, false), func, mod);
} else if (count == 2) {
const [route, method, _ext ] = path.split(".");
const mod = require(file);
const func = mod.default;
app[method](fixRoute("/api" + route, false), func, mod);
}
}
}
And call it in our plugin
buchta.earlyHook = earlyHook;
+ fsRouter(app);
You may get an error saying a function getFiles
is missing, just import it like so import { getFiles } from "buchta/src/utils/fs";
What does the router do? It imports your file, uses the default exported function as the route function and everything else will be sent to Elysia, such as the beforeHandle
function which will be executed before the route function.
How to handle param routes?
It is very simple, just like in nuxt, create file[hello].post.ts
which be registered as route/api/:hello
with methodPOST
. I opened Postman and sent a request on that route
Here you can find the code
import { Context } from "elysia";
export default (ctx: Context) => {
return {
headers: ctx.headers,
params: ctx.params
};
}
And as you can see
Elysia responded with json object containing headers and params!
Let's quickly test our /api/hello
And that worked too! When you look into the console. There should be something like
Before handle
{}
What about wildcards? Simply call the file
*.ts
.
What about every method?route.all.ts
You can learn more here
All is done!
Congratulations! You made it all the way through!
Now enjoy your nuxt-like full-stack framework!
You can find the source code on github
If you liked this experience give Elysia
, Buchta
repositories a ⭐
This part is for Vue plugins, svelte doesn't have plugins so you can skip this
Vue Plugins
Till buchta
v0.6 is out, the Vue plugin has a temporary solution on how to use Vue plugins. Currently we will focus on 3rd party vue components primevue
Let's get started
Install primevue
with bun
bun i primevue
and create file vue.config.ts
for example
import { App } from "buchta/plugins/vue";
import PrimeVue from "primevue/config";
import ToastService from "primevue/toastservice";
App.use(PrimeVue, { ripple: true });
App.use(ToastService);
App.clientUse("PrimeVue", "{ ripple: true }", "import PrimeVue from 'primevue/config';");
App.clientUse("ToastService", "undefined", "import ToastService from 'primevue/toastservice';");
This will setup both PrimeVue and ToastService it comes with
Import the file in bux.config.ts
+ import "./vue.config.ts";
Open pages/index.vue
and let's add primevue CSS and component imports
<script setup>
+ import "primevue/resources/primevue.min.css";
+ import "primevue/resources/themes/lara-light-indigo/theme.css";
// ...
+ import Button from 'primevue/button';
+ import Toast from 'primevue/toast';
+ import { useToast } from 'primevue/usetoast';
+ const toast = useToast();
+ const showSuccess = () => {
+ toast.add({ severity: 'success', summary: 'Success Message', detail: 'Message Content', life: 3000 });
+ };
</script>
// in template under <Counter />
+ <Toast />
+ <Button label="Click" @click="showSuccess" />
Hold on! Before you restart the server, open plugin.ts
and add the css()
plugin into plugins
. Import the function from buchta/plugins/css
And now when you save everything and restart the server and open localhost:3000/
and click the indigo button a notification should show up.
Congratulations! You have just set up primevue components!
Top comments (0)