For agents: an index of these docs is at /docs/llms.txt, and this page as markdown is at /docs/config.md.
Config
Biome preset, tsconfig bases and CI.
Signet's house Biome preset and TypeScript bases, so an app extends one config instead of carrying a hand-copied one that drifts.
bun add -d @dapplelight/signet-config @biomejs/biome typescriptBiome
{ "$schema": "https://biomejs.dev/schemas/2.5.14/schema.json", "extends": ["@dapplelight/signet-config/biome"], "files": { "includes": ["**", "!**/dist"] }}Needs Biome 2. The preset is the same config create-signet-app scaffolds and
Signet lints itself with: 2-space indent, 100 columns, double quotes, no
semicolons (asNeeded), organize imports on, useHookAtTopLevel as an error,
noExplicitAny as a warning, Tailwind directives parsed in CSS. It turns on
vcs.useIgnoreFile, so the repo needs a .gitignore.
Keep files, overrides and anything product-specific in your own
biome.json; your settings win over the preset's.
The preset carries "root": false. That is for Signet's own repo, where the
file sits in a subdirectory and would otherwise be rejected as a second root
config. Biome does not inherit root through extends, so your config stays a
root config (the test checks this).
TypeScript
{ "extends": "@dapplelight/signet-config/tsconfig/react.json", "compilerOptions": { "paths": { "@/*": ["./src/*"] }, "types": ["vite/client"] }, "include": ["src"]}| File | For | Adds to base |
|---|---|---|
tsconfig/base.json |
scripts, shared packages | — |
tsconfig/react.json |
Vite + React SPA | DOM libs, jsx: react-jsx |
tsconfig/worker.json |
Cloudflare Workers | no DOM (a Worker has no document) |
base.json: strict, moduleResolution: "bundler", module: "ESNext",
target and lib ES2022, isolatedModules, moduleDetection: "force",
resolveJsonModule, noFallthroughCasesInSwitch, skipLibCheck, noEmit,
and types: [].
- No
baseUrl. TypeScript 7 removed it.pathsresolve relative to the tsconfig that declares them, so declarepathsin your own file. typesis empty. List what you use:["vite/client"],["bun"],["node"]. Atypesyou set replaces the base's; it does not add to it.- Workers need runtime types.
worker.jsondeliberately has no DOM lib. Add"types": ["@cloudflare/workers-types"], or runwrangler typesand add the generatedworker-configuration.d.tstoinclude. - Emitting? The bases set
noEmit. A build config setsnoEmit: falseand its ownoutDir,declaration, and so on.
Reusable CI
Signet also ships .github/workflows/ci-bun.yml, a workflow_call workflow:
checkout, setup-bun, install, lint, typecheck, test, and an optional build.
jobs: ci: uses: dapplelight/signet/.github/workflows/ci-bun.yml@main with: bun-version: 1.3.11 # default: latest working-directory: . # default fetch-depth: 0 # default 1; 0 for scripts that diff against the base branch build: true # default false; runs build-command test: bun test # every command is overridable; "" skips the stepThe Signet repo is private, so this needs a one-off setting. On GitHub, open
dapplelight/signet → Settings → Actions → General → Access, and choose
"Accessible from repositories owned by the user 'dapplelight'". Until then a caller
fails with "workflow was not found". The calling repos must be owned by the same
account.
It covers what every repo repeats. Repo-specific jobs stay in the repo:
app-a's e2e, type-ramp and app-a-rules checks, app-b's slop:ci,
dup, Neon integration and PR risk label. (For app-b, slop:ci and
dup can ride along: lint: bun run lint && bun run slop:ci && bun run dup.)
There is no shared deploy workflow: app-a deploys from deploy.yml (wrangler
for the API Worker, Pages for four frontends), and app-b has no deploy
workflow at all; it deploys with bun run api:deploy / web:deploy.
How app-a and app-b differ today
Surveyed 2026-09-24, read-only. Adopting the preset means deciding each row.
Biome
| Preset (house) | app-a | app-b | |
|---|---|---|---|
| Biome schema | 2.5.14 | 2.4.10 | 2.5.14 |
| Semicolons | asNeeded |
asNeeded |
always |
| Quotes, indent, width | double, 2 spaces, 100 | same | same |
| Trailing commas | default (all) |
all (explicit) |
all (explicit) |
| Recommended rules | default (on) | recommended: true |
preset: "recommended" |
| Unused imports | default | warn |
error, unused variables error too |
useHookAtTopLevel |
error |
default | default |
| Organize imports | on | not set | on |
| CSS | Tailwind directives parsed | CSS lint + format off, *.css excluded |
Tailwind directives parsed, noImportantStyles off |
| Extras | — | noArrayIndexKey, noNonNullAssertion, noAutofocus, useButtonType warn; four a11y rules off; useNodejsImportProtocol off; globals: ["process"] |
noConsole off, but error in apps/api |
app-b on semicolons: extending the preset reformats every file.
Either run biome check --write once in its own commit, or keep
"javascript": { "formatter": { "semicolons": "always" } } in its biome.json.
TypeScript
| Preset | app-a | app-b | |
|---|---|---|---|
| Shape | base + react + worker | root base for packages; web and admin standalone; Astro apps extend astro/tsconfigs/strict |
one root config; web extends it for Next |
module |
ESNext |
ESNext |
Preserve |
jsx |
react-jsx (react.json) |
react-jsx |
preserve (Next) |
| Emit | noEmit |
root emits declarations, source maps, incremental |
noEmit |
types |
[] |
per app (vite/client, node, bun) |
["bun"] at the root |
| Other | moduleDetection: force, noFallthroughCasesInSwitch |
esModuleInterop, forceConsistentCasingInFileNames |
— |
Both are strict with moduleResolution: "bundler" and no baseUrl. A Next app
extends react.json and sets "jsx": "preserve" plus the next plugin.
CI
| app-a | app-b | |
|---|---|---|
| Trigger | pull requests to main | push to main and every PR |
| Bun | pinned 1.3.11 | latest |
| Shape | parallel jobs: lint (full history), typecheck, web test, API test, build, e2e | one job: typecheck, lint, slop:ci, dup, test |
| Also | deploy.yml |
neon-integration.yml, pr-risk.yml |