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

lautjs

v0.6.7

Published

An islands meta-framework for Bun 1.4. Preact + signals, kinu UI, no build step.

Readme

Laut

An islands meta-framework for Bun 1.4, with no build step at all. Preact + signals, kinu UI built in, JSX everywhere, native ES modules and an import map in the browser.

Laut is take a rest after work or it can be sea — the thing that surrounds the islands, and the part of your page that never ships a byte of JavaScript.

Building an app with Laut? → GUIDE.md


What Laut gives an app

| | | | ------------------ | -------------------------------------------------------------------------------------------------------- | | Runtime | Bun 1.4 — one process, Bun.serve, Bun.Transpiler | | UI | Preact 10 + @preact/signals | | Components | kinu — wired up out of the box | | Rendering Type | Server-rendered HTML, islands hydrated on the client — on load, on idle, on visible, or on a media query | | Routing | An explicit route table + a client router that swaps <main> | | Styling | Plain CSS, co-located with components, linked automatically | | i18n | Optional, built in — JSON tables, one signal, works in sea and islands | | Build | None. No bundler, no dist/, no manifest, no codegen |

The whole idea in one line: the server writes the page as HTML — in sea —and the interactive part in islands. The full explanation is in the guide.


Repo layout

laut/
├── src/              the framework — this is what ships, as `lautjs`
├── template/         the starter app `bun run scaffold` copies
├── create/           `create-lautjs` — what `bun create lautjs my-app` runs
├── scripts/          scaffold / sync-create / link / unlink — dev tooling, not published
├── GUIDE.md          how to build an app with Laut
└── README.md         you are here

Two packages, one name

laut was already taken on npm, so the framework publishes as lautjs and the scaffolder as create-lautjs. There is no alias: an app depends on "lautjs": "^0.4.0", it lands at node_modules/lautjs, and every import … from "lautjs" resolves to it directly.

The name is load-bearing on both sides of the wire. lautjs/client and the lautjs/ prefix are baked into the import map the browser is served, so the bare specifier has to resolve in the browser exactly as it does on the server — an app that renames it renders and then fails to hydrate.

src/ — the framework

