From 1a5fff7e8336373125fc3a35e19c5c2f7c09760d Mon Sep 17 00:00:00 2001 From: Sven Rogge Date: Wed, 1 Apr 2026 00:36:51 +0200 Subject: [PATCH 1/4] fix: improve bundle size and fix hash handling in navigation - Remove debug console.log from scroll-restoration.ts - Remove dead code (if (save) wrapper) in scroll-restoration.ts - Fix href getter to include hash when constructing from path array - Forward hash when reconstructing NavOpts in redirects - Replace new Array() with [] for smaller compiled output - Add sideEffects: false to package.json for better tree-shaking - Add tests for href with hash --- packages/esroute-lit/package.json | 1 + packages/esroute/package.json | 1 + packages/esroute/src/nav-opts.spec.ts | 17 +++++++++++++++++ packages/esroute/src/nav-opts.ts | 2 +- packages/esroute/src/route-resolver.ts | 2 +- packages/esroute/src/router.ts | 1 + packages/esroute/src/scroll-restoration.ts | 7 ++----- 7 files changed, 24 insertions(+), 7 deletions(-) diff --git a/packages/esroute-lit/package.json b/packages/esroute-lit/package.json index e8d0535..5bc5784 100644 --- a/packages/esroute-lit/package.json +++ b/packages/esroute-lit/package.json @@ -3,6 +3,7 @@ "version": "0.12.0", "description": "A small efficient client-side routing library for lit, written in TypeScript.", "main": "dist/index.js", + "sideEffects": false, "license": "MIT", "author": "Sven Rogge ", "repository": "github:sv2dev/esroute", diff --git a/packages/esroute/package.json b/packages/esroute/package.json index 2a64131..3431e47 100644 --- a/packages/esroute/package.json +++ b/packages/esroute/package.json @@ -4,6 +4,7 @@ "description": "A small efficient framework-agnostic client-side routing library, written in TypeScript.", "types": "dist/index.d.ts", "main": "dist/index.js", + "sideEffects": false, "license": "MIT", "author": "Sven Rogge ", "repository": "github:sv2dev/esroute", diff --git a/packages/esroute/src/nav-opts.spec.ts b/packages/esroute/src/nav-opts.spec.ts index d0813b4..c77d187 100644 --- a/packages/esroute/src/nav-opts.spec.ts +++ b/packages/esroute/src/nav-opts.spec.ts @@ -97,6 +97,23 @@ describe("NavOpts", () => { }); }); + describe("href with hash", () => { + it("should include hash in href when constructed from path array", () => { + const opts = new NavOpts(["foo"], { hash: "section" }); + expect(opts.href).toBe("/foo#section"); + }); + + it("should include both search and hash in href", () => { + const opts = new NavOpts(["foo"], { search: { a: "b" }, hash: "top" }); + expect(opts.href).toBe("/foo?a=b#top"); + }); + + it("should include hash in href when constructed from StrictNavMeta", () => { + const opts = new NavOpts({ path: ["foo"], hash: "bar" }); + expect(opts.href).toBe("/foo#bar"); + }); + }); + describe("go", () => { it("should create a new NavigateOpts instance with a new path", () => { const opts1 = new NavOpts(["a", "b"]); diff --git a/packages/esroute/src/nav-opts.ts b/packages/esroute/src/nav-opts.ts index baf2f9b..59ffe83 100644 --- a/packages/esroute/src/nav-opts.ts +++ b/packages/esroute/src/nav-opts.ts @@ -73,7 +73,7 @@ export class NavOpts implements NavMeta { if (!this._h) { const s = new URLSearchParams(this.search).toString(); const p = `/${this.path!.join("/")}`; - this._h = `${p}${s ? `?${s}` : ""}`; + this._h = `${p}${s ? `?${s}` : ""}${this.hash ? `#${this.hash}` : ""}`; } return this._h; } diff --git a/packages/esroute/src/route-resolver.ts b/packages/esroute/src/route-resolver.ts index cde9c31..b69ec2f 100644 --- a/packages/esroute/src/route-resolver.ts +++ b/packages/esroute/src/route-resolver.ts @@ -22,7 +22,7 @@ export const resolve = async ( notFound: Resolve ): Promise> => { let value: NavOpts | T = opts; - const navPath = new Array>(); + const navPath: NavOpts[] = []; while (value instanceof NavOpts && navPath.length <= MAX_REDIRECTS) { opts = value; navPath.push(opts); diff --git a/packages/esroute/src/router.ts b/packages/esroute/src/router.ts index a287e7f..f9c68a2 100644 --- a/packages/esroute/src/router.ts +++ b/packages/esroute/src/router.ts @@ -221,6 +221,7 @@ export const createRouter = ( replace: true, search: opts.search, state: opts.state, + hash: opts.hash, }) ); } diff --git a/packages/esroute/src/scroll-restoration.ts b/packages/esroute/src/scroll-restoration.ts index ec4a39f..7f127ef 100644 --- a/packages/esroute/src/scroll-restoration.ts +++ b/packages/esroute/src/scroll-restoration.ts @@ -32,14 +32,11 @@ export const restoreHandling = ({ offset + container.scrollTop); - if (save) { - window.addEventListener("beforeunload", save); - window.addEventListener("visibilitychange", save); - } + window.addEventListener("beforeunload", save); + window.addEventListener("visibilitychange", save); window.addEventListener("popstate", () => set(getStatePos())); return ({ opts: { hash, pop } }: { opts: NavOpts }) => { const fromState = getStatePos(); - console.log(location.pathname, pop, fromState); if (fromState) return set(fromState); if (!fromState && hash && getHashPos) return set(getHashPos(hash)); container.scrollTop = 0; From 0550e54b7944442552d7911f761f41ad0557c6d1 Mon Sep 17 00:00:00 2001 From: Sven Rogge Date: Wed, 1 Apr 2026 00:37:00 +0200 Subject: [PATCH 2/4] chore: bump versions to 0.12.1 --- packages/esroute-lit/package.json | 4 ++-- packages/esroute/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/esroute-lit/package.json b/packages/esroute-lit/package.json index 5bc5784..8088d4a 100644 --- a/packages/esroute-lit/package.json +++ b/packages/esroute-lit/package.json @@ -1,6 +1,6 @@ { "name": "@esroute/lit", - "version": "0.12.0", + "version": "0.12.1", "description": "A small efficient client-side routing library for lit, written in TypeScript.", "main": "dist/index.js", "sideEffects": false, @@ -22,7 +22,7 @@ "typescript": "^5.4.5" }, "dependencies": { - "esroute": "^0.12.0", + "esroute": "^0.12.1", "lit": "^3.1.1" } } diff --git a/packages/esroute/package.json b/packages/esroute/package.json index 3431e47..42f450e 100644 --- a/packages/esroute/package.json +++ b/packages/esroute/package.json @@ -1,6 +1,6 @@ { "name": "esroute", - "version": "0.12.0", + "version": "0.12.1", "description": "A small efficient framework-agnostic client-side routing library, written in TypeScript.", "types": "dist/index.d.ts", "main": "dist/index.js", From bf3d13c9472f3aca2b0358f7d6a902457a47afbc Mon Sep 17 00:00:00 2001 From: Sven Rogge Date: Wed, 1 Apr 2026 00:44:43 +0200 Subject: [PATCH 3/4] docs: add changelog documenting release history Adds a Keep a Changelog formatted changelog documenting all releases from v0.1.0 through v0.12.1, including features, fixes, and breaking changes for each version. --- CHANGELOG.md | 130 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..d7a3b2c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,130 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.12.1] - 2026-04-01 + +### Fixed +- `href` getter now includes the hash fragment when `NavOpts` is constructed from a path array (previously the hash was silently dropped, breaking `history.pushState` for hash-based navigation) +- Hash is now preserved through redirects in `resolveCurrent()` + +### Changed +- Added `sideEffects: false` to both packages' `package.json` — bundlers can now tree-shake unused exports (e.g. drop `restoreHandling` when only `createRouter` is imported) +- Minor bundle size reduction: removed dead code in `scroll-restoration`, replaced `new Array()` with `[]` + +## [0.12.0] - 2026-04-01 + +### Added +- Type-safe routing: `createRouter` now infers a `RoutePaths` type from the routes configuration, and `router.go()` enforces typed paths at compile time +- Typed state: `NavOpts` and `createRouter` are now generic over the state type + +### Fixed +- `NavOpts.state` is now always `null` instead of `undefined` when no state is provided +- Route resolution now correctly returns not-found when an intermediate path segment is unmatched (previously could match a route deeper in the tree) + +## [0.11.1] - 2026-03-31 + +### Fixed +- Source maps are now included in the published npm package + +## [0.11.0] - 2025-10-21 + +### Added +- `router.render(defer?)` — pass a defer function to batch multiple successive navigations without intermediate renders. The router resolves only the final navigation once the deferred work completes. + +## [0.10.0] - 2025-04-17 + +### Fixed +- Search params passed via `NavMeta` are no longer overridden by the search string embedded in an href +- State defaults to `null` (previously could be `undefined`) + +## [0.9.0] - 2024-04-16 + +### Added +- `router.go()` now accepts a function `(prev: NavOpts) => NavMeta` to patch the current navigation state in place (uses `replaceState` by default) + +### Fixed +- Corrected TypeScript overload types for `router.go()` + +## [0.8.3] - 2024-04-16 + +### Added +- `router.current` exposes the current `NavOpts` after the last completed resolution + +## [0.8.1] - 2024-01-17 + +### Fixed +- Router no longer becomes permanently unresponsive when a route resolution promise rejects + +## [0.8.0] - 2023-04-12 + +### Added +- Scroll restoration via `restoreHandling()` — saves and restores scroll position on navigation, supports hash-based anchor scrolling and a configurable container/offset + +### Changed +- `NavOpts` now exposes `hash` and `pop` fields + +## [0.6.0] - 2022-10-18 + +### Added +- Guards: add a `"?"` key to any route object to register a guard function. If the guard returns a `NavOpts`, the navigation is redirected. +- `routes` is now optional in `createRouter()` (defaults to `{}`) + +## [0.5.4] - 2022-09-21 + +### Added +- `router.routes` is now exposed on the `Router` interface, allowing routes to be modified after creation + +### Fixed +- Fixed virtual route (`""` key) resolution ordering + +## [0.5.0] - 2022-09-18 + +### Changed +- **Breaking**: `createRouter` is now a factory function instead of a class — use `createRouter({ routes, ... })` instead of `new Router(...)`) +- Reduced bundle size by removing unused abstractions + +### Removed +- `NavOpts.equals()` method + +## [0.4.2] - 2022-07-01 + +### Fixed +- Guard was not being called on index routes (`""` key) +- Fixed ESM import paths in `@esroute/lit` + +## [0.4.1] - 2022-04-18 + +### Added +- `@esroute/lit` package: `renderRoutes` Lit directive for declarative route rendering in Lit components + +## [0.3.0] - 2022-04-13 + +### Changed +- Converted the resolver to a plain function for better tree-shaking +- Route spec compilation is now opt-in (not compiled by default) + +## [0.2.0] - 2022-04-08 + +### Added +- Deep link support in the route spec (nested path configuration) + +## [0.1.1] - 2022-01-01 + +### Fixed +- Minor fixes and dependency updates + +## [0.1.0] - 2022-01-01 + +### Added +- Initial release of `esroute` +- Framework-agnostic client-side router with `createRouter()` +- Path-based route matching with wildcard (`*`) support +- Virtual routes (`""` key) for middleware/layout patterns +- Anchor click interception +- Promise-based serialized navigation From 67aee834c819894822d61b82414833f3711297fd Mon Sep 17 00:00:00 2001 From: Sven Rogge Date: Wed, 1 Apr 2026 00:53:47 +0200 Subject: [PATCH 4/4] fix: attach visibilitychange listener to document instead of window --- packages/esroute/src/scroll-restoration.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/esroute/src/scroll-restoration.ts b/packages/esroute/src/scroll-restoration.ts index 7f127ef..0970167 100644 --- a/packages/esroute/src/scroll-restoration.ts +++ b/packages/esroute/src/scroll-restoration.ts @@ -33,7 +33,7 @@ export const restoreHandling = ({ container.scrollTop); window.addEventListener("beforeunload", save); - window.addEventListener("visibilitychange", save); + document.addEventListener("visibilitychange", save); window.addEventListener("popstate", () => set(getStatePos())); return ({ opts: { hash, pop } }: { opts: NavOpts }) => { const fromState = getStatePos();