silgi

import { defineConfig } from "silgi" is for silgi.config.ts. The config guide is Configuration; this page lists the exports.

defineConfig

Returns the config unchanged. It exists so the editor checks the keys.

function defineConfig(config: SilgiConfig): SilgiConfig
import { defineConfig } from "silgi";

export default defineConfig({
  framework: "react",
  app: { baseURL: "/", head: { titleTemplate: "%s · Shop" } },
  runtimeConfig: { apiSecret: "", public: { apiBase: "/api" } },
  routeRules: { "/blog/**": { prerender: true } },
});

defineEnv

The default export of app/env.ts: returns the entries unchanged, typed. Each key is a variable name; the build reads the names, public and static from the file's text, so the object is written out as a literal.

function defineEnv<const T extends EnvSchema>(env: T): T

#silgi/env/private exports every name (server code only; client code that imports it fails the build), #silgi/env/public the public names that are not static, and a static name is read as import.meta.env.<NAME>, written into both bundles at build. Values are validated when the server starts (static ones when the build starts); a failure stops with Invalid environment variables: and one line per bad name, a private value reported only by its length.

// app/env.ts
import { defineEnv } from "silgi";

export default defineEnv({
  API_KEY: {},
  PORT: { schema: (raw) => Number(raw), default: "3000", description: "Port of the HTTP server" },
  GA_ID: { public: true },
  SHOW_DEBUG: { public: true, static: true, schema: (raw) => raw === "true", default: "false" },
});

EnvEntry, EnvSchema, EnvOutput

EnvEntry is one variable: { public?: boolean; static?: boolean; schema?: StandardSchemaV1 | ((raw: string | undefined) => unknown); default?: string; description?: string }. schema is a Standard Schema or a function that returns the value or throws; default replaces an unset value, not an empty one; static needs public. EnvSchema is Record<string, EnvEntry>. EnvOutput<E> is the type a variable reads as: the schema's output, the function's return type, else string.

SilgiConfig, SilgiConfigBlock, ResolvedConfig

SilgiConfigBlock is the type of an env override block ($development, $production, $test, $env.<mode>): a SilgiConfig without rootDir, extends, dev and the block keys.

SilgiConfig is what you write: every key is optional. ResolvedConfig is what Silgi reads after it merged your file, the layers in extends and the defaults: every key is set, paths are absolute, and layers is added. Modules and the build see the resolved one as silgi.options.

KeyType
rootDir, srcDir, serverDir, publicDir, buildDir, outputDirstring
framework"react" | "vue" | "preact" | "svelte" | "solid" | "html" | (string & {}); any other string is a third-party @silgi/<id> adapter
preset"bun" | "node" | "static" | "cloudflare" | "vercel" | "netlify" | (string & {}); any other string is a third-party preset
ssr, dev, componentsboolean
app{ baseURL?, buildAssetsDir?, head?, rootId?, trailingSlash?: "ignore" | "redirect", scripts?: boolean }; scripts: false ships no JavaScript and needs the html adapter
runtimeConfig{ public?: Record<string, unknown>; [key: string]: unknown }
appConfigRecord<string, unknown>
routeRulesRecord<string, RouteRule>
modulesa list of a path or package name, a defineModule result, a setup function, or [entry, options]
storageRecord<string, { driver: string; [option: string]: unknown }>
devStorageRecord<string, { driver: string; [option: string]: unknown }>; mounts that replace the storage mounts of the same name under silgi dev and while the build prerenders, after $development.storage is applied; the build still bundles the drivers of storage
server{ trustProxy?: boolean; maxBodySize?: number; maxRequestBodySize?: number; asyncContext?: boolean; openapi?: boolean | OpenAPIConfig; graphql?: boolean | GraphQLConfig }; maxBodySize (default 1 MiB) limits a request body read by a handler, readBody and readFormData; a handler's form.maxBodySize replaces it for that handler (Handlers); asyncContext (default true) runs every request inside the request context so useEvent() finds it, false leaves the context to tracing alone (useEvent)
prerender{ routes?, crawl?, ignore?, failOnError?, concurrency? }
build{ minify?, sourcemap?: "none" | "linked" | "external", define?, plugins?: BunPlugin[], external?, compressPublic?: boolean | { gzip?, brotli?, zstd? }, assetsInlineLimit?: number, importProtection?: { client?: { deny?: Array<string | RegExp> }; server?: { deny?: Array<string | RegExp> } }, keyedComposables?: KeyedComposable[], envPrefix?: string | string[] }; envPrefix (default ["SILGI_PUBLIC_"]) names the variables, read when the build starts, that become keys of import.meta.env in both bundles beside MODE, DEV, PROD, SSR and BASE_URL, and "" is refused; keyedComposables lists { name, source, argumentLength } entries the build gives a key, in addition to the adapter's useState (2), useAsyncData (3) and useLazyAsyncData (3) (KeyedComposable)
imports{ autoImport?: boolean; dirs?: string[] }
extendsstring[]
node{ requestTimeout?: number; headersTimeout?: number; maxConnections?: number }; limits of the server the node preset starts: requestTimeout ms a request may take to arrive (0 = no timeout, default 300000), headersTimeout ms for its headers (default 66000, or requestTimeout when that is smaller; not above requestTimeout), maxConnections a positive integer (default no cap)
vercel{ functions?: { maxDuration?: number; memory?: number; regions?: string[]; [key: string]: unknown } }; functions is merged into the function's .vc-config.json, where runtime, handler, launcherType, shouldAddHelpers and supportsResponseStreaming keep the preset's values; functions must be an object
netlify{ functions?: { region?: string; memory?: number | string; vcpu?: number; [key: string]: unknown } }; functions is merged into the function's config in server.mjs, where path and preferStatic keep the preset's values; functions must be an object
cloudflare{ env?: string, bindings?: Record<string, { id?: string }> }; env is the wrangler environment whose env.<name> block of wrangler.json(c) the cloudflare build copies into the top level of .output/wrangler.json; an unknown name fails the build, and it must be a non-empty string. bindings gives, by binding name, the id of the D1 database, R2 bucket (its name) or KV namespace that @silgi/db, @silgi/blob or a cloudflare-kv or cloudflare-r2 storage mount binds; env SILGI_CF_<NAME>_ID is the fallback, and the build writes the d1_databases, r2_buckets or kv_namespaces entry into .output/wrangler.json unless the app's wrangler config declares the binding. A value that is not an object of { id } objects with a non-empty string id fails the load (cloudflare.bindings.DB.id must be a non-empty string (got ""))
scheduledTasksRecord<string, TaskName | TaskName[]>: a cron expression, always in UTC, to the tasks it runs one after another, each with payload undefined and ctx.scheduledTime, ctx.cron set; five fields (minute hour day month weekday) with *, numbers, ranges, steps after * or a range, lists and the names JAN–DEC, SUN–SAT (0 and 7 are Sunday), or @yearly, @annually, @monthly, @weekly, @daily, @midnight, @hourly; when both day fields are restricted, a day matches when either does. A bad expression, six fields, a date that never comes (0 0 30 2 *) or an unknown task name fails the build naming it; the bun and node presets run schedules (Bun.cron and a timer), vercel writes Cron Jobs that call <baseURL>_silgi/cron (answered only with authorization: Bearer <CRON_SECRET>; 404 and one error log without CRON_SECRET, 401 with a wrong one) netlify writes one scheduled function per expression and cloudflare writes triggers.crons to .output/wrangler.json (weekday names, 0 9 * * 1 as 0 9 * * MON, since Cloudflare reads 1 as Sunday) and runs them from the worker's scheduled handler, any other preset fails the build, and on bun and node each process runs them, so several replicas run a task several times; a platform may also deliver one cron call twice, so write scheduled tasks to be idempotent
tasks{ http?: { token: { runtimeConfig: string } }, runOnStart?: TaskName[], queue?: { name: string, binding?: string } }. queue is where enqueue sends on cloudflare: name is the Cloudflare queue (a-z, 0-9, _ and -, at most 63 characters, else tasks.queue.name must be a queue name of a-z, 0-9, _ and - (got "Jobs")), binding its producer binding (default TASKS_QUEUE), which the build adds to .output/wrangler.json as queues.producers unless the app's wrangler config declares that binding (a declared producer sending to another queue fails the build: tasks.queue.name is "jobs", but the app's wrangler config sends the TASKS_QUEUE producer to queue "jobs-staging"; set tasks.queue.name to the same queue, or remove that producer), and { queue: name } to queues.consumers unless the app lists that queue: the worker's queue handler runs each message's task and acks the message when it resolved or retries it when it threw (Cloudflare's retries and dead-letter queue then apply). On vercel name is the Vercel Queues topic: enqueue publishes to it, and the build writes the private consumer function .vercel/output/functions/_silgi/vercel/queue.func with the trigger { type: "queue/v2beta", topic: name }, which runs each message's task and leaves a message whose task threw for Vercel to deliver again (Vercel Queues is a beta feature logs at build); another preset logs tasks.queue is used on the cloudflare and vercel presets only at build. http (off by default) serves POST <baseURL>_silgi/tasks/<name> in a build: a JSON body { payload? } runs the task and answers { result } (500 Task "x" returned a value JSON cannot hold for a Map, a bigint …); the request needs authorization: Bearer <token>, compared in constant time with the runtime config key the option names (SILGI_<KEY> overrides it), else 401 with www-authenticate: Bearer; an unknown task is 404 only after the token passed; a runtime value unset or under 32 characters answers 503 Task endpoint is not configured and logs once at error. A literal token fails the load: tasks.http.token must be { runtimeConfig: "<key>" }; a token in the config file ends up in the repository. runOnStart (default []) runs those tasks once, in order, with payload undefined, after the server plugins and before a bun or node server listens; under silgi dev once per start of the dev server, not on a hot rebuild; on static once in the build, before the prerender. A failure logs at error and reaches task:error, and the server keeps running; vercel, netlify and cloudflare fail the build (run the task with silgi task run --prod from the deploy step, or through tasks.http on cloudflare). An unknown task name fails the build. silgi task list [root] prints each task with its description and schedules; silgi task run <name> [--payload '<json>'] [--prod] [--port <n>] [root] runs one through a running silgi dev of the same root (GET/POST <baseURL>_silgi/tasks[/<name>], dev only), else in process with the dev config, or with --prod on Bun with the production config, and prints the result as one line of JSON
devServer{ port?, host?, https?: { key, cert, ca?, passphrase? }, allowedHosts?, prefetch?: boolean, tasks?: boolean } (tasks, default false: silgi dev runs scheduledTasks) (prefetch, default false: in silgi dev, visible prefetch is off and hover and focus prefetch load chunks only, because every load runs on a server that recompiles; true keeps hover and focus data prefetch as in a build; devServer.prefetch must be a boolean) (key, cert and ca are PEM text or a file path from the app root; with https, silgi dev serves over TLS)
aliasRecord<string, string>; the defaults are ~ and @ (the srcDir) and #shared. #server is not an alias: import … from "#server" reads the door of the root that holds the importing file, server/index.ts, and #server/<name> a layer's or a module's door, in a door file only; a path below it, such as #server/posts/q, resolves in neither the build nor tsc (see Handlers)
typescript{ strict?: boolean; tsConfig?: Record<string, unknown>; typedPaths?: boolean }; strict (default true) is compilerOptions.strict and tsConfig is merged over the compilerOptions of .silgi/tsconfig.json and the four context files (typescript.strict must be a boolean, typescript.tsConfig must be an object); typedPaths: false types Link to, navigate() and redirect() targets string
hooks{ [name in keyof SilgiHooks]?: SilgiHooks[name] }: the build hooks listed in Modules, one function per name; a name that is not one logs [silgi] Unknown hook "build:dne" (did you mean "build:done"?)

app.baseURL and app.buildAssetsDir get a leading and a trailing / ("docs" becomes "/docs/"); a baseURL with a scheme, a ? or a # is an error. The config is checked when it loads, and all problems are thrown together as Invalid config (silgi.config.ts): with one line each: booleans (ssr, components, build.minify, server.trustProxy, imports.autoImport, prerender.crawl, prerender.failOnError, devServer.prefetch, devServer.tasks), build.compressPublic (a boolean or an object with only the keys gzip, brotli and zstd, each a boolean), devServer.port (an integer from 0 to 65535, a string is not converted), build.assetsInlineLimit (a non-negative integer), build.importProtection.client.deny and build.importProtection.server.deny (arrays of strings and RegExps), server.maxBodySize, server.maxRequestBodySize and prerender.concurrency (positive integers), build.sourcemap, app.trailingSlash, prerender.routes and prerender.ignore, routeRules[...].redirect, routeRules[...].basicAuth, and a srcDir that does not exist. A top-level key that is neither a key above nor the configKey of a loaded module logs [silgi] Unknown config key "runtimeconfig" (did you mean "runtimeConfig"?) and the build goes on. A config that still sets build.target or experimental, which were removed, logs one line each and loads. A config file that fails to load is reported as Cannot load silgi.config.ts: with the file, line and column; a config that default-exports a function is an error, use defineConfig.

With build.sourcemap unset, a production build writes no client source maps and the server bundle is "linked"; set "external" or "linked" to publish the client maps.

A file that a stylesheet loads with url() (a font, an image) is a hashed file under app.buildAssetsDir when it is larger than build.assetsInlineLimit bytes (default 4096), so a change to the CSS does not make visitors download it again. A smaller file stays inline in the CSS as a data: URI, and 0 emits every file. A file that a script imports is always a hashed file.

import type { SilgiConfig, ResolvedConfig } from "silgi";

export const config = { framework: "vue", ssr: false } satisfies SilgiConfig;

export function describe(resolved: ResolvedConfig): string {
  return `${resolved.framework} on ${resolved.preset}, output in ${resolved.outputDir}`;
}

ConfigHead, BuildConfig, KeyedComposable, PrerenderConfig, StorageMount

The types of app.head, of build, of one entry of build.keyedComposables, of prerender and of one value of storage, for a helper or a wrapper that builds that part of the config. ConfigHead is HeadInput with titleTemplate as a string only, because the config is serialized to the client.

A call of the adapter's three composables gets, whatever its arguments (the run time drops it when the call gave a key), and a call of an entry you add gets, when it has fewer than argumentLength arguments, { [Symbol.for("silgi.key")]: "$s<8 hex>" } as its last argument, in the server build, the client build and silgi dev; the hex is the start of the SHA-256 of <file relative to rootDir, with />:<line>:<column> of the call. Only a call of a name imported from its source counts (@silgi/<framework> for the three defaults). A module adds an entry with silgi.options.build.keyedComposables.push({ name, source, argumentLength }) in its setup; an entry with the name and source of a default replaces it. See State and Data.

const keyed: KeyedComposable = { name: "useCart", source: "~/cart", argumentLength: 1 }
const prerender: PrerenderConfig = { routes: ["/about"], crawl: true }
const mount: StorageMount = { driver: "memory" }

SilgiHooks, TsConfigFile

The build hooks as an interface of name to function. hooks in the config is { [name in keyof SilgiHooks]?: SilgiHooks[name] }, so a wrong name or a wrong signature is a type error. silgi/kit exports the same type.

HookFunction
config:resolved(options: ResolvedConfig) => void | Promise<void>
modules:done() => void | Promise<void>
pages:extend(routes: RouteRecord[]) => void | Promise<void>
server:extend(serverRoutes: ServerRouteRecord[]) => void | Promise<void>
templates:extend(templates: Template[]) => void | Promise<void>
prepare:types(contexts: { app: TsConfigFile; server: TsConfigFile; shared: TsConfigFile; node: TsConfigFile }) => void | Promise<void>; TsConfigFile is { compilerOptions: Record<string, unknown>; include: string[]; exclude: string[] }, a context tsconfig before it is written, with paths relative to .silgi/
build:before() => void | Promise<void>
build:client:config, build:server:config(config: Bun.BuildConfig) => void | Promise<void>
build:done(outputs: { client: Bun.BuildOutput; server: Bun.BuildOutput }) => void | Promise<void>
prerender:routes(routes: Set<string>) => void | Promise<void>
dev:ready(info: { url: string }) => void | Promise<void>
close() => void | Promise<void>

SilgiModuleOptions

An empty interface that a module fills in to type its options. SilgiConfig gets one optional key for every key of it, so the options go under the module's configKey with type checking. silgi/kit exports the same type. See Modules.

import { defineConfig } from "silgi";

declare module "silgi" {
  interface SilgiModuleOptions {
    sitemap?: { hostname?: string };
  }
}

export default defineConfig({ modules: ["./modules/sitemap.ts"], sitemap: { hostname: "https://example.com" } });

HeadInput

The type that useHead takes: title, titleTemplate, meta, link, script, style, htmlAttrs and bodyAttrs; a tag may have a key. It is the same HeadInput that silgi/app exports. app.head in the config is typed by ConfigHead, the same shape with titleTemplate as a string only. See Head.

import { defineConfig } from "silgi";
import type { HeadInput } from "silgi";

const head = {
  title: "Silgi",
  titleTemplate: "%s · Silgi",
  meta: [{ name: "description", content: "A full-stack framework for Bun" }],
  link: [{ rel: "icon", href: "/favicon.svg" }],
} satisfies HeadInput;

export default defineConfig({ app: { head } });

RouteRule

One value of routeRules. Rules are matched by pattern and the more specific pattern wins. See Deploy and Storage and cache.

KeyType
prerenderboolean
ssrboolean
swrnumber | boolean
cache{ maxAge?, swr?, staleMaxAge?, varies? } | false
headersRecord<string, string>
redirectstring | { to: string; status?: number }
corsboolean | Record<string, unknown>
proxystring
noindexboolean
basicAuth{ username: string; password: string | { runtimeConfig: string }; realm?: string } | false

basicAuth asks for HTTP Basic credentials (see requireBasicAuth) for every path under the pattern, public files and prerendered pages included, before static files and routing; false lifts a less specific rule. Take the password from { runtimeConfig: "<key>" }: a literal in the config file warns at build, a key the config does not declare fails it. A CORS preflight passes without credentials when the same rule has cors. The rule covers paths, so a handler call (/_silgi/fn/<id>) is protected by defineMiddleware({ server: basicAuth(…) }) on the handler. A path that is a file stands for the route it serves, so a rule on /admin also guards /admin/index.html, and paths match exactly: /ADMIN is not /admin. On static the build fails; on vercel, netlify and cloudflare a prerendered page under the rule fails the build and a file of the output's public/ (including the /_silgi/ bundles under a /** rule) warns, since the platform serves them before the function.

import { defineConfig } from "silgi";
import type { RouteRule } from "silgi";

const docs: RouteRule = { prerender: true, headers: { "x-robots-tag": "all" } };
const legacy: RouteRule = { redirect: { to: "/docs", status: 301 } };

export default defineConfig({ routeRules: { "/docs/**": docs, "/old": legacy } });