HTML Over The Wire On The Bun
Squirt is a (🚧 pre-alpha 🚧) do-everything SSR/HOTW/AHAH server and framework built on Bun.
- Civet support
- Next/Astro-style filesystem routing
- Live reload
- Hyperscript-style HTML/CSS (all elements, properties, and at-rules are available on
globalThis😆) - SOON: tiny client-side runtime for declarative interactivity
bun add @squirt/markup @squirt/serverIf you are using TypeScript and/or Civet, configure your tsconfig.json:
{
"compilerOptions": {
"types": [
"bun-types",
"@squirt/markup",
"@squirt/server"
]
}
}bun run squirt
All routes are contained in the src/routes directory of your project.
Files with a leading dot . or underscore _ are ignored.
Files named index.* matching route patterns will act as default routes for the directory.
All routes are modules. Named exports matching HTTP methods (in lowercase) will route those methods (except for export del which will match DELETE requests.)
Pages and stylesheets can export default to match GET, while API routes with export default will match all methods. All WebSocket routes should export default, as GET is the only valid method to upgrade the connection.
The export can be an object or a function. If it is a function, it will be called and passed a Context object, which contains the request, route, and url properties. If the route is dynamic, a second object will be passed containing the route parameters and values.
Use src/public for static files.
*.html.ts*.html.civet*.html.js
Page routes return HTML, typically created with the @squirt/markup builder DSL. Simple example:
// index.html.ts
export default ({ url }) => [
doctype.html5,
html(
head(
title("hello world!"),
liveReload(development),
),
body(
div.hello(
`hello from ${url.pathname}!`,
color("lime")
),
),
),
]Page routes can also directly return a Response to override rendering.
*.css.ts*.css.civet*.css.js
Stylesheet routes return CSS, also typically created with the @squirt/markup builder DSL. Unlike other routes, the .css extension is matched in the URL.
// theme.css.ts
export default [
rule("body",
backgroundColor("black"),
color("silver"),
)
]*.api.ts*.api.civet*.api.js
API routes should return a Response object.
// echo.api.ts
export function get({ url }: Context) {
return new Response(url.search.substring(1))
}*.socket.ts*.socket.civet*.socket.js
Socket routes should return an object with methods matching the Bun WebSocket event handlers:
// upper.socket.ts
export default <SocketHandler>{
open(ws) {
console.log("server socket connected")
},
message(ws, message) {
ws.sendText(message.toString().toUpperCase())
},
}Routes can be parameterized with square-braced file or directory names (such as [foo].api.ts or [user]/index.html.ts)
Rest-style dynamic routes work, as well: [...myParam].html.ts
The liveReload() function can be included in a page - this will embed a <script> tag which reloads the page when the source changes. Currently this reloads any page when any source changes. You can pass an optional boolean to enable/disable this setting.
root: absolute path to the project's root directory.production: true in productiondevelopment: true in developmentredirect(location: string, temporary: boolean = false): returns a 302 redirectResponse, or 307 iftemporaryistrue.
Squirt is crazy with globals, so it provides the ability to define your own. Project-specific globals can be defined in files matching these patterns:
*.global.ts*.global.civet*.global.js
These will work with Live Reload. You can use the default export with an object containing keys, or use named exports. Example:
// db.global.ts
import _db from "./db"
declare global {
const db: typeof _db
}
export default {
db: _db
}Files matching these patterns are Context extensions:
*.context.ts*.context.civet*.context.js
These are run on each request to augment the Context object with your own values. They should export a default function which returns an object containing additional keys/values. Example:
// session.global.ts
declare global {
// Augmenting the global Context with a possible session field
interface Context {
session?: MySessionType
}
// Creating a Context type for when session is known to be present
interface SessionContext extends Context {
session: MySessionType
}
}
export default ({ request }: Context) => {
const session = getMySession(request)
return { session }
}Here is an example adding a utility for marking hyperlinks matching the current page with a CSS class:
// utility.context.ts
declare global {
interface Context {
href(href: string): any
}
}
export default ({ url }: Context) => ({
href(href: string) {
return [{ href }, url.pathname === href && { class: "current" }]
}
})Which can be then used in a elements:
a("Profile", context.href("/profile")),
a("Inbox", context.href("/inbox")),Layouts and partial views can be simply be defined as functions and imported.
// site.layout.ts
export default (_title: any, _content: any) => [
doctype.html5,
html(
head(
meta({ charset: "UTF-8" }),
meta({ name: "viewport", content: "width=device-width, initial-scale=1.0" }),
title(_title),
liveReload(development),
),
body(_content),
)
]All HTML element and CSS property/at-rule names are defined globally as functions which create virtual DOM nodes. At render time, these are converted to HTML and CSS.
TypeScript/JavaScript example:
div(
span("Password: "),
input({ type: "password" })
style(
rule.someClass(
color.red,
),
),
)Civet example:
div [
span "Password: "
input { type: "password" }
style [
rule ".some-class",
color.red
]
]Strings, numbers, arrays, etc. are supported as children. null, undefined, and false will render as empty. Anything unrecognized will be converted with .toString(). Attributes are defined with plain {} objects. Multiple objects can be defined for conveninent composition, and these can appear after child elements, text nodes, etc.
a.someClass("My Link", { href: "/my_link" }, { class: "another-class" })The raw function will skip HTML escaping for its contents:
raw("<span>hello!</span>")The var element is named _var due to conflict with the JS keyword.
You can apply classes directly to element functions:
div(
div.redBold("this is bold and red!"),
div.redBold.alsoItalic("this has two classes!")
)Class names are automatically converted to kebab-case.
All standard and known vendor-specific CSS properties are global functions:
color("#ff0000"),
border("solid 1px red"),
webkitBorderImageWidth("4px"),
_continue("auto"), // conflicts with JS keywords are prefixed with underscoreStandard values are also available as properties on these functions:
color.red,
borderStyle.dashed,
_continue.auto,The rule function can be used within style elements to define CSS rules. Custom properties may use the prop function.
style(
rule(".red-bold",
color.red,
fontWeight.bold,
prop("-some-nonstandard", "value"),
)
)Element functions may be used as selectors:
rule(textarea,
borderColor.black,
)Class names may be used as selectors (these are converted to kebab-case):
rule.container(
width("1200px"),
)You can add CSS properties directly to elements:
div(
color.red,
fontWeight.bold,
"this is bold and red!",
)Rules may be nested:
rule(".danger",
color.red,
rule(".icon",
float.right,
),
)Child selectors can be combined with the parent selector, similar to Sass and Less.js. This example produces two rules, the second with the selector .danger.large:
rule(".danger",
color.red,
rule("&.large",
fontSize("40px")
),
)Nested selectors with pseudo-classes do the same:
rule(a,
color.red,
textDecorationLine.none,
rule(":hover",
textDecorationLine.underline,
),
)Squirt detects multiple selectors in a rule and will generate the necessary CSS:
rule("input, textarea",
border("solid 1px gray"),
rule(":hover, :focus",
borderColor.black,
),
)Media queries and other at-rules are supported
with the $ prefix:
$media("(prefers-color-scheme: dark)",
rule(":root",
prop("--fg", "white"),
prop("--bg", "black"),
),
)$layer(
rule("p",
color.red,
)
)