docs: Phase 5 — v8 README, migration guide, deploy recipes, RFC draft
README rewritten for the v8 branch: honest size claim (7.97 kB min+gzip for the whole framework, with the v7 '4kb' context), the zero-build story up front, bankai v10, package map, credits to yoshuawuyts, the choojs contributors and the pirxpilot fork line. docs/migrating-v7-to-v8.md: the codemod path, the specifier map, and every deliberate behavior change spelled out. docs/deploy.md: plain Node, Docker, proxy/CDN — including the HTTP/3 answer (h3 is infrastructure's job; bankai's contract is the 103 + Link headers that any hints-aware edge, h3 included, propagates; server push is dead everywhere and never existed in h3) — and web-standard runtimes. docs/rfc.md: the draft announcement for choojs/choo — continuation framing, pings to yoshuawuyts and pirxpilot, the npm-rights ask, API feedback questions, and a three-week comment window with a silence-is-consent close. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
This commit is contained in:
co-authored by
Claude Fable 5
parent
99f9da1e3b
commit
b957b02410
@@ -1,701 +1,129 @@
|
||||
<h1 align="center">Choo</h1>
|
||||
<h1 align="center">choo</h1>
|
||||
|
||||
<div align="center">
|
||||
:steam_locomotive::train::train::train::train::train:
|
||||
<strong>🚂🚋🚋🚋🚋🚋</strong>
|
||||
</div>
|
||||
<div align="center">
|
||||
<strong>Fun functional programming</strong>
|
||||
</div>
|
||||
<div align="center">
|
||||
A <code>4kb</code> framework for creating sturdy frontend applications
|
||||
The sturdy frontend framework — now with a modern engine under the same hood.
|
||||
</div>
|
||||
|
||||
<br />
|
||||
<br>
|
||||
|
||||
<div align="center">
|
||||
<!-- Stability -->
|
||||
<a href="https://nodejs.org/api/documentation.html#documentation_stability_index">
|
||||
<img src="https://img.shields.io/badge/stability-experimental-orange.svg?style=flat-square"
|
||||
alt="API stability" />
|
||||
</a>
|
||||
<!-- NPM version -->
|
||||
<a href="https://npmjs.org/package/choo">
|
||||
<img src="https://img.shields.io/npm/v/choo.svg?style=flat-square"
|
||||
alt="NPM version" />
|
||||
</a>
|
||||
<!-- Build Status -->
|
||||
<a href="https://travis-ci.org/choojs/choo">
|
||||
<img src="https://img.shields.io/travis/choojs/choo/master.svg?style=flat-square"
|
||||
alt="Build Status" />
|
||||
</a>
|
||||
<!-- Test Coverage -->
|
||||
<a href="https://codecov.io/github/choojs/choo">
|
||||
<img src="https://img.shields.io/codecov/c/github/choojs/choo/master.svg?style=flat-square"
|
||||
alt="Test Coverage" />
|
||||
</a>
|
||||
<!-- Downloads -->
|
||||
<a href="https://npmjs.org/package/choo">
|
||||
<img src="https://img.shields.io/npm/dt/choo.svg?style=flat-square"
|
||||
alt="Download" />
|
||||
</a>
|
||||
<!-- Standard -->
|
||||
<a href="https://standardjs.com">
|
||||
<img src="https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat-square"
|
||||
alt="Standard" />
|
||||
</a>
|
||||
</div>
|
||||
> **This is the v8 branch.** choo v7 is on `master` and stays untouched
|
||||
> until 8.0.0 ships. Everything below describes v8: same API, same
|
||||
> philosophy, rebuilt for the platform of 2026. See
|
||||
> [`docs/v8.md`](docs/v8.md) for status and the full plan.
|
||||
|
||||
<div align="center">
|
||||
<h3>
|
||||
<a href="https://choo.io">
|
||||
Website
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/choojs/choo-handbook">
|
||||
Handbook
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://github.com/YerkoPalma/awesome-choo">
|
||||
Ecosystem
|
||||
</a>
|
||||
<span> | </span>
|
||||
<!-- <a href="https://github.com/trainyard/choo-cli"> -->
|
||||
<!-- CLI -->
|
||||
<!-- </a> -->
|
||||
<!-- <span> | </span> -->
|
||||
<a href="https://github.com/choojs/choo/blob/master/.github/CONTRIBUTING.md">
|
||||
Contributing
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://www.reddit.com/r/choojs/">
|
||||
Reddit
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="https://webchat.freenode.net/?channels=choo">
|
||||
Chat
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
## What is this
|
||||
|
||||
<div align="center">
|
||||
<sub>The little framework that could. Built with ❤︎ by
|
||||
<a href="https://twitter.com/yoshuawuyts">Yoshua Wuyts</a> and
|
||||
<a href="https://github.com/choojs/choo/graphs/contributors">
|
||||
contributors
|
||||
</a>
|
||||
</div>
|
||||
Choo is a small framework for building frontend applications with plain
|
||||
JavaScript and HTML. State lives in stores, changes flow through an event
|
||||
emitter, views are tagged template literals, and re-renders morph the real
|
||||
DOM. The whole framework — core, html engine, morphing, hydration, router,
|
||||
event bus — is **7.97 kB min+gzip** (7.15 kB brotli), enforced by CI.
|
||||
|
||||
## Table of Contents
|
||||
- [Features](#features)
|
||||
- [Example](#example)
|
||||
- [Philosophy](#philosophy)
|
||||
- [Events](#events)
|
||||
- [State](#state)
|
||||
- [Routing](#routing)
|
||||
- [Server Rendering](#server-rendering)
|
||||
- [Components](#components)
|
||||
- [Optimizations](#optimizations)
|
||||
- [FAQ](#faq)
|
||||
- [API](#api)
|
||||
- [Installation](#installation)
|
||||
- [See Also](#see-also)
|
||||
- [Support](#support)
|
||||
|
||||
## Features
|
||||
- __minimal size:__ weighing `4kb`, Choo is a tiny little framework
|
||||
- __event based:__ our performant event system makes writing apps easy
|
||||
- __small api:__ with only 6 methods there's not much to learn
|
||||
- __minimal tooling:__ built for the cutting edge `browserify` compiler
|
||||
- __isomorphic:__ renders seamlessly in both Node and browsers
|
||||
- __very cute:__ choo choo!
|
||||
|
||||
## Example
|
||||
```js
|
||||
var html = require('choo/html')
|
||||
var devtools = require('choo-devtools')
|
||||
var choo = require('choo')
|
||||
import choo from '@choojs/core'
|
||||
import html from '@choojs/html'
|
||||
|
||||
var app = choo()
|
||||
app.use(devtools())
|
||||
app.use(countStore)
|
||||
app.route('/', mainView)
|
||||
app.mount('body')
|
||||
const app = choo()
|
||||
|
||||
function mainView (state, emit) {
|
||||
return html`
|
||||
<body>
|
||||
<h1>count is ${state.count}</h1>
|
||||
<button onclick=${onclick}>Increment</button>
|
||||
</body>
|
||||
`
|
||||
|
||||
function onclick () {
|
||||
emit('increment', 1)
|
||||
}
|
||||
}
|
||||
|
||||
function countStore (state, emitter) {
|
||||
app.use((state, emitter) => {
|
||||
state.count = 0
|
||||
emitter.on('increment', function (count) {
|
||||
state.count += count
|
||||
emitter.on('increment', (n) => {
|
||||
state.count += n
|
||||
emitter.emit('render')
|
||||
})
|
||||
}
|
||||
```
|
||||
Want to see more examples? Check out the [Choo handbook][handbook].
|
||||
|
||||
## Philosophy
|
||||
We believe programming should be fun and light, not stern and stressful. It's
|
||||
cool to be cute; using serious words without explaining them doesn't make for
|
||||
better results - if anything it scares people off. We don't want to be scary,
|
||||
we want to be nice and fun, and then _casually_ be the best choice around.
|
||||
_Real casually._
|
||||
|
||||
We believe frameworks should be disposable, and components recyclable. We don't
|
||||
want a web where walled gardens jealously compete with one another. By making
|
||||
the DOM the lowest common denominator, switching from one framework to another
|
||||
becomes frictionless. Choo is modest in its design; we don't believe it will
|
||||
be top of the class forever, so we've made it as easy to toss out as it is to
|
||||
pick up.
|
||||
|
||||
We don't believe that bigger is better. Big APIs, large complexities, long
|
||||
files - we see them as omens of impending userland complexity. We want everyone
|
||||
on a team, no matter the size, to fully understand how an application is laid
|
||||
out. And once an application is built, we want it to be small, performant and
|
||||
easy to reason about. All of which makes for easy to debug code, better results
|
||||
and super smiley faces.
|
||||
|
||||
## Events
|
||||
At the core of Choo is an event emitter, which is used for both application
|
||||
logic but also to interface with the framework itself. The package we use for
|
||||
this is [nanobus](https://github.com/choojs/nanobus).
|
||||
|
||||
You can access the emitter through `app.use(state, emitter, app)`, `app.route(route,
|
||||
view(state, emit))` or `app.emitter`. Routes only have access to the
|
||||
`emitter.emit` method to encourage people to separate business logic from
|
||||
render logic.
|
||||
|
||||
The purpose of the emitter is two-fold: it allows wiring up application code
|
||||
together, and splitting it off nicely - but it also allows communicating with
|
||||
the Choo framework itself. All events can be read as constants from
|
||||
`state.events`. Choo ships with the following events built in:
|
||||
|
||||
### `'DOMContentLoaded'`|`state.events.DOMCONTENTLOADED`
|
||||
Choo emits this when the DOM is ready. Similar to the DOM's
|
||||
`'DOMContentLoaded'` event, except it will be emitted even if the listener is
|
||||
added _after_ the DOM became ready. Uses
|
||||
[document-ready](https://github.com/bendrucker/document-ready) under the hood.
|
||||
|
||||
### `'render'`|`state.events.RENDER`
|
||||
This event should be emitted to re-render the DOM. A common pattern is to
|
||||
update the `state` object, and then emit the `'render'` event straight after.
|
||||
Note that `'render'` will only have an effect once the `DOMContentLoaded` event
|
||||
has been fired.
|
||||
|
||||
### `'navigate'`|`state.events.NAVIGATE`
|
||||
Choo emits this event whenever routes change. This is triggered by either
|
||||
`'pushState'`, `'replaceState'` or `'popState'`.
|
||||
|
||||
### `'pushState'`|`state.events.PUSHSTATE`
|
||||
This event should be emitted to navigate to a new route. The new route is added
|
||||
to the browser's history stack, and will emit `'navigate'` and `'render'`.
|
||||
Similar to
|
||||
[history.pushState](http://devdocs.io/dom/history_api).
|
||||
|
||||
### `'replaceState'`|`state.events.REPLACESTATE`
|
||||
This event should be emitted to navigate to a new route. The new route replaces
|
||||
the current entry in the browser's history stack, and will emit `'navigate'`
|
||||
and `'render'`. Similar to
|
||||
[history.replaceState](http://devdocs.io/dom/history#history-replacestate).
|
||||
|
||||
### `'popState'`|`state.events.POPSTATE`
|
||||
This event is emitted when the user hits the 'back' button in their browser.
|
||||
The new route will be a previous entry in the browser's history stack, and
|
||||
immediately afterward the`'navigate'` and `'render'`events will be emitted.
|
||||
Similar to [history.popState](http://devdocs.io/dom_events/popstate). (Note
|
||||
that `emit('popState')` will _not_ cause a popState action - use
|
||||
`history.go(-1)` for that - this is different from the behaviour of `pushState`
|
||||
and `replaceState`!)
|
||||
|
||||
### `'DOMTitleChange'`|`state.events.DOMTITLECHANGE`
|
||||
This event should be emitted whenever the `document.title` needs to be updated.
|
||||
It will set both `document.title` and `state.title`. This value can be used
|
||||
when server rendering to accurately include a `<title>` tag in the header.
|
||||
This is derived from the
|
||||
[DOMTitleChanged event](https://developer.mozilla.org/en-US/docs/Web/Events/DOMTitleChanged).
|
||||
|
||||
## State
|
||||
Choo comes with a shared state object. This object can be mutated freely, and
|
||||
is passed into the view functions whenever `'render'` is emitted. The state
|
||||
object comes with a few properties set.
|
||||
|
||||
When initializing the application, `window.initialState` is used to provision
|
||||
the initial state. This is especially useful when combined with server
|
||||
rendering. See [server rendering](#server-rendering) for more details.
|
||||
|
||||
### `state.events`
|
||||
A mapping of Choo's built in events. It's recommended to extend this object
|
||||
with your application's events. By defining your event names once and setting
|
||||
them on `state.events`, it reduces the chance of typos, generally autocompletes
|
||||
better, makes refactoring easier and compresses better.
|
||||
|
||||
### `state.params`
|
||||
The current params taken from the route. E.g. `/foo/:bar` becomes available as
|
||||
`state.params.bar` If a wildcard route is used (`/foo/*`) it's available as
|
||||
`state.params.wildcard`.
|
||||
|
||||
### `state.query`
|
||||
An object containing the current queryString. `/foo?bin=baz` becomes `{ bin:
|
||||
'baz' }`.
|
||||
|
||||
### `state.href`
|
||||
An object containing the current href. `/foo?bin=baz` becomes `/foo`.
|
||||
|
||||
### `state.route`
|
||||
The current name of the route used in the router (e.g. `/foo/:bar`).
|
||||
|
||||
### `state.title`
|
||||
The current page title. Can be set using the `DOMTitleChange` event.
|
||||
|
||||
### `state.components`
|
||||
An object _recommended_ to use for local component state.
|
||||
|
||||
### `state.cache(Component, id, [...args])`
|
||||
Generic class cache. Will lookup Component instance by id and create one if not
|
||||
found. Useful for working with stateful [components](#components).
|
||||
|
||||
## Routing
|
||||
Choo is an application level framework. This means that it takes care of
|
||||
everything related to routing and pathnames for you.
|
||||
|
||||
### Params
|
||||
Params can be registered by prepending the route name with `:routename`, e.g.
|
||||
`/foo/:bar/:baz`. The value of the param will be saved on `state.params` (e.g.
|
||||
`state.params.bar`). Wildcard routes can be registered with `*`, e.g. `/foo/*`.
|
||||
The value of the wildcard will be saved under `state.params.wildcard`.
|
||||
|
||||
### Default routes
|
||||
Sometimes a route doesn't match, and you want to display a page to handle it.
|
||||
You can do this by declaring `app.route('*', handler)` to handle all routes
|
||||
that didn't match anything else.
|
||||
|
||||
### Querystrings
|
||||
Querystrings (e.g. `?foo=bar`) are ignored when matching routes. An object
|
||||
containing the key-value mappings exists as `state.query`.
|
||||
|
||||
### Hash routing
|
||||
By default, hashes are ignored when routing. When enabling hash routing
|
||||
(`choo({ hash: true })`) hashes will be treated as part of the url, converting
|
||||
`/foo#bar` to `/foo/bar`. This is useful if the application is not mounted at
|
||||
the website root. Unless hash routing is enabled, if a hash is found we check if
|
||||
there's an anchor on the same page, and will scroll the element into view. Using
|
||||
both hashes in URLs and anchor links on the page is generally not recommended.
|
||||
|
||||
### Following links
|
||||
By default all clicks on `<a>` tags are handled by the router through the
|
||||
[nanohref](https://github.com/choojs/nanohref) module. This can be
|
||||
disabled application-wide by passing `{ href: false }` to the application
|
||||
constructor. The event is not handled under the following conditions:
|
||||
- the click event had `.preventDefault()` called on it
|
||||
- the link has a `target="_blank"` attribute with `rel="noopener noreferrer"`
|
||||
- a modifier key is enabled (e.g. `ctrl`, `alt`, `shift` or `meta`)
|
||||
- the link's href starts with protocol handler such as `mailto:` or `dat:`
|
||||
- the link points to a different host
|
||||
- the link has a `download` attribute
|
||||
|
||||
:warn: Note that we only handle `target=_blank` if they also have
|
||||
`rel="noopener noreferrer"` on them. This is needed to [properly sandbox web
|
||||
pages](https://mathiasbynens.github.io/rel-noopener/).
|
||||
|
||||
### Navigating programmatically
|
||||
To navigate routes you can emit `'pushState'`, `'popState'` or
|
||||
`'replaceState'`. See [#events](#events) for more details about these events.
|
||||
|
||||
## Server Rendering
|
||||
Choo was built with Node in mind. To render on the server call
|
||||
`.toString(route, [state])` on your `choo` instance.
|
||||
|
||||
```js
|
||||
var html = require('choo/html')
|
||||
var choo = require('choo')
|
||||
|
||||
var app = choo()
|
||||
app.route('/', function (state, emit) {
|
||||
return html`<div>Hello ${state.name}</div>`
|
||||
})
|
||||
|
||||
var state = { name: 'Node' }
|
||||
var string = app.toString('/', state)
|
||||
|
||||
console.log(string)
|
||||
// => '<div>Hello Node</div>'
|
||||
```
|
||||
|
||||
When starting an application in the browser, it's recommended to provide the
|
||||
same `state` object available as `window.initialState`. When the application is
|
||||
started, it'll be used to initialize the application state. The process of
|
||||
server rendering, and providing an initial state on the client to create the
|
||||
exact same document is also known as "rehydration".
|
||||
|
||||
For security purposes, after `window.initialState` is used it is deleted from
|
||||
the `window` object.
|
||||
|
||||
```html
|
||||
<html>
|
||||
<head>
|
||||
<script>window.initialState = { initial: 'state' }</script>
|
||||
</head>
|
||||
app.route('/', (state, emit) => html`
|
||||
<body>
|
||||
<h1>count is ${state.count}</h1>
|
||||
<button onclick=${() => emit('increment', 1)}>Increment</button>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
`)
|
||||
|
||||
## Components
|
||||
From time to time there will arise a need to have an element in an application
|
||||
hold a self-contained state or to not rerender when the application does. This
|
||||
is common when using 3rd party libraries to e.g. display an interactive map or a
|
||||
graph and you rely on this 3rd party library to handle modifications to the DOM.
|
||||
Components come baked in to Choo for these kinds of situations. See
|
||||
[nanocomponent][nanocomponent] for documentation on the component class.
|
||||
|
||||
```javascript
|
||||
// map.js
|
||||
var html = require('choo/html')
|
||||
var mapboxgl = require('mapbox-gl')
|
||||
var Component = require('choo/component')
|
||||
|
||||
module.exports = class Map extends Component {
|
||||
constructor (id, state, emit) {
|
||||
super(id)
|
||||
this.local = state.components[id] = {}
|
||||
}
|
||||
|
||||
load (element) {
|
||||
this.map = new mapboxgl.Map({
|
||||
container: element,
|
||||
center: this.local.center
|
||||
})
|
||||
}
|
||||
|
||||
update (center) {
|
||||
if (center.join() !== this.local.center.join()) {
|
||||
this.map.setCenter(center)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
createElement (center) {
|
||||
this.local.center = center
|
||||
return html`<div></div>`
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// index.js
|
||||
var choo = require('choo')
|
||||
var html = require('choo/html')
|
||||
var Map = require('./map.js')
|
||||
|
||||
var app = choo()
|
||||
app.route('/', mainView)
|
||||
app.mount('body')
|
||||
|
||||
function mainView (state, emit) {
|
||||
return html`
|
||||
<body>
|
||||
<button onclick=${onclick}>Where am i?</button>
|
||||
${state.cache(Map, 'my-map').render(state.center)}
|
||||
</body>
|
||||
`
|
||||
|
||||
function onclick () {
|
||||
emit('locate')
|
||||
}
|
||||
}
|
||||
|
||||
app.use(function (state, emitter) {
|
||||
state.center = [18.0704503, 59.3244897]
|
||||
emitter.on('locate', function () {
|
||||
window.navigator.geolocation.getCurrentPosition(function (position) {
|
||||
state.center = [position.coords.longitude, position.coords.latitude]
|
||||
emitter.emit('render')
|
||||
})
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
### Caching components
|
||||
When working with stateful components, one will need to keep track of component
|
||||
instances – `state.cache` does just that. The component cache is a function
|
||||
which takes a component class and a unique id (`string`) as its first two
|
||||
arguments. Any following arguments will be forwarded to the component constructor
|
||||
together with `state` and `emit`.
|
||||
## Why v8
|
||||
|
||||
The default class cache is an LRU cache (using [nanolru][nanolru]), meaning it
|
||||
will only hold on to a fixed amount of class instances (`100` by default) before
|
||||
starting to evict the least-recently-used instances. This behavior can be
|
||||
overridden with [options](#app--chooopts).
|
||||
- **No build step, anywhere.** Templates parse at runtime (cached per
|
||||
call site in a WeakMap, clone-based instantiation — as fast as the old
|
||||
compile-time transform, without the toolchain). An import map and a
|
||||
`<script type="module">` is a complete development setup:
|
||||
see [`examples/counter/index.html`](examples/counter/index.html).
|
||||
- **Isomorphic by contract.** One app module. The browser mounts it, the
|
||||
server imports the same file and calls `toString(route)` or streams it
|
||||
with `toStream(route)` — a web-standard `ReadableStream`, so it plugs
|
||||
into Node, Deno, Bun, and edge runtimes alike.
|
||||
- **Streaming SSR.** Async values in child position flush the shell
|
||||
first and stream the rest in document order. Store data via
|
||||
`state.prefetch` promises. Hydration *adopts* the server DOM (same
|
||||
element references, form state kept) and warns precisely when server
|
||||
and client markup disagree.
|
||||
- **Async routes, finally.** `app.route('/big', lazy(() => import('./big.js')))`
|
||||
— loading view or held tree while in flight, awaited on the server.
|
||||
([choojs/choo#653](https://github.com/choojs/choo/pull/653), answered.)
|
||||
- **Modern platform, no shims.** ESM only, Node ≥ 24, Baseline
|
||||
Widely Available browsers. The router speaks WHATWG URL and survives
|
||||
emoji, literal `%`, and Unicode normalization differences.
|
||||
|
||||
## Optimizations
|
||||
Choo is reasonably fast out of the box. But sometimes you might hit a scenario
|
||||
where a particular part of the UI slows down the application, and you want to
|
||||
speed it up. Here are some optimizations that are possible.
|
||||
## bankai v10
|
||||
|
||||
### Caching DOM elements
|
||||
Sometimes we want to tell the algorithm to not evaluate certain nodes (and its
|
||||
children). This can be because we're sure they haven't changed, or perhaps
|
||||
because another piece of code is managing that part of the DOM tree. To achieve
|
||||
this `nanomorph` evaluates the `.isSameNode()` method on nodes to determine if
|
||||
they should be updated or not.
|
||||
The isomorphic compiler & server, rebuilt on Vite 8 / Rolldown. Only the
|
||||
client ever bundles — v8 server code is plain ESM Node runs as-authored.
|
||||
|
||||
```js
|
||||
var el = html`<div>node</div>`
|
||||
|
||||
// tell nanomorph to not compare the DOM tree if they're both divs
|
||||
el.isSameNode = function (target) {
|
||||
return (target && target.nodeName && target.nodeName === 'DIV')
|
||||
}
|
||||
```console
|
||||
$ bankai start app.js # dev: HMR + streaming SSR on every request
|
||||
$ bankai build app.js # client bundle, manifest, service worker,
|
||||
# brotli/gzip precompression, --prerender
|
||||
$ bankai serve # prod: 103 Early Hints, immutable assets,
|
||||
# streaming SSR, --h2
|
||||
$ bankai inspect # raw/gzip/brotli size report
|
||||
```
|
||||
|
||||
### Reordering lists
|
||||
It's common to work with lists of elements on the DOM. Adding, removing or
|
||||
reordering elements in a list can be rather expensive. To optimize this you can
|
||||
add an `id` attribute to a DOM node. When reordering nodes it will compare
|
||||
nodes with the same ID against each other, resulting in far fewer re-renders.
|
||||
This is especially potent when coupled with DOM node caching.
|
||||
Conventions over config: `style.css` next to your entry is bundled for
|
||||
the client (server code never sees it), `sw.js` becomes a service worker
|
||||
with the precache manifest injected, `<title>` follows your
|
||||
`DOMTitleChange` emits — on the server too.
|
||||
|
||||
```js
|
||||
var el = html`
|
||||
<section>
|
||||
<div id="first">hello</div>
|
||||
<div id="second">world</div>
|
||||
</section>
|
||||
`
|
||||
## Packages
|
||||
|
||||
| package | what it is |
|
||||
|---|---|
|
||||
| `@choojs/core` | the Choo class: stores, router, emitter, `toString`/`toStream`, `lazy()` |
|
||||
| `@choojs/html` | tagged templates: DOM in the browser, strings/streams on the server, `morph`, `hydrate`, `raw` |
|
||||
| `@choojs/component` | stateful components with lifecycle hooks; the hydration boundary |
|
||||
| `@choojs/devtools` | `window.choo` console tooling |
|
||||
| `@choojs/migrate` | `choo-migrate` — the v7 → v8 codemod |
|
||||
| `bankai` | the compiler & server above |
|
||||
|
||||
## Migrating from v7
|
||||
|
||||
```console
|
||||
$ npx @choojs/migrate .
|
||||
```
|
||||
|
||||
### Pruning dependencies
|
||||
We use the `require('assert')` module from Node core to provide helpful error
|
||||
messages in development. In production you probably want to strip this using
|
||||
[unassertify][unassertify].
|
||||
converts simple CJS, remaps every specifier, and tells you honestly what
|
||||
it didn't dare touch. The API surface is unchanged — `choo()` still works
|
||||
without `new`. See [`docs/migrating-v7-to-v8.md`](docs/migrating-v7-to-v8.md).
|
||||
|
||||
To convert inlined HTML to valid DOM nodes we use `require('nanohtml')`. This has
|
||||
overhead during runtime, so for production environments we should unwrap this
|
||||
using the [nanohtml transform][nanohtml].
|
||||
## Development
|
||||
|
||||
Setting up browserify transforms can sometimes be a bit of hassle; to make this
|
||||
more convenient we recommend using [bankai build][bankai] to build your assets for production.
|
||||
|
||||
## FAQ
|
||||
### Why is it called Choo?
|
||||
Because I thought it sounded cute. All these programs talk about being
|
||||
_"performant"_, _"rigid"_, _"robust"_ - I like programming to be light, fun and
|
||||
non-scary. Choo embraces that.
|
||||
|
||||
Also imagine telling some business people you chose to rewrite something
|
||||
critical for serious bizcorp using a train themed framework.
|
||||
:steam_locomotive::train::train::train:
|
||||
|
||||
### Is it called Choo, Choo.js or...?
|
||||
It's called "Choo", though we're fine if you call it "Choo-choo" or
|
||||
"Chugga-chugga-choo-choo" too. The only time "choo.js" is tolerated is if /
|
||||
when you shimmy like you're a locomotive.
|
||||
|
||||
### Does Choo use a virtual-dom?
|
||||
Choo uses [nanomorph][nanomorph], which diffs real DOM nodes instead of
|
||||
virtual nodes. It turns out that [browsers are actually ridiculously good at
|
||||
dealing with DOM nodes][morphdom-bench], and it has the added benefit of
|
||||
working with _any_ library that produces valid DOM nodes. So to put a long
|
||||
answer short: we're using something even better.
|
||||
|
||||
### How can I support older browsers?
|
||||
Template strings aren't supported in all browsers, and parsing them creates
|
||||
significant overhead. To optimize we recommend running `browserify` with
|
||||
[nanohtml][nanohtml] as a global transform or using [bankai][bankai] directly.
|
||||
```sh
|
||||
$ browserify -g nanohtml
|
||||
```console
|
||||
$ npm install
|
||||
$ npm test # 109 tests on node:test, zero framework deps
|
||||
$ npm run test:e2e # real Chromium: hydration, streaming, bankai
|
||||
$ npm run size # the wire-size budget
|
||||
$ npm run bench # vs nanohtml v1 and µhtml
|
||||
```
|
||||
|
||||
### Is choo production ready?
|
||||
Sure.
|
||||
## Credits
|
||||
|
||||
## API
|
||||
This section provides documentation on how each function in Choo works. It's
|
||||
intended to be a technical reference. If you're interested in learning choo for
|
||||
the first time, consider reading through the [handbook][handbook] first
|
||||
:sparkles:
|
||||
|
||||
### `app = choo([opts])`
|
||||
Initialize a new `choo` instance. `opts` can also contain the following values:
|
||||
- __opts.history:__ default: `true`. Listen for url changes through the
|
||||
history API.
|
||||
- __opts.href:__ default: `true`. Handle all relative `<a
|
||||
href="<location>"></a>` clicks and call `emit('render')`
|
||||
- __opts.cache:__ default: `undefined`. Override default class cache used by
|
||||
`state.cache`. Can be a a `number` (maximum number of instances in cache,
|
||||
default `100`) or an `object` with a [nanolru][nanolru]-compatible API.
|
||||
- __opts.hash:__ default: `false`. Treat hashes in URLs as part of the pathname,
|
||||
transforming `/foo#bar` to `/foo/bar`. This is useful if the application is
|
||||
not mounted at the website root.
|
||||
|
||||
### `app.use(callback(state, emitter, app))`
|
||||
Call a function and pass it a `state`, `emitter` and `app`. `emitter` is an instance
|
||||
of [nanobus](https://github.com/choojs/nanobus/). You can listen to
|
||||
messages by calling `emitter.on()` and emit messages by calling
|
||||
`emitter.emit()`. `app` is the same Choo instance. Callbacks passed to `app.use()` are commonly referred to as
|
||||
`'stores'`.
|
||||
|
||||
If the callback has a `.storeName` property on it, it will be used to identify
|
||||
the callback during tracing.
|
||||
|
||||
See [#events](#events) for an overview of all events.
|
||||
|
||||
### `app.route(routeName, handler(state, emit))`
|
||||
Register a route on the router. The handler function is passed `app.state`
|
||||
and `app.emitter.emit` as arguments. Uses [nanorouter][nanorouter] under the
|
||||
hood.
|
||||
|
||||
See [#routing](#routing) for an overview of how to use routing efficiently.
|
||||
|
||||
### `app.mount(selector)`
|
||||
Start the application and mount it on the given `querySelector`,
|
||||
the given selector can be a String or a DOM element.
|
||||
|
||||
In the browser, this will _replace_ the selector provided with the tree returned from `app.start()`.
|
||||
If you want to add the app as a child to an element, use `app.start()` to obtain the tree and manually append it.
|
||||
|
||||
On the server, this will save the `selector` on the app instance.
|
||||
When doing server side rendering, you can then check the `app.selector` property to see where the render result should be inserted.
|
||||
|
||||
Returns `this`, so you can easily export the application for server side rendering:
|
||||
|
||||
```js
|
||||
module.exports = app.mount('body')
|
||||
```
|
||||
|
||||
### `tree = app.start()`
|
||||
Start the application. Returns a tree of DOM nodes that can be mounted using
|
||||
`document.body.appendChild()`.
|
||||
|
||||
### `app.toString(location, [state])`
|
||||
Render the application to a string. Useful for rendering on the server.
|
||||
|
||||
### `choo/html`
|
||||
Create DOM nodes from template string literals. Exposes
|
||||
[nanohtml](https://github.com/choojs/nanohtml). Can be optimized using
|
||||
[nanohtml][nanohtml].
|
||||
|
||||
### `choo/html/raw`
|
||||
Exposes [nanohtml/raw](https://github.com/shama/nanohtml#unescaping) helper for rendering raw HTML content.
|
||||
|
||||
## Installation
|
||||
```sh
|
||||
$ npm install choo
|
||||
```
|
||||
|
||||
## See Also
|
||||
- [bankai](https://github.com/choojs/bankai) - streaming asset compiler
|
||||
- [stack.gl](http://stack.gl/) - open software ecosystem for WebGL
|
||||
- [yo-yo](https://github.com/maxogden/yo-yo) - tiny library for modular UI
|
||||
- [tachyons](https://github.com/tachyons-css/tachyons) - functional CSS for
|
||||
humans
|
||||
- [sheetify](https://github.com/stackcss/sheetify) - modular CSS bundler for
|
||||
`browserify`
|
||||
|
||||
## Support
|
||||
Creating a quality framework takes a lot of time. Unlike others frameworks,
|
||||
Choo is completely independently funded. We fight for our users. This does mean
|
||||
however that we also have to spend time working contracts to pay the bills.
|
||||
This is where you can help: by chipping in you can ensure more time is spent
|
||||
improving Choo rather than dealing with distractions.
|
||||
|
||||
### Sponsors
|
||||
Become a sponsor and help ensure the development of independent quality
|
||||
software. You can help us keep the lights on, bellies full and work days sharp
|
||||
and focused on improving the state of the web. [Become a
|
||||
sponsor](https://opencollective.com/choo#sponsor)
|
||||
|
||||
<a href="https://opencollective.com/choo/sponsor/0/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/1/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/2/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/3/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/4/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/5/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/6/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/7/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/8/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/9/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/10/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/11/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/12/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/13/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/14/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/15/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/16/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/17/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/18/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/19/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/20/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/21/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/22/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/23/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/24/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/25/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/26/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/27/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/28/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/sponsor/29/website" target="_blank"><img src="https://opencollective.com/choo/sponsor/29/avatar.svg"></a>
|
||||
|
||||
### Backers
|
||||
Become a backer, and buy us a coffee (or perhaps lunch?) every month or so.
|
||||
[Become a backer](https://opencollective.com/choo#backer)
|
||||
|
||||
<a href="https://opencollective.com/choo/backer/0/website" target="_blank"><img src="https://opencollective.com/choo/backer/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/1/website" target="_blank"><img src="https://opencollective.com/choo/backer/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/2/website" target="_blank"><img src="https://opencollective.com/choo/backer/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/3/website" target="_blank"><img src="https://opencollective.com/choo/backer/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/4/website" target="_blank"><img src="https://opencollective.com/choo/backer/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/5/website" target="_blank"><img src="https://opencollective.com/choo/backer/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/6/website" target="_blank"><img src="https://opencollective.com/choo/backer/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/7/website" target="_blank"><img src="https://opencollective.com/choo/backer/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/8/website" target="_blank"><img src="https://opencollective.com/choo/backer/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/9/website" target="_blank"><img src="https://opencollective.com/choo/backer/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/10/website" target="_blank"><img src="https://opencollective.com/choo/backer/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/11/website" target="_blank"><img src="https://opencollective.com/choo/backer/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/12/website" target="_blank"><img src="https://opencollective.com/choo/backer/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/13/website" target="_blank"><img src="https://opencollective.com/choo/backer/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/14/website" target="_blank"><img src="https://opencollective.com/choo/backer/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/15/website" target="_blank"><img src="https://opencollective.com/choo/backer/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/16/website" target="_blank"><img src="https://opencollective.com/choo/backer/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/17/website" target="_blank"><img src="https://opencollective.com/choo/backer/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/18/website" target="_blank"><img src="https://opencollective.com/choo/backer/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/19/website" target="_blank"><img src="https://opencollective.com/choo/backer/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/20/website" target="_blank"><img src="https://opencollective.com/choo/backer/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/21/website" target="_blank"><img src="https://opencollective.com/choo/backer/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/22/website" target="_blank"><img src="https://opencollective.com/choo/backer/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/23/website" target="_blank"><img src="https://opencollective.com/choo/backer/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/24/website" target="_blank"><img src="https://opencollective.com/choo/backer/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/25/website" target="_blank"><img src="https://opencollective.com/choo/backer/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/26/website" target="_blank"><img src="https://opencollective.com/choo/backer/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/27/website" target="_blank"><img src="https://opencollective.com/choo/backer/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/28/website" target="_blank"><img src="https://opencollective.com/choo/backer/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/choo/backer/29/website" target="_blank"><img src="https://opencollective.com/choo/backer/29/avatar.svg"></a>
|
||||
|
||||
## License
|
||||
[MIT](https://tldrlegal.com/license/mit-license)
|
||||
|
||||
[nanocomponent]: https://github.com/choojs/nanocomponent
|
||||
[nanolru]: https://github.com/s3ththompson/nanolru
|
||||
[bankai]: https://github.com/choojs/bankai
|
||||
[nanohtml]: https://github.com/choojs/nanohtml
|
||||
[browserify]: https://github.com/substack/node-browserify
|
||||
[budo]: https://github.com/mattdesl/budo
|
||||
[es2020]: https://github.com/yoshuawuyts/es2020
|
||||
[handbook]: https://github.com/yoshuawuyts/choo-handbook
|
||||
[hyperx]: https://github.com/substack/hyperx
|
||||
[morphdom-bench]: https://github.com/patrick-steele-idem/morphdom#benchmarks
|
||||
[nanomorph]: https://github.com/choojs/nanomorph
|
||||
[nanorouter]: https://github.com/choojs/nanorouter
|
||||
[yo-yo]: https://github.com/maxogden/yo-yo
|
||||
[unassertify]: https://github.com/unassert-js/unassertify
|
||||
[window-performance]: https://developer.mozilla.org/en-US/docs/Web/API/Performance
|
||||
Choo v8 carries forward a decade of work by
|
||||
[Yoshua Wuyts](https://github.com/yoshuawuyts) and the
|
||||
[choojs contributors](https://github.com/choojs), and builds on the
|
||||
ESM groundwork of the [@pirxpilot](https://github.com/pirxpilot) fork
|
||||
line. Per-file attribution headers name the module and version each port
|
||||
came from. MIT, as always.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# Deploying a choo v8 + bankai v10 app
|
||||
|
||||
The server side of a v8 app is plain ESM — there is no server bundle.
|
||||
Deploy is: ship the source + `dist/`, run a server.
|
||||
|
||||
## Plain Node (the blessed path)
|
||||
|
||||
```console
|
||||
$ bankai build app.js --title "my app" --prerender /
|
||||
$ bankai serve --h2 --port 443
|
||||
```
|
||||
|
||||
`bankai serve` does static assets (immutable + precompressed), 103 Early
|
||||
Hints, streaming SSR, and `window.initialState`. `--h2` matters:
|
||||
browsers only act on Early Hints over HTTP/2 or HTTP/3. For a public
|
||||
deployment use a real certificate (the built-in one is a generated
|
||||
localhost cert) or front it with a TLS-terminating proxy.
|
||||
|
||||
## Docker
|
||||
|
||||
```dockerfile
|
||||
FROM node:24-slim
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --omit=dev
|
||||
COPY . .
|
||||
RUN npx bankai build app.js
|
||||
EXPOSE 8080
|
||||
CMD ["npx", "bankai", "serve"]
|
||||
```
|
||||
|
||||
## Behind a proxy / CDN (and the HTTP/3 answer)
|
||||
|
||||
Node has no production HTTP/3 server yet (QUIC support in core is still
|
||||
experimental and not exposed as an h3 listener), and that's fine: h3 is
|
||||
infrastructure's job. Run `bankai serve` on h1 or h2 behind:
|
||||
|
||||
- **Caddy** — serves h2 + h3 by default, forwards to bankai:
|
||||
```
|
||||
example.com {
|
||||
reverse_proxy localhost:8080
|
||||
}
|
||||
```
|
||||
- **nginx ≥ 1.29** — `listen 443 quic;` for h3 plus
|
||||
`early_hints on;`-era support for forwarding the 103.
|
||||
- **Cloudflare / Fastly** — h3 at the edge automatically; both
|
||||
understand and propagate Early Hints (Cloudflare has since 2021).
|
||||
|
||||
bankai's contract with these layers is just headers: it emits the 103
|
||||
interim response and repeats the `Link` header on the final response, so
|
||||
any hints-aware edge can act on them — including over h3/QUIC, where
|
||||
Early Hints work exactly as on h2. What you should *not* expect is
|
||||
HTTP/2-style server push: it's removed from Chrome and Firefox and was
|
||||
never in HTTP/3. Hints + preload is the whole story now.
|
||||
|
||||
## Web-standard runtimes (Deno, Bun, edge)
|
||||
|
||||
`toStream()` returns a WHATWG `ReadableStream`, so the render half needs
|
||||
no adapter at all:
|
||||
|
||||
```js
|
||||
import createApp from './app.js'
|
||||
import { documentHead, documentTail, resolveApp } from 'bankai'
|
||||
|
||||
export default {
|
||||
async fetch (req) {
|
||||
const app = createApp()
|
||||
const state = {}
|
||||
const url = new URL(req.url)
|
||||
const body = app.toStream(url.pathname + url.search, state)
|
||||
// stream the body through a TransformStream that wraps head + tail,
|
||||
// or buffer if the route is small — your call, it's a web stream
|
||||
return new Response(body, {
|
||||
headers: { 'content-type': 'text/html; charset=utf-8' }
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Serve `dist/` via the platform's static hosting and reuse
|
||||
`dist/bankai.json` for the asset links. A first-class edge adapter
|
||||
(head/tail wrapping + Early Hints via the platform APIs) is on the v8
|
||||
roadmap; the primitives are deliberately already web-standard.
|
||||
@@ -0,0 +1,65 @@
|
||||
# Migrating a choo v7 app to v8
|
||||
|
||||
The API you know is intact: `choo()`, `app.use`, `app.route`,
|
||||
`app.mount`, `app.toString`, `emit`, stores, `state.events`. What changed
|
||||
is the plumbing: ESM everywhere, new package names, no compile step, and
|
||||
a platform baseline of Node ≥ 24 + Baseline Widely Available browsers.
|
||||
|
||||
## The fast path
|
||||
|
||||
```console
|
||||
$ npx @choojs/migrate . # rewrite in place
|
||||
$ npx @choojs/migrate --dry . # or report only
|
||||
```
|
||||
|
||||
The codemod converts simple top-level CJS to ESM, remaps specifiers, and
|
||||
prints a note for everything it won't guess at. Then:
|
||||
|
||||
1. Add `"type": "module"` to package.json.
|
||||
2. Swap dependencies for `@choojs/core`, `@choojs/html`, and friends.
|
||||
3. Run your app. Read the console: hydration now *tells you* when server
|
||||
and client markup disagree.
|
||||
|
||||
## Specifier map
|
||||
|
||||
| v7 | v8 |
|
||||
|---|---|
|
||||
| `choo` | `@choojs/core` |
|
||||
| `choo/html`, `nanohtml` | `@choojs/html` |
|
||||
| `nanohtml/raw` | `@choojs/html/raw` |
|
||||
| `nanomorph` | `@choojs/html/morph` |
|
||||
| `nanocomponent`, `choo/component` | `@choojs/component` |
|
||||
| `choo-devtools` | `@choojs/devtools` |
|
||||
| `choo-lazy-route` | `lazy()` from `@choojs/core` |
|
||||
| `nanobus`, `nanorouter`, `nanohref`, `nanotiming` | built into `@choojs/core` |
|
||||
| `nanoquery` | built in (`state.query`); use `URLSearchParams` elsewhere |
|
||||
| `nanoraf`, `nanoassert`, `nanolru` | retired — platform/built-in |
|
||||
|
||||
## Behavior changes to know about
|
||||
|
||||
- **URLs decode once, and never crash.** A literal `%` in a path routed
|
||||
v7 into a `URIError`; v8 keeps the raw segment. Params are no longer
|
||||
double-decoded (`%2540` → `%40`, not `@`). Routes and locations are
|
||||
NFC-normalized, so `café` matches however the é was composed.
|
||||
`state.href` is decoded for reading.
|
||||
- **Event handlers never serialize.** v7's server renderer emitted
|
||||
`onclick=""`; v8 emits nothing — handlers are behavior, not markup.
|
||||
- **Whitespace is preserved as authored** in browser renders (v7's
|
||||
browser transform collapsed it). Server and browser output are now
|
||||
byte-identical, which is what makes hydration adoption work.
|
||||
- **`mount()` hydrates.** First render adopts server DOM in place and
|
||||
warns on real mismatches; `<script>` tags and whitespace are ignored.
|
||||
- **`toString()` got stricter, `toStream()` got capable.** Sync renders
|
||||
behave exactly as v7. Async content — `state.prefetch` promises, lazy
|
||||
routes, promise-valued template holes — requires `toStream()`, and
|
||||
`toString()` says so instead of misrendering.
|
||||
- **Views can't return arrays** (unchanged from v7) and document-level
|
||||
roots (`html\`<body>…\``) work in the browser renderer too.
|
||||
|
||||
## Things that moved to bankai
|
||||
|
||||
sheetify → put a `style.css` next to your entry (client-only by
|
||||
construction). `split-require` → native `import()` + `lazy()`.
|
||||
`bankai start/build/serve/inspect` replace the v9 pipeline; HTTP/2 push
|
||||
never shipped and is dead in browsers — bankai v10 sends 103 Early Hints
|
||||
instead.
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
# RFC: choo v8 — same soul, modern engine
|
||||
|
||||
> Draft, to be posted as an issue on choojs/choo. Placeholders in
|
||||
> [brackets]. Tone check before posting: continuation, not correction.
|
||||
|
||||
---
|
||||
|
||||
Hi everyone — long-time choo user, org member, still shipping things
|
||||
with it. I'd like to propose (and have prototyped) a v8.
|
||||
|
||||
**The short version:** choo's design aged beautifully. The toolchain
|
||||
around it didn't — browserify, the nanohtml transform, Babel 6, Travis.
|
||||
v8 keeps the API and the philosophy and replaces every dead dependency
|
||||
with either the platform or ~300 lines we own. I've built the whole
|
||||
thing on a branch to make this a concrete conversation rather than a
|
||||
wishlist: **[link to v8 branch]**.
|
||||
|
||||
## What stays exactly the same
|
||||
|
||||
The 7-line counter is still the 7-line counter. `choo()`, stores,
|
||||
`emit`, tagged template views, morphing re-renders, `toString()` on the
|
||||
server. `choo()` still works without `new`. A codemod
|
||||
(`npx @choojs/migrate`) moves v7 apps across — validated against this
|
||||
repo's own example app.
|
||||
|
||||
## What changes
|
||||
|
||||
- **ESM only, Node ≥ 24, Baseline browsers. Zero compile steps.** The
|
||||
nanohtml browserify transform is replaced by a runtime template cache
|
||||
(parse once per call site, clone per render — the µhtml technique,
|
||||
adapted to our morph-based model). Consequence: **you can develop a
|
||||
choo app with an import map and view-source**, no tooling at all.
|
||||
- **Twelve packages become six.** nanobus/nanorouter/nanohref/nanotiming
|
||||
fold into `@choojs/core` as attributed ports; nanoquery/nanoraf/
|
||||
nanoassert retire to the platform. Smaller maintenance surface is the
|
||||
point — single-maintainer fatigue is what stalled v7, and I don't want
|
||||
to rebuild that failure mode.
|
||||
- **SSR grows up: `toStream()`.** Web-standard `ReadableStream`,
|
||||
progressive flushing through async template holes, `state.prefetch`
|
||||
for store data, and hydration that *adopts* server DOM and warns on
|
||||
real mismatches. Answers the async-route question too — #653 finally
|
||||
gets both halves: `lazy(() => import('./view.js'))` in the browser,
|
||||
awaited by `toStream` on the server.
|
||||
- **bankai v10** — same one-command soul, rebuilt as a thin shell over
|
||||
Vite 8/Rolldown. Only the client bundles (v8 server code runs
|
||||
as-authored — no server build to rot). 103 Early Hints replace the
|
||||
HTTP/2 push story, `style.css`/`sw.js` conventions replace sheetify
|
||||
and the env-var service-worker dance, `--prerender` and `--h2`
|
||||
included.
|
||||
- **The size claim, honestly restated.** v7 said 4kb for choo alone
|
||||
(templates compiled away by the transform). v8's *entire* framework —
|
||||
core + html engine + morph + hydration — is 7.97 kB min+gzip
|
||||
/ 7.15 kB brotli, enforced by CI. Apples-to-apples it's smaller;
|
||||
the README now says the true number.
|
||||
|
||||
Numbers, tests, and the full decision log live on the branch:
|
||||
109 unit tests (the v7 suite among them, behavior preserved) + 7
|
||||
Playwright tests in real Chromium covering hydration, streaming, and
|
||||
bankai dev/prod. Also fixed along the way: the non-ASCII/`%` URL
|
||||
crashes (routing now parses with WHATWG URL, decodes once, and
|
||||
NFC-normalizes).
|
||||
|
||||
## What I'm asking
|
||||
|
||||
1. **@yoshuawuyts** — a blessing costs one emoji and would mean a lot.
|
||||
Zero obligation beyond that; the credits already say what this
|
||||
builds on.
|
||||
2. **@pirxpilot** — your ESM fork line is the only living continuation
|
||||
of this code and v8's core started from that groundwork. I'd love to
|
||||
co-maintain rather than fork-in-parallel; either way, thank you.
|
||||
3. **npm publish rights** for `choo`, `bankai`, and the nano*
|
||||
packages, or a nod to ship under `@choojs/*` with the old names as
|
||||
deprecation pointers after 8.0.0 exists (nothing gets deprecated
|
||||
before then; master and v7 stay untouched).
|
||||
4. **API feedback**, especially: `lazy()` wrapper vs thenable route
|
||||
handlers; the `state.prefetch` contract; anything in the migration
|
||||
doc that reads as a betrayal rather than an upgrade.
|
||||
|
||||
**Comment window: three weeks from posting.** After that I'll take
|
||||
silence as consent, keep working on the v8 branch in the open, and cut
|
||||
pre-releases under a `next` tag. If this lands wrong for anyone, say so
|
||||
— the plan bends.
|
||||
|
||||
🚂🚋🚋🚋🚋🚋
|
||||
+17
-5
@@ -108,11 +108,23 @@ Server string rendering is on par with nanohtml v1 (~13k ops/s, within
|
||||
All proven end-to-end in real Chromium: both dev and prod pages
|
||||
hydrate with zero console errors and zero mismatch warnings, and
|
||||
the 103 interim response is asserted at the HTTP level.
|
||||
- [ ] Phase 4 follow-ups: HTTP/2 dev server with local certs (browsers
|
||||
only act on Early Hints over h2/h3), static prerender of
|
||||
enumerable routes, SSR `<title>` from state (needs a head hook in
|
||||
toStream), edge-runtime deploy recipe (`new Response(toStream())`
|
||||
— the server core is already web-standard).
|
||||
- [x] Phase 4 follow-ups: `bankai serve --h2` (local certs via openssl,
|
||||
103 verified with a real h2 client), `bankai build --prerender`,
|
||||
SSR `<title>` from state (the server reads the first stream chunk
|
||||
before writing the head), the `style.css` convention (client-only
|
||||
CSS — the sheetify answer), and the CI wire-size budget
|
||||
(`npm run size`: 7.97 kB min+gzip for the whole framework,
|
||||
budget 8.5 kB).
|
||||
- [x] Phase 5: README rewritten for v8 (honest size claim, credits),
|
||||
`docs/migrating-v7-to-v8.md`, `docs/deploy.md` (Node, Docker,
|
||||
proxy/CDN with the HTTP/3 story — h3 is infrastructure's job,
|
||||
bankai's contract is the 103 + Link headers any hints-aware edge
|
||||
propagates — and web-standard runtimes), and `docs/rfc.md`: the
|
||||
draft announcement to post on choojs/choo, with the pings, the
|
||||
asks, and the three-week comment window.
|
||||
- [ ] Post-RFC: publish pre-releases under a next tag, refresh
|
||||
choo.io/handbook (separate repos), deprecation pointers on retired
|
||||
packages only after 8.0.0, first-class edge adapter for bankai.
|
||||
- [ ] Phase 4: bankai v10 (Vite 8/Rolldown shell, SSR middleware,
|
||||
103 Early Hints, service worker, precompression)
|
||||
- [ ] Phase 5: docs, examples, launch
|
||||
|
||||
Reference in New Issue
Block a user