| File | What it does | | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | | index.ts | The server-side public surface. Everything an app imports from lautjs. | | config.ts | The Config / AppProps contract, and the one module-level slot holding it. | | server.ts | createServer() — the Bun routes: app source, /lautjs/*, /vendor/*, /public/*, /__dev. | | render.tsx | page() / renderPage() — full document vs. <main> fragment vs. shell swap. | | Island.tsx | createIsland(registry) — stamps the data-island wrapper carrying its own module URL and its when strategy. | | Head.tsx | <Head> — is the <head>: SEO and social tags, stylesheets, import map, preloads, boot script, dev socket. | | DevReload.tsx | The dev script tag. Renders null in production. | | preload.ts | What bunfig.toml preloads: installs the .css loader. | | css.d.ts | Ambient *.css module, so import "./Foo.css" typechecks. | | runtime/transpile.ts | Per-request .tsx → ESM, cached by mtime. Rewrites CSS imports. | | runtime/vendor.ts | Bare specifier → node_modules file, and the import map. | | runtime/css.ts | The Bun plugin that records which stylesheets the graph imported. | | runtime/respond.ts | Compression, etags, 304s, file serving. | | runtime/dev.ts | The file watcher and the /__dev SSE stream. | | runtime/api.ts | route(), HttpError — the JSON endpoint plumbing. | | client/* | The browser half: boot, hydrate (island scheduling), nav (router), css. Served at /lautjs/*. | | i18n/index.ts | The isomorphic translation engine. Holds no tables of its own. |

The one rule

src/ never imports app code. Everything it needs arrives through createServer(config). That is what lets the framework live in node_modules and still know nothing about you — and it is the rule to check any change against.

Two consequences worth remembering:

  • The island registry, the layout map and the locale tables all live in the app. The framework only ever sees a string it passes back.
  • Document.tsx is the app's, not Laut's -- but the <head> inside it is not app business, so <Head> is it: the element and the SEO and social tags, the stylesheet links, the inlined sheets, the import map, the module preloads, the boot script and the dev socket, in the order they have to be in. What the app still supplies is what is genuinely its own: any extra tags as children, and the shape of the <body>. kinu's sheet and /public/app.css are linked by default -- every app links them -- with styles for more and noDefaultCSS to drop both. stylesheets(), inlineStyles(), getImportMap(), clientPreloads() and <DevReload/> stay exported for a hand-rolled head.
  • Head.tsx is the one component in src/ that renders app-shaped markup, and it still holds no app knowledge: everything it prints arrives as HeadData, which renderPage resolves from the page's title / meta exports and createServer({ site }). Merge, absolute URLs and title template happen there, so an app never writes an og: tag.
  • Stylesheets under cssInlineStylesheet (4 KB) are pasted into the document in production rather than linked, so request count stops scaling with island count. <Head> renders both halves; a hand-rolled head that links stylesheets() without inlineStyles() loses those sheets in production -- dev links everything, so dev will not show it, and Laut warns instead.
  • ZERO_JS is the same shape of thing as layout: a page exports it, the framework passes it through to head.ZERO_JS without acting on it, and <Head> is what actually leaves the import map, the preloads and the boot script out. Laut's only opinion is a dev-mode warning when such a page renders an island, since that island can never hydrate.

How the browser gets framework code

The app's boot.ts does import { start } from "lautjs/client". That is a bare specifier, so the import map has to resolve it:

"lautjs/client": "/lautjs/client/index.ts"
"lautjs/":       "/lautjs/"

/lautjs/* is served by createServer out of import.meta.dir — wherever Laut is installed — transpiled on demand exactly like app source. Same URL every time, so the browser keeps one instance.

lautjs (bare) is deliberately not in the import map: src/index.ts is server-only, and an island that imports it should fail loudly rather than half-work.


Developing

bun install
bun run check      # tsc --noEmit over src/
bun run format

Trying it in a real app

# 1. create an app anywhere (or use one you already have)
bun run scaffold ../my-app

# 2. point it at this working tree, in place of the published package
bun run link ../my-app

# 3. run it
cd ../my-app && bun install && bun dev

Skip step 2 to run against npm — a scaffolded app already depends on the published Laut.

Undoing that

bun run unlink ../my-app             # restore the app's own copies
bun run unlink ../my-app --global    # …and unregister lautjs from Bun's registry
bun run unlink --global              # just unregister; leave apps alone

Editing Laut while an app runs

bun --hot does not watch inside node_modules, so restart the app's server after changing Laut's server code. Browser-side modules (src/client/*) are re-fetched on reload and need nothing.

Publishing

Two tarballs. The framework, from the repo root:

bun run check
bun pm pack            # inspect the tarball before you trust it
npm publish            # or: bun publish

The scaffolder, from create/ — its prepack re-vendors template/ into the tarball, so publish it after any template change or bun create keeps handing out the old starter:

cd create
npm publish            # runs `bun ../scripts/sync-create.ts` first

Bump create-lautjs whenever template/ changes. The "lautjs": "^X.Y.Z" range in template/package.json is only a fallback: the scaffolder asks npm for lautjs@latest and rewrites the range as it writes the new app's package.json, so an app is born on the newest framework even when bunx hands out a months-old create-lautjs. It keeps the vendored range if the registry is unreachable, and warns when the scaffolder itself is behind — that one only bunx create-lautjs@<version> can fix, since bunx caches create-lautjs@latest under its own lockfile in $TMPDIR.


Known gaps

Whether Laut fits before finding out the hard way:

  • No file-based routing. routes.ts is a hand-written object.
  • No nested layouts. A page picks one layout, flat.
  • No streaming SSR / Suspense. load() is awaited, then the page renders.
  • No CSS modules, no scoping compiler. Convention only — [data-island="Name"] .thing.
  • No islands inside islands as a first-class concept — a nested interactive component is just a child of its island and hydrates with it.
  • No prefetch on hover. Navigation starts on click.
  • The import map is the dependency system. A package with deep internal imports or conditional exports may need several vendor entries, or may not work in the browser at all without a bundler.
  • Symlinks under public/ are followed. /public/* serves whatever path it resolves to; nothing re-checks that the real path is still inside the project. Placing a symlink there means already having write access to the repo -- which is exactly where a dependency's postinstall script stands. Covered as known behaviour by LAUT-SRV-11.

Testing, and the standard it borrows from, are in TESTING.md.


License

MIT.