npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@human-synthesis/norns

v0.0.19

Published

Norns — SvelteKit with Civet, Pug, and the .n / .civet / .c file extensions

Readme

Norns

AI-driven software architecture and development framework, based on Svelte.

SvelteKit with Pug + Civet and the .n / .c file extensions — preconfigured. The .c extension is recognised as an alias for .civet; both compile through Civet.

Includes a small runtime layer: feature-folder modularity, a DI container, route/page wrappers with valibot validation, and a migrations CLI.

Stack

  • Svelte 5 — components and runes
  • SvelteKit 2 — file-system routing, SSR, endpoints
  • Pug — templates
  • Civet — script (TypeScript-flavored, indented)
  • Tailwind CSS v4 — recommended styling (consumer-installed)
  • Vite — bundler
  • bun — runtime / package manager

Install

bun add -D @human-synthesis/norns @sveltejs/kit svelte

Or use the norns-app starter, which has everything wired up.

Setup

svelte.config.js:

import { nornsConfig } from '@human-synthesis/norns/config';

export default nornsConfig({
  // your overrides here
});

vite.config.js:

import { defineConfig } from 'vite';
import { sveltekit } from '@sveltejs/kit/vite';
import { nornsCivetPlugin } from '@human-synthesis/norns/vite';

export default defineConfig({
  plugins: [nornsCivetPlugin(), sveltekit()]
});

package.json:

{
  "scripts": {
    "dev": "norns dev",
    "build": "norns build",
    "preview": "norns preview",
    "migrate": "norns migrate"
  }
}

Auto-imports

nornsAutoImport() returns an object that's both a Svelte preprocessor (for .n / .svelte files) and a Vite plugin (for standalone .c / .civet modules). The same instance has all four resolvers: framework helpers, project components, library presets, and (opt-in) project utilities. Wire it in both places — Svelte's compiler ignores the Vite hooks, Vite ignores the Svelte hooks:

// svelte.config.js
import { nornsConfig } from '@human-synthesis/norns/config';
import { nornsPreprocess } from '@human-synthesis/norns/preprocess';
import { nornsAutoImport } from '@human-synthesis/norns/auto-import';

export default nornsConfig({
  preprocess: [
    ...nornsPreprocess(),
    nornsAutoImport({
      componentDirs: ['src/lib/components', 'src/routes']
    })
  ]
});
// vite.config.js
import { nornsCivetPlugin } from '@human-synthesis/norns/vite';
import { nornsAutoImport } from '@human-synthesis/norns/auto-import';

export default {
  plugins: [
    nornsCivetPlugin(),
    nornsAutoImport(),
    sveltekit()
  ]
};

What gets auto-imported

| Layer | Resolves | Examples | |-------|----------|----------| | Helpers | Hardcoded module-name lists, optionally path-gated | onMount from svelte, redirect from @sveltejs/kit, page from $app/state (client) or @human-synthesis/norns/server (server) | | Components (dir scan) | Capitalised basenames in componentDirs | <Card> → $lib/components/Card.svelte; <Game> → ./Game.n (route-colocated, importer-relative) | | Components (preset map) | Bare-specifier Record<name, importPath> from a UI library | <Btn> → '@human-synthesis/norns-ui/components/Btn.n' (used verbatim) | | Project utilities (opt-in) | Named exports (export const X, export X := …, export { a, b }) found in files matching exportGlobs | notes from $lib/notes/server/public when exportGlobs: ['src/lib/**/public.c'] |

Resolution priority is helpers → component dir → component preset → exports. A name picked up earlier shadows a later match silently — first-match-wins lets you override a library preset by dropping a file under your own componentDirs.

exportGlobs is off by default: project code (facades, schemas, services, stores) is imported explicitly unless you opt in. The recommended opt-in is barrel scope only (['src/lib/**/public.c']) so a feature's internals never leak through auto-import. Server-path files (/server/, *.server.*, +server.*, hooks.server.*) are never auto-imported into client files, and a name exported from two files in the same scope is logged and excluded. The starter and demo apps do not enable it. (The older exportDirs option was replaced by exportGlobs in 0.0.11.)

Path emission:

  • Files inside $lib emit $lib/... paths (portable, friendly to the dts file).
  • Files outside $lib emit a path relative to the importer.
  • Project-utility paths are stripped of their file extension ('$lib/notes/server/public', not …/public.c); the configured extensions array does the rest.

Files without a <script> block get one prepended automatically when a known component is referenced from markup. Runes ($state, $derived, $effect, $props) are Svelte compiler globals — no import needed; the plugin doesn't touch them.

Defaults

  • Helpers: svelte, svelte/store, @sveltejs/kit, $app/state (non-server paths), @human-synthesis/norns/server (server paths only).
  • Component dirs: ['src/lib/components'].
  • Component extensions: ['.svelte', '.n'].
  • Export globs: [] (off) — opt in with e.g. ['src/lib/**/public.c']. SvelteKit route/hook files (+*, hooks.*) are excluded from the export scan since their named exports (load, actions, handle, …) are framework-consumed.
  • Export extensions: ['.c', '.civet', '.js']. .ts is excluded by default — regex-based scanning can't reliably tell value exports from type-only ones under verbatimModuleSyntax.

UI library presets

A preset is a function returning a config slice — typically a components map. Compose it with your own config:

// vite.config.js
import { presetUI } from '@human-synthesis/norns-ui/auto-import';

const ui = presetUI();

export default {
  plugins: [
    nornsCivetPlugin(),
    nornsAutoImport({
      components: ui.components   // { Btn: '@human-synthesis/norns-ui/components/Btn.n', … }
    }),
    sveltekit()
  ]
};

Drop src/lib/components/Btn.n in your project and it shadows the preset's Btn silently — componentDirs resolves first.

Roadmap. Helpers from a preset (e.g. toast() from a UI library) currently can't merge with the defaults — passing helpers to nornsAutoImport replaces the default list. A presets (or additionalHelpers) option to extend without replacing is a planned follow-up; for now, presets only deliver components.

Full options reference

| Option | Default | Notes | |--------|---------|-------| | helpers | DEFAULT_HELPERS (5 modules) | Pass false to disable. Each entry: { from, imports[], match? } where match is a regex tested against the filename. | | componentDirs | ['src/lib/components'] | false or [] to disable. | | componentExtensions | ['.svelte', '.n'] | | | components | null | Record<name, importPath> — bare-specifier preset map. | | exportGlobs | [] | Off by default. Opt in with e.g. ['src/lib/**/public.c']. | | exportExtensions | ['.c', '.civet', '.js'] | | | libRoot | 'src/lib' | Project-relative root that libAlias maps to. | | libAlias | '$lib' | Alias prefix emitted in import paths. | | root | process.cwd() | Project root. |

Runtime — feature folders + DI

Wire your hooks once:

# src/hooks.server.c
import { boot } from '@human-synthesis/norns/server'

features := import.meta.glob './lib/*/server/module.c', { eager: true }
app := await boot { features }

{ handle, handleError } := app
export { handle, handleError }

Each feature is a folder under src/lib/<feature>/:

src/lib/notes/
  server/
    module.c        # registers DI bindings + migrations
    repo.c          # SQL / data access
    service.c       # business logic
    public.c        # the ONLY file other features may import
  shared/
    schema.c        # valibot validation schemas

Routes use thin wrappers from @human-synthesis/norns/server:

# src/routes/notes/+page.server.c
import { page } from '@human-synthesis/norns/server'
import { notes } from '$lib/notes/server/public'
import { createNoteSchema } from '$lib/notes/shared/schema'

export load := page.load
  handler: ({ container }) =>
    notes: notes(container).list()

export actions := page.actions
  create:
    input: createNoteSchema
    run: ({ input, container }) =>
      id := notes(container).create input
      throw redirect 303, `/notes/${id}`

