Skip to content

errilaz/squirt

Repository files navigation

Squirt

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

Usage

Install

bun add @squirt/markup @squirt/server

Types

If you are using TypeScript and/or Civet, configure your tsconfig.json:

{
  "compilerOptions": {
    "types": [
      "bun-types",
      "@squirt/markup",
      "@squirt/server"
    ]
  }
}

Use

bun run squirt

Routing

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.

Page Routes

  • *.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.

Stylesheet Routes

  • *.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 Routes

  • *.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))
}

WebSocket Routes

  • *.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())
  },
}

Dynamic Routes

Routes can be parameterized with square-braced file or directory names (such as [foo].api.ts or [user]/index.html.ts)

Wildcard Routes

Rest-style dynamic routes work, as well: [...myParam].html.ts

Live Reload

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.

Globals

  • root: absolute path to the project's root directory.
  • production: true in production
  • development: true in development
  • redirect(location: string, temporary: boolean = false): returns a 302 redirect Response, or 307 if temporary is true.

Extending Globals

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
}

Extending Context

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 & Partials

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),
  )
]

Markup

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.

Element Class Shorthand

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.

Styles

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 underscore

Standard values are also available as properties on these functions:

color.red,
borderStyle.dashed,
_continue.auto,

Rules

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"),
  )
)

Rule Shorthand Selectors

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"),
)

Inline Styles

You can add CSS properties directly to elements:

div(
  color.red,
  fontWeight.bold,
  "this is bold and red!",
)

Nested Rules

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,
  ),
)

At-rules

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,
  )
)

About

HTML Over The Wire On The Bun

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages