├── .gitignore
├── .yarnrc
├── LICENSE
├── README.md
├── example
├── .gitignore
├── index.html
├── index.tsx
├── package.json
├── tsconfig.json
└── yarn.lock
├── package.json
├── src
├── index.ts
└── utils.ts
├── test
└── index.test.tsx
├── tsconfig.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | node_modules
4 | .cache
5 | dist
6 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Kelson Warner
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.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # react-chrome-extension-router
2 |
3 | > A dead simple routing solution for browser extensions
4 |
5 | [](https://www.npmjs.com/package/react-chrome-extension-router) [](https://standardjs.com)
6 |
7 | ## Install
8 |
9 | ```bash
10 | npm install --save react-chrome-extension-router
11 | ```
12 |
13 | ## Usage
14 |
15 | ```jsx
16 | import * as React from 'react';
17 | import * as ReactDOM from 'react-dom';
18 | import {
19 | goBack,
20 | goTo,
21 | popToTop,
22 | Link,
23 | Router,
24 | getCurrent,
25 | getComponentStack,
26 | } from 'react-chrome-extension-router';
27 |
28 | const Three = ({ message }: any) => (
29 |
popToTop()}>
30 |
{message}
31 |
Click me to pop to the top
32 |
33 | );
34 |
35 | const Two = ({ message }: any) => (
36 |
37 | This is component Two. I was passed a message:
38 |
{message}
39 |
goBack()}>
40 | Click me to go back to component One
41 |
42 |
goTo(Three, { message })}>
43 | Click me to go to component Three!
44 |
45 |
46 | );
47 |
48 | const One = () => {
49 | return (
50 |
51 | This is component One. Click me to route to component Two
52 |
53 | );
54 | };
55 |
56 | const App = () => {
57 | useEffect(() => {
58 | const { component, props } = getCurrent();
59 | console.log(
60 | component
61 | ? `There is a component on the stack! ${component} with ${props}`
62 | : `The current stack is empty so Router's direct children will be rendered`
63 | );
64 | const components = getComponentStack();
65 | console.log(`The stack has ${components.length} components on the stack`);
66 | });
67 | return (
68 |
69 |
70 |
71 | );
72 | };
73 |
74 | ReactDOM.render( , document.getElementById('root'));
75 | ```
76 |
77 | [](https://codesandbox.io/s/agitated-satoshi-sccqr?fontsize=14)
78 |
79 | ## License
80 |
81 | MIT © [kelsonpw](https://github.com/kelsonpw)
82 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .cache
3 | dist
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Playground
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import 'react-app-polyfill/ie11';
2 | import * as React from 'react';
3 | import * as ReactDOM from 'react-dom';
4 | import {
5 | goBack,
6 | goTo,
7 | popToTop,
8 | Link,
9 | Router,
10 | getCurrent,
11 | getComponentStack,
12 | } from 'react-chrome-extension-router';
13 |
14 | const Three = ({ message }: any) => (
15 | popToTop()}>
16 |
{message}
17 |
Click me to pop to the top
18 |
19 | );
20 |
21 | const Two = ({ message }: any) => (
22 |
23 | This is component Two. I was passed a message:
24 |
{message}
25 |
goBack()}>
26 | Click me to go back to component One
27 |
28 |
goTo(Three, { message })}>
29 | Click me to go to component Three!
30 |
31 |
32 | );
33 |
34 | const One = () => {
35 | return (
36 |
37 | This is component One. Click me to route to component Two
38 |
39 | );
40 | };
41 |
42 | const App = () => {
43 | const logComponentStack = React.useCallback(() => {
44 | const { component, props } = getCurrent();
45 | console.log(
46 | component
47 | ? `There is a component on the stack! ${component} with ${props}`
48 | : `The current stack is empty so Router's direct children will be rendered`
49 | );
50 | const components = getComponentStack();
51 | console.log(`The stack has ${components.length} components on the stack`);
52 | }, []);
53 |
54 | React.useEffect(() => {
55 | const timeoutId = setTimeout(logComponentStack, 5000);
56 |
57 | return () => clearTimeout(timeoutId);
58 | }, [logComponentStack]);
59 |
60 | return (
61 |
62 |
63 |
64 | );
65 | };
66 |
67 | ReactDOM.render( , document.getElementById('root'));
68 |
--------------------------------------------------------------------------------
/example/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "example",
3 | "version": "1.0.0",
4 | "main": "index.js",
5 | "license": "MIT",
6 | "scripts": {
7 | "start": "parcel index.html",
8 | "build": "parcel build index.html"
9 | },
10 | "dependencies": {
11 | "react-app-polyfill": "^1.0.0",
12 | "react-chrome-extension-router": "^1.1.0"
13 | },
14 | "alias": {
15 | "react": "../node_modules/react",
16 | "react-dom": "../node_modules/react-dom/profiling",
17 | "scheduler/tracing": "../node_modules/scheduler/tracing-profiling"
18 | },
19 | "devDependencies": {
20 | "@types/react": "^16.9.11",
21 | "@types/react-dom": "^16.8.4",
22 | "parcel": "^1.12.3",
23 | "typescript": "^3.4.5"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/example/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowSyntheticDefaultImports": false,
4 | "target": "es5",
5 | "module": "commonjs",
6 | "jsx": "react",
7 | "moduleResolution": "node",
8 | "noImplicitAny": false,
9 | "noUnusedLocals": false,
10 | "noUnusedParameters": false,
11 | "removeComments": true,
12 | "strictNullChecks": true,
13 | "preserveConstEnums": true,
14 | "sourceMap": true,
15 | "lib": ["es2015", "es2016", "dom"],
16 | "baseUrl": ".",
17 | "types": ["node"]
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.4.0",
3 | "license": "MIT",
4 | "main": "dist/index.js",
5 | "typings": "dist/index.d.ts",
6 | "files": [
7 | "dist"
8 | ],
9 | "scripts": {
10 | "start": "tsdx watch",
11 | "build": "tsdx build",
12 | "test": "tsdx test --passWithNoTests",
13 | "lint": "tsdx lint",
14 | "prepare": "tsdx build"
15 | },
16 | "peerDependencies": {
17 | "react": ">=16"
18 | },
19 | "husky": {
20 | "hooks": {
21 | "pre-commit": "tsdx lint"
22 | }
23 | },
24 | "prettier": {
25 | "printWidth": 80,
26 | "semi": true,
27 | "singleQuote": true,
28 | "trailingComma": "es5"
29 | },
30 | "name": "react-chrome-extension-router",
31 | "description": "A dead simple routing solution for browser extensions using React",
32 | "keywords": [
33 | "react",
34 | "react-router",
35 | "router",
36 | "react router",
37 | "chrome",
38 | "browser",
39 | "extension",
40 | "chrome-extension",
41 | "micro router",
42 | "micro react router"
43 | ],
44 | "author": "Kelson Warner",
45 | "repository": {
46 | "type": "git",
47 | "url": "https://github.com/kelsonpw/react-chrome-extension-router.git"
48 | },
49 | "module": "dist/react-chrome-extension-router.esm.js",
50 | "devDependencies": {
51 | "@testing-library/dom": "^7.28.1",
52 | "@testing-library/react": "^9.4.0",
53 | "@types/jest": "^24.0.25",
54 | "@types/react": "^16.9.17",
55 | "@types/react-dom": "^16.9.4",
56 | "husky": "^4.0.7",
57 | "react": "^16.12.0",
58 | "react-dom": "^16.12.0",
59 | "tsdx": "^0.12.3",
60 | "tslib": "^1.10.0",
61 | "typescript": "^3.7.4"
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { useForceUpdate } from './utils';
3 |
4 | type RouterStackItem = {
5 | component: React.ComponentType;
6 | props: any;
7 | };
8 |
9 | let stack: RouterStackItem[] = [];
10 |
11 | let forceUpdateStack: (() => void)[] = [];
12 |
13 | function useForceUpdateStack(): void {
14 | const update = useForceUpdate();
15 |
16 | React.useEffect(() => {
17 | forceUpdateStack.push(update);
18 |
19 | return () => {
20 | const index = forceUpdateStack.indexOf(update);
21 | if (index > -1) {
22 | forceUpdateStack.splice(index, 1);
23 | }
24 | };
25 | }, [update]);
26 | }
27 |
28 | function forceUpdate() {
29 | forceUpdateStack.forEach(fn => fn());
30 | }
31 |
32 | function goTo(comp: React.ComponentType, props: any = {}): void {
33 | stack.push({ component: comp, props });
34 | forceUpdate();
35 | }
36 |
37 | function goBack(): void {
38 | if (stack.length) {
39 | stack.pop();
40 | }
41 | forceUpdate();
42 | }
43 |
44 | function popToTop(): void {
45 | stack = [];
46 | forceUpdate();
47 | }
48 |
49 | function getCurrent(): RouterStackItem {
50 | return stack[stack.length - 1] || { component: false, props: null };
51 | }
52 |
53 | function getComponentStack(): RouterStackItem[] {
54 | return stack;
55 | }
56 |
57 | interface LinkProps {
58 | id?: string;
59 | component: React.ComponentType;
60 | children?: React.ReactNode;
61 | props?: any;
62 | href?: string;
63 | className?: string;
64 | tag?: React.ComponentType | keyof JSX.IntrinsicElements;
65 | onClick?: (event: React.MouseEvent) => void;
66 | }
67 |
68 | function Link({
69 | id = '',
70 | component,
71 | children,
72 | props = {},
73 | href = '',
74 | className = '',
75 | tag = 'a',
76 | onClick,
77 | ...restProps
78 | }: LinkProps & React.HTMLProps) {
79 | const onClickHandler = React.useCallback(
80 | (evt: React.MouseEvent) => {
81 | evt.preventDefault();
82 | if (component) {
83 | goTo(component, props);
84 | }
85 | if (!component && href) {
86 | window.open(href);
87 | }
88 | onClick && onClick(evt);
89 | },
90 | [component, props, href, onClick]
91 | );
92 |
93 | return React.createElement(
94 | tag,
95 | {
96 | href,
97 | className,
98 | id,
99 | onClick: onClickHandler,
100 | ...restProps,
101 | },
102 | children
103 | );
104 | }
105 |
106 | interface NavLinkProps {
107 | id?: string;
108 | component: React.ComponentType;
109 | children?: React.ReactNode;
110 | props?: any;
111 | href?: string;
112 | className?: string;
113 | activeClassName?: string;
114 | tag?: React.ComponentType | keyof JSX.IntrinsicElements;
115 | onClick?: (event: React.MouseEvent) => void;
116 | }
117 |
118 | function NavLink({
119 | id = '',
120 | component,
121 | children,
122 | props = {},
123 | href = '',
124 | className = '',
125 | activeClassName = '',
126 | tag = 'a',
127 | onClick,
128 | ...restProps
129 | }: NavLinkProps & React.HTMLProps) {
130 | const onClickHandler = React.useCallback(
131 | (evt: React.MouseEvent) => {
132 | evt.preventDefault();
133 | if (component) {
134 | goTo(component, props);
135 | }
136 | if (!component && href) {
137 | window.open(href);
138 | }
139 | onClick && onClick(evt);
140 | },
141 | [component, props, href, onClick]
142 | );
143 |
144 | useForceUpdateStack();
145 |
146 | if (stack.length > 0 && stack[stack.length - 1].component === component) {
147 | className = activeClassName + ' ' + className;
148 | }
149 |
150 | return React.createElement(
151 | tag,
152 | {
153 | href,
154 | className,
155 | id,
156 | onClick: onClickHandler,
157 | ...restProps,
158 | },
159 | children
160 | );
161 | }
162 |
163 | interface RouterProps {
164 | children: React.ReactNode;
165 | }
166 |
167 | const emptyStackComponent: RouterStackItem = {
168 | component: ({ children }: any) => children,
169 | props: {},
170 | };
171 |
172 | function Router({ children }: RouterProps) {
173 | useForceUpdateStack();
174 |
175 | const { component: Component, props } =
176 | stack[stack.length - 1] || emptyStackComponent;
177 |
178 | return React.createElement(Component, props, children);
179 | }
180 |
181 | export {
182 | goBack,
183 | getCurrent,
184 | getComponentStack,
185 | goTo,
186 | popToTop,
187 | LinkProps,
188 | RouterProps,
189 | Link,
190 | NavLink,
191 | Router,
192 | };
193 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from 'react';
2 |
3 | function useForceUpdate(): () => void {
4 | const [, dispatch] = useState(Object.create(null));
5 |
6 | return useCallback(() => {
7 | dispatch(Object.create(null));
8 | }, [dispatch]);
9 | }
10 |
11 | export { useForceUpdate };
12 |
--------------------------------------------------------------------------------
/test/index.test.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { render, fireEvent } from '@testing-library/react';
3 | import { goBack, goTo, popToTop, Link, Router } from '../src/index';
4 |
5 | const Three = ({ message }: any) => (
6 | popToTop()}>
7 |
{message}
8 |
Click me to pop to the top
9 |
10 | );
11 |
12 | const Two = ({ message }: any) => (
13 |
14 | This is component Two. I was passed a message:
15 |
{message}
16 |
goBack()}>
17 | Click me to go back to component One
18 |
19 |
goTo(Three, { message })}>
20 | Click me to go to component Three!
21 |
22 |
23 | );
24 |
25 | const One = () => {
26 | return (
27 |
28 | This is component One. Click me to route to component Two
29 |
30 | );
31 | };
32 |
33 | const App = () => (
34 |
35 |
36 |
37 | );
38 |
39 | describe('react-chrome-extension-router', () => {
40 | it('allows navigation between pages', () => {
41 | const { getByText } = render( );
42 |
43 | expect(
44 | getByText('This is component One. Click me to route to component Two')
45 | ).not.toBeNull();
46 |
47 | fireEvent.click(
48 | getByText('This is component One. Click me to route to component Two')
49 | );
50 |
51 | expect(getByText('I came from component one!')).not.toBeNull();
52 |
53 | fireEvent.click(getByText('Click me to go back to component One'));
54 |
55 | expect(
56 | getByText('This is component One. Click me to route to component Two')
57 | ).not.toBeNull();
58 |
59 | fireEvent.click(
60 | getByText('This is component One. Click me to route to component Two')
61 | );
62 |
63 | fireEvent.click(getByText('Click me to go to component Three!'));
64 |
65 | expect(getByText('I came from component one!')).not.toBeNull();
66 |
67 | fireEvent.click(getByText('Click me to pop to the top'));
68 |
69 | expect(
70 | getByText('This is component One. Click me to route to component Two')
71 | ).not.toBeNull();
72 | });
73 | });
74 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["src", "types", "test"],
3 | "compilerOptions": {
4 | "target": "es5",
5 | "module": "esnext",
6 | "lib": ["dom", "esnext"],
7 | "importHelpers": true,
8 | "declaration": true,
9 | "sourceMap": true,
10 | "rootDir": "./",
11 | "strict": true,
12 | "noImplicitAny": true,
13 | "strictNullChecks": true,
14 | "strictFunctionTypes": true,
15 | "strictPropertyInitialization": true,
16 | "noImplicitThis": true,
17 | "alwaysStrict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noImplicitReturns": true,
21 | "noFallthroughCasesInSwitch": true,
22 | "moduleResolution": "node",
23 | "baseUrl": "./",
24 | "paths": {
25 | "*": ["src/*", "node_modules/*"]
26 | },
27 | "jsx": "react",
28 | "esModuleInterop": true
29 | }
30 | }
31 |
--------------------------------------------------------------------------------