The wrappers handle: input parsing, valibot validation, container resolution, and consistent error mapping. route() reads JSON, form and (through the app-wide serializer) TRON bodies with one readBody(); page.actions uses the same reader, though SvelteKit itself only dispatches form-encoded POSTs to actions, so JSON/TRON clients target a +server.c route.

List endpoints and caching

Large lists stay out of load: a route() serves one page at a time, and the page owns the paging state. listQuery() is the query convention (?page=&pageSize=&sort=&dir=&q=, validated: unknown sort keys are a 400) and listResult() the { data, total, page, pageSize } envelope that norns-ui's useList() and a TRON path: '$.data' contract expect:

# src/routes/api/notes/+server.c
export GET := route
  query: listQuery { sort: ['title', 'updated_at'], defaultSort: 'updated_at', defaultDir: 'desc', pageSize: 25 }
  handler: async ({ query, container }) =>
    { data, total } := await notes(container).page query   # query.offset / pageSize / sort / dir / q
    listResult query, data, total

cache: { ttl } turns a GET route into a cacheable one: Cache-Control + ETag on the response, If-None-Match answered with a 304, and on Cloudflare Workers (event.platform.caches) the encoded body is stored in the edge cache so the handler and the serializer run once per TTL. Entries vary on Accept by default, so JSON and TRON never mix; private: true skips the shared cache. The response carries x-norns-cache: hit|miss. Note that wrangler's dev platform proxy implements the Cache API as a no-op, so locally every request is a miss; the header logic is still exercised.

export GET := route
  serializer: tronSerializer({ columnar: true })
  cache: { ttl: 30 }
  handler: ({ container }) => notes(container).stats()

CLI

The norns binary wraps Vite and adds the checks an agent (or a human) should run before calling a change done:

norns dev | build | preview        # vite, with framework-source watching in workspace mode
norns lint [--json]                # Civet / Pug pitfall scan over .c / .civet / .n (templates AND script blocks) + vite.config
norns check [--json] [--warnings]  # preprocess + compile every .n / .c / .civet through your svelte.config.js; file:line:column errors
norns diag <file>                  # the JS Civet emits for a .c / .civet / .n script block
norns diag --template <file.n>     # the Svelte source the compiler sees after Pug / Civet / auto-import preprocessing
norns migrate status | up | create <feature>/<name>

Verification order for a change: lint, check, build, then curl through dev. lint catches the known traps (isnt, := $state reassigned, +each with of, leading { in Pug, #{} interpolation); check is the full compile. Pug and Civet errors are mapped to the line you wrote (not to svelte-preprocess's mixin prelude or the script block's own numbering). Svelte compile errors inside Pug-rendered markup are reported against the preprocessed output with a note, because Pug emits no source map — diag --template shows that output. svelte-check never reads .n / .c, so it is not a pass signal for Norns code.

Migrations

bun run migrate create notes/add_pinned    # scaffold migrations/notes/<ts>_add_pinned.sql
bun run migrate up                         # apply pending migrations
bun run migrate status                     # list applied + pending

Migration files live at <project>/migrations/<feature>/*.sql. The CLI tracks applied migrations in a norns_migrations table.

v1 supports SQLite via better-sqlite3. For Cloudflare D1 use wrangler d1 migrations apply. Postgres / libSQL via the CLI are planned.

Drivers

The db helpers wire Drizzle across multiple targets:

# module.c — Node + better-sqlite3 in dev
import { betterSqlite } from '@human-synthesis/norns/server'
db := await betterSqlite 'data/app.db', { pragma: ['journal_mode = WAL'] }
app.single 'db', => db

D1, libSQL, and Postgres factories ship in the same module; the driver packages are user-installed (peer-style).

License

MIT © Daniel Teodoroiu / Human Synthesis. Built on top of SvelteKit and Svelte © Svelte Contributors, MIT licensed.