Getting started

Silgi is a full-stack web framework for Bun. This page creates an app on the html library, runs it, and builds it.

Before the first release

The packages are not on npm yet, so bunx silgi init and the bun install of a starter do not resolve. Until they are published, clone the repository, link silgi and the adapter of your library, create the app with the cloned CLI, and link the packages into it. The starter lists silgi with a version range, and bun link --save keeps an existing range, so use bun add with link: specifiers: it rewrites both entries and installs. The name silgi on npm belongs to an unrelated package, not to this project, so never install it from the registry. The sample uses react; for another library link packages/adapter-<library>, pass --framework <library>, and add @silgi/<library>@link:@silgi/<library>. Skip the bunx command in the next section: the app already exists, and the packages are installed.

git clone https://github.com/productdevbook/cati.git
cd cati && bun install
(cd packages/silgi && bun link)
(cd packages/adapter-react && bun link)
cd ..
bun cati/packages/silgi/bin/silgi.ts init my-app --framework react
cd my-app
bun add silgi@link:silgi @silgi/react@link:@silgi/react

This section goes when the packages are published.

Create an app

silgi init writes a starter app into a directory. Run it with bunx, which fetches the silgi package. Pass the library with --framework: one of react, vue, preact, svelte, solid or html. The command refuses a directory that is not empty unless you add --force. With --force it overwrites the starter's files, including package.json, and replaces {{name}} only in the files it writes from the template; your other files are left alone. It refuses to write through a symbolic link.

bunx silgi init my-app --framework html
cd my-app
bun install

The starter has a package.json with the scripts dev, build, preview, typecheck (silgi typecheck: it writes the types, then runs tsc, vue-tsc or svelte-check from the app's own node_modules, so .vue and .svelte files are checked, with TypeScript 6 as the typescript of those two starters) and postinstall (silgi typegen), a tsconfig.json that references the four generated contexts (see Modules), a silgi.config.ts, and the app/ and server/ folders described in Directory structure.

Configure it

silgi.config.ts default-exports the config. The only key the starter sets is the library:

import { defineConfig } from "silgi";

export default defineConfig({ framework: "html" });

Run the dev server

silgi dev starts the dev server with hot reload on http://localhost:3000. Use --port and --host to change them.

bun run dev
  • When the default port is busy, the server takes the next free one, up to 10 ports on, and logs Port 3000 is in use, using 3001. A port you chose (--port, PORT or devServer.port) is never replaced: if it is busy the command exits with code 1 and prints Port <n> is in use; pick another with --port.
  • --host with no value listens on 0.0.0.0, as --host 0.0.0.0 does. The Local: line then shows localhost, and one Network: line per network interface follows it.
  • silgi --help lists the commands and silgi --version prints the version of silgi.
  • silgi dev restarts when silgi.config.*, bunfig.toml or a .env file changes. If the new config is broken, it logs the error and waits; the server starts again at the next save of one of those files.

To serve the dev server over HTTPS, set devServer.https to a key and a certificate. Each of key, cert and ca is PEM text or a path to a PEM file, resolved from the app root; passphrase unlocks an encrypted key. silgi dev then listens with TLS and prints a https:// address. Browsers warn about a self-signed certificate.

import { defineConfig } from "silgi";

export default defineConfig({
  framework: "react",
  devServer: {
    port: 3000,
    https: { key: "./certs/key.pem", cert: "./certs/cert.pem" },
  },
});

Write a page

Every file under app/pages/ is a page, and its path is its URL. With the html library a page is a function that returns a string:

import { useHead, useRoute } from "@silgi/html";

const escape = (value: unknown) => String(value).replace(/[&<>"']/g, (char) => `&#${char.charCodeAt(0)};`);

export default function Home(): string {
  const route = useRoute();
  useHead({ title: "Home" });
  return `<main>
  <h1>My app</h1>
  <p>Route: ${escape(route.path)}</p>
</main>`;
}

Add a handler

Server code is a handler: a typed function in server/ that pages call and, with a route, a URL too. The starter has one in server/hello.ts. A handler is reachable only when server/index.ts exports it, so the starter's server/index.ts holds export { hello } from "./hello.ts";.

import { defineHandler } from "silgi/server";

export const hello = defineHandler({
  route: "/api/hello",
  method: "GET",
  handler: () => ({ message: "Hello" }),
});

GET /api/hello answers {"message":"Hello"}. A page reads a handler by importing it from #server; Data shows the calls and Server the file that makes a handler reachable. A page that needs a value before it renders adds a load file next to it, here blog/[slug].load.ts for blog/[slug].ts:

import type { Load } from "silgi/app";

export const load = (({ params }) => ({ title: `Post ${params.slug}` })) satisfies Load;

Build and preview

silgi build writes the production build into .output/. The default bun build runs on Bun (bun .output/server/index.mjs); build with --preset node to run it with Node. silgi preview serves that build: it runs the server of a bun or node build and serves the files of a static build. For cloudflare, vercel and netlify it prints the platform's own command instead.

bun run build
bun run preview

Generate a static site

silgi generate builds with the static preset and prerenders every route it can reach by following links from /. The result is .output/public/, which any static host can serve. The starter has no generate script, so run it with bunx, which uses the app's own silgi.

bunx silgi generate

Commands

Every command takes the app's directory as its first argument, . by default. --debug prints the stack of a failing command and writes the build's virtual modules to .silgi/virtual/.

CommandUsage
initsilgi init [dir] --framework <id> [--force]
devsilgi dev [root] [--port <n>] [--host <name>] [--dotenv <file>] [--mode <name>]
buildsilgi build [root] [--preset <id>] [--dotenv <file>] [--mode <name>] [--analyze]
generatesilgi generate [root] [--preset <id>] [--dotenv <file>] [--mode <name>] [--analyze]
previewsilgi preview [root] [--port <n>] [--host <name>] [--dotenv <file>] [--mode <name>]
typegensilgi typegen [root] [--mode <name>]
infosilgi info [root]
  • --preset picks the deploy target; see Deploy.
  • --dotenv <file> loads environment variables from a file, resolved from the app root. It only fills variables that are not set yet, so it does not override .env, .env.<mode>, .env.local or the shell.
  • --mode <name> (letters, digits, - and _) sets import.meta.env.MODE and reads <root>/.env.<name> and then <root>/.env.<name>.local after Bun's own .env, .env.<NODE_ENV> and .env.local: they replace a value from those files and never a value set in the shell, and .env.<name>.local wins over .env.<name>. Without --mode, MODE is development for dev and production otherwise, and no mode file is read. NODE_ENV is not changed by --mode.
  • --analyze prints the largest modules of each client chunk after the build.
  • typegen writes .silgi/tsconfig.json and .silgi/types/ without building.
  • info prints the Silgi and Bun versions, the package manager (from the nearest lockfile), the OS, the library, the preset, the modules and the resolved directories.
  • A command that takes a root reads that root's .env, .env.<mode> and .env.local, wherever you run it from: silgi build apps/web run in a monorepo uses apps/web/.env. The mode is development for dev and production for build, generate and preview, so silgi dev reads .env.development and never .env.production. A variable that is already set, in the shell for example, wins over the files.
  • build and generate list the output files with their size and gzip size. Above 50 files, the files of public/ are summed per directory (public/blog/ (3000 files)); --debug lists every file.
  • A command that fails exits with code 1 and prints the message. A build error names the file, the line and the column.

Deploy targets other than a static host are in Deploy. Routing and Data are the next pages to read.