@real-router/browser-plugin 0.11.5 → 0.11.6
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/package.json +5 -5
- package/src/constants.ts +17 -0
- package/src/factory.ts +57 -0
- package/src/index.ts +48 -0
- package/src/plugin.ts +118 -0
- package/src/types.ts +20 -0
- package/src/url-utils.ts +27 -0
- package/src/validation.ts +10 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@real-router/browser-plugin",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.6",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Browser integration plugin with History API, hash routing, and popstate support",
|
|
6
6
|
"main": "./dist/cjs/index.js",
|
|
@@ -8,7 +8,6 @@
|
|
|
8
8
|
"types": "./dist/esm/index.d.mts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
-
"development": "./src/index.ts",
|
|
12
11
|
"types": {
|
|
13
12
|
"import": "./dist/esm/index.d.mts",
|
|
14
13
|
"require": "./dist/cjs/index.d.ts"
|
|
@@ -18,7 +17,8 @@
|
|
|
18
17
|
}
|
|
19
18
|
},
|
|
20
19
|
"files": [
|
|
21
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"src"
|
|
22
22
|
],
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
},
|
|
45
45
|
"sideEffects": false,
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@real-router/core": "^0.45.
|
|
47
|
+
"@real-router/core": "^0.45.2"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@testing-library/jest-dom": "6.9.1",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"build": "tsdown --config-loader unrun",
|
|
60
60
|
"type-check": "tsc --noEmit",
|
|
61
61
|
"lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
|
|
62
|
-
"lint:package": "
|
|
62
|
+
"lint:package": "publint",
|
|
63
63
|
"lint:types": "attw --pack .",
|
|
64
64
|
"build:dist-only": "tsdown --config-loader unrun"
|
|
65
65
|
}
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// packages/browser-plugin/modules/constants.ts
|
|
2
|
+
|
|
3
|
+
import type { BrowserPluginOptions } from "./types";
|
|
4
|
+
|
|
5
|
+
export const defaultOptions: Required<BrowserPluginOptions> = {
|
|
6
|
+
forceDeactivate: true,
|
|
7
|
+
base: "",
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Source identifier for transitions triggered by browser events.
|
|
12
|
+
* Used to distinguish browser-initiated navigation (back/forward buttons)
|
|
13
|
+
* from programmatic navigation (router.navigate()).
|
|
14
|
+
*/
|
|
15
|
+
export const source = "popstate";
|
|
16
|
+
|
|
17
|
+
export const LOGGER_CONTEXT = "browser-plugin";
|
package/src/factory.ts
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { getPluginApi } from "@real-router/core/api";
|
|
2
|
+
import {
|
|
3
|
+
createSafeBrowser,
|
|
4
|
+
normalizeBase,
|
|
5
|
+
safelyEncodePath,
|
|
6
|
+
} from "browser-env";
|
|
7
|
+
|
|
8
|
+
import { defaultOptions, source } from "./constants";
|
|
9
|
+
import { BrowserPlugin } from "./plugin";
|
|
10
|
+
import { extractPath } from "./url-utils";
|
|
11
|
+
import { validateOptions } from "./validation";
|
|
12
|
+
|
|
13
|
+
import type { BrowserPluginOptions } from "./types";
|
|
14
|
+
import type { PluginFactory, Router } from "@real-router/core";
|
|
15
|
+
import type { Browser, SharedFactoryState } from "browser-env";
|
|
16
|
+
|
|
17
|
+
export function browserPluginFactory(
|
|
18
|
+
opts?: Partial<BrowserPluginOptions>,
|
|
19
|
+
browser?: Browser,
|
|
20
|
+
): PluginFactory {
|
|
21
|
+
validateOptions(opts);
|
|
22
|
+
|
|
23
|
+
const options: Required<BrowserPluginOptions> = {
|
|
24
|
+
...defaultOptions,
|
|
25
|
+
...opts,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
options.base = normalizeBase(options.base);
|
|
29
|
+
|
|
30
|
+
const resolvedBrowser =
|
|
31
|
+
browser ??
|
|
32
|
+
createSafeBrowser(
|
|
33
|
+
() =>
|
|
34
|
+
safelyEncodePath(
|
|
35
|
+
extractPath(globalThis.location.pathname, options.base),
|
|
36
|
+
) + globalThis.location.search,
|
|
37
|
+
"browser-plugin",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const forceDeactivate = options.forceDeactivate;
|
|
41
|
+
const transitionOptions = { forceDeactivate, source, replace: true as const };
|
|
42
|
+
|
|
43
|
+
const shared: SharedFactoryState = { removePopStateListener: undefined };
|
|
44
|
+
|
|
45
|
+
return function browserPlugin(routerBase) {
|
|
46
|
+
const plugin = new BrowserPlugin(
|
|
47
|
+
routerBase as Router,
|
|
48
|
+
getPluginApi(routerBase),
|
|
49
|
+
options,
|
|
50
|
+
resolvedBrowser,
|
|
51
|
+
transitionOptions,
|
|
52
|
+
shared,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return plugin.getPlugin();
|
|
56
|
+
};
|
|
57
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// packages/browser-plugin/modules/index.ts
|
|
2
|
+
/* eslint-disable @typescript-eslint/method-signature-style -- method syntax required for declaration merging overload (property syntax causes TS2717) */
|
|
3
|
+
// Public API exports for browser-plugin
|
|
4
|
+
|
|
5
|
+
import type { Params, State } from "@real-router/core";
|
|
6
|
+
|
|
7
|
+
// Main plugin factory
|
|
8
|
+
export { browserPluginFactory } from "./factory";
|
|
9
|
+
|
|
10
|
+
// Types
|
|
11
|
+
export type { BrowserPluginOptions } from "./types";
|
|
12
|
+
|
|
13
|
+
export type { Browser } from "browser-env";
|
|
14
|
+
|
|
15
|
+
// Type guards
|
|
16
|
+
export { isStateStrict as isState } from "type-guards";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Module augmentation for real-router.
|
|
20
|
+
* Extends Router interface with browser plugin methods.
|
|
21
|
+
*/
|
|
22
|
+
declare module "@real-router/core" {
|
|
23
|
+
interface Router {
|
|
24
|
+
/**
|
|
25
|
+
* Builds full URL for a route with base path and hash prefix.
|
|
26
|
+
* Added by browser plugin.
|
|
27
|
+
*/
|
|
28
|
+
buildUrl: (name: string, params?: Params) => string;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Matches URL and returns corresponding state.
|
|
32
|
+
* Added by browser plugin.
|
|
33
|
+
*/
|
|
34
|
+
matchUrl: (url: string) => State | undefined;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Replaces current history state without triggering navigation.
|
|
38
|
+
* Added by browser plugin.
|
|
39
|
+
*/
|
|
40
|
+
replaceHistoryState: (
|
|
41
|
+
name: string,
|
|
42
|
+
params?: Params,
|
|
43
|
+
title?: string,
|
|
44
|
+
) => void;
|
|
45
|
+
|
|
46
|
+
start(path?: string): Promise<State>;
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createPopstateHandler,
|
|
3
|
+
createPopstateLifecycle,
|
|
4
|
+
createStartInterceptor,
|
|
5
|
+
createReplaceHistoryState,
|
|
6
|
+
shouldReplaceHistory,
|
|
7
|
+
updateBrowserState,
|
|
8
|
+
} from "browser-env";
|
|
9
|
+
|
|
10
|
+
import { buildUrl, urlToPath } from "./url-utils";
|
|
11
|
+
|
|
12
|
+
import type { BrowserPluginOptions } from "./types";
|
|
13
|
+
import type {
|
|
14
|
+
NavigationOptions,
|
|
15
|
+
Params,
|
|
16
|
+
Router,
|
|
17
|
+
State,
|
|
18
|
+
Plugin,
|
|
19
|
+
} from "@real-router/core";
|
|
20
|
+
import type { PluginApi } from "@real-router/core/api";
|
|
21
|
+
import type { Browser, SharedFactoryState } from "browser-env";
|
|
22
|
+
|
|
23
|
+
export class BrowserPlugin {
|
|
24
|
+
readonly #router: Router;
|
|
25
|
+
readonly #browser: Browser;
|
|
26
|
+
readonly #removeStartInterceptor: () => void;
|
|
27
|
+
readonly #removeExtensions: () => void;
|
|
28
|
+
readonly #lifecycle: Pick<Plugin, "onStart" | "onStop" | "teardown">;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
router: Router,
|
|
32
|
+
api: PluginApi,
|
|
33
|
+
options: Required<BrowserPluginOptions>,
|
|
34
|
+
browser: Browser,
|
|
35
|
+
transitionOptions: {
|
|
36
|
+
source: string;
|
|
37
|
+
replace: true;
|
|
38
|
+
forceDeactivate?: boolean;
|
|
39
|
+
},
|
|
40
|
+
shared: SharedFactoryState,
|
|
41
|
+
) {
|
|
42
|
+
this.#router = router;
|
|
43
|
+
this.#browser = browser;
|
|
44
|
+
|
|
45
|
+
this.#removeStartInterceptor = createStartInterceptor(api, browser);
|
|
46
|
+
|
|
47
|
+
const pluginBuildUrl = (route: string, params?: Params) => {
|
|
48
|
+
const path = router.buildPath(route, params);
|
|
49
|
+
|
|
50
|
+
return buildUrl(path, options.base);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
this.#removeExtensions = api.extendRouter({
|
|
54
|
+
buildUrl: pluginBuildUrl,
|
|
55
|
+
matchUrl: (url: string) => {
|
|
56
|
+
const path = urlToPath(url, options.base);
|
|
57
|
+
|
|
58
|
+
return path ? api.matchPath(path) : undefined;
|
|
59
|
+
},
|
|
60
|
+
replaceHistoryState: createReplaceHistoryState(
|
|
61
|
+
api,
|
|
62
|
+
router,
|
|
63
|
+
browser,
|
|
64
|
+
pluginBuildUrl,
|
|
65
|
+
),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const handler = createPopstateHandler({
|
|
69
|
+
router,
|
|
70
|
+
api,
|
|
71
|
+
browser,
|
|
72
|
+
allowNotFound: api.getOptions().allowNotFound,
|
|
73
|
+
transitionOptions,
|
|
74
|
+
loggerContext: "browser-plugin",
|
|
75
|
+
buildUrl: (name: string, params?: Params) =>
|
|
76
|
+
router.buildUrl(name, params),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.#lifecycle = createPopstateLifecycle({
|
|
80
|
+
browser,
|
|
81
|
+
shared,
|
|
82
|
+
handler,
|
|
83
|
+
cleanup: () => {
|
|
84
|
+
this.#removeStartInterceptor();
|
|
85
|
+
this.#removeExtensions();
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
getPlugin(): Plugin {
|
|
91
|
+
return {
|
|
92
|
+
...this.#lifecycle,
|
|
93
|
+
|
|
94
|
+
onTransitionSuccess: (
|
|
95
|
+
toState: State,
|
|
96
|
+
fromState: State | undefined,
|
|
97
|
+
navOptions: NavigationOptions,
|
|
98
|
+
) => {
|
|
99
|
+
const replaceHistory = shouldReplaceHistory(
|
|
100
|
+
navOptions,
|
|
101
|
+
toState,
|
|
102
|
+
fromState,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const url = this.#router.buildUrl(toState.name, toState.params);
|
|
106
|
+
|
|
107
|
+
const shouldPreserveHash =
|
|
108
|
+
!fromState || fromState.path === toState.path;
|
|
109
|
+
|
|
110
|
+
const finalUrl = shouldPreserveHash
|
|
111
|
+
? url + this.#browser.getHash()
|
|
112
|
+
: url;
|
|
113
|
+
|
|
114
|
+
updateBrowserState(toState, finalUrl, replaceHistory, this.#browser);
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// packages/browser-plugin/src/types.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Browser plugin configuration.
|
|
5
|
+
*/
|
|
6
|
+
export interface BrowserPluginOptions {
|
|
7
|
+
/**
|
|
8
|
+
* Force deactivation of current route even if canDeactivate returns false.
|
|
9
|
+
*
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
12
|
+
forceDeactivate?: boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base path for all routes (e.g., "/app" for hosted at /app/).
|
|
16
|
+
*
|
|
17
|
+
* @default ""
|
|
18
|
+
*/
|
|
19
|
+
base?: string;
|
|
20
|
+
}
|
package/src/url-utils.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// packages/browser-plugin/src/url-utils.ts
|
|
2
|
+
|
|
3
|
+
import { safeParseUrl } from "browser-env";
|
|
4
|
+
|
|
5
|
+
import { LOGGER_CONTEXT } from "./constants";
|
|
6
|
+
|
|
7
|
+
export function extractPath(pathname: string, base: string): string {
|
|
8
|
+
if (base && pathname.startsWith(base)) {
|
|
9
|
+
const stripped = pathname.slice(base.length);
|
|
10
|
+
|
|
11
|
+
return stripped.startsWith("/") ? stripped : `/${stripped}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return pathname;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildUrl(path: string, base: string): string {
|
|
18
|
+
return base + path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function urlToPath(url: string, base: string): string | null {
|
|
22
|
+
const parsedUrl = safeParseUrl(url, LOGGER_CONTEXT);
|
|
23
|
+
|
|
24
|
+
return parsedUrl
|
|
25
|
+
? extractPath(parsedUrl.pathname, base) + parsedUrl.search
|
|
26
|
+
: null;
|
|
27
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createOptionsValidator } from "browser-env";
|
|
2
|
+
|
|
3
|
+
import { LOGGER_CONTEXT, defaultOptions } from "./constants";
|
|
4
|
+
|
|
5
|
+
import type { BrowserPluginOptions } from "./types";
|
|
6
|
+
|
|
7
|
+
export const validateOptions = createOptionsValidator<BrowserPluginOptions>(
|
|
8
|
+
defaultOptions,
|
|
9
|
+
LOGGER_CONTEXT,
|
|
10
|
+
);
|