├── .eslintrc.json
├── .github
└── workflows
│ └── tests.yml
├── .gitignore
├── .prettierignore
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
├── browser-router.spec.ts
├── browser-router.ts
├── entity.ts
├── helper.ts
├── index.ts
├── manual.typings.d.ts
└── router-group.ts
├── tsconfig.build.json
├── tsconfig.json
└── webpack.config.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "ignorePatterns": ["coverage", "node_modules", "dist"],
3 | "env": {
4 | "node": true,
5 | "es2022": true
6 | },
7 | "parser": "@typescript-eslint/parser",
8 | "parserOptions": {
9 | "ecmaVersion": "latest",
10 | "sourceType": "module",
11 | "project": "tsconfig.json"
12 | },
13 | "extends": [
14 | "eslint:recommended",
15 | "plugin:@typescript-eslint/recommended",
16 | "plugin:import/recommended",
17 | "plugin:import/typescript",
18 | "plugin:prettier/recommended"
19 | ],
20 | "settings": {
21 | "import/parsers": {
22 | "@typescript-eslint/parser": [".ts"]
23 | },
24 | "import/resolver": {
25 | "typescript": {
26 | "alwaysTryTypes": true
27 | }
28 | }
29 | },
30 | "rules": {
31 | "semi": ["warn", "always"],
32 | "complexity": ["error"],
33 | "curly": ["error", "all"],
34 | "no-restricted-syntax": ["error", "LabeledStatement", "WithStatement"],
35 | "no-implicit-coercion": ["error", { "boolean": false }],
36 | "no-new-wrappers": ["error"],
37 | "no-new-object": ["error"],
38 | "new-cap": ["error", { "newIsCap": false, "capIsNew": false }],
39 | "no-array-constructor": ["error"],
40 | "max-params": ["error", 5],
41 | "max-lines": ["error", 500],
42 | "max-statements": ["error", 50],
43 | "one-var": ["error", "never"],
44 | "max-nested-callbacks": ["error", 3],
45 | "no-negated-condition": "error",
46 | "no-case-declarations": 0,
47 | "class-methods-use-this": 0,
48 | "no-param-reassign": 0,
49 | "no-use-before-define": 0,
50 | "no-plusplus": 0,
51 | "no-console": 0,
52 | "prefer-destructuring": 0,
53 | "consistent-return": 0,
54 | "guard-for-in": 0,
55 | "max-classes-per-file": 0,
56 |
57 | "@typescript-eslint/no-unused-vars": ["warn", { "vars": "all", "args": "none" }],
58 | "import/no-named-as-default-member": ["off"],
59 | "import/first": ["warn"],
60 | "import/no-duplicates": ["warn"],
61 | "import/order": ["warn"],
62 | "@typescript-eslint/ban-ts-comment": ["error", { "ts-expect-error": true }],
63 | "@typescript-eslint/no-inferrable-types": 0,
64 | "@typescript-eslint/ban-types": 0,
65 | "@typescript-eslint/explicit-module-boundary-types": 0,
66 | "@typescript-eslint/no-explicit-any": 0,
67 | "prettier/prettier": "warn"
68 | },
69 | "overrides": [
70 | {
71 | "files": ["webpack.config.ts"],
72 | "env": {
73 | "browser": false,
74 | "node": true
75 | }
76 | },
77 | {
78 | "files": ["src/**/*{.,-}{spec,it}.ts"],
79 | "env": {
80 | "jest": true
81 | },
82 | "rules": {
83 | "@typescript-eslint/no-unused-vars": 0,
84 | "@typescript-eslint/unbound-method": 0,
85 | "@typescript-eslint/no-empty-function": 0,
86 | "max-lines": ["error", 1200]
87 | }
88 | }
89 | ]
90 | }
91 |
--------------------------------------------------------------------------------
/.github/workflows/tests.yml:
--------------------------------------------------------------------------------
1 | name: tests
2 | on: [push, pull_request]
3 |
4 | jobs:
5 | tests:
6 | runs-on: ubuntu-latest
7 | strategy:
8 | matrix:
9 | node: [16, 18]
10 | name: Run Tests on ${{ matrix.node }}
11 |
12 | steps:
13 | - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
14 | - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
15 | - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
16 | - name: Check out repository code
17 | uses: actions/checkout@v3
18 | - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
19 | - run: echo "🖥️ The workflow is now ready to test your code on the runner."
20 |
21 | - uses: actions/setup-node@v3
22 | with:
23 | node-version: ${{ matrix.node }}
24 |
25 | - name: Install
26 | run: yarn install
27 |
28 | - name: Run Tests
29 | run: yarn test
30 | - name: Submit coverage
31 | env:
32 | COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
33 | run: cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /coverage
3 | /dist
4 | .DS_Store
5 | /.rpt2_cache
6 | yarn-error.log
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | dist
3 | coverage
4 | .tmp
5 |
6 | // we will do this via eslint instead
7 | .js
8 | .jsx
9 | .ts
10 | .tsx
11 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // This will make VsCode to recommend these extensions by default (the ones related to the tools we use)
3 | "recommendations": [
4 | // make vscode to verify our eslint rules while coding
5 | // https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint
6 | "dbaeumer.vscode-eslint",
7 | // make vscode to verify our prettier formatting rules while coding
8 | // https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode
9 | "esbenp.prettier-vscode"
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "./node_modules/typescript/lib",
3 |
4 | "editor.defaultFormatter": "esbenp.prettier-vscode",
5 | "editor.formatOnSave": true,
6 | // we will do this via eslint instead
7 | "[javascript, javascriptreact, typescript, typescriptreact]": {
8 | "editor.formatOnSave": false
9 | },
10 |
11 | "eslint.alwaysShowStatus": true,
12 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"],
13 | "editor.codeActionsOnSave": ["source.fixAll.eslint"]
14 | }
15 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 rogerpadilla
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # prouter
2 |
3 | [](https://github.com/rogerpadilla/prouter/blob/main/LICENSE)
4 | [](https://github.com/rogerpadilla/prouter)
5 | [](https://coveralls.io/github/rogerpadilla/prouter)
6 | [](https://www.npmjs.com/prouter)
7 |
8 | Fast, unopinionated, minimalist client-side routing library inspired by the simplicity and flexibility of [express middlewares](https://expressjs.com/en/guide/writing-middleware.html).
9 |
10 | Essentially, give `prouter` a list of path expressions (routes) and a callback function (handler) for each one, and `prouter` will automatically invoke these callbacks according to the active path in the URL.
11 |
12 | ## Why prouter?
13 |
14 | - **Performance:** [fast](https://github.com/rogerpadilla/prouter/blob/master/src/browser-router.spec.ts#L7) and tiny size (currently under 5kb before gzipping) are both must-haves to smoothly run in any mobile or desktop browser.
15 | - **KISS principle everywhere:** do only one thing and do it well, routing! Guards? conditional execution? generic pre and post middlewares? all that and more is easily achievable with prouter (see examples below).
16 | - **Learn once:** express router is very powerful, flexible, and simple, why not bring a similar API to the frontend? Under the hood, prouter uses the same (wonderful) library that `express` for parsing routes [path-to-regexp](https://github.com/pillarjs/path-to-regexp) (so it allows the same flexibility to declare routes). Read more about the concept of middlewares [here](https://expressjs.com/en/guide/writing-middleware.html).
17 | - **Unobtrusive:** it is designed from the beginning to play well with vanilla JavaScript or with any other library or framework.
18 | - **Forward-thinking:** written in TypeScript for the future and transpiled to es5 with UMD format for the present... thus it transparently supports any module style: es6, commonJS, AMD. By default, prouter uses the modern [history](https://developer.mozilla.org/en-US/docs/Web/API/History_API) API for routing.
19 | - Unit tests for every feature are created.
20 |
21 | Do you like Prouter? [please give it a 🌟](https://github.com/rogerpadilla/prouter)
22 |
23 | ## Installation
24 |
25 | ```bash
26 | # With NPM
27 | npm install prouter --save
28 |
29 | # Or with Yarn
30 | yarn prouter --save
31 |
32 | # Or just include it using a 'script' tag in your HTML file
33 |
34 | ```
35 |
36 | ## Examples
37 |
38 | ### basic
39 |
40 | ```js
41 | // Using es6 modules
42 | import { browserRouter } from 'prouter';
43 |
44 | // Instantiate the router
45 | const router = browserRouter();
46 |
47 | // Declare the paths and its respective handlers
48 | router
49 | .use('/', async (req, resp) => {
50 | const people = await personService.find();
51 | const html = PersonListCmp(people);
52 | document.querySelector('.router-outlet') = html;
53 | // end the request-response cycle
54 | resp.end();
55 | })
56 | .use('/about', (req, resp) => {
57 | document.querySelector('.router-outlet') =
58 | `
Some static content for the About page.
`;
59 | // end the request-response cycle
60 | resp.end();
61 | });
62 |
63 | // start listening for navigation events
64 | router.listen();
65 | ```
66 |
67 | ### guard middleware which conditionally avoid executing next handlers and prevent changing the path in the URL
68 |
69 | ```js
70 | // Using commonJs modules
71 | const prouter = require('prouter');
72 |
73 | // Instantiate the router
74 | const router = prouter.browserRouter({
75 | processHashChange: true // this allows to process 'hash' changes in the URL.
76 | });
77 |
78 | // Declare the paths and its respective handlers
79 | router
80 | .use('*', (req, resp, next) => {
81 | // this handler will run for any routing event, before any other handlers
82 |
83 | const isAllowed = authService.validateHasAccessToUrl(req.path);
84 |
85 | if (!isAllowed) {
86 | showAlert("You haven't rights to access the page: " + destPath);
87 | // end the request-response cycle, avoid executing other handlers
88 | // and prevent changing the path in the URL.
89 | resp.preventNavigation = true;
90 | resp.end();
91 | return;
92 | }
93 |
94 | // pass control to the next handler
95 | next();
96 | })
97 | .use('/', (req, resp) => {
98 | // do some stuff...
99 | // and end the request-response cycle
100 | resp.end();
101 | })
102 | .use('/admin', (req, resp) => {
103 | // do some stuff...
104 | // and end the request-response cycle
105 | resp.end();
106 | });
107 |
108 | // start listening for navigation events
109 | router.listen();
110 |
111 | // programmatically try to navigate to any route in your router
112 | router.push('/admin');
113 | ```
114 |
115 | ### run a generic middleware (for doing some generic stuff) after running specific handlers
116 |
117 | ```js
118 | import { browserRouter } from 'prouter';
119 |
120 | // Instantiate the router
121 | const router = browserRouter();
122 |
123 | // Declare the paths and its respective handlers
124 | router
125 | .use('/', async (req, resp, next) => {
126 | const people = await personService.find();
127 | const html = PersonListCmp(people);
128 | document.querySelector('.router-outlet') = html;
129 | // pass control to the next handler
130 | next();
131 | })
132 | .use('*', (req, resp) => {
133 | // do some (generic) stuff...
134 | // and end the request-response cycle
135 | resp.end();
136 | });
137 |
138 | // start listening for navigation events
139 | router.listen();
140 | ```
141 |
142 | ### modularize your routing code in different files using Router Group
143 |
144 | ```js
145 | import { browserRouter, routerGroup } from 'prouter';
146 |
147 | // this can be in a different file for modularization of the routes,
148 | // and then import it in your main routes file and mount it.
149 | const productRouterGroup = routerGroup();
150 |
151 | productRouterGroup
152 | .use('/', (req, resp) => {
153 | // do some stuff...
154 | // and end the request-response cycle
155 | resp.end();
156 | })
157 | .use('/create', (req, resp) => {
158 | // do some stuff...
159 | // and end the request-response cycle
160 | resp.end();
161 | })
162 | .use('/:id(\\d+)', (req, resp) => {
163 | const id = req.params.id;
164 | // do some stuff with the 'id'...
165 | // and end the request-response cycle
166 | resp.end();
167 | });
168 |
169 | // Instantiate the router
170 | const router = browserRouter();
171 |
172 | // Declare the paths and its respective handlers
173 | router
174 | .use('*', (req, resp, next) => {
175 | // this handler will run for any routing event, before any other handlers
176 | console.log('request info', req);
177 | // pass control to the next handler
178 | next();
179 | })
180 | .use('/', (req, resp) => {
181 | // do some stuff...
182 | // and end the request-response cycle
183 | resp.end();
184 | })
185 | // mount the product's group of handlers using this base path
186 | .use('/product', productRouterGroup);
187 |
188 | // start listening for the routing
189 | router.listen();
190 |
191 | // programmatically navigate to the detail of the product with this ID
192 | router.push('/product/123');
193 | ```
194 |
195 | ### full example: modularized routing, generic pre handler acting as a guard, generic post handler
196 |
197 | ```js
198 | import { browserRouter, routerGroup } from 'prouter';
199 |
200 | // this can be in a different file for modularization of the routes,
201 | // and then import it in your main routes file and mount it.
202 | const productRouterGroup = routerGroup();
203 |
204 | productRouterGroup
205 | .use('/', (req, resp, next) => {
206 | // do some stuff...
207 | // and pass control to the next handler
208 | next();
209 | })
210 | .use('/create', (req, resp, next) => {
211 | // do some stuff...
212 | // and pass control to the next handler
213 | next();
214 | })
215 | .use('/:id(\\d+)', (req, resp, next) => {
216 | const id = req.params.id;
217 | // do some stuff with the 'id'...
218 | // and pass control to the next handler
219 | next();
220 | });
221 |
222 | // Instantiate the router
223 | const router = browserRouter();
224 |
225 | // Declare the paths and its respective handlers
226 | router
227 | .use('*', (req, resp, next) => {
228 |
229 | // this handler will run for any routing event, before any other handlers
230 |
231 | const isAllowed = authService.validateHasAccessToUrl(req.path);
232 |
233 | if (!isAllowed) {
234 | showAlert("You haven't rights to access the page: " + destPath);
235 | // end the request-response cycle, avoid executing next handlers
236 | // and prevent changing the path in the URL.
237 | resp.preventNavigation = true;
238 | resp.end();
239 | return;
240 | }
241 |
242 | // pass control to the next handler
243 | next();
244 | })
245 | .use('/', (req, resp, next) => {
246 |
247 | const doInfiniteScroll = () => {
248 | // do infinite scroll ...
249 | };
250 |
251 | const onNavigation = (navigationEvt) => {
252 | console.log('new path', navigationEvt.oldPath);
253 | console.log('old path', navigationEvt.newPath);
254 | // if navigating, then remove the listener for the window.scroll.
255 | router.off('navigation', onNavigation);
256 | window.removeEventListener('scroll', doInfiniteScroll);
257 | };
258 |
259 | window.addEventListener('scroll', doInfiniteScroll);
260 |
261 | // subscribe to the navigation event
262 | router.on('navigation', onNavigation);
263 |
264 | // and pass control to the next handler
265 | next();
266 | })
267 | .use('/login', () => {
268 | openLoginModal();
269 | // as this route opens a modal, we would want to prevent navigation in this handler,
270 | // so end the request-response cycle, avoid executing next handlers
271 | // and prevent changing the path in the URL.
272 | resp.preventNavigation = true;
273 | resp.end();
274 | })
275 | .use('/admin', (req, resp, next) => {
276 | // do some stuff...
277 | // and pass control to the next handler
278 | next();
279 | })
280 | // mount the product's group of handlers using this base path
281 | .use('/product', productRouterGroup)
282 | .use('*', (req, res, next) => {
283 |
284 | // this handler will run for any routing event, after the other handlers
285 |
286 | // req.listening will be true when this callback was called due to a
287 | // client-side navigation (useful to differentiate client-side vs
288 | // server-side rendering - when using a mix of both SSR and CSR)
289 | if (req.listening) {
290 | const title = inferTitleFromPath(req.path, APP_TITLE);
291 | updatePageTitle(title);
292 | }
293 |
294 | // end the request-response cycle
295 | resp.end();
296 | });
297 |
298 | // start listening for the routing
299 | router.listen();
300 |
301 |
302 | // the below code is an example about how you could capture clicks on links,
303 | // and accordingly, trigger routing navigation in your app
304 | // (typically, you would put it in a separated file)
305 |
306 | export function isNavigationPath(path: string) {
307 | return !!path && !path.startsWith('javascript:void');
308 | }
309 |
310 | export function isExternalPath(path: string) {
311 | return /^https?:\/\//.test(path);
312 | }
313 |
314 | export function isApplicationPath(path: string) {
315 | return isNavigationPath(path) && !isExternalPath(path);
316 | }
317 |
318 | document.body.addEventListener('click', (evt) => {
319 |
320 | const target = evt.target as Element;
321 | let link: Element;
322 |
323 | if (target.nodeName === 'A') {
324 | link = target;
325 | } else {
326 | link = target.closest('a');
327 | if (!link) {
328 | return;
329 | }
330 | }
331 |
332 | const url = link.getAttribute('href');
333 |
334 | // do nothing if it is not an app's internal link
335 | if (!isApplicationPath(url)) {
336 | return;
337 | }
338 |
339 | // avoid the default browser's behaviour when clicking on a link
340 | // (i.e. do not reload the page).
341 | evt.preventDefault();
342 |
343 | // it is a normal app's link, so trigger the routing navigation
344 | router.push(url);
345 | });
346 | ```
347 |
348 | ### see more advanced usages in the [unit tests.](https://github.com/rogerpadilla/prouter/blob/master/src/browser-router.spec.ts)
349 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "prouter",
3 | "description": "Fast, unopinionated, minimalist client-side routing library inspired by the simplicity and flexibility of express middlewares",
4 | "version": "10.0.27",
5 | "main": "prouter.min.js",
6 | "homepage": "https://github.com/rogerpadilla/prouter",
7 | "bugs": {
8 | "url": "https://github.com/rogerpadilla/prouter/issues"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/rogerpadilla/prouter.git"
13 | },
14 | "scripts": {
15 | "eslint": "eslint --fix 'src/**/*.ts'",
16 | "lint": "npm run eslint",
17 | "test": "rimraf coverage && npm run lint && jest",
18 | "test.watch": "npm run lint && rimraf coverage && jest --watchAll",
19 | "start": "rimraf dist && webpack --watch",
20 | "build": "rimraf dist && NODE_ENV=production webpack"
21 | },
22 | "keywords": [
23 | "client-side",
24 | "browser",
25 | "web",
26 | "mobile",
27 | "router",
28 | "routing",
29 | "library",
30 | "middleware"
31 | ],
32 | "license": "MIT",
33 | "contributors": [
34 | "Roger Padilla "
35 | ],
36 | "dependencies": {
37 | "path-to-regexp": "~0.1.11"
38 | },
39 | "devDependencies": {
40 | "@types/jest": "^29.5.14",
41 | "@types/node": "^22.8.4",
42 | "@types/webpack": "^5.28.5",
43 | "@typescript-eslint/eslint-plugin": "^8.12.2",
44 | "@typescript-eslint/parser": "^8.12.2",
45 | "copy-webpack-plugin": "^12.0.2",
46 | "coveralls": "^3.1.1",
47 | "eslint": "^8.57.0",
48 | "eslint-config-prettier": "^9.1.0",
49 | "eslint-import-resolver-typescript": "^3.6.3",
50 | "eslint-plugin-import": "^2.31.0",
51 | "eslint-plugin-prettier": "^5.2.1",
52 | "jest": "^29.7.0",
53 | "jest-environment-jsdom": "^29.7.0",
54 | "prettier": "^3.3.3",
55 | "rimraf": "^6.0.1",
56 | "source-map-loader": "^5.0.0",
57 | "ts-jest": "^29.2.5",
58 | "ts-loader": "^9.5.1",
59 | "ts-node": "^10.9.2",
60 | "typescript": "^5.6.3",
61 | "webpack": "^5.95.0",
62 | "webpack-cli": "^5.1.4"
63 | },
64 | "prettier": {
65 | "trailingComma": "none",
66 | "tabWidth": 2,
67 | "semi": true,
68 | "singleQuote": true,
69 | "printWidth": 120
70 | },
71 | "jest": {
72 | "verbose": true,
73 | "testEnvironment": "jsdom",
74 | "roots": [
75 | "/src"
76 | ],
77 | "transform": {
78 | "^.+\\.tsx?$": "ts-jest"
79 | },
80 | "testMatch": [
81 | "**/*.spec.ts"
82 | ],
83 | "moduleFileExtensions": [
84 | "ts",
85 | "js",
86 | "json",
87 | "node"
88 | ],
89 | "collectCoverage": true,
90 | "coverageReporters": [
91 | "html",
92 | "lcov"
93 | ],
94 | "coverageDirectory": "coverage"
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/browser-router.spec.ts:
--------------------------------------------------------------------------------
1 | // tslint:disable:max-file-line-count
2 |
3 | import { browserRouter } from './browser-router';
4 | import { ProuterBrowserRouter } from './entity';
5 | import { routerGroup } from './router-group';
6 |
7 | describe('browserRouter', () => {
8 | // Ensure each test completes in less than this short amout of milliseconds.
9 | jest.setTimeout(20);
10 |
11 | let router: ProuterBrowserRouter;
12 |
13 | beforeAll(() => {
14 | const htmlElementsCache = {} as Record;
15 | document.querySelector = jest.fn((selector: string) => {
16 | if (!selector) {
17 | return undefined;
18 | }
19 | if (!htmlElementsCache[selector]) {
20 | const newElement = document.createElement('div');
21 | htmlElementsCache[selector] = newElement;
22 | }
23 | return htmlElementsCache[selector];
24 | });
25 | });
26 |
27 | beforeEach(() => {
28 | history.pushState(undefined, '', '/');
29 | location.hash = '';
30 | router = browserRouter();
31 | });
32 |
33 | afterEach(() => {
34 | router.stop();
35 | });
36 |
37 | it('basic', (done) => {
38 | expect(router.getPath()).toBe('/');
39 |
40 | router
41 | .use('/', (req, res) => {
42 | expect(req.path).toBe('/');
43 | expect(req.queryString).toBe('');
44 | expect(req.query).toEqual({});
45 | expect(router.getPath()).toBe('/');
46 | res.end();
47 | done();
48 | })
49 | .listen();
50 | });
51 |
52 | it('basic chain - no push', (done) => {
53 | expect(router.getPath()).toBe('/');
54 |
55 | let msg = '';
56 |
57 | router
58 | .use('/', (req, res, next) => {
59 | expect(req.path).toBe('/');
60 | expect(req.queryString).toBe('');
61 | expect(req.query).toEqual({});
62 | expect(router.getPath()).toBe('/');
63 | msg = 'changed';
64 | next();
65 | })
66 | .use('*', (req, res) => {
67 | expect(msg).toBe('changed');
68 | expect(router.getPath()).toBe('/');
69 | res.end();
70 | done();
71 | })
72 | .listen();
73 | });
74 |
75 | it('basic chain - push', (done) => {
76 | expect(router.getPath()).toBe('/');
77 |
78 | let msg = '';
79 |
80 | router.use('/about', (req, res, next) => {
81 | expect(req.path).toBe('/about');
82 | expect(req.queryString).toBe('');
83 | expect(req.query).toEqual({});
84 | expect(router.getPath()).toBe('/');
85 | msg = 'changed';
86 | res.end();
87 | });
88 |
89 | router.on('navigation', (navigationEvt) => {
90 | expect(router.getPath()).toBe('/about');
91 | expect(navigationEvt.oldPath).toBe('/');
92 | expect(navigationEvt.newPath).toBe('/about');
93 | expect(msg).toBe('changed');
94 | done();
95 | });
96 |
97 | router.push('/about');
98 | });
99 |
100 | it('basic chain - push - old browser', (done) => {
101 | const _URL = window.URL;
102 | // Emulates old browsers which doesn't supports URL constructor
103 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment
104 | // @ts-expect-error
105 | delete window.URL;
106 | const domCreateElement = document.createElement;
107 |
108 | // Router will use 'createElement("a")' as fallback for parsing paths
109 | // when the URL's constructor is not available (old browsers).
110 | document.createElement = (tag: string) => {
111 | if (tag === 'a') {
112 | // tslint:disable-next-line:no-any
113 | return new _URL('', 'http://example.com') as any;
114 | }
115 | return domCreateElement(tag);
116 | };
117 |
118 | expect(router.getPath()).toBe('/');
119 |
120 | let msg = '';
121 |
122 | router
123 | .use('/about', (req, res, next) => {
124 | expect(req.path).toBe('/about');
125 | expect(req.queryString).toBe('');
126 | expect(req.query).toEqual({});
127 | msg = 'changed';
128 | next();
129 | })
130 | .use('*', (req, res, next) => {
131 | expect(msg).toBe('changed');
132 | window.URL = _URL;
133 | document.createElement = domCreateElement;
134 | res.end();
135 | done();
136 | });
137 |
138 | router.push('/about');
139 | });
140 |
141 | it('process current path when listen', (done) => {
142 | router
143 | .use('/', (req, res) => {
144 | expect(req.listening).toBeFalsy();
145 | res.end();
146 | done();
147 | })
148 | .listen();
149 | });
150 |
151 | it('proper listening - push', (done) => {
152 | router
153 | .use('/something', () => {
154 | fail('This should not be called');
155 | })
156 | .use('/about', (req, res) => {
157 | expect(req.listening).toBeTruthy();
158 | res.end();
159 | done();
160 | })
161 | .listen();
162 |
163 | router.push('/about');
164 | });
165 |
166 | it('parameters', (done) => {
167 | router
168 | .use('/some-path/:id(\\d+)/:tag', (req, res) => {
169 | expect(req.params.id).toBe('16');
170 | expect(req.params.tag).toBe('abc');
171 | res.end();
172 | done();
173 | })
174 | .listen();
175 |
176 | router.push('/some-path/16/abc');
177 | });
178 |
179 | it('query', (done) => {
180 | router.use('/something', (req, res) => {
181 | expect(req.queryString).toBe('?first=5&second=6');
182 | expect(req.query).toEqual({ first: '5', second: '6' });
183 | res.end();
184 | done();
185 | });
186 |
187 | router.push('/something?first=5&second=6');
188 | });
189 |
190 | it('parameters & query', (done) => {
191 | router
192 | .use('/something/:param1/:param2', (req, res) => {
193 | expect(req.params).toEqual({ param1: '16', param2: '18' });
194 | expect(req.queryString).toBe('?first=5&second=6');
195 | expect(req.query).toEqual({ first: '5', second: '6' });
196 | res.end();
197 | done();
198 | })
199 | .listen();
200 |
201 | router.push('/something/16/18?first=5&second=6');
202 | });
203 |
204 | it('divided parameters', (done) => {
205 | router
206 | .use('/something/:param1/other/:param2', (req, res) => {
207 | expect(req.params).toEqual({ param1: '16', param2: '18' });
208 | expect(req.query).toEqual({ first: '5', second: '6' });
209 | res.end();
210 | done();
211 | })
212 | .listen();
213 |
214 | router.push('/something/16/other/18?first=5&second=6');
215 | });
216 |
217 | it('do not call if no match', (done) => {
218 | router
219 | .use('/abc/:p1/other/:p2', () => {
220 | fail('This should not be called');
221 | })
222 | .use('*', (req, res) => {
223 | res.end();
224 | done();
225 | });
226 |
227 | router.push('/something/16/other/18?q1=5&q2=6');
228 | });
229 |
230 | it('pass custom params to next middleware', (done) => {
231 | router
232 | .use('/something/:p1/other/:p2', (req, res, next) => {
233 | expect(req.query).toEqual({ q1: '5', q2: '6' });
234 | req.query.q3 = '7';
235 | req.params.customParam = 123;
236 | next();
237 | })
238 | .use('*', (req, res) => {
239 | expect(req.query).toEqual({ q1: '5', q2: '6', q3: '7' });
240 | expect(req.params).toMatchObject({ p1: '16', p2: '18', customParam: 123 });
241 | res.end();
242 | done();
243 | });
244 |
245 | router.push('/something/16/other/18?q1=5&q2=6');
246 | });
247 |
248 | it('order', (done) => {
249 | expect(router.getPath()).toBe('/');
250 |
251 | let msg = '';
252 |
253 | router
254 | .use('/about', (req, res, next) => {
255 | expect(req.path).toBe('/about');
256 | expect(req.queryString).toBe('');
257 | expect(req.query).toEqual({});
258 | expect(router.getPath()).toBe('/');
259 | msg = 'hello';
260 | next();
261 | })
262 | .use('*', (req, res) => {
263 | expect(msg).toBe('hello');
264 | expect(router.getPath()).toBe('/');
265 | res.end();
266 | done();
267 | });
268 |
269 | router.push('/about');
270 | });
271 |
272 | it('end and prevent navigation', () => {
273 | expect(router.getPath()).toBe('/');
274 |
275 | router
276 | .use('/about', (req, res, next) => {
277 | expect(req.path).toBe('/about');
278 | expect(req.queryString).toBe('');
279 | expect(req.query).toEqual({});
280 | expect(router.getPath()).toBe('/');
281 | expect(res.preventNavigation).toBeUndefined();
282 | res.preventNavigation = true;
283 | next();
284 | })
285 | .use('*', (req, res) => {
286 | expect(res.preventNavigation).toBe(true);
287 | res.end();
288 | });
289 |
290 | router.push('/about');
291 |
292 | expect(router.getPath()).toBe('/');
293 | });
294 |
295 | it('next() in every callback', (done) => {
296 | expect(router.getPath()).toBe('/');
297 |
298 | router
299 | .use('/about', (req, res, next) => {
300 | expect(req.path).toBe('/about');
301 | expect(req.queryString).toBe('');
302 | expect(req.query).toEqual({});
303 | expect(router.getPath()).toBe('/');
304 | next();
305 | })
306 | .use('*', (req, res, next) => {
307 | expect(router.getPath()).toBe('/');
308 | next();
309 | done();
310 | });
311 |
312 | router.push('/about');
313 | });
314 |
315 | it('Throws error if try to listen more than once', () => {
316 | router.listen();
317 |
318 | expect(() => {
319 | router.listen();
320 | }).toThrowError();
321 | });
322 |
323 | it('RouterGroup', (done) => {
324 | const group = routerGroup();
325 |
326 | group.use('/ask', () => {
327 | done();
328 | });
329 |
330 | router.use('/question', group);
331 |
332 | router.push('/question/ask');
333 | });
334 |
335 | it('RouterGroup with params', (done) => {
336 | const group = routerGroup();
337 |
338 | group.use('/:p1/other/:p2', (req, res) => {
339 | expect(req.path).toBe('/something/16/other/18');
340 | res.end();
341 | done();
342 | });
343 |
344 | router.use('/something', group);
345 |
346 | router.push('/something/16/other/18?q1=5&q2=6');
347 | });
348 |
349 | it('Emulate browsers with URL support', (done) => {
350 | // tslint:disable-next-line:no-unnecessary-class
351 | class MyURL {
352 | constructor(path: string) {
353 | const parser = document.createElement('a');
354 | parser.href = 'http://example.com' + path;
355 | const propsToCopy = ['pathname', 'hash', 'hostname', 'host', 'search'] satisfies (keyof typeof parser)[];
356 | for (const prop of propsToCopy) {
357 | (this as any)[prop] = parser[prop];
358 | }
359 | }
360 | }
361 |
362 | const _URL = window.URL;
363 | window.URL = MyURL as typeof _URL;
364 |
365 | router.use('/about', (req, res) => {
366 | res.end();
367 | window.URL = _URL;
368 | done();
369 | });
370 |
371 | router.push('/about');
372 | });
373 |
374 | it('should produce navigation event', (done) => {
375 | expect(router.getPath()).toBe('/');
376 |
377 | router
378 | .use('/hello', (req, res, next) => {
379 | expect(req.path).toBe('/hello');
380 | expect(req.queryString).toBe('');
381 | expect(req.query).toEqual({});
382 | expect(router.getPath()).toBe('/');
383 | next();
384 | })
385 | .listen();
386 |
387 | router.on('navigation', (navigationEvt) => {
388 | expect(router.getPath()).toBe('/hello');
389 | expect(navigationEvt.oldPath).toBe('/');
390 | expect(navigationEvt.newPath).toBe('/hello');
391 | done();
392 | });
393 |
394 | router.push('/hello');
395 | });
396 |
397 | it('should not produce navigation event', () => {
398 | expect(router.getPath()).toBe('/');
399 |
400 | router
401 | .use('/', (req, res, next) => {
402 | expect(req.path).toBe('/');
403 | expect(req.queryString).toBe('');
404 | expect(req.query).toEqual({});
405 | expect(router.getPath()).toBe('/');
406 | next();
407 | })
408 | .listen();
409 |
410 | router.on('navigation', () => {
411 | fail('Should not navigate since no match in the registered handlers');
412 | });
413 |
414 | router.push('/hello');
415 | });
416 |
417 | it('should unsubscribe from navigation event', (done) => {
418 | expect(router.getPath()).toBe('/');
419 |
420 | router
421 | .use('/hello', (req, res, next) => {
422 | expect(req.path).toBe('/hello');
423 | expect(req.queryString).toBe('');
424 | expect(req.query).toEqual({});
425 | expect(router.getPath()).toBe('/');
426 | next();
427 | done();
428 | })
429 | .listen();
430 |
431 | const onNavigation = () => {
432 | fail('Should not enter here since unsubscribed');
433 | };
434 |
435 | router.on('navigation', onNavigation);
436 |
437 | router.off('navigation', onNavigation);
438 |
439 | router.push('/hello');
440 | });
441 |
442 | it('should ignore "hash" changes by default', (done) => {
443 | expect(router.getPath()).toBe('/');
444 |
445 | let counter = 0;
446 |
447 | router
448 | .use('/', (req, res, next) => {
449 | expect(req.path).toBe('/');
450 | expect(req.queryString).toBe('');
451 | expect(req.query).toEqual({});
452 | counter++;
453 | next();
454 | })
455 | .listen();
456 |
457 | window.onhashchange = () => {
458 | expect(counter).toBe(1);
459 | done();
460 | };
461 |
462 | location.hash = 'something';
463 | });
464 |
465 | it('should not ignore "hash" changes if configured', (done) => {
466 | const localRouter = browserRouter({
467 | processHashChange: true
468 | });
469 |
470 | expect(localRouter.getPath()).toBe('/');
471 |
472 | let counter = 0;
473 |
474 | localRouter
475 | .use('/', (req, res, next) => {
476 | expect(req.path).toBe('/');
477 | expect(req.queryString).toBe('');
478 | expect(req.query).toEqual({});
479 | counter++;
480 | next();
481 | })
482 | .listen();
483 |
484 | window.onhashchange = () => {
485 | expect(counter).toBe(2);
486 | done();
487 | };
488 |
489 | location.hash = 'something';
490 | });
491 |
492 | it('should trigger "prouter.onnavigation" event on "history.back"', (done) => {
493 | expect(router.getPath()).toBe('/');
494 |
495 | history.pushState(undefined, '', '/hello');
496 |
497 | const init = () => {
498 | router
499 | .use('/', (req, res, next) => {
500 | expect(req.path).toBe('/');
501 | expect(req.queryString).toBe('');
502 | expect(req.query).toEqual({});
503 | expect(router.getPath()).toBe('/');
504 | window.removeEventListener('popstate', init);
505 | next();
506 | done();
507 | })
508 | .listen();
509 | };
510 |
511 | window.addEventListener('popstate', init);
512 |
513 | history.back();
514 | });
515 |
516 | it('should trigger "prouter.onnavigation" event on "history.forward"', (done) => {
517 | expect(router.getPath()).toBe('/');
518 |
519 | let counter = 0;
520 |
521 | history.pushState(undefined, '', '/hello');
522 |
523 | const init = () => {
524 | counter++;
525 | if (counter < 2) {
526 | history.forward();
527 | return;
528 | }
529 | router
530 | .use('/hello', (req, res, next) => {
531 | expect(req.path).toBe('/hello');
532 | expect(req.queryString).toBe('');
533 | expect(req.query).toEqual({});
534 | expect(router.getPath()).toBe('/hello');
535 | window.removeEventListener('popstate', init);
536 | next();
537 | done();
538 | })
539 | .listen();
540 | };
541 |
542 | window.addEventListener('popstate', init);
543 |
544 | history.back();
545 | });
546 | });
547 |
--------------------------------------------------------------------------------
/src/browser-router.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ProuterSubscriptors,
3 | ProuterSubscriptionType,
4 | ProuterSubscriptorCallback,
5 | ProuterNavigationEvent,
6 | ProuterBrowserRouter,
7 | ProuterBrowserOptions,
8 | ProuterParsedHandler,
9 | ProuterRequestCallback,
10 | ProuterGroup,
11 | ProuterRouter,
12 | ProuterProcessPathCallback,
13 | ProuterResponse,
14 | ProuterProcessPathOptions,
15 | ProuterNextMiddleware
16 | } from './entity';
17 | import { routerHelper } from './helper';
18 |
19 | export function browserRouter(options: ProuterBrowserOptions = {}) {
20 | const handlers: ProuterParsedHandler[] = [];
21 | let listening = false;
22 | let previousPath = routerHelper.getPath();
23 | const subscriptors: ProuterSubscriptors = {
24 | navigation: []
25 | };
26 |
27 | const onPopState = () => {
28 | const newPath = routerHelper.getPath();
29 | /* 'popstate' event is also triggered for 'hash' changes (in the URL),
30 | * ignore them if the 'processHashChange' option is not provided and if the
31 | * path didn't changed. */
32 | if (!options.processHashChange && newPath === previousPath) {
33 | return;
34 | }
35 | br.processCurrentPath();
36 | triggerOnNavigation({ oldPath: previousPath, newPath });
37 | previousPath = newPath;
38 | };
39 |
40 | const triggerOnNavigation = (navigationEvt: ProuterNavigationEvent) => {
41 | subscriptors.navigation.forEach((subscriptor) => {
42 | subscriptor(navigationEvt);
43 | });
44 | };
45 |
46 | const br: ProuterBrowserRouter = {
47 | listen() {
48 | if (listening) {
49 | throw new Error('Already listening.');
50 | }
51 | br.processCurrentPath();
52 | addEventListener('popstate', onPopState);
53 | listening = true;
54 | },
55 |
56 | stop() {
57 | removeEventListener('popstate', onPopState);
58 | },
59 |
60 | use(path: string, callback: ProuterRequestCallback | ProuterGroup): ProuterRouter {
61 | if (typeof callback === 'function') {
62 | const pathExp = routerHelper.stringToRegexp(path);
63 | handlers.push({ path, pathExp, callback });
64 | } else {
65 | for (const handler of callback.handlers) {
66 | const itPath = path + handler.path;
67 | const pathExp = routerHelper.stringToRegexp(itPath);
68 | handlers.push({ path: itPath, pathExp, callback: handler.callback });
69 | }
70 | }
71 | return this;
72 | },
73 |
74 | processPath(path: string, processPathCallback?: ProuterProcessPathCallback) {
75 | const requestProcessors = routerHelper.obtainRequestProcessors(path, handlers);
76 |
77 | if (requestProcessors.length === 0) {
78 | return;
79 | }
80 |
81 | const listeningSnapshop = listening;
82 | let wasProcessPathCallbackCalled: boolean;
83 | let index = 0;
84 |
85 | const response: ProuterResponse = {
86 | end() {
87 | if (processPathCallback && !wasProcessPathCallbackCalled) {
88 | wasProcessPathCallbackCalled = true;
89 | const opts: ProuterProcessPathOptions = { preventNavigation: response.preventNavigation };
90 | processPathCallback(opts);
91 | }
92 | }
93 | };
94 |
95 | /** Call the middlewares for the given path. */
96 | const next: ProuterNextMiddleware = () => {
97 | // If next was called and the last processor was already executed then automatically stop.
98 | if (index === requestProcessors.length) {
99 | response.end();
100 | return;
101 | }
102 |
103 | const reqProc = requestProcessors[index];
104 | reqProc.request.listening = listeningSnapshop;
105 |
106 | index++;
107 |
108 | reqProc.callback(reqProc.request, response, next);
109 | };
110 |
111 | next();
112 | },
113 |
114 | getPath: routerHelper.getPath,
115 |
116 | push(newPath: string) {
117 | br.processPath(newPath, (opts) => {
118 | if (!opts || !opts.preventNavigation) {
119 | const oldPath = br.getPath();
120 | history.pushState(undefined, '', newPath);
121 | triggerOnNavigation({ oldPath, newPath });
122 | }
123 | });
124 | },
125 |
126 | processCurrentPath() {
127 | const path = br.getPath();
128 | br.processPath(path);
129 | },
130 |
131 | on(type: ProuterSubscriptionType, callback: ProuterSubscriptorCallback) {
132 | subscriptors[type].push(callback);
133 | },
134 |
135 | off(type: ProuterSubscriptionType, callback: ProuterSubscriptorCallback) {
136 | subscriptors[type] = subscriptors[type].filter((cb) => {
137 | return cb !== callback;
138 | });
139 | }
140 | };
141 |
142 | return br;
143 | }
144 |
--------------------------------------------------------------------------------
/src/entity.ts:
--------------------------------------------------------------------------------
1 | export interface ProuterRequestCallback {
2 | // tslint:disable-next-line:no-any
3 | (req: ProuterRequest, resp: ProuterResponse, next: ProuterNextMiddleware): any;
4 | }
5 |
6 | export interface ProuterPath {
7 | readonly path: string;
8 | readonly queryString: string;
9 | readonly query: ProuterStringMap;
10 | }
11 |
12 | export interface ProuterStringMap {
13 | // tslint:disable-next-line:no-any
14 | [prop: string]: any;
15 | }
16 |
17 | export interface ProuterPathKey {
18 | readonly name: string | number;
19 | readonly prefix: string;
20 | readonly delimiter: string;
21 | readonly optional: boolean;
22 | readonly repeat: boolean;
23 | readonly pattern: string;
24 | readonly partial: boolean;
25 | }
26 |
27 | export interface ProuterPathExp extends RegExp {
28 | keys: ProuterPathKey[];
29 | }
30 |
31 | export interface ProuterHandler {
32 | readonly path: string;
33 | readonly callback: ProuterRequestCallback;
34 | }
35 |
36 | export interface ProuterParsedHandler extends ProuterHandler {
37 | readonly pathExp: ProuterPathExp;
38 | }
39 |
40 | export interface ProuterRequest extends ProuterPath {
41 | params: ProuterStringMap;
42 | listening?: boolean;
43 | }
44 |
45 | export interface ProuterProcessPathOptions {
46 | preventNavigation?: boolean;
47 | }
48 |
49 | export interface ProuterResponse extends ProuterProcessPathOptions {
50 | end(): void;
51 | }
52 |
53 | export interface ProuterRequestProcessor {
54 | readonly request: ProuterRequest;
55 | readonly callback: ProuterRequestCallback;
56 | }
57 |
58 | export interface ProuterProcessPathCallback {
59 | (opts?: ProuterProcessPathOptions): void;
60 | }
61 |
62 | export interface ProuterNextMiddleware {
63 | (): void;
64 | }
65 |
66 | export interface ProuterGroup {
67 | readonly handlers: ProuterHandler[];
68 | use(path: string, callback: ProuterRequestCallback): ProuterGroup;
69 | }
70 |
71 | export interface ProuterRouter {
72 | use(path: string, callback: ProuterRequestCallback | ProuterGroup): ProuterRouter;
73 | listen(): void;
74 | processPath(path: string, processPathCallback?: ProuterProcessPathCallback): void;
75 | }
76 |
77 | export interface ProuterBrowserOptions {
78 | readonly processHashChange?: boolean;
79 | }
80 |
81 | export interface ProuterBrowserRouter extends ProuterRouter {
82 | processCurrentPath(): void;
83 | getPath(): string;
84 | push(path: string): void;
85 | stop(): void;
86 | on(type: ProuterSubscriptionType, callback: ProuterSubscriptorCallback): void;
87 | off(type: ProuterSubscriptionType, callback: ProuterSubscriptorCallback): void;
88 | }
89 |
90 | export interface ProuterNavigationEvent {
91 | readonly oldPath: string;
92 | readonly newPath: string;
93 | }
94 |
95 | export interface ProuterSubscriptorCallback {
96 | (evt: ProuterNavigationEvent): void;
97 | }
98 |
99 | export interface ProuterSubscriptors {
100 | navigation: ProuterSubscriptorCallback[];
101 | }
102 |
103 | export type ProuterSubscriptionType = keyof ProuterSubscriptors;
104 |
105 | // Dont delete this dummy, TS do not create the definition of the file if only interfaces
106 | export const prouterSomethingToMakeTsToExportThisFile = 1;
107 |
--------------------------------------------------------------------------------
/src/helper.ts:
--------------------------------------------------------------------------------
1 | import * as pathToRegexp from 'path-to-regexp';
2 |
3 | import {
4 | ProuterPath,
5 | ProuterPathExp,
6 | ProuterRequestProcessor,
7 | ProuterRequest,
8 | ProuterParsedHandler,
9 | ProuterPathKey
10 | } from './entity';
11 |
12 | export const routerHelper = {
13 | getPath() {
14 | return decodeURI(location.pathname + location.search);
15 | },
16 |
17 | stringToRegexp(str: string) {
18 | const keys: ProuterPathKey[] = [];
19 | const resp = pathToRegexp(str, keys) as ProuterPathExp;
20 | resp.keys = keys;
21 | return resp;
22 | },
23 |
24 | parseQuery(str: string) {
25 | const searchObj: { [key: string]: string } = {};
26 |
27 | if (str === '') {
28 | return searchObj;
29 | }
30 |
31 | const qs = str.slice(1);
32 | const args = qs.split('&');
33 |
34 | for (const arg of args) {
35 | const paramKv = arg.split('=');
36 | searchObj[decodeURIComponent(paramKv[0])] = decodeURIComponent(paramKv[1]);
37 | }
38 |
39 | return searchObj;
40 | },
41 |
42 | parsePath(path: string) {
43 | let url: URL | HTMLAnchorElement;
44 |
45 | if (typeof URL === 'function') {
46 | url = new URL(path, 'http://example.com');
47 | } else {
48 | url = document.createElement('a');
49 | url.href = 'http://example.com' + path;
50 | }
51 |
52 | const parsedPath: Partial = {
53 | path: url.pathname,
54 | queryString: url.search,
55 | query: routerHelper.parseQuery(url.search)
56 | };
57 |
58 | return parsedPath;
59 | },
60 |
61 | /**
62 | * Obtain the request processors for the given path according to the handlers in the router.
63 | */
64 | obtainRequestProcessors(path: string, handlers: ProuterParsedHandler[]) {
65 | const parsedPath = routerHelper.parsePath(path);
66 | const requestProcessors: ProuterRequestProcessor[] = [];
67 | const req = parsedPath as ProuterRequest;
68 | req.params = {};
69 |
70 | for (const handler of handlers) {
71 | const result = handler.pathExp.exec(req.path);
72 |
73 | if (result) {
74 | const params = result.slice(1);
75 | const keys = handler.pathExp.keys;
76 |
77 | for (let i = 0; i < params.length; i++) {
78 | req.params[keys[i].name] = decodeURIComponent(params[i]);
79 | }
80 |
81 | requestProcessors.push({ callback: handler.callback, request: req });
82 | }
83 | }
84 |
85 | return requestProcessors;
86 | }
87 | };
88 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './helper';
2 | export * from './router-group';
3 | export * from './browser-router';
4 | export * from './entity';
5 |
--------------------------------------------------------------------------------
/src/manual.typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'path-to-regexp';
2 |
--------------------------------------------------------------------------------
/src/router-group.ts:
--------------------------------------------------------------------------------
1 | import { ProuterRequestCallback, ProuterGroup } from './entity';
2 |
3 | export function routerGroup() {
4 | const groupObj: ProuterGroup = {
5 | handlers: [],
6 |
7 | use(path: string, callback: ProuterRequestCallback) {
8 | groupObj.handlers.push({ path, callback });
9 | return groupObj;
10 | }
11 | };
12 |
13 | return groupObj;
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/tsconfig",
3 | "extends": "./tsconfig.json",
4 | "include": ["src"],
5 | "exclude": ["**/*.spec.ts"]
6 | }
7 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES5",
4 | "module": "CommonJS",
5 | "moduleResolution": "Node",
6 | "sourceMap": true,
7 | "strict": true,
8 | "forceConsistentCasingInFileNames": true,
9 | "declaration": true,
10 | "declarationDir": "dist",
11 | "lib": ["ESNext", "DOM"],
12 | "skipLibCheck": true
13 | },
14 | "exclude": ["node_modules", "dist", "coverage"]
15 | }
16 |
--------------------------------------------------------------------------------
/webpack.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-non-null-assertion */
2 | import * as path from 'path';
3 | import { Configuration, ProgressPlugin } from 'webpack';
4 | import * as CopyWebpackPlugin from 'copy-webpack-plugin';
5 |
6 | type Mode = 'development' | 'production';
7 | const mode = (process.env.NODE_ENV as Mode) ?? 'development';
8 | const isProductionMode = mode === 'production';
9 | console.debug('*** mode', mode);
10 |
11 | const config: Configuration = {
12 | mode,
13 | profile: true,
14 | bail: isProductionMode,
15 | devtool: 'source-map',
16 |
17 | resolve: {
18 | extensions: ['.ts', '.js']
19 | },
20 |
21 | entry: {
22 | 'prouter.min': './src/index.ts'
23 | },
24 |
25 | output: {
26 | path: path.resolve('dist'),
27 | publicPath: '/',
28 | filename: '[name].js',
29 | chunkFilename: '[id].chunk.js',
30 | library: 'prouter',
31 | libraryTarget: 'umd'
32 | },
33 |
34 | module: {
35 | rules: [
36 | /*
37 | * Source map loader support for *.js files
38 | * Extracts SourceMaps for source files that as added as sourceMappingURL comment.
39 | *
40 | * See: https://github.com/webpack/source-map-loader
41 | */
42 | {
43 | test: /\.js$/,
44 | use: 'source-map-loader',
45 | enforce: 'pre'
46 | },
47 |
48 | {
49 | test: /\.ts$/,
50 | loader: 'ts-loader',
51 | exclude: /node_modules/,
52 | options: {
53 | configFile: 'tsconfig.build.json'
54 | }
55 | }
56 | ]
57 | },
58 |
59 | plugins: [new CopyWebpackPlugin({ patterns: ['package.json', 'README.md', 'LICENSE'] })]
60 | };
61 |
62 | if (isProductionMode) {
63 | config.plugins!.unshift(new ProgressPlugin());
64 | }
65 |
66 | module.exports = config;
67 |
--------------------------------------------------------------------------------