{"_id":"h3-rules","_rev":"2-8e3ece53f9da3a30fb09d8ab7240de2a","name":"h3-rules","dist-tags":{"latest":"0.1.0"},"versions":{"0.0.1":{"name":"h3-rules","version":"0.0.1","keywords":["cache","h3","nitro","proxy","redirect","route-rules","routing"],"license":"MIT","_id":"h3-rules@0.0.1","maintainers":[{"name":"pi0","email":"pyapar@gmail.com"}],"homepage":"https://github.com/h3js/h3-rules#readme","bugs":{"url":"https://github.com/h3js/h3-rules/issues"},"dist":{"shasum":"f1a6ace9d2a8c26db588cc47d7fa47a860947cb3","tarball":"https://registry.npmjs.org/h3-rules/-/h3-rules-0.0.1.tgz","fileCount":9,"integrity":"sha512-2+HIW2+uzpYGvYkP2emjy6VHYDInaxvrSfjaHNGw/oY5x9JUdzN5SfcBGwcPgbKPvrAKj8gZMATGnoPKHBsuRA==","signatures":[{"sig":"MEUCIQD7m/Tkf/fwrJnAvNvaeXjDfLJUkFvphj0Tvp7yEUNrWAIgaNwW2DqoW15qRRB9j+y2iAVFzf6fxJaH9qEoMhkwhEw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":49787},"type":"module","types":"./dist/index.d.mts","exports":{".":"./dist/index.mjs","./compiler":"./dist/compiler.mjs"},"scripts":{"dev":"vitest dev","fmt":"automd && oxlint . --fix && oxfmt .","lint":"oxlint . && oxfmt --check .","test":"pnpm lint && pnpm typecheck && vitest run --coverage","bench":"node --expose-gc bench/match.mjs","build":"obuild","prepack":"pnpm build","release":"pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags","typecheck":"tsgo --noEmit --skipLibCheck","bench:size":"node bench/bundle-size.mjs"},"_npmUser":{"name":"pi0","email":"pyapar@gmail.com"},"repository":{"url":"git+https://github.com/h3js/h3-rules.git","type":"git"},"_npmVersion":"11.16.0","description":"Declarative route rules (redirect, proxy, headers, cache, basic auth, CORS) for H3 v2 — runtime-agnostic, extracted from Nitro.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"ufo":"^1.6.4","rou3":"^0.9.1","ocache":"^0.1.5"},"_hasShrinkwrap":false,"packageManager":"pnpm@11.10.0","devDependencies":{"h3":"^2.0.1-rc.23","oxfmt":"latest","automd":"latest","mitata":"^1.0.34","obuild":"latest","oxlint":"latest","vitest":"latest","esbuild":"^0.28.1","typescript":"latest","@types/node":"latest","changelogen":"latest","@vitest/coverage-v8":"latest","@typescript/native-preview":"latest"},"peerDependencies":{"h3":"^2.0.1-rc.23"},"_npmOperationalInternal":{"tmp":"tmp/h3-rules_0.0.1_1783552872867_0.3052085702769143","host":"s3://npm-registry-packages-npm-production"}},"0.1.0":{"name":"h3-rules","version":"0.1.0","description":"Declarative route rules (redirect, proxy, headers, cache, basic auth, CORS) for H3 v2 — runtime-agnostic, extracted from Nitro.","keywords":["cache","h3","nitro","proxy","redirect","route-rules","routing"],"license":"MIT","repository":{"type":"git","url":"git+https://github.com/h3js/h3-rules.git"},"type":"module","sideEffects":false,"types":"./dist/index.d.mts","exports":{".":"./dist/index.mjs","./cache":"./dist/cache.mjs","./proxy":"./dist/proxy.mjs","./compiler":"./dist/compiler.mjs"},"scripts":{"bench":"node --expose-gc bench/match.mjs","bench:size":"node bench/bundle-size.mjs","build":"obuild","dev":"vitest dev","fmt":"automd && oxlint . --fix && oxfmt .","lint":"oxlint . && oxfmt --check .","prepack":"pnpm build","release":"pnpm test && pnpm build && changelogen --release && npm publish && git push --follow-tags","test":"pnpm lint && pnpm typecheck && vitest run --coverage","typecheck":"tsc --noEmit --skipLibCheck"},"dependencies":{"rou3":"^0.9.1","ufo":"^1.6.4"},"devDependencies":{"@types/node":"latest","@vitest/coverage-v8":"latest","automd":"latest","changelogen":"latest","esbuild":"^0.28.1","h3":"^2.0.1-rc.23","mitata":"^1.0.34","obuild":"latest","ocache":"^0.2.0","oxfmt":"latest","oxlint":"latest","typescript":"latest","vitest":"latest"},"peerDependencies":{"h3":"^2.0.1-rc.25","ocache":">=0.2.0"},"peerDependenciesMeta":{"ocache":{"optional":true}},"packageManager":"pnpm@11.10.0","_id":"h3-rules@0.1.0","bugs":{"url":"https://github.com/h3js/h3-rules/issues"},"homepage":"https://github.com/h3js/h3-rules#readme","_nodeVersion":"24.18.0","_npmVersion":"11.16.0","dist":{"integrity":"sha512-MrUUH1E/yoM7hvpKVbLs/CFeindwiTn7Fcfo+zXDpfuRYbcpJDPkYGGttCSiHNJmjUQNYQ5yn9veUIqFonYXuw==","shasum":"a05d59a144b7df74b0fa3bee0efdf03d65bb3a9e","tarball":"https://registry.npmjs.org/h3-rules/-/h3-rules-0.1.0.tgz","fileCount":16,"unpackedSize":80504,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEGS55Es6bcpI/0hJT/bSSyBzYAjOSG+CRtvWLrZ+5SKAiB+rCj9p/hdf2P2A2vyPziSHjNOQI/ajmq1IyTnzbtQyA=="}]},"_npmUser":{"name":"pi0","email":"pyapar@gmail.com"},"directories":{},"maintainers":[{"name":"pi0","email":"pyapar@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/h3-rules_0.1.0_1783797026611_0.030675858603582906"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-08T23:21:12.787Z","modified":"2026-07-11T19:10:26.880Z","0.0.1":"2026-07-08T23:21:13.018Z","0.1.0":"2026-07-11T19:10:26.774Z"},"bugs":{"url":"https://github.com/h3js/h3-rules/issues"},"license":"MIT","homepage":"https://github.com/h3js/h3-rules#readme","keywords":["cache","h3","nitro","proxy","redirect","route-rules","routing"],"repository":{"type":"git","url":"git+https://github.com/h3js/h3-rules.git"},"description":"Declarative route rules (redirect, proxy, headers, cache, basic auth, CORS) for H3 v2 — runtime-agnostic, extracted from Nitro.","maintainers":[{"name":"pi0","email":"pyapar@gmail.com"}],"readme":"# h3-rules\n\n<!-- automd:badges color=yellow -->\n\n[![npm version](https://img.shields.io/npm/v/h3-rules?color=yellow)](https://npmjs.com/package/h3-rules)\n[![npm downloads](https://img.shields.io/npm/dm/h3-rules?color=yellow)](https://npm.chart.dev/h3-rules)\n\n<!-- /automd -->\n\nDeclarative route rules for [H3](https://h3.dev): Define route rules (redirect, proxy, headers, cache, basic auth, CORS) to patterns.\n\n## Usage\n\n### H3 Middleware\n\nAdd the `routeRules` middleware to your H3 app:\n\n```ts\nimport { H3, serve } from \"h3\";\nimport { routeRules } from \"h3-rules\";\nimport { cache } from \"h3-rules/cache\"; // only needed for cache/swr rules (requires ocache)\nimport { proxy } from \"h3-rules/proxy\"; // only needed for proxy rules\n\nconst app = new H3();\n\napp.use(\n  routeRules(\n    {\n      \"/blog/**\": { swr: 60 },\n      \"/old/**\": { redirect: { to: \"/new/**\", status: 301 } },\n      \"/api/proxy/**\": { proxy: \"https://example.com/**\" },\n      \"/assets/**\": { headers: { \"cache-control\": \"s-maxage=31536000\" } },\n      \"/admin/**\": { basicAuth: { username: \"admin\", password: \"secret\" } },\n      \"/api/**\": { cors: true },\n      \"GET /api/cached/**\": { swr: 60 }, // method-scoped\n    },\n    { handlers: { cache, proxy } },\n  ),\n);\n\nserve(app);\n```\n\nPatterns are matched with [rou3](https://github.com/h3js/rou3) against `event.url.pathname`. **All** matching patterns apply, merged from least to most specific. The merged rule map is exposed as `event.context.routeRules`, and rules with runtime behavior run as middleware before the route handler. Match results are memoized per `method + pathname` by default (treat them as read-only; pass `memoize: false` to opt out — see [Memoization](#memoization)).\n\n### Rules\n\n| Rule        | Behavior                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |\n| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| `headers`   | Set response headers. Applied to the final response (after `cache`/`redirect`/`proxy`), so a `cache-control` here overrides ocache's computed value.                                                                                                                                                                                                                                                                                                                           |\n| `redirect`  | Redirect (string defaults to status `307`; `/**` targets append the matched tail). The request query is forwarded with full fidelity (multi-valued params preserved, appended after any query baked into the target).                                                                                                                                                                                                                                                          |\n| `proxy`     | Proxy the request to another origin or in-app path (same `/**` tail behavior). Opt-in handler from `h3-rules/proxy` (see [Proxying](#proxying-h3-rulesproxy)).                                                                                                                                                                                                                                                                                                                 |\n| `cache`     | Wrap the matched route handler with a cached handler. Needs a registered handler — see [Caching](#caching-h3-rulescache).                                                                                                                                                                                                                                                                                                                                                      |\n| `basicAuth` | HTTP Basic Authentication (runs before redirect/proxy/cache).                                                                                                                                                                                                                                                                                                                                                                                                                  |\n| `cors`      | Handle CORS via h3's [`handleCors`](https://h3.dev/utils/security#cors). `true` = permissive defaults; an object = h3 `CorsOptions` (origin allowlist, `credentials`, `maxAge`, …). Preflight (`OPTIONS`) is answered directly, before `basicAuth`. `credentials: true` requires an explicit `origin` (allowlist or validation function) — combining it with a wildcard origin throws at startup, since `Access-Control-Allow-Origin: *` is invalid for credentialed requests. |\n| `swr`       | Shortcut for `cache: { swr: true, maxAge?: number }` (`swr: 0` is valid; `swr: false` resets an inherited `cache`).                                                                                                                                                                                                                                                                                                                                                            |\n\nSetting a rule to `false` on a more specific pattern resets it:\n\n```ts\nrouteRules({\n  \"/admin/**\": { basicAuth: { username: \"admin\", password: \"secret\" } },\n  \"/admin/public/**\": { basicAuth: false },\n});\n```\n\nNote that `cors: true` normalizes to an empty options object (`{}`), so on a more specific pattern it shallow-merges with cors options inherited from broader patterns rather than restoring permissive defaults — use `cors: false` to reset inherited CORS, or an explicit options object to override specific fields.\n\nKeys may carry an optional HTTP method prefix (`\"GET /api/**\"`). A key without a method applies to all methods; for the same pattern, method-scoped rules merge after (override) method-agnostic ones.\n\nUnknown keys (e.g. `prerender`, `isr`, or your own) are **data-only**: they flow through matching and merging and can be read from `event.context.routeRules`, but have no runtime handler.\n\n### Caching (`h3-rules/cache`)\n\nThe core package ships **no caching implementation** — `cache` (and the `swr` shortcut) need a registered `cache` rule handler, and matcher construction throws if a rule set uses them without one (pass `handlers: { cache: undefined }` to deliberately keep them data-only). The ready-made handler is backed by [ocache](https://github.com/unjs/ocache) and lives in the `h3-rules/cache` subpath, so ocache (an **optional** peer dependency — install it alongside `h3-rules`) stays out of every bundle that doesn't cache:\n\n```ts\nimport { routeRules } from \"h3-rules\";\nimport { cache, createOcacheRuleHandler } from \"h3-rules/cache\";\n\n// default: ocache with in-memory storage, wired with h3's response glue\napp.use(routeRules({ \"/blog/**\": { swr: 60 } }, { handlers: { cache } }));\n\n// custom storage / defaults (one handler instance per matcher):\napp.use(\n  routeRules(rules, {\n    handlers: {\n      cache: createOcacheRuleHandler({\n        storage: myStorage, // ocache storage (minimal get/set) — note: process-global (`setStorage`)\n        defaults: { staleMaxAge: 60 }, // ocache defaults incl. hooks (rule options win)\n      }),\n    },\n  }),\n);\n```\n\nTo plug in your own caching (no ocache at all), build a handler from the **core** factory — `defineCachedHandler` receives the matched route handler plus the merged rule options (`group`/`name` pre-filled) and returns the cached wrapper. This is the integration point for frameworks (e.g. Nitro's unstorage / `useStorage()` wiring):\n\n```ts\nimport { createCacheRuleHandler } from \"h3-rules\";\n\nconst cache = createCacheRuleHandler({\n  defineCachedHandler: (handler, opts) => myCachedHandler(handler, opts),\n});\n```\n\nThe declarative rule options (`RouteRuleConfig[\"cache\"]`) are the ocache-compatible `CacheRuleOptions` schema owned by `h3-rules`; implementation hooks (`getKey`, `shouldCache`, `getMaxAge`, …) are not rule data — pass them via the handler factory's `defaults`.\n\n### Proxying (`h3-rules/proxy`)\n\nLike caching, the `proxy` rule is **opt-in**: its handler imports h3's `proxyRequest` (the request-forwarding machinery), so it lives in the `h3-rules/proxy` subpath and stays out of every bundle that doesn't proxy. Register it explicitly, and matcher construction throws if a rule set uses `proxy` without a handler (pass `handlers: { proxy: undefined }` to deliberately keep the rule data-only):\n\n```ts\nimport { routeRules } from \"h3-rules\";\nimport { proxy } from \"h3-rules/proxy\";\n\napp.use(\n  routeRules({ \"/api/proxy/**\": { proxy: \"https://example.com/**\" } }, { handlers: { proxy } }),\n);\n```\n\n### Utils\n\n```ts\nimport {\n  createRouteRulesMatcher,\n  normalizeRouteRules,\n  mergeMatchedRouteRules,\n  ruleHandlers,\n} from \"h3-rules\";\nimport { cache } from \"h3-rules/cache\";\n\nconst matcher = createRouteRulesMatcher(normalizeRouteRules(config), {\n  baseURL: \"/base\", // prefix all patterns\n  preMerge: true, // pre-merge pattern chains at startup (throws on ambiguous rule sets)\n  handlers: {\n    // add or override rule handlers by name; `undefined` = data-only.\n    // A handler is `{ handler, order? }` — `order` is a number controlling\n    // execution order (lower runs first, default 0). Built-in bands:\n    // cors (-3), basicAuth (-2), headers (-1), everything else 0.\n    myRule: {\n      handler: (matched) => (event, next) => {\n        /* ... */\n      },\n    },\n    // cache/swr rules need a registered cache handler (none by default):\n    cache,\n  },\n});\n\nconst { routeRules, routeRuleMiddleware } = matcher(\"GET\", \"/blog/post\");\n```\n\n#### Memoization\n\nFor a given `method + pathname` the merged result is fully deterministic, so it can be memoized: repeat requests skip the rule lookups, path canonicalization, merging, and middleware construction (~8× faster on a mixed path sweep; far larger on hot paths, where a match becomes a single map lookup). The `routeRules()` middleware memoizes **by default** — pass `memoize: false` to opt out, or `memoize: { max }` to tune the cap. For the lower-level matchers, memoization is decoupled from construction — opt in by wrapping any matcher with `memoizeRouteRulesMatcher(matcher, opts?)`:\n\n```ts\nimport { createRouteRulesMatcher, memoizeRouteRulesMatcher } from \"h3-rules\";\n\nconst matcher = memoizeRouteRulesMatcher(createRouteRulesMatcher(normalizeRouteRules(config)));\n```\n\nEntries are capped (default `1024`, tune with `{ max }`) with FIFO eviction, so unbounded dynamic paths cannot grow the cache. Memoized results are shared across requests — treat `event.context.routeRules` as read-only (rule `options` objects are shared with the registered rule data either way). It wraps a compiled matcher the same way (`memoizeRouteRulesMatcher(createMatcherFromFind(findRouteRules))`); keeping it out of `createMatcherFromFind`/`createRouteRulesMatcher` lets an un-memoized bundle tree-shake it away.\n\n#### Pre-merging\n\n`preMerge: true` resolves each pattern's subsumption chain up front (at matcher startup, or at build time via the compiler option) so per-request resolution takes only the most specific matched layer instead of merging all matched layers (~20% faster on cold paths; composes with `memoizeRouteRulesMatcher` for warm ones). It is exact, but requires an unambiguous rule set: if two patterns partially overlap (e.g. `/a/*/c` vs `/a/b/*` — the most specific match would be ambiguous) or use patterns it cannot analyze (regex params), the **runtime matcher throws at startup** (a misconfigured `preMerge` is a startup error). The **compiler is fail-safe**: it emits a `console.warn` and falls back to plain compilation so the build still produces a correct matcher. Method-scoped and method-agnostic rules, `false` resets, and per-rule `params` behave identically to the default per-request merge (a tested invariant).\n\n### Compiled matcher (`h3-rules/compiler`)\n\nFor build-time codegen, compile your rules into a `findRouteRules` function so `rou3` stays out of the runtime bundle:\n\n```ts\nimport { compileRouteRules } from \"h3-rules/compiler\";\n\nconst mod = compileRouteRules(config, {\n  preMerge: true, // optional: bake pre-merged chains into the generated matcher\n});\nmod.code; // whole module (also `String(mod)` / template interpolation)\n// -> import { headers as __ruleHandlers__$headers } from \"h3-rules\";\n// -> export const findRouteRules = (method, path) => ...;\n```\n\n`compileRouteRules` returns the module split into its two composable halves — `imports` (the handler `import` statements) and `body` (the `export const findRouteRules = …` declaration) — plus `code` (the whole module, same as `String(mod)`). Take `code` to write a standalone module, or hoist `imports` and inline `body` to weave the matcher into a larger generated module.\n\nThe compiler entrypoints normalize their input themselves (compilation is build-time, so the pass is free) — pass authored config directly. An already-normalized rule set (from your own `normalizeRouteRules` call) is equally valid input: normalization is idempotent.\n\nAt runtime, wrap `findRouteRules` with `createMatcherFromFind(findRouteRules)` (for memoization, compose `memoizeRouteRulesMatcher(createMatcherFromFind(findRouteRules))`). Compiled and runtime matchers produce identical results. Unlike the compiler, `createRouteRulesMatcher` takes **normalized** rules — this keeps normalization out of runtime bundles; `routeRules()` is the auto-normalizing runtime entry point.\n\nTo skip the hand-written wrapper, pass `matcher` so the generated module exports a ready-to-use matcher alongside `findRouteRules`:\n\n```ts\ncompileRouteRules(config, { matcher: true });\n// -> export const findRouteRules = …;\n// -> import { createMatcherFromFind } from \"h3-rules\";\n// -> export const matcher = createMatcherFromFind(findRouteRules);\n\n// rename the export, or bake in memoization:\ncompileRouteRules(config, { matcher: { name: \"routeMatcher\", memoize: true } });\n// -> import { createMatcherFromFind, memoizeRouteRulesMatcher } from \"h3-rules\";\n// -> export const routeMatcher = memoizeRouteRulesMatcher(createMatcherFromFind(findRouteRules));\n```\n\n`matcher: true` names the export `matcher`; pass a string to rename it, or `{ name?, memoize? }` to also wrap in `memoizeRouteRulesMatcher` (`memoize: { max }` tunes the cap). `memoizeRouteRulesMatcher` is imported **only** when `memoize` is set, so an un-memoized matcher export still tree-shakes it away. The infra import counts toward `mod.imports`.\n\nThe generated module imports **only the rule handlers the rule set uses** — most built-ins are a named export of `h3-rules` (`headers`, `redirect`, `basicAuth`, `cors`), except the opt-in subpath handlers: `cache` imports from `h3-rules/cache` (the ocache-backed handler; requires the optional `ocache` peer only when a cache rule exists) and `proxy` from `h3-rules/proxy` (pulls h3's `proxyRequest` only when a proxy rule exists). Unused handlers and their dependencies (rou3's matcher always, ocache/ufo when `cache`/`redirect`/`proxy` are unused) tree-shake out of the bundle.\n\nWhere each handler is imported from is controlled by `runtimeRules` — a record keyed by rule name whose value is either a module id (the module must export a member named exactly as the rule key, e.g. `cache: \"#nitro/cache\"` imports `cache`) or `{ source, export }` when the export is named something else. It is merged **over** the built-in preset (`DEFAULT_RUNTIME_RULES`: every built-in from `h3-rules`, except `cache` from `h3-rules/cache` and `proxy` from `h3-rules/proxy`), so you only list what you add or change — the built-ins stay registered. Handlers sharing a source collapse into one import statement:\n\n```ts\nimport { compileRouteRules } from \"h3-rules/compiler\";\n\ncompileRouteRules(config, {\n  runtimeRules: {\n    cache: \"#nitro/cache\", // repoint the built-in cache at your own module\n    isr: { source: \"#nitro/rules\", export: \"handleISR\" }, // custom rule + export\n  },\n});\n// -> import { handleISR as __ruleHandlers__$isr } from \"#nitro/rules\";\n// -> import { cache as __ruleHandlers__$cache } from \"#nitro/cache\";\n// (redirect, headers, … still import from \"h3-rules\" when used)\n```\n\nA custom `cache` handler can be built with `createOcacheRuleHandler` — the ocache-wired factory from `h3-rules/cache` (`storage`/`defaults`) — or the core injection factory `createCacheRuleHandler` from `h3-rules` (`defineCachedHandler`); see [Caching](#caching-h3-rulescache).\n\n### Extending rule types\n\n`RouteRuleConfig` (the config you author) is a **closed** interface — unknown keys are compile errors, so a typo like `redirct` is caught at build time. To add custom or data-only rules, declare them via module augmentation. Augment `RouteRuleConfig` for the config input, and `RouteRules` (which stays open) so the key is typed on the normalized/matched result too:\n\n```ts\ndeclare module \"h3-rules\" {\n  interface RouteRuleConfig {\n    /** Incremental Static Regeneration (handled at build time). */\n    isr?: number | boolean;\n    /** Add this route to the prerender queue. */\n    prerender?: boolean;\n    /** A data-only rule with no runtime handler. */\n    audience?: \"public\" | \"internal\";\n  }\n  interface RouteRules {\n    isr?: number | boolean;\n    prerender?: boolean;\n    audience?: \"public\" | \"internal\";\n  }\n}\n```\n\nData-only rules (no matching handler) still flow through normalization and merge untouched at runtime — augmentation only re-opens the typing.\n\n## Development\n\n<details>\n\n<summary>local development</summary>\n\n- Clone this repository\n- Install latest LTS version of [Node.js](https://nodejs.org/en/)\n- Enable [Corepack](https://github.com/nodejs/corepack) using `corepack enable`\n- Install dependencies using `pnpm install`\n- Run interactive tests using `pnpm dev`\n\n</details>\n\n## License\n\nPublished under the [MIT](https://github.com/h3js/h3-rules/blob/main/LICENSE) license 💛.\n","readmeFilename":"README.md"}