singlepage-router 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,24 +10,51 @@ A micro client-side router with a clean TypeScript rewrite. Full `History` and `
10
10
 
11
11
  ---
12
12
 
13
+ ## What's different from `page`
14
+
15
+ `singlepage-router` is a TypeScript rewrite of the classic [`page`](https://www.npmjs.com/package/page) package by [@visionmedia](https://github.com/visionmedia). The routing behaviour and API are intentionally kept familiar, but the internals have been modernised throughout:
16
+
17
+ - **Written in TypeScript** — full type safety out of the box. No need for a separate `@types/` package. All classes, options, and callbacks are fully typed.
18
+ - **Modern dual ESM/CJS build** — ships both `import` and `require` formats with a proper `exports` map. The original `page` predates the ESM ecosystem and relies on a single UMD/CommonJS bundle.
19
+ - **Class-based internals** — `PageInstance`, `Context`, and `Route` are proper ES classes, replacing the original's prototype chain manipulation and constructor functions.
20
+ - **No legacy code** — all `var` declarations, bitwise `~indexOf` tricks, IE-era guards, and the HTML5-History-API polyfill support have been removed. Targets modern browsers only.
21
+ - **Updated `path-to-regexp`** — uses v6 versus the original's pinned v1.2.x, bringing improved pattern support and security fixes.
22
+ - **`sideEffects: false`** — explicitly marked for bundler tree-shaking, so unused exports are dropped cleanly.
23
+ - **Named exports** — alongside the default export, `Context`, `Route`, `PageInstance`, `createPage`, and all types are individually importable, which the original does not support cleanly.
24
+
25
+ The public API — `page('/path', handler)`, `page()`, `page.show()`, `page.back()`, `page.exit()` etc. — works the same way as `page`, so migration is straightforward.
26
+
27
+ ---
28
+
29
+ ## Credits
30
+
31
+ This package is a TypeScript rewrite of [**page**](https://www.npmjs.com/package/page), the original micro client-side router created by [TJ Holowaychuk (@visionmedia)](https://github.com/visionmedia). The core routing concepts, middleware chain design, `Context` model, and overall API shape all originate from that project.
32
+
33
+ If you want the battle-tested original with a long history of production use and a large community, go use [`page`](https://github.com/visionmedia/page.js). This package exists as a modernised alternative for projects that want TypeScript support and a native ESM build from the start.
34
+
35
+ Full credit and thanks to TJ and all contributors to the original `page.js` project.
36
+
37
+ ---
38
+
13
39
  ## Installation
14
40
 
15
41
  ```bash
16
42
  npm install singlepage-router
43
+ ```
17
44
 
18
45
  ---
19
46
 
20
47
  ## Quick Start
21
48
 
22
49
  ```ts
23
- import singlepage from 'singlepage-router';
50
+ import page from 'singlepage-router';
24
51
 
25
- singlepage('/', () => render('home'));
26
- singlepage('/users', () => render('users'));
27
- singlepage('/users/:id', (ctx) => render('user', ctx.params.id));
28
- singlepage('*', () => render('404'));
52
+ page('/', () => render('home'));
53
+ page('/users', () => render('users'));
54
+ page('/users/:id', (ctx) => render('user', ctx.params.id));
55
+ page('*', () => render('404'));
29
56
 
30
- singlepage(); // start the router
57
+ page(); // start the router
31
58
  ```
32
59
 
33
60
  ---
@@ -37,28 +64,28 @@ singlepage(); // start the router
37
64
  ### Basic routes
38
65
 
39
66
  ```ts
40
- import singlepage from 'singlepage-router';
67
+ import page from 'singlepage-router';
41
68
 
