@hypen-space/core 0.4.82 → 0.4.84
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 +10 -4
- package/dist/app.d.ts +78 -1
- package/dist/app.js +102 -46
- package/dist/app.js.map +6 -5
- package/dist/components/builtin.js +102 -46
- package/dist/components/builtin.js.map +6 -5
- package/dist/engine-base.d.ts +19 -0
- package/dist/engine-base.js +32 -1
- package/dist/engine-base.js.map +3 -3
- package/dist/index.browser.js +117 -85
- package/dist/index.browser.js.map +7 -6
- package/dist/index.d.ts +4 -0
- package/dist/index.js +318 -85
- package/dist/index.js.map +9 -7
- package/dist/managed-router.d.ts +121 -5
- package/dist/portable.d.ts +62 -0
- package/dist/router.d.ts +7 -1
- package/dist/router.js +60 -81
- package/dist/router.js.map +6 -5
- package/dist/state.js +49 -46
- package/dist/state.js.map +5 -4
- package/dist/types.d.ts +12 -1
- package/package.json +7 -1
- package/src/app.ts +131 -1
- package/src/engine-base.ts +55 -0
- package/src/index.ts +13 -0
- package/src/managed-router.ts +285 -35
- package/src/portable.ts +113 -0
- package/src/router.ts +25 -50
- package/src/state.ts +11 -70
- package/src/types.ts +23 -1
package/dist/managed-router.d.ts
CHANGED
|
@@ -2,15 +2,64 @@
|
|
|
2
2
|
* Managed Router — orchestrates module mount/unmount on route changes.
|
|
3
3
|
*
|
|
4
4
|
* When the router navigates to a route:
|
|
5
|
-
* 1.
|
|
6
|
-
*
|
|
5
|
+
* 1. Deactivates and unmounts the previous module (either persisting it for
|
|
6
|
+
* later reuse or destroying it).
|
|
7
|
+
* 2. Mounts the new module (creating it fresh or restoring from cache) and
|
|
8
|
+
* activates it.
|
|
7
9
|
*
|
|
8
10
|
* Module names are used as state prefixes (lowercased) for isolation.
|
|
11
|
+
*
|
|
12
|
+
* ## Persistence (default: on for module-backed routes)
|
|
13
|
+
*
|
|
14
|
+
* By default, any route whose `component` resolves to a registered module
|
|
15
|
+
* definition (or provides one inline via `route.module`) has its module
|
|
16
|
+
* instance **persisted** across navigations. This preserves module state
|
|
17
|
+
* (e.g. fetched data, form inputs, scroll position in state) so that
|
|
18
|
+
* navigating away and back doesn't re-trigger the initial "loading" state
|
|
19
|
+
* that usually lives in `onCreated`.
|
|
20
|
+
*
|
|
21
|
+
* Opt out by setting `persist: false` on the module definition. Routes
|
|
22
|
+
* without a module definition are unchanged — they have nothing to persist.
|
|
23
|
+
*
|
|
24
|
+
* ## Persistence cache bound (LRU)
|
|
25
|
+
*
|
|
26
|
+
* The persisted-module cache is an insertion-ordered LRU with a default
|
|
27
|
+
* cap of `DEFAULT_MAX_PERSISTED_MODULES` (= 10). Persisting a module
|
|
28
|
+
* past the cap evicts the least-recently-persisted entry and tears it
|
|
29
|
+
* down via `instance.destroy()` + `unregisterModule()` — the same path a
|
|
30
|
+
* `persist: false` module takes on unmount. Restoring from the cache
|
|
31
|
+
* counts as a touch (the entry is removed on mount and re-inserted on
|
|
32
|
+
* next unmount, placing it at the MRU end).
|
|
33
|
+
*
|
|
34
|
+
* The cap matches the engine's `DEFAULT_ROUTER_CACHE_SIZE` for the
|
|
35
|
+
* Router IR subtree cache, so SDK-level module retention and
|
|
36
|
+
* engine-level DOM-subtree retention stay in lockstep by default.
|
|
37
|
+
* Override via the `maxPersistedModules` constructor option. Pass
|
|
38
|
+
* `Infinity` to disable eviction (only advisable for bounded route
|
|
39
|
+
* sets — otherwise a long session will leak).
|
|
40
|
+
*
|
|
41
|
+
* ## Lifecycle on navigation
|
|
42
|
+
*
|
|
43
|
+
* First visit to a route:
|
|
44
|
+
* `construct → onCreated → onActivated`
|
|
45
|
+
* Navigate away:
|
|
46
|
+
* `onDeactivated` (then either persist in cache OR `onDestroyed`)
|
|
47
|
+
* Revisit (persisted):
|
|
48
|
+
* `onActivated` (only — `onCreated` does not re-run)
|
|
49
|
+
*
|
|
50
|
+
* Use `onActivated` for per-navigation side effects like refreshing data;
|
|
51
|
+
* use `onCreated` for one-time module setup.
|
|
9
52
|
*/
|
|
10
53
|
import type { IEngine, HypenModuleDefinition } from "./app.js";
|
|
11
54
|
import { HypenModuleInstance, HypenApp } from "./app.js";
|
|
12
55
|
import type { HypenRouter } from "./router.js";
|
|
13
56
|
import type { HypenGlobalContext } from "./context.js";
|
|
57
|
+
/**
|
|
58
|
+
* Default upper bound on `persistedModules`. Mirrors
|
|
59
|
+
* `DEFAULT_ROUTER_CACHE_SIZE` in the engine's Router IR node so the SDK
|
|
60
|
+
* module cache and the engine subtree cache evict in lockstep.
|
|
61
|
+
*/
|
|
62
|
+
export declare const DEFAULT_MAX_PERSISTED_MODULES = 10;
|
|
14
63
|
export interface RouteDefinition {
|
|
15
64
|
/** Route path pattern (e.g., "/", "/profile/:id") */
|
|
16
65
|
path: string;
|
|
@@ -19,6 +68,16 @@ export interface RouteDefinition {
|
|
|
19
68
|
/** Inline module definition (alternative to registry lookup) */
|
|
20
69
|
module?: HypenModuleDefinition;
|
|
21
70
|
}
|
|
71
|
+
export interface ManagedRouterOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Maximum number of persisted module instances retained across
|
|
74
|
+
* navigations. When persistence would push the cache past this cap,
|
|
75
|
+
* the least-recently-persisted entry is destroyed. Defaults to
|
|
76
|
+
* `DEFAULT_MAX_PERSISTED_MODULES`. Pass `Infinity` to disable
|
|
77
|
+
* eviction.
|
|
78
|
+
*/
|
|
79
|
+
maxPersistedModules?: number;
|
|
80
|
+
}
|
|
22
81
|
export declare class ManagedRouter {
|
|
23
82
|
private router;
|
|
24
83
|
private engine;
|
|
@@ -28,22 +87,67 @@ export declare class ManagedRouter {
|
|
|
28
87
|
private activeModule;
|
|
29
88
|
private activeRoute;
|
|
30
89
|
private unsubscribe;
|
|
31
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Cached instances for module-backed routes, keyed by the lowercase
|
|
92
|
+
* module id. Entries are populated on unmount (when `persist` is truthy)
|
|
93
|
+
* and consulted on mount to restore state across navigations.
|
|
94
|
+
*
|
|
95
|
+
* Persistence is the default for any route whose `component` or `module`
|
|
96
|
+
* resolves to a module definition; opt out by setting `persist: false`
|
|
97
|
+
* on the definition.
|
|
98
|
+
*
|
|
99
|
+
* Bounded by `maxPersistedModules`. Map insertion order drives LRU
|
|
100
|
+
* eviction: restoring from the cache removes the entry (so the next
|
|
101
|
+
* persist re-inserts it at the MRU end), and persisting past the cap
|
|
102
|
+
* destroys the oldest entry.
|
|
103
|
+
*/
|
|
32
104
|
private persistedModules;
|
|
33
|
-
|
|
105
|
+
private readonly maxPersistedModules;
|
|
106
|
+
/**
|
|
107
|
+
* Serialized chain of in-flight route transitions. Because mount /
|
|
108
|
+
* unmount are now async (they await `onActivated` / `onDeactivated`),
|
|
109
|
+
* back-to-back navigations must not interleave. Each `handleRouteChange`
|
|
110
|
+
* appends to this chain so transitions run strictly in order.
|
|
111
|
+
*/
|
|
112
|
+
private navPromise;
|
|
113
|
+
constructor(router: HypenRouter, engine: IEngine, registry: HypenApp, globalContext: HypenGlobalContext, options?: ManagedRouterOptions);
|
|
34
114
|
/**
|
|
35
115
|
* Add a route definition.
|
|
36
116
|
*/
|
|
37
117
|
addRoute(route: RouteDefinition): this;
|
|
38
118
|
/**
|
|
39
119
|
* Start listening for route changes and mount the initial route.
|
|
120
|
+
*
|
|
121
|
+
* Also installs the `@router.*` engine action handlers so DSL authors can
|
|
122
|
+
* write `.onClick(@router.push, to: "/x")` / `@router.back` /
|
|
123
|
+
* `@router.replace` / `@router.forward` and have them dispatched against
|
|
124
|
+
* this session's `HypenRouter` without any per-example wiring. The
|
|
125
|
+
* reserved namespace is set up by `hypen-engine` in `ir/expand.rs`.
|
|
40
126
|
*/
|
|
41
127
|
start(): void;
|
|
128
|
+
/**
|
|
129
|
+
* Register handlers for the reserved `router.*` action namespace.
|
|
130
|
+
*
|
|
131
|
+
* All navigations go through `queueMicrotask` so the router state mutation
|
|
132
|
+
* lands after the engine finishes dispatching the action — synchronous
|
|
133
|
+
* writes would re-enter the engine's WASM state proxy, which the Rust side
|
|
134
|
+
* rejects ("recursive use of an object").
|
|
135
|
+
*/
|
|
136
|
+
private installRouterActions;
|
|
42
137
|
/**
|
|
43
138
|
* Stop listening and unmount the active module.
|
|
44
139
|
* Also destroys all persisted modules.
|
|
140
|
+
*
|
|
141
|
+
* Waits for any in-flight navigation to settle before tearing down so
|
|
142
|
+
* lifecycle hooks always complete in order.
|
|
143
|
+
*/
|
|
144
|
+
stop(): Promise<void>;
|
|
145
|
+
/**
|
|
146
|
+
* Returns a promise that resolves once all currently-queued route
|
|
147
|
+
* transitions have finished. Useful in tests to await lifecycle
|
|
148
|
+
* ordering without reaching into internals.
|
|
45
149
|
*/
|
|
46
|
-
|
|
150
|
+
waitForNavigation(): Promise<void>;
|
|
47
151
|
/**
|
|
48
152
|
* Get the currently active module instance.
|
|
49
153
|
*/
|
|
@@ -53,7 +157,19 @@ export declare class ManagedRouter {
|
|
|
53
157
|
*/
|
|
54
158
|
getActiveRoute(): RouteDefinition | null;
|
|
55
159
|
private handleRouteChange;
|
|
160
|
+
private processRouteChange;
|
|
56
161
|
private matchRoute;
|
|
57
162
|
private mount;
|
|
58
163
|
private unmountActive;
|
|
164
|
+
/**
|
|
165
|
+
* Trim `persistedModules` down to `maxPersistedModules` by destroying
|
|
166
|
+
* the oldest entries (FIFO in Map insertion order, which tracks LRU
|
|
167
|
+
* because `mount()` removes on restore and `unmountActive()` re-inserts
|
|
168
|
+
* at the MRU end on every persist).
|
|
169
|
+
*
|
|
170
|
+
* Errors during eviction are logged — one misbehaving `onDestroyed` hook
|
|
171
|
+
* must not block the router from shrinking the cache on the next
|
|
172
|
+
* navigation.
|
|
173
|
+
*/
|
|
174
|
+
private evictPersistedOverflow;
|
|
59
175
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portable helper DI seam for `@hypen-space/core`.
|
|
3
|
+
*
|
|
4
|
+
* `@hypen-space/core` is WASM-free: it ships to contexts (tree-shakers,
|
|
5
|
+
* browser remote-client bundles) that must not pull in the 2 MB engine
|
|
6
|
+
* binary. But every Hypen runtime — Node via `@hypen-space/server`,
|
|
7
|
+
* browser SPA via `@hypen-space/web-engine`, tests via the bun preload
|
|
8
|
+
* — composes core with a host that HAS loaded the engine. Those
|
|
9
|
+
* packages call [`setPortableImpl`] at their init time, plugging the
|
|
10
|
+
* canonical Rust-engine impls (`hypen-engine-rs/src/portable/`) into
|
|
11
|
+
* core.
|
|
12
|
+
*
|
|
13
|
+
* There is no TypeScript fallback. A core instance whose portable
|
|
14
|
+
* helpers are called before `setPortableImpl` has run throws a clear
|
|
15
|
+
* error pointing at the fix. Reasons:
|
|
16
|
+
*
|
|
17
|
+
* * Any fallback is a second implementation to maintain, exactly the
|
|
18
|
+
* drift tax this whole refactor eliminated for the other four SDKs.
|
|
19
|
+
* * Core's own consumers of the portable helpers (`HypenRouter.matchPath`,
|
|
20
|
+
* `createObservableState`'s `notifyChange`) are never reached in
|
|
21
|
+
* remote-client-only browser bundles — those bundles drive UI purely
|
|
22
|
+
* from server-pushed patches.
|
|
23
|
+
* * Callers who DO reach the helpers (server, web-engine, tests) always
|
|
24
|
+
* have an engine available.
|
|
25
|
+
*/
|
|
26
|
+
import type { StateChange } from "./state";
|
|
27
|
+
export interface PortableImpl {
|
|
28
|
+
diffState(oldState: any, newState: any, basePath?: string): StateChange;
|
|
29
|
+
matchPath(pattern: string, path: string): {
|
|
30
|
+
params: Record<string, string>;
|
|
31
|
+
} | null;
|
|
32
|
+
pathGet(value: any, path: string): unknown;
|
|
33
|
+
pathHas(value: any, path: string): boolean;
|
|
34
|
+
pathSet(value: any, path: string, newValue: any): any;
|
|
35
|
+
pathDelete(value: any, path: string): {
|
|
36
|
+
value: any;
|
|
37
|
+
removed: boolean;
|
|
38
|
+
};
|
|
39
|
+
encodeUriComponent(input: string): string;
|
|
40
|
+
decodeUriComponent(input: string): string;
|
|
41
|
+
parseQuery(fullPath: string): {
|
|
42
|
+
path: string;
|
|
43
|
+
query: Record<string, string>;
|
|
44
|
+
};
|
|
45
|
+
buildUrl(path: string, query: Record<string, string>): string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Install the engine-backed [`PortableImpl`]. Called once by
|
|
49
|
+
* `@hypen-space/server` at import time (synchronous wasm-node load) or
|
|
50
|
+
* by `@hypen-space/web-engine` inside `Engine.init()` once the browser
|
|
51
|
+
* wasm module resolves.
|
|
52
|
+
*
|
|
53
|
+
* Passing `null` resets to the "not installed" throwing default — used
|
|
54
|
+
* by test teardown when you want to verify the no-engine error path.
|
|
55
|
+
*/
|
|
56
|
+
export declare function setPortableImpl(impl: PortableImpl | null): void;
|
|
57
|
+
/**
|
|
58
|
+
* Live portable API. Every call dispatches to whatever
|
|
59
|
+
* [`setPortableImpl`] most recently installed.
|
|
60
|
+
*/
|
|
61
|
+
export declare const portable: PortableImpl;
|
|
62
|
+
export type { StatePath } from "./state";
|
package/dist/router.d.ts
CHANGED
|
@@ -73,7 +73,13 @@ export declare class HypenRouter {
|
|
|
73
73
|
*/
|
|
74
74
|
getState(): RouteState;
|
|
75
75
|
/**
|
|
76
|
-
* Match a pattern against a path
|
|
76
|
+
* Match a pattern against a path.
|
|
77
|
+
*
|
|
78
|
+
* Thin wrapper over [`portable.matchPath`] — the three-case matcher
|
|
79
|
+
* (exact / `/prefix/*` wildcard / `:param`) lives in the Rust engine
|
|
80
|
+
* at `hypen-engine-rs/src/portable/route.rs` and is reached through
|
|
81
|
+
* the WASM installed by server / web-engine, with a TS fallback for
|
|
82
|
+
* standalone core use.
|
|
77
83
|
*/
|
|
78
84
|
matchPath(pattern: string, path: string): RouteMatch | null;
|
|
79
85
|
/**
|
package/dist/router.js
CHANGED
|
@@ -21,6 +21,53 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
21
21
|
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
// src/portable.ts
|
|
25
|
+
function notInstalled(name) {
|
|
26
|
+
throw new Error(`[@hypen-space/core] Portable helper "${name}" called before the engine was installed. ` + `Import @hypen-space/server, @hypen-space/web-engine, or call setPortableImpl() before use.`);
|
|
27
|
+
}
|
|
28
|
+
var current = {
|
|
29
|
+
diffState: () => notInstalled("diffState"),
|
|
30
|
+
matchPath: () => notInstalled("matchPath"),
|
|
31
|
+
pathGet: () => notInstalled("pathGet"),
|
|
32
|
+
pathHas: () => notInstalled("pathHas"),
|
|
33
|
+
pathSet: () => notInstalled("pathSet"),
|
|
34
|
+
pathDelete: () => notInstalled("pathDelete"),
|
|
35
|
+
encodeUriComponent: () => notInstalled("encodeUriComponent"),
|
|
36
|
+
decodeUriComponent: () => notInstalled("decodeUriComponent"),
|
|
37
|
+
parseQuery: () => notInstalled("parseQuery"),
|
|
38
|
+
buildUrl: () => notInstalled("buildUrl")
|
|
39
|
+
};
|
|
40
|
+
function setPortableImpl(impl) {
|
|
41
|
+
if (impl === null) {
|
|
42
|
+
current = {
|
|
43
|
+
diffState: () => notInstalled("diffState"),
|
|
44
|
+
matchPath: () => notInstalled("matchPath"),
|
|
45
|
+
pathGet: () => notInstalled("pathGet"),
|
|
46
|
+
pathHas: () => notInstalled("pathHas"),
|
|
47
|
+
pathSet: () => notInstalled("pathSet"),
|
|
48
|
+
pathDelete: () => notInstalled("pathDelete"),
|
|
49
|
+
encodeUriComponent: () => notInstalled("encodeUriComponent"),
|
|
50
|
+
decodeUriComponent: () => notInstalled("decodeUriComponent"),
|
|
51
|
+
parseQuery: () => notInstalled("parseQuery"),
|
|
52
|
+
buildUrl: () => notInstalled("buildUrl")
|
|
53
|
+
};
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
current = impl;
|
|
57
|
+
}
|
|
58
|
+
var portable = {
|
|
59
|
+
diffState: (o, n, b) => current.diffState(o, n, b),
|
|
60
|
+
matchPath: (p, path) => current.matchPath(p, path),
|
|
61
|
+
pathGet: (v, p) => current.pathGet(v, p),
|
|
62
|
+
pathHas: (v, p) => current.pathHas(v, p),
|
|
63
|
+
pathSet: (v, p, nv) => current.pathSet(v, p, nv),
|
|
64
|
+
pathDelete: (v, p) => current.pathDelete(v, p),
|
|
65
|
+
encodeUriComponent: (s) => current.encodeUriComponent(s),
|
|
66
|
+
decodeUriComponent: (s) => current.decodeUriComponent(s),
|
|
67
|
+
parseQuery: (f) => current.parseQuery(f),
|
|
68
|
+
buildUrl: (p, q) => current.buildUrl(p, q)
|
|
69
|
+
};
|
|
70
|
+
|
|
24
71
|
// src/state.ts
|
|
25
72
|
var IS_PROXY = Symbol.for("hypen.isProxy");
|
|
26
73
|
var RAW_TARGET = Symbol.for("hypen.rawTarget");
|
|
@@ -80,51 +127,7 @@ function deepClone(obj) {
|
|
|
80
127
|
return cloneInternal(obj);
|
|
81
128
|
}
|
|
82
129
|
function diffState(oldState, newState, basePath = "") {
|
|
83
|
-
|
|
84
|
-
const newValues = {};
|
|
85
|
-
function diff(oldVal, newVal, path) {
|
|
86
|
-
if (oldVal === newVal)
|
|
87
|
-
return;
|
|
88
|
-
if (typeof oldVal !== "object" || typeof newVal !== "object" || oldVal === null || newVal === null) {
|
|
89
|
-
if (oldVal !== newVal) {
|
|
90
|
-
paths.push(path);
|
|
91
|
-
newValues[path] = newVal;
|
|
92
|
-
}
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (Array.isArray(oldVal) || Array.isArray(newVal)) {
|
|
96
|
-
if (!Array.isArray(oldVal) || !Array.isArray(newVal) || oldVal.length !== newVal.length) {
|
|
97
|
-
paths.push(path);
|
|
98
|
-
newValues[path] = newVal;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
for (let i = 0;i < newVal.length; i++) {
|
|
102
|
-
const itemPath = path ? `${path}.${i}` : `${i}`;
|
|
103
|
-
diff(oldVal[i], newVal[i], itemPath);
|
|
104
|
-
}
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
const oldKeys = new Set(Object.keys(oldVal));
|
|
108
|
-
const newKeys = new Set(Object.keys(newVal));
|
|
109
|
-
for (const key of newKeys) {
|
|
110
|
-
const propPath = path ? `${path}.${key}` : key;
|
|
111
|
-
if (!oldKeys.has(key)) {
|
|
112
|
-
paths.push(propPath);
|
|
113
|
-
newValues[propPath] = newVal[key];
|
|
114
|
-
} else {
|
|
115
|
-
diff(oldVal[key], newVal[key], propPath);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
for (const key of oldKeys) {
|
|
119
|
-
if (!newKeys.has(key)) {
|
|
120
|
-
const propPath = path ? `${path}.${key}` : key;
|
|
121
|
-
paths.push(propPath);
|
|
122
|
-
newValues[propPath] = undefined;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
diff(oldState, newState, basePath);
|
|
127
|
-
return { paths, newValues };
|
|
130
|
+
return portable.diffState(oldState, newState, basePath);
|
|
128
131
|
}
|
|
129
132
|
function createObservableState(initialState, options) {
|
|
130
133
|
const opts = options || { onChange: () => {} };
|
|
@@ -735,46 +738,22 @@ class HypenRouter {
|
|
|
735
738
|
return getStateSnapshot(this.state);
|
|
736
739
|
}
|
|
737
740
|
matchPath(pattern, path) {
|
|
738
|
-
if (!pattern || typeof pattern !== "string")
|
|
741
|
+
if (!pattern || typeof pattern !== "string")
|
|
739
742
|
return null;
|
|
740
|
-
|
|
741
|
-
if (!path || typeof path !== "string") {
|
|
743
|
+
if (!path || typeof path !== "string")
|
|
742
744
|
return null;
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
params: {},
|
|
747
|
-
query: this.state.query,
|
|
748
|
-
path
|
|
749
|
-
};
|
|
750
|
-
}
|
|
751
|
-
if (pattern.endsWith("/*")) {
|
|
752
|
-
const prefix = pattern.slice(0, -2);
|
|
753
|
-
if (path === prefix || path.startsWith(prefix + "/")) {
|
|
754
|
-
return {
|
|
755
|
-
params: {},
|
|
756
|
-
query: this.state.query,
|
|
757
|
-
path
|
|
758
|
-
};
|
|
759
|
-
}
|
|
760
|
-
return null;
|
|
761
|
-
}
|
|
762
|
-
const paramNames = [];
|
|
763
|
-
const regexPattern = pattern.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
764
|
-
paramNames.push(name);
|
|
765
|
-
return "([^/]+)";
|
|
766
|
-
}).replace(/\*/g, ".*");
|
|
767
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
768
|
-
const match = path.match(regex);
|
|
769
|
-
if (!match)
|
|
745
|
+
const cleanPath = path.split("?")[0] ?? path;
|
|
746
|
+
const result = portable.matchPath(pattern, cleanPath);
|
|
747
|
+
if (!result)
|
|
770
748
|
return null;
|
|
771
749
|
const params = {};
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
750
|
+
for (const [name, raw] of Object.entries(result.params)) {
|
|
751
|
+
try {
|
|
752
|
+
params[name] = decodeURIComponent(raw);
|
|
753
|
+
} catch {
|
|
754
|
+
params[name] = raw;
|
|
776
755
|
}
|
|
777
|
-
}
|
|
756
|
+
}
|
|
778
757
|
return {
|
|
779
758
|
params,
|
|
780
759
|
query: this.state.query,
|
|
@@ -829,4 +808,4 @@ export {
|
|
|
829
808
|
HypenRouter
|
|
830
809
|
};
|
|
831
810
|
|
|
832
|
-
//# debugId=
|
|
811
|
+
//# debugId=8162B923E6ED164564756E2164756E21
|