diff --git a/README.md b/README.md index 77a8417..40b287a 100644 --- a/README.md +++ b/README.md @@ -1,701 +1,129 @@ -

Choo

+

choo

- :steam_locomotive::train::train::train::train::train: + ๐Ÿš‚๐Ÿš‹๐Ÿš‹๐Ÿš‹๐Ÿš‹๐Ÿš‹
- Fun functional programming -
-
- A 4kb framework for creating sturdy frontend applications + The sturdy frontend framework โ€” now with a modern engine under the same hood.
-
+
-
- - - API stability - - - - NPM version - - - - Build Status - - - - Test Coverage - - - - Download - - - - Standard - -
+> **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. -
-

- - Website - - | - - Handbook - - | - - Ecosystem - - | - - - - - - Contributing - - | - - Reddit - - | - - Chat - -

-
+## What is this -
- The little framework that could. Built with โค๏ธŽ by - Yoshua Wuyts and - - contributors - -
+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` - -

count is ${state.count}

- - - ` - - 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 `` 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. diff --git a/docs/deploy.md b/docs/deploy.md new file mode 100644 index 0000000..ecff2c2 --- /dev/null +++ b/docs/deploy.md @@ -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. diff --git a/docs/migrating-v7-to-v8.md b/docs/migrating-v7-to-v8.md new file mode 100644 index 0000000..b5a5248 --- /dev/null +++ b/docs/migrating-v7-to-v8.md @@ -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. diff --git a/docs/rfc.md b/docs/rfc.md new file mode 100644 index 0000000..4c8af56 --- /dev/null +++ b/docs/rfc.md @@ -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. + +๐Ÿš‚๐Ÿš‹๐Ÿš‹๐Ÿš‹๐Ÿš‹๐Ÿš‹ diff --git a/docs/v8.md b/docs/v8.md index 9610743..3014cd8 100644 --- a/docs/v8.md +++ b/docs/v8.md @@ -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