42
- singlepage('/', (ctx, next) => {
69
+ page('/', (ctx, next) => {
43
70
  console.log('home', ctx.pathname);
44
71
  });
45
72
 
46
- singlepage('/about', (ctx, next) => {
73
+ page('/about', (ctx, next) => {
47
74
  console.log('about page');
48
75
  });
49
76
 
50
- singlepage(); // start
77
+ page(); // start
51
78
  ```
52
79
 
53
80
  ### Route parameters
54
81
 
55
82
  ```ts
56
- singlepage('/users/:id', (ctx) => {
83
+ page('/users/:id', (ctx) => {
57
84
  const { id } = ctx.params;
58
85
  console.log('user id:', id);
59
86
  });
60
87
 
61
- singlepage('/posts/:year/:month/:slug', (ctx) => {
88
+ page('/posts/:year/:month/:slug', (ctx) => {
62
89
  const { year, month, slug } = ctx.params;
63
90
  console.log(`Post: ${slug} from ${month}/${year}`);
64
91
  });
@@ -69,29 +96,30 @@ singlepage('/posts/:year/:month/:slug', (ctx) => {
69
96
  Handlers receive a `next` function. Call it to pass control to the next matching handler, just like Express middleware.
70
97
 
71
98
  ```ts
72
- import singlepage, { Context } from 'singlepage-router';
99
+ import page from 'singlepage-router';
100
+ import type { Callback } from 'singlepage-router';
73
101
 
74
- function authenticate(ctx: Context, next: () => void) {
75
- if (!isLoggedIn()) return singlepage.show('/login');
102
+ const authenticate: Callback = (ctx, next) => {
103
+ if (!isLoggedIn()) return page.show('/login');
76
104
  next();
77
- }
105
+ };
78
106
 
79
- function loadUser(ctx: Context, next: () => void) {
107
+ const loadUser: Callback = (ctx, next) => {
80
108
  ctx.state.user = fetchUser(ctx.params.id);
81
109
  next();
82
- }
110
+ };
83
111
 
84
- function renderProfile(ctx: Context) {
112
+ const renderProfile: Callback = (ctx) => {
85
113
  render('profile', ctx.state.user);
86
- }
114
+ };
87
115
 
88
- singlepage('/profile/:id', authenticate, loadUser, renderProfile);
116
+ page('/profile/:id', authenticate, loadUser, renderProfile);
89
117
  ```
90
118
 
91
119
  ### Wildcard / catch-all
92
120
 
93
121
  ```ts
94
- singlepage('*', (ctx) => {
122
+ page('*', (ctx) => {
95
123
  console.log('no route matched:', ctx.path);
96
124
  render('404');
97
125
  });
@@ -101,11 +129,11 @@ singlepage('*', (ctx) => {
101
129
 
102
130
  ```ts
103
131
  // Declarative redirect — from one path to another
104
- singlepage.redirect('/old-path', '/new-path');
132
+ page.redirect('/old-path', '/new-path');
105
133
 
106
134
  // Imperative redirect from inside a handler
107
- singlepage('/legacy', () => {
108
- singlepage.redirect('/modern');
135
+ page('/legacy', () => {
136
+ page.redirect('/modern');
109
137
  });
110
138
  ```
111
139
 
@@ -113,13 +141,13 @@ singlepage('/legacy', () => {
113
141
 
114
142
  ```ts
115
143
  // Push a new history entry
116
- singlepage.show('/users/42');
144
+ page.show('/users/42');
117
145
 
118
146
  // Replace the current history entry (no new entry added)
119
- singlepage.replace('/users/42');
147
+ page.replace('/users/42');
120
148
 
121
149
  // Go back — falls back to a path if there is no history
122
- singlepage.back('/home');
150
+ page.back('/home');
123
151
  ```
124
152
 
125
153
  ### Exit handlers
@@ -127,11 +155,11 @@ singlepage.back('/home');
127
155
  Exit handlers run when navigating *away* from a route. Useful for teardown, unsaved-change guards, or cancelling in-flight requests.
128
156
 
129
157
  ```ts
130
- singlepage('/editor', (ctx) => {
158
+ page('/editor', (ctx) => {
131
159
  startEditor();
132
160
  });
133
161
 
134
- singlepage.exit('/editor', (ctx, next) => {
162
+ page.exit('/editor', (ctx, next) => {
135
163
  if (hasUnsavedChanges()) {
136
164
  if (!confirm('Leave without saving?')) return;
137
165
  }
@@ -145,7 +173,7 @@ singlepage.exit('/editor', (ctx, next) => {
145
173
  For environments that can't use the HTML5 History API (e.g. `file://` protocol):
146
174
 
147
175
  ```ts
148
- singlepage.start({ hashbang: true });
176
+ page.start({ hashbang: true });
149
177
 
150
178
  // URLs will look like: /#!/users/42
151
179
  ```
@@ -155,12 +183,12 @@ singlepage.start({ hashbang: true });
155
183
  If your app is not served from the root, set a base path:
156
184
 
157
185
  ```ts
158
- singlepage.base('/my-app');
186
+ page.base('/my-app');
159
187
 
160
- singlepage('/dashboard', () => render('dashboard'));
188
+ page('/dashboard', () => render('dashboard'));
161
189
  // Matches: /my-app/dashboard
162
190
 
163
- singlepage();
191
+ page();
164
192
  ```
165
193
 
166
194
  ### Multiple isolated instances
@@ -182,23 +210,23 @@ publicRouter();
182
210
 
183
211
  Every handler receives a `Context` instance with the following properties:
184
212
 
185
- | Property | Type | Description |
186
- |-----------------|-----------------------------------|------------------------------------------|
187
- | `path` | `string` | The path without base |
188
- | `canonicalPath` | `string` | The full path including base |
189
- | `pathname` | `string` | Path without querystring or hash |
190
- | `querystring` | `string` | Querystring without the leading `?` |
191
- | `hash` | `string` | Hash fragment without the `#` |
192
- | `params` | `Record<string, string>` | Matched route parameters |
193
- | `state` | `Record<string, unknown>` | Arbitrary state stored in history entry |
194
- | `handled` | `boolean` | Whether the context was handled |
195
- | `routePath` | `string` | The route pattern that matched |
213
+ | Property | Type | Description |
214
+ |-----------------|---------------------------|-----------------------------------------|
215
+ | `path` | `string` | The path without base |
216
+ | `canonicalPath` | `string` | The full path including base |
217
+ | `pathname` | `string` | Path without querystring or hash |
218
+ | `querystring` | `string` | Querystring without the leading `?` |
219
+ | `hash` | `string` | Hash fragment without the `#` |
220
+ | `params` | `Record<string, string>` | Matched route parameters |
221
+ | `state` | `Record<string, unknown>` | Arbitrary state stored in history entry |
222
+ | `handled` | `boolean` | Whether the context was handled |
223
+ | `routePath` | `string` | The route pattern that matched |
196
224
 
197
225
  ```ts
198
- singlepage('/search', (ctx) => {
226
+ page('/search', (ctx) => {
199
227
  console.log(ctx.querystring); // "q=typescript&page=2"
200
228
  console.log(ctx.hash); // "results"
201
- console.log(ctx.state); // any state passed via singlepage.show()
229
+ console.log(ctx.state); // any state passed via page.show()
202
230
  });
203
231
  ```
204
232
 
@@ -206,46 +234,46 @@ singlepage('/search', (ctx) => {
206
234
 
207
235
  ## API Reference
208
236
 
209
- ### `singlepage(path, ...handlers)`
237
+ ### `page(path, ...handlers)`
210
238
  Register a route.
211
239
 
212
- ### `singlepage()` / `singlepage.start(options?)`
240
+ ### `page()` / `page.start(options?)`
213
241
  Start the router. Dispatches the current URL immediately.
214
242
 
215
- | Option | Type | Default | Description |
216
- |----------------------|-----------|---------|--------------------------------------------------|
217
- | `dispatch` | `boolean` | `true` | Dispatch the current route on start |
218
- | `click` | `boolean` | `true` | Intercept link clicks automatically |
219
- | `popstate` | `boolean` | `true` | Listen to `popstate` events |
220
- | `hashbang` | `boolean` | `false` | Use `#!` URLs instead of History API |
221
- | `decodeURLComponents`| `boolean` | `true` | Decode URL params and querystrings |
222
- | `window` | `Window` | `window`| Use a custom window object |
243
+ | Option | Type | Default | Description |
244
+ |-----------------------|-----------|---------|--------------------------------------------|
245
+ | `dispatch` | `boolean` | `true` | Dispatch the current route on start |
246
+ | `click` | `boolean` | `true` | Intercept link clicks automatically |
247
+ | `popstate` | `boolean` | `true` | Listen to `popstate` events |
248
+ | `hashbang` | `boolean` | `false` | Use `#!` URLs instead of History API |
249
+ | `decodeURLComponents` | `boolean` | `true` | Decode URL params and querystrings |
250
+ | `window` | `Window` | `window`| Use a custom window object |
223
251
 
224
- ### `singlepage.stop()`
252
+ ### `page.stop()`
225
253
  Unbind all event listeners and stop the router.
226
254
 
227
- ### `singlepage.show(path, state?, dispatch?, push?)`
255
+ ### `page.show(path, state?, dispatch?, push?)`
228
256
  Navigate to `path`, pushing a new history entry.
229
257
 
230
- ### `singlepage.replace(path, state?, init?, dispatch?)`
258
+ ### `page.replace(path, state?, init?, dispatch?)`
231
259
  Navigate to `path`, replacing the current history entry.
232
260
 
233
- ### `singlepage.back(fallback?, state?)`
261
+ ### `page.back(fallback?, state?)`
234
262
  Go back in history. If no history exists, navigates to `fallback`.
235
263
 
236
- ### `singlepage.redirect(from, to?)`
264
+ ### `page.redirect(from, to?)`
237
265
  Register a redirect from one path to another, or immediately redirect to a path.
238
266
 
239
- ### `singlepage.base(path?)`
267
+ ### `page.base(path?)`
240
268
  Get or set the base path.
241
269
 
242
- ### `singlepage.strict(enable?)`
270
+ ### `page.strict(enable?)`
243
271
  Get or set strict trailing-slash matching.
244
272
 
245
- ### `singlepage.exit(path, ...handlers)`
273
+ ### `page.exit(path, ...handlers)`
246
274
  Register exit handlers for a route.
247
275
 
248
- ### `singlepage.configure(options)`
276
+ ### `page.configure(options)`
249
277
  Reconfigure the router after start.
250
278
 
251
279
  ### `createPage()`
@@ -258,13 +286,14 @@ Create a new isolated router instance.
258
286
  ```
259
287
  your-project/
260
288
  ├── src/
261
- └── page.ts
289
+ ├── index.ts ← public entry point
290
+ │ └── singlepage.ts ← router implementation
262
291
  ├── scripts/
263
292
  │ └── fix-cjs-ext.mjs
264
- ├── dist/ ← generated, do not edit
265
- │ ├── esm/page.js
266
- │ ├── cjs/page.cjs
267
- │ └── types/page.d.ts
293
+ ├── dist/ ← generated by build, do not edit
294
+ │ ├── esm/index.js
295
+ │ ├── cjs/index.cjs
296
+ │ └── types/index.d.ts
268
297
  ├── package.json
269
298
  ├── tsconfig.json
270
299
  ├── tsconfig.esm.json
@@ -284,8 +313,8 @@ npm run build
284
313
  Individual build steps:
285
314
 
286
315
  ```bash
287
- npm run build:esm # ESM output → dist/esm/
288
- npm run build:cjs # CJS output → dist/cjs/
316
+ npm run build:esm # ESM output → dist/esm/
317
+ npm run build:cjs # CJS output → dist/cjs/
289
318
  npm run build:types # Type declarations → dist/types/
290
319
  ```
291
320
 
@@ -293,4 +322,4 @@ npm run build:types # Type declarations → dist/types/
293
322
 
294
323
  ## License
295
324
 
296
- MIT
325
+ MIT
@@ -1,5 +1,4 @@
1
1
  "use strict";
2
- // src/index.ts
3
2
  var __importDefault = (this && this.__importDefault) || function (mod) {
4
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAI,eAAe;;;;;;AAEf,iDAAsD;AAA7C,yHAAA,OAAO,OAAA;AAAE,2GAAA,UAAU,OAAA;AAE5B,iDAA+D;AAAtD,wGAAA,OAAO,OAAA;AAAE,sGAAA,KAAK,OAAA;AAAE,6GAAA,YAAY,OAAA"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;AAAA,iDAAsD;AAA7C,yHAAA,OAAO,OAAA;AAAE,2GAAA,UAAU,OAAA;AAE5B,iDAA+D;AAAtD,wGAAA,OAAO,OAAA;AAAE,sGAAA,KAAK,OAAA;AAAE,6GAAA,YAAY,OAAA"}
package/dist/esm/index.js CHANGED
@@ -1,4 +1,3 @@
1
- // src/index.ts
2
1
  export { default, createPage } from './singlepage.js';
3
2
  export { Context, Route, PageInstance } from './singlepage.js';
4
3
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAI,eAAe;AAEf,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAEI,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
package/package.json CHANGED
@@ -1,66 +1,70 @@
1
- {
2
- "name": "singlepage-router",
3
- "version": "1.0.0",
4
- "description": "Micro client-side router TypeScript rewrite",
5
- "author": "",
6
- "license": "MIT",
7
- "keywords": [
8
- "router",
9
- "client-side",
10
- "history",
11
- "pushstate",
12
- "hashbang"
13
- ],
14
- "repository": {
15
- "type": "git",
16
- "url": "https://github.com/filiperak/singlepage"
17
- },
18
- "type": "module",
19
-
20
- "source": "src/index.ts",
21
-
22
- "main": "./dist/cjs/index.cjs",
23
- "module": "./dist/esm/index.js",
24
- "types": "./dist/types/index.d.ts",
25
-
26
- "exports": {
27
- ".": {
28
- "import": {
29
- "types": "./dist/types/index.d.ts",
30
- "default": "./dist/esm/index.js"
31
- },
32
- "require": {
33
- "types": "./dist/types/index.d.cts",
34
- "default": "./dist/cjs/index.cjs"
35
- }
36
- }
37
- },
38
-
39
- "files": [
40
- "dist"
41
- ],
42
-
43
- "sideEffects": false,
44
-
45
- "scripts": {
46
- "build": "npm run build:esm && npm run build:cjs && npm run build:types",
47
- "build:esm": "tsc -p tsconfig.esm.json",
48
- "build:cjs": "tsc -p tsconfig.cjs.json && node scripts/fix-cjs-ext.mjs",
49
- "build:types": "tsc -p tsconfig.types.json",
50
- "dev": "tsc -p tsconfig.esm.json --watch",
51
- "clean": "rm -rf dist",
52
- "prepublishOnly": "npm run clean && npm run build"
53
- },
54
-
55
- "dependencies": {
56
- "path-to-regexp": "^6.3.0"
57
- },
58
-
59
- "devDependencies": {
60
- "typescript": "^5.4.5"
61
- },
62
-
63
- "engines": {
64
- "node": ">=16"
65
- }
1
+ {
2
+ "name": "singlepage-router",
3
+ "version": "1.0.1",
4
+ "description": "Micro client-side router with History API, hashbang, and middleware support. TypeScript rewrite of page.js with full type safety and dual ESM/CJS output.",
5
+ "author": "openchubura",
6
+ "license": "MIT",
7
+ "keywords": [
8
+ "router",
9
+ "client-side",
10
+ "history",
11
+ "pushstate",
12
+ "hashbang"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/filiperak/singlepage"
17
+ },
18
+ "homepage": "https://github.com/filiperak/singlepage-router#readme",
19
+ "bugs": {
20
+ "url": "https://github.com/filiperak/singlepage-router/issues"
21
+ },
22
+ "type": "module",
23
+
24
+ "source": "src/index.ts",
25
+
26
+ "main": "./dist/cjs/index.cjs",
27
+ "module": "./dist/esm/index.js",
28
+ "types": "./dist/types/index.d.ts",
29
+
30
+ "exports": {
31
+ ".": {
32
+ "import": {
33
+ "types": "./dist/types/index.d.ts",
34
+ "default": "./dist/esm/index.js"
35
+ },
36
+ "require": {
37
+ "types": "./dist/types/index.d.cts",
38
+ "default": "./dist/cjs/index.cjs"
39
+ }
40
+ }
41
+ },
42
+
43
+ "files": [
44
+ "dist"
45
+ ],
46
+
47
+ "sideEffects": false,
48
+
49
+ "scripts": {
50
+ "build": "npm run build:esm && npm run build:cjs && npm run build:types",
51
+ "build:esm": "tsc -p tsconfig.esm.json",
52
+ "build:cjs": "tsc -p tsconfig.cjs.json && node scripts/fix-cjs-ext.mjs",
53
+ "build:types": "tsc -p tsconfig.types.json",
54
+ "dev": "tsc -p tsconfig.esm.json --watch",
55
+ "clean": "rm -rf dist",
56
+ "prepublishOnly": "npm run clean && npm run build"
57
+ },
58
+
59
+ "dependencies": {
60
+ "path-to-regexp": "^6.3.0"
61
+ },
62
+
63
+ "devDependencies": {
64
+ "typescript": "^5.4.5"
65
+ },
66
+
67
+ "engines": {
68
+ "node": ">=16"
69
+ }
66
70
  }