NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
npm · #1964 most downloaded on npm
Lightweight and fast router for JavaScript.
Last release today
03 Oct 2026
Release timing varies
gaps range from 9 days to 4 months
Nearly every release is documented
notes for 34 of 37 stable releases
Nothing withdrawn
no release was ever pulled
2 years old
37 releases · first in 2024
Nothing published for this version
regexp: Build determinize patterns lazily
One column per month.
routeToRegExp output for them no longer backtracks polynomially, which closes the ReDoS class of CVE-2024-45296 ( e3e9a25 , 0b8fde4 ).
This release finishes aligning rou3's pattern syntax with URLPattern. The biggest change is that * is now a greedy catch-all that spans segments. Check the sections below that apply to how you use rou3.
* is a greedy catch-all (#240)* used to match a single segment ([^/]*). It is now URLPattern's * ((.*)), so it also matches across /. A trailing /foo/* still matches /foo, as in v0.11.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/foo/* |
/foo/a/b |
no match | { 0: "a/b" } |
/foo/* |
/foo/ |
{} |
{ 0: "" } |
/foo/* |
/foo |
{} |
{} |
/*/x |
/a/b/x |
no match | { 0: "a/b" } |
/*.png |
/a/b.png |
no match | { 0: "a/b" } |
/**.md |
/a/b.md |
{ _: "a", 0: "b" } |
{ 0: "a/b" } |
/a/:x?/* |
/a/b |
{ 0: "b" } |
{ x: "b" } |
* now apply at every depth. For example, "/admin/*": { auth: false } now covers /admin/a/b, not just /admin/a.* before more of the route (/*/x) needs at least one segment, so it does not match /x.Migrate:
/users/:id?. To get v0.11's exact behavior, use /users{/([^\x2f]*)}?.* with ([^\x2f]*). For example, /*.png becomes /([^\x2f]*).png.A route can now hold only one of *, **, :x+, :x*, (.*) or :x(.*). Routes with more than one throw rou3: a route can have only one ....
| Pattern | v0.11 | Write instead |
|---|---|---|
/**/*.png |
{ _: "a", 0: "b" } on /a/b.png |
/**/:file.png |
/file-*-*.png |
{ 0: "a", 1: "b" } |
/file-:a-*.png, or a constraint like ([^\x2f]*) for one of them |
/*/x/*, /*/**, /*/:p+ |
accepted | a named :param for all but one |
(.*) and :name(.*) are catch-alls (#242)A written (.*) is now the same as *, and :name(.*) is a * keyed by name. Both match across segments, as in URLPattern. In v0.11 they were limited to one segment, while routeToRegExp already matched across /.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/foo/(.*) |
/foo/a/b |
no match | { 0: "a/b" } |
/foo/:p(.*) |
/foo/a/b |
no match | { p: "a/b" } |
/foo/:p(.*) |
/foo/ |
no match | { p: "" } |
:p(.*)?, (.*)+ and other modifiers on these groups now throw, as they do on *./ ((.+), (.*?), ([^x]*)) still stay within one segment.Migrate: for a single-segment value, use :p or :p([^\x2f]*).
** has a numbered key (#234)A bare ** is now an unnamed capture, as in URLPattern. It is keyed "0", "1", … together with * and unnamed (…) groups, in pattern order across the whole pattern.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/foo/** |
/foo/a/b |
{ _: "a/b" } |
{ 0: "a/b", _: "a/b" } |
/foo/** |
/foo |
{ _: "" } |
{} |
/x{(\d+)}?/* |
/x/b |
{ 0: "b" } |
{ 1: "b" } |
params._ still works, but it is deprecated.** that matches zero segments now leaves its key out. It used to set _: "".**, or after an optional group that holds unnamed captures, shift up. A left-out group still uses up its numbers.routeToRegExp output, the ** group is _0, _1, … instead of _.InferRouteParams, the ** key is string | undefined, plus a deprecated _?: string.Migrate: read params["0"] ?? "" instead of params._, or name the capture: /foo/**:path.
addRoute now percent-encodes a pattern's literal text once, like URLPattern. Lookup paths are never decoded or encoded, so pass an encoded pathname such as new URL(req.url).pathname.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/café |
/caf%C3%A9 |
no match | match |
/café |
/café |
match | no match |
/a b |
/a%20b |
no match | match |
routeToRegExp, routeNodeKeys and the compiled matchers also use the encoded text. For example, routeToRegExp("/café") is ^\/caf%C3%A9\/?$.Migrate: look up routes with the raw, encoded pathname. Stop calling decodeURI() / decodeURIComponent() on it before findRoute.
:name+, :name* and **:name now behave like URLPattern's [^/]+(?:/[^/]+)*: each segment they take needs a value.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/a/:x* |
/a// |
{ x: "" } |
no match |
/foo/:bar+ |
/foo/a//b |
{ bar: "a//b" } |
no match |
/foo/**:bar |
/foo//a |
{ bar: "/a" } |
no match |
/foo/:bar+ |
/foo/a/b/ |
{ bar: "a/b" } |
{ bar: "a/b" } (one trailing slash is still ignored) |
These paths now fall through to a less specific route, or to no match.
Migrate: to accept empty segments, use *, ** or :name(.*).
A ? param that follows a capture in the same segment is now matched in place, as in URLPattern. The capture before it is greedy, and the route that includes the param no longer wins.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/:a(\d+):b? |
/12 |
{ a: "1", b: "2" } |
{ a: "12" } |
/*-:x? |
/-- |
{ 0: "", x: "-" } |
{ 0: "-" } |
pre-:x? still matches pre- and pre-<value>.{:name}? group that follows text and ends its segment is read as :name?. For example, /a/*{:x}? is /a/*:x?.Migrate: if you need the param to take its share, constrain the capture before it, e.g. /:a(\d):b?.
} ends a param before a regex group (#236)A regex group right after a {…} group that ends in a param is now an unnamed capture next to the param, as in URLPattern. It is no longer that param's constraint.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/{:foo}(.*) |
/foobarbaz |
{ foo: "foobarbaz" } |
{ foo: "f", 0: "oobarbaz" } |
/{:foo}?(…), /:foo{}(…) and /:foo{(x)}? change the same way./{:foo}{*}, /{:foo}?*, /{:foo}(x)?, /*{*}, and {?} / {+} after a param now throw.Migrate: write the constraint directly on the param: /:foo(.*).
{/…} group is absolute (#239)A pattern that starts with a {/…} group no longer gets an extra / in front.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
{/:a}?/b |
/b |
no match | {} |
{/:a}?/b |
/x/b |
no match | { a: "x" } |
{/:a}?/b |
//b |
{} |
no match |
Text right after a leading {/…}? ({/a}?b, {/:a}?.png) now throws.
. / .. segments in patterns are resolved (#246). and .. segments, including %2e forms, are resolved when a route is added, the same way new URL() resolves a path.
| Pattern | Path | v0.11 | v0.12 |
|---|---|---|---|
/foo/../bar |
/bar |
no match | match |
/foo/../bar |
/foo/../bar |
match | no match |
/\.\./bar |
/../bar |
match | match (escaped dots stay literal) |
/:id/.. |
accepted | throws |
rou3: `.` / `..` segment next to a param, catch-all or group.removeRoute resolves dot segments too. For example, removeRoute(router, "GET", "/docs/../api") removes /api.normalize: true.Migrate: escape the dots (\.\.) to match a literal .. segment.
| Pattern | v0.12 | Write instead |
|---|---|---|
a raw tab, LF or CR anywhere ("/a\tb") |
throws | %09, %0A, %0D (or \t, \n, \r inside a constraint) |
-- / && in a constraint's character class (/:x([[a-z]--a])) |
throws (URLPattern reads these as v-flag set operations) |
escape them for literal chars: [a\-\-z], [a\&\&b] |
routeToRegExp:
** groups are named _0, _1, … (#234).:name+ / :name* / **:name use [^/]+(?:/[^/]+)* (#241).* that can't be inlined now throws rou3: the regex for "…" repeats a named group…. Examples: /{b}?/*, /files/*{.:ext}?/raw.regExpToRoute:
** (old _ groups are still accepted).-- / && inside a constraint class.\/([^/]*)\/?$, which no route now matches.compareRoutes: ("/a/*", "/a/**") is now "equal" (it was "subset"). /a/:x* now equals /a{/**:y}? and is a subset of /a/**.routeNodeKeys: "/a/*" gives ["/a/**"] (it was ["/a/*"]). Param segments are keyed :_0, :_1, ….normalize: true: a trailing . / .. keeps its trailing slash, as in WHATWG URL. /a//. is now /a// and no longer matches /a.InferRouteParams: a trailing whole-segment *, (.*) or :name(.*) is typed string | undefined. **<text> is one key.findAllRoutes lists each addRoute call once. Before, /a/:x?/:y? on /a/b returned the route twice ({ x } and { y }). Now it returns it once, with the variant findRoute picks (1c8aaa3)./f/:name.png and /f/:name.:ext(png|jpg) both beat /f/:name.:ext on /f/a.png (0f437bc)./blog/:year-:month-:day.html) now match in linear time in findRoute and the compiled matchers. routeToRegExp output for them no longer backtracks polynomially, which closes the ReDoS class of CVE-2024-45296 (e3e9a25, 0b8fde4).RouterContext properties are marked @internal (c0f6015).:name* capture an empty value (#232)** capture like URLPattern (#234)} before a regex group (#236)/ to a pattern that starts with a {/…} group (#239)* a greedy catch-all like URLPattern (#240):name+ / :name* like URLPattern (#241)(.*) and :name(.*) as greedy catch-alls like URLPattern (#242)-- / && in a constraint class like URLPattern (#245). / .. segments in patterns like URLPattern (#246)RouterContext props as internal (c0f6015)The old (router, "name", opts) form still works but is deprecated.
Most apps can upgrade with no changes. Route patterns now follow URLPattern more closely, so check the sections below that apply to how you use rou3.
- ends a param name (#222, #230)Param names are now [A-Za-z_]\w*, as in URLPattern. A - is no longer part of the name, so :year-:month is two params again.
| Pattern | Path | v0.10 | v0.11 |
|---|---|---|---|
/api/:test-id |
/api/abc |
{ "test-id": "abc" } |
no match |
/api/:test-id |
/api/abc-id |
{ "test-id": "abc-id" } |
{ test: "abc" } |
/blog/:year-:month |
/blog/2024-05 |
{ "year-": "2024-0", month: "5" } |
{ year: "2024", month: "05" } |
/blog/:id{-:title}? |
/blog/1-hi |
{ "id-": "1-h", title: "i" } |
{ id: "1", title: "hi" } |
{ or } also ends a name: /:a{b}? is the param a followed by an optional b (it used to be one param named ab).InferRouteParams reads names the same way, so the types change too (/blog/:year-:month gives { year, month }).Migrate: rename params that contain -, e.g. :test-id → :test_id or :testId.
When a segment holds more than one param, the first one now matches as little as it can, like URLPattern.
| Pattern | Path | v0.10 | v0.11 |
|---|---|---|---|
/:name.:ext |
/a.tar.gz |
{ name: "a.tar", ext: "gz" } |
{ name: "a", ext: "tar.gz" } |
/:a-:b |
/x-y-z |
{ "a-": "x-y-", b: "z" } |
{ a: "x", b: "y-z" } |
Migrate: to keep the old split, constrain the param that follows: /:name.:ext(\w+) gives { name: "a.tar", ext: "gz" }.
prefix-:param? makes only the param optional (#230)A ? on a param that does not start its segment now makes only that param optional, not the whole segment.
| Pattern | Path | v0.10 | v0.11 |
|---|---|---|---|
/a/pre-:x? |
/a/pre-b |
{ x: "b" } |
{ x: "b" } |
/a/pre-:x? |
/a/pre- |
no match | match (no x) |
/a/pre-:x? |
/a |
match | no match |
Migrate: to make the whole segment optional, write /a/{pre-:x}?.
A :name, :name+ or **:name now needs a value, as in URLPattern. Handlers no longer need to guard against "" for these params.
| Pattern | Path | v0.10 | v0.11 |
|---|---|---|---|
/foo/:bar |
/foo// |
{ bar: "" } |
no match |
/foo/:bar/x |
/foo//x |
{ bar: "" } |
no match |
/foo/:bar+ |
/foo// |
{ bar: "" } |
no match |
/foo/**:rest |
/foo// |
{ rest: "" } |
no match |
/foo/*, /foo/**, /foo/:bar* |
/foo// |
match | match (unchanged) |
Migrate: if you need to accept an empty segment, use *, **, :name*, or a constraint that allows empty, such as :id(\d*).
A route added with method "" could disappear when a route for a specific method landed on the same place in the tree, even if that method route didn't match. Now both are always considered, and the more specific route wins. On a tie, the method route still wins.
addRoute(router, "", "/u/*", "any");
addRoute(router, "GET", "/u/:id(\\d+)", "get");
findRoute(router, "GET", "/u/abc"); // v0.10: undefined v0.11: "any"
findAllRoutes(router, "GET", "/u/42"); // v0.10: ["get"] v0.11: ["any", "get"]findAllRoutes (and compiled matchAll) now return both routes, method-agnostic first, so a "" middleware or auth route is no longer silently dropped."" route now beats a broader method route: with "" /u/:id(\d+) and GET /u/*, GET /u/1 returns the "" route (v0.10 returned GET /u/*).Migrate: nothing to change unless you relied on a method route hiding a "" route on the same path. If so, expect the extra match in findAllRoutes.
Outside a regex constraint, any \x is now a literal x, as in URLPattern. Before, the router kept the backslash in static segments, while routeToRegExp already dropped it, so the two disagreed.
| Pattern | Path | v0.10 | v0.11 |
|---|---|---|---|
/foo\.bar |
/foo.bar |
no match | match |
/a\*b |
/a*b |
no match | match |
/a/\(:x |
/a/(1 |
no match | { x: "1" } |
\\ is a literal backslash.\/ or a trailing \ throws.Migrate: if a route really needs a backslash in the path, write \\.
These now throw, because the router tests a constraint against one segment while routeToRegExp puts it inside the whole-path regex, so they matched different paths.
| Pattern | v0.11 | Write instead |
|---|---|---|
/:slug((?!admin)\w+) |
throws (look-around) | /:slug(\w+) plus a static /admin route, which always wins over a param |
/:x(^\d+$) |
throws (anchor) | /:x(\d+) (constraints already match the whole segment) |
/:x((a)) |
throws (capturing group inside a constraint) | /:x((?:a)) |
/a/(), /a/(?:a|b), /a/(?<n>x) |
throws (empty or (? group) |
/a/:x((?:a|b)), /a/:n(x) |
Migrate: rewrite constraints as shown. Look-arounds have no direct replacement: register the excluded paths as their own routes or check the value in your handler.
addRoute (and routeToRegExp) now throw rou3: <what> (<your pattern>) for syntax that used to be accepted with a surprising meaning. The error quotes the route, so failures show up at startup.
| Pattern | v0.10 | Write instead |
|---|---|---|
/a/:x(\d+)+, /a/pre-:x+ |
dropped the constraint or prefix (/a/:x(\d+)+ matched /a/b/c as { x: "b/c" }) |
+ / * only after a whole-segment :name (/a/:x+) |
/a/**?, /a/*+, /:x??, /p/**:x? |
accepted, with an unclear meaning | /p/:x* for an optional catch-all |
/foo? |
never matched (lookup paths have no query) | /foo\? for a literal ? |
/a**b |
read as two wildcards | /a*b, or /a\*\*b for literal stars |
/a/**:x.json, /a/**:x(\d+) |
param named "x.json" / "x(\d+)" |
a plain **:name as the whole last segment |
/:0, /:1st, /:café, /:id$ |
accepted as names | /:v1, /:_1, /:cafe, /:id\$ |
/a/:, /a/x: |
literal : |
/a/\: |
/a/:x/:x, /a/:x.:x |
later value won, or a raw SyntaxError |
unique names |
/a/{b, /a/b}, /a/{{b}} |
literal or mis-parsed braces | \{ / \} for literal braces, no nested groups |
/a/{b}+, /a{/b}* |
threw a non-rou3: error |
not supported (only {…} and {…}?) |
| U+FFFD–U+FFFF in a pattern | accepted | not allowed (used internally) |
Migrate: fix each pattern the error points to, using the right-hand column.
regExpToRoute rejects unanchored regexes and u / v flags (#218)An unanchored regex matches any path that contains it, so turning it into a whole-path route was wrong. The u / v flags change what a constraint means, so they can't be represented either.
| Input | v0.10 | v0.11 |
|---|---|---|
/\/users\/(?<id>\d+)/ |
/users/:id(\d+) |
throws |
/^\/users/ |
/users |
throws |
/^\/a\/(?<x>\p{L}+)$/u |
/a/:x(\p{L}+) |
throws |
/^\/users$/ |
/users |
/users |
routeToRegExp are always anchored and keep working. The one exception is the v0.10 output for :x(\d+)+-style routes, which now throws since that pattern itself is rejected.Migrate: anchor hand-written regexes with ^…$ and drop the u / v flags.
compileRouterToString emits route data as plain JSON (#225)Route data in the generated code now always goes through JSON.stringify, so the output is always valid code.
| Route data | v0.10 output | v0.11 output |
|---|---|---|
new Date(0) |
invalid code | "1970-01-01T00:00:00.000Z" |
{ toJSON: () => ({ a: 1 }) } |
invalid code | { a: 1 } |
{ toJSON: () => "code" } |
emitted code as raw JS |
emitted as the string "code" |
| a function, symbol or bigint (anywhere in the data) | silently dropped, or a raw TypeError |
throws a rou3: error |
compileRouterToString now takes an options object: compileRouterToString(router, { functionName, matchAll, serialize }). The old (router, "name", opts) form still works but is deprecated.RouterCompilerOptions is deprecated in favor of CompileRouterToStringOptions and CompileRouterOptions.compileRouter<T>(router, { matchAll: true }) is now typed as returning an array.Migrate: if your data contains functions or used toJSON() to emit code, pass serialize: (data) => "<js expression>".
package.json now declares "engines": { "node": ">=20.19.0" }.
findRoute and findAllRoutes return a fresh { data, params } object on every call. Static matches and params: false results no longer expose internal fields, and mutating a result no longer affects later lookups (#219).0, false, "") is returned as-is instead of null. Missing data is still null (#219).findOverlappingRoutes reports a route with optional parts once, and reports distinct routes separately even when they share the same data object (#220).matchAll overloads and JSON route data (#225)- no word char follows (#222)\x as a literal x, reject anchors in constraints (#228)matchAll overloads and JSON route data (#225)- no word char follows (#222)\x as a literal x, reject anchors in constraints (#228)matchAll overloads and JSON route data (#225)\x as a literal x, reject anchors in constraints (#228)router: Throw a clear error for a ( that does not close in its segment
router: Quote the pattern as written in the one- error
Most apps can upgrade with no changes. Check the sections below that apply to how you use rou3.
Most apps can upgrade with no changes. Check the sections below that apply to how you use rou3.
Lookup now ignores at most one trailing slash, the same as the non-strict defaults of Express, find-my-way and Hono. Registering routes works as before.
| Path | v0.9 | v0.10 |
|---|---|---|
/users/123/ |
✅ { id: "123" } |
✅ { id: "123" } |
/users/123//, /about// |
✅ | ❌ no match |
// (root /) |
✅ | ❌ no match |
/users// on /users/:id |
❌ | ✅ { id: "" } |
Migrate: if you need to accept repeated trailing slashes, collapse them before lookup, e.g. path.replace(/\/{2,}$/, "/").
** is no longer terminal (#216)Segments after ** used to be silently dropped. They are now matched from the end of the path:
addRoute(router, "GET", "/**/_payload.json", payload);
findRoute(router, "GET", "/blog/post/_payload.json"); // params: { _: "blog/post" }
addRoute(router, "GET", "/**.md", doc); // shorthand for `/**/*.md`/a/**/b used to behave like /a/**. Now the path must end in /b. If you want the old behavior, register /a/**./a/:x+/b now requires the trailing /b (URLPattern semantics).**. /**/** and /a/:x+/b/:y+ now throw.removeRoute(router, "GET", "/a/**/b") no longer removes /a/**.** matches, matches are ranked from the last segment backwards (literal > regex param > param > **). Routers without such routes behave as before.removeRoute removes only the route you pass (#202)Previously, removing a route also removed every other route on the same node and method. For example, removing /a/:id also removed /a/:userId, and removing /a/** also removed /a/**:rest. Now only the given pattern is removed.
Migrate: if your code relied on this, call removeRoute once for each pattern you registered.
undefined (#198)InferRouteParams now types optional params (:id?, :id*) and a trailing bare * (which can match zero segments) as string | undefined. It also strips constraints and modifiers from param names (:id(\d+) → id).
Migrate: handle undefined where you assumed a string, e.g. params.id ?? "".
routeToRegExp output changed (#211, #204, #217)The regex now matches exactly the paths findRoute matches. If you use it as a guard or for scope checks, you should upgrade:
/api/** no longer matches /apifoo./admin/**:p didn't match /admin/x\ry, even though the router routed that path.** are kept (see above).Other changes:
regexp and Rust regex./a/:x on /a//) leaves the group unset (undefined), while the router returns ""./files/:path/**:path, /a/:_/**) now throw a rou3: error instead of a raw SyntaxError./files/:name{.:ext}?, /users{/:id}?/posts/:post) no longer emits duplicate group names, so it compiles on Node 22.regExpToRoute still reads regexes from older versions. Some routes come back in an equivalent form, e.g. /a/:x?/:y? → /a{/:x/:y?}?.normalize: true now drops a .. that would go above the root (/x/../../foo → /foo) instead of keeping a literal .. segment... segments in path normalization (6ebebb6)findRoute matches (6368bf5).. segments in path normalization (6ebebb6)findRoute matches (6368bf5)Add routeNodeKeys() for radix-node identity
find: Remove avoidable allocations in dynamic lookup path
compareRoutes() util (#191)Add route pattern overlap utilities ( #183 ) ( docs )
Support hyphens in route parameter names
⚠️ Url pattern compatibility ( #178 ) ( docs )
params.length > 0 instead of using a Set to deduplicate static nodes - prevent deopts from accessing s out of bound for some cases in compiler output - use .concat instead of spread syntax for faster params array cloning and disable the lint rule that prefer spread (3575022)add: Allow escaping : and * using backslash
: and * using backslash (#175)compare changes 🩹 Fixes Handle root wildcard routes ( #173 ) ❤️ Contributors Kricsleo ( @kricsleo )
compiler: Eval data once in iot mode
compare changes 📦 Build Reduce dist size
compare changes 🔥 Performance Prefer String.charCodeAt ( #170 ) 🩹 Fixes Match correct regex param ( #169 ) 💅 Refactors Simplify compiler logic ( 9b7ad
addRoute: Always normalize path and method
types: Add InferRouteParams for param extraction
InferRouteParams for param extraction (#168)compiler: Support custom data serialize option
serialize option (c633ed1)compiler: Add missing return statement for matchAll
compiler: Support matchAll mode
matchAll mode (#165)routeToRegExp: Match wildcard without trailing slashes as well
_ (205430d)_utils.ts to object.ts (0f72045)compiler: Treeshake empty conditions
This release adds a new experimental compiler.
findRoute (#158)compileRouterToString (#159){ toJSON } (064ae0d)/compiler subpath (8c8c12e)test:compiler for full tests (636795f)compare changes ### 📦 Build - Export NullProtoObj
compare changes ### 🚀 Enhancements - routeToRegExp util (#153) ### 📦 Build - Export MatchedRoute type (#152) ### 🏡 Docs - Update readme for CDN usage
routeToRegExp util (#153)MatchedRoute type (#152)compare changes ### 💅 Refactors - Explicit extensions and types (2043cca) ### 📦 Build - Use obuild
findAll: Last named segment is required
compare changes ### 💅 Refactors - Reduce bundle overhead
⚠️ Last named segment should be required
Object.create(null) (2b7ac09)⚠️ Support multiple entries with same route
compare changes ### 🚀 Enhancements - ⚠️ findAllRoutes
findAllRoutes (#117)method, path order (#114)matcher: Match param in last segment
Radix3 migrated to rou3 (see https://github.com/unjs/rou3/issues/108)
Radix3 migrated to rou3 (see https://github.com/unjs/rou3/issues/108)
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →