feat(core): add @choojs/core — ESM port of choo with consolidated nano* internals

Same public API as choo 7.1.0 (use/route/start/mount/toString/emit; choo()
callable with or without new, Choo exported). nanobus, nanorouter+wayfarer,
nanolru+component cache, nanoraf, nanohref, nanotiming, document-ready and
scroll-to-anchor are ported into lib/ with per-file attribution; nanoquery
is replaced by URLSearchParams. The v7 node test suite is ported from tape
to node:test and passes unchanged in behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014NgfSjHE11oFpoSnLVKLXd
This commit is contained in:
Bendik Aagaard Lynghaug
2026-09-08 15:59:26 +02:00
co-authored by Claude Fable 5
parent 90cca611c9
commit 4ad3d02207
12 changed files with 1322 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
// Ported from nanorouter 4.0.0 and wayfarer 7.0.1 with its trie (MIT)
// https://github.com/choojs/nanorouter — https://github.com/yoshuawuyts/wayfarer
// Consolidated into a single module: trie-based router with :params and
// * wildcards, plus nanorouter's URL normalization on top.
import { ok, equal, notEqual } from './assert.js'
function has (object, property) {
return Object.prototype.hasOwnProperty.call(object, property)
}
class Trie {
constructor () {
this.trie = { nodes: {} }
}
// create a node on the trie at route and return it
create (route) {
equal(typeof route, 'string', 'route should be a string')
// strip leading '/' and split routes
const routes = route.replace(/^\//, '').split('/')
const self = this
function createNode (index, trie) {
const thisRoute = has(routes, index) && routes[index]
if (thisRoute === false) return trie
let node = null
if (/^:|^\*/.test(thisRoute)) {
// if node is a name match, set name and append to ':' node
if (!has(trie.nodes, '$$')) {
node = { nodes: {} }
trie.nodes.$$ = node
} else {
node = trie.nodes.$$
}
if (thisRoute[0] === '*') {
trie.wildcard = true
}
trie.name = thisRoute.replace(/^:|^\*/, '')
} else if (!has(trie.nodes, thisRoute)) {
node = { nodes: {} }
trie.nodes[thisRoute] = node
} else {
node = trie.nodes[thisRoute]
}
return createNode(index + 1, node)
}
return createNode(0, self.trie)
}
// match a route on the trie and return the node
match (route) {
equal(typeof route, 'string', 'route should be a string')
const routes = route.replace(/^\//, '').split('/')
const params = {}
function search (index, trie) {
// either there's no match, or we're done searching
if (trie === undefined) return undefined
const thisRoute = routes[index]
if (thisRoute === undefined) return trie
if (has(trie.nodes, thisRoute)) {
// match regular routes first
return search(index + 1, trie.nodes[thisRoute])
} else if (trie.name) {
// match named routes
try {
params[trie.name] = decodeURIComponent(thisRoute)
} catch (e) {
return search(index, undefined)
}
return search(index + 1, trie.nodes.$$)
} else if (trie.wildcard) {
// match wildcards
try {
params.wildcard = decodeURIComponent(routes.slice(index).join('/'))
} catch (e) {
return search(index, undefined)
}
// return early, or else search may keep recursing through the wildcard
return trie.nodes.$$
} else {
// no matches found
return search(index + 1)
}
}
let node = search(0, this.trie)
if (!node) return undefined
node = Object.assign({}, node)
node.params = params
return node
}
// mount a trie onto a node at route
mount (route, trie) {
equal(typeof route, 'string', 'route should be a string')
equal(typeof trie, 'object', 'trie should be a object')
const split = route.replace(/^\//, '').split('/')
let node = null
if (split.length === 1) {
node = this.create(split[0])
} else {
node = this.create(split.join('/'))
}
Object.assign(node.nodes, trie.nodes)
if (trie.name) node.name = trie.name
// delegate properties from '/' to the new node
// '/' cannot be reached once mounted
if (node.nodes['']) {
Object.keys(node.nodes['']).forEach(function (key) {
if (key === 'nodes') return
node[key] = node.nodes[''][key]
})
Object.assign(node.nodes, node.nodes[''].nodes)
delete node.nodes[''].nodes
}
}
}
function wayfarer (dft) {
const _default = (dft || '').replace(/^\//, '')
const _trie = new Trie()
emit._trie = _trie
emit.on = on
emit.emit = emit
emit.match = match
emit._wayfarer = true
return emit
function on (route, cb) {
equal(typeof route, 'string')
equal(typeof cb, 'function')
route = route || '/'
if (cb._wayfarer && cb._trie) {
_trie.mount(route, cb._trie.trie)
} else {
const node = _trie.create(route)
node.cb = cb
node.route = route
}
return emit
}
function emit (route, ...args) {
const matched = match(route)
return matched.cb(matched.params, ...args)
}
function match (route) {
notEqual(route, undefined, "'route' must be defined")
const matched = _trie.match(route)
if (matched && matched.cb) return new Route(matched)
const dft = _trie.match(_default)
if (dft && dft.cb) return new Route(dft)
throw new Error("route '" + route + "' did not match")
}
function Route (matched) {
this.cb = matched.cb
this.route = matched.route
this.params = matched.params
}
}
// electron support
const isLocalFile = /file:\/\//.test(
typeof window === 'object' &&
window.location &&
window.location.origin
)
/* eslint-disable no-useless-escape */
const electron = '^(file:\/\/|\/)(.*\.html?\/?)?'
const protocol = '^(http(s)?(:\/\/))?(www\.)?'
const domain = '[a-zA-Z0-9-_\.]+(:[0-9]{1,5})?(\/{1})?'
const qs = '[\?].*$'
/* eslint-enable no-useless-escape */
const stripElectron = new RegExp(electron)
const prefix = new RegExp(protocol + domain)
const normalize = new RegExp('#')
const suffix = new RegExp(qs)
// replace everything in a route but the pathname and hash
function pathname (routename, isElectron) {
if (isElectron) routename = routename.replace(stripElectron, '')
else routename = routename.replace(prefix, '')
return decodeURI(routename.replace(suffix, '').replace(normalize, '/'))
}
export default class Nanorouter {
constructor (opts) {
opts = opts || {}
this.router = wayfarer(opts.default || '/404')
}
on (routename, listener) {
equal(typeof routename, 'string')
routename = routename.replace(/^[#/]/, '')
this.router.on(routename, listener)
}
emit (routename) {
equal(typeof routename, 'string')
routename = pathname(routename, isLocalFile)
return this.router.emit(routename)
}
match (routename) {
equal(typeof routename, 'string')
routename = pathname(routename, isLocalFile)
return this.router.match(routename)
}
}
export { Trie, wayfarer, ok }