Match-id-e51d4589cde12b17ff1c8a88cd583ba7e7d5b6f3

This commit is contained in:
* 2023-09-01 11:24:56 +08:00
commit 74715c2565
88 changed files with 392 additions and 7526 deletions

View File

@ -1,9 +1,10 @@
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import babel from '@rollup/plugin-babel';
import nodeResolve from '@rollup/plugin-node-resolve';
import execute from 'rollup-plugin-execute';
import fs from 'fs';
import { terser } from 'rollup-plugin-terser';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@ -19,20 +20,31 @@ if (!fs.existsSync(path.join(output, 'connectRouter'))) {
fs.mkdirSync(path.join(output, 'connectRouter'), { recursive: true });
}
const routerBuildConfig = {
input: { router: routerEntry },
output: [
const routerBuildConfig = mode => {
const prod = mode.startsWith('prod');
const outputList = [
{
dir: path.resolve(output, 'router/cjs'),
sourcemap: 'inline',
file: path.join(output, `router/cjs/router.${prod ? 'min.' : ''}js`),
sourcemap: 'true',
format: 'cjs',
},
{
dir: path.resolve(output, 'router/esm'),
sourcemap: 'inline',
format: 'esm',
file: path.join(output, `router/umd/router.${prod ? 'min.' : ''}js`),
name: `HorizonRouter`,
sourcemap: 'true',
format: 'umd',
},
],
];
if (!prod) {
outputList.push({
file: path.join(output, `router/esm/router.js`),
sourcemap: 'true',
format: 'esm',
});
}
return {
input: routerEntry,
output: outputList,
plugins: [
nodeResolve({
extensions,
@ -45,23 +57,36 @@ const routerBuildConfig = {
extensions,
}),
execute('npm run build-types-router'),
prod && terser(),
],
};
};
const connectRouterConfig = {
input: { connectRouter: connectRouterEntry },
output: [
const connectRouterConfig = mode => {
const prod = mode.startsWith('prod');
const outputList = [
{
dir: path.resolve(output, 'connectRouter/cjs'),
sourcemap: 'inline',
file: path.join(output, `connectRouter/cjs/connectRouter.${prod ? 'min.' : ''}js`),
sourcemap: 'true',
format: 'cjs',
},
{
dir: path.resolve(output, 'connectRouter/esm'),
sourcemap: 'inline',
format: 'esm',
file: path.join(output, `connectRouter/umd/connectRouter.${prod ? 'min.' : ''}js`),
name: 'HorizonRouter',
sourcemap: 'true',
format: 'umd',
},
],
];
if (!prod) {
outputList.push({
file: path.join(output, `connectRouter/esm/connectRouter.js`),
sourcemap: 'true',
format: 'esm',
});
}
return {
input: connectRouterEntry,
output: outputList,
plugins: [
nodeResolve({
extensions,
@ -74,6 +99,7 @@ const connectRouterConfig = {
extensions,
}),
execute('npm run build-types-all'),
prod && terser(),
copyFiles([
{
from: path.join(__dirname, 'src/configs/package.json'),
@ -82,7 +108,7 @@ const connectRouterConfig = {
]),
],
};
};
function copyFiles(copyPairs) {
return {
@ -96,5 +122,9 @@ function copyFiles(copyPairs) {
};
}
export default [routerBuildConfig, connectRouterConfig];
export default [
routerBuildConfig('dev'),
routerBuildConfig('prod'),
connectRouterConfig('dev'),
connectRouterConfig('prod'),
];

View File

@ -1,25 +0,0 @@
import { Action, Path } from '../history/types';
type Location = Partial<Path>;
export declare enum ActionName {
LOCATION_CHANGE = "$horizon-router/LOCATION_CHANGE",
CALL_HISTORY_METHOD = "$horizon-router/CALL_HISTORY_METHOD"
}
export type ActionMessage = {
type: ActionName.LOCATION_CHANGE;
payload: {
location: Location;
action: Action;
isFirstRendering: boolean;
};
} | {
type: ActionName.CALL_HISTORY_METHOD;
payload: {
method: string;
args: any;
};
};
export declare const onLocationChanged: (location: Location, action: Action, isFirstRendering?: boolean) => ActionMessage;
export declare const push: (...args: any) => ActionMessage;
export declare const replace: (...args: any) => ActionMessage;
export declare const go: (...args: any) => ActionMessage;
export {};

View File

@ -1,3 +0,0 @@
import { ActionMessage } from './actions';
import { History } from '../history/types';
export declare function routerMiddleware(history: History): (_: any) => (next: any) => (action: ActionMessage) => any;

View File

@ -1,11 +0,0 @@
export { getConnectedRouter } from './connectedRouter';
export declare const connectRouter: (history: import("../router").History<unknown>) => (state?: {
location: Partial<import("../router").Location<unknown>> & {
query?: Record<string, any>;
};
action: import("../history/types").Action;
}, { type, payload }?: {
type?: import("./actions").ActionName;
payload?: any;
}) => any;
export { routerMiddleware } from './dispatch';

View File

@ -1,16 +0,0 @@
import { ActionName } from './actions';
import { Action, History } from '../history/types';
import { Location } from '../router';
type LocationWithQuery = Partial<Location> & {
query?: Record<string, any>;
};
type InitRouterState = {
location: LocationWithQuery;
action: Action;
};
type Payload = {
type?: ActionName;
payload?: any;
};
export declare function createConnectRouter(): (history: History) => (state?: InitRouterState, { type, payload }?: Payload) => any;
export {};

View File

@ -1,10 +0,0 @@
import { HistoryProps, Listener, Navigation, Prompt } from './types';
import transitionManager from './transitionManager';
export declare function getBaseHistory<S>(transitionManager: transitionManager<S>, setListener: (delta: number) => void, browserHistory: History): {
go: (step: number) => void;
goBack: () => void;
goForward: () => void;
listen: (listener: Listener<S>) => () => void;
block: (prompt?: Prompt<S>) => () => void;
getUpdateStateFunc: (historyProps: HistoryProps<S>) => (nextState: Navigation<S> | undefined) => void;
};

View File

@ -1,8 +0,0 @@
import { BaseOption, DefaultStateType, History } from './types';
export type BrowserHistoryOption = {
/**
* forceRefresh为True时跳转时会强制刷新页面
*/
forceRefresh?: boolean;
} & BaseOption;
export declare function createBrowserHistory<S = DefaultStateType>(options?: BrowserHistoryOption): History<S>;

View File

@ -1,4 +0,0 @@
export declare function isBrowser(): boolean;
export declare function getDefaultConfirmation(message: string, callBack: (result: boolean) => void): void;
export declare function isSupportHistory(): boolean;
export declare function isSupportsPopState(): boolean;

View File

@ -1,7 +0,0 @@
import { BaseOption, DefaultStateType, History } from './types';
export type urlHashType = 'slash' | 'noslash';
type HashHistoryOption = {
hashType?: urlHashType;
} & BaseOption;
export declare function createHashHistory<S = DefaultStateType>(option?: HashHistoryOption): History<S>;
export {};

View File

@ -1,11 +0,0 @@
import { Action, CallBackFunc, ConfirmationFunc, Listener, Location, Navigation, Prompt, TManager } from './types';
declare class TransitionManager<S> implements TManager<S> {
private prompt;
private listeners;
constructor();
setPrompt(prompt: Prompt<S>): () => void;
addListener(func: Listener<S>): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}
export default TransitionManager;

View File

@ -1,56 +0,0 @@
export type BaseOption = {
basename?: string;
getUserConfirmation?: ConfirmationFunc;
};
export interface HistoryProps<T = unknown> {
readonly action: Action;
readonly location: Location<T>;
length: number;
}
export interface History<T = unknown> extends HistoryProps<T> {
createHref(path: Partial<Path>): string;
push(to: To, state?: T): void;
replace(to: To, state?: T): void;
listen(listener: Listener<T>): () => void;
block(prompt: Prompt<T>): () => void;
go(index: number): void;
goBack(): void;
goForward(): void;
}
export declare enum Action {
pop = "POP",
push = "PUSH",
replace = "REPLACE"
}
export declare enum EventType {
PopState = "popstate",
HashChange = "hashchange"
}
export type Path = {
pathname: string;
search: string;
hash: string;
};
export type HistoryState<T> = {
state?: T;
key: string;
};
export type DefaultStateType = unknown;
export type Location<T = unknown> = Path & HistoryState<T>;
export type To = string | Partial<Path>;
export interface Listener<T = unknown> {
(navigation: Navigation<T>): void;
}
export interface Navigation<T = unknown> {
action: Action;
location: Location<T>;
}
export type Prompt<S> = string | boolean | null | ((location: Location<S>, action: Action) => void);
export type CallBackFunc = (isJump: boolean) => void;
export type ConfirmationFunc = (message: string, callBack: CallBackFunc) => void;
export interface TManager<S> {
setPrompt(next: Prompt<S>): () => void;
addListener(func: (navigation: Navigation<S>) => void): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}

View File

@ -1,14 +0,0 @@
import { Action, Location, Path, To } from './types';
export declare function createPath(path: Partial<Path>): string;
export declare function parsePath(url: string): Partial<Path>;
export declare function createLocation<S>(current: string | Location, to: To, state?: S, key?: string): Readonly<Location<S>>;
export declare function isLocationEqual(p1: Partial<Path>, p2: Partial<Path>): boolean;
export declare function addHeadSlash(path: string): string;
export declare function stripHeadSlash(path: string): string;
export declare function normalizeSlash(path: string): string;
export declare function hasBasename(path: string, prefix: string): Boolean;
export declare function stripBasename(path: string, prefix: string): string;
export declare function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T): {
getDelta: (to: S, form: S) => number;
addRecord: (current: S, newRecord: S, action: Action) => void;
};

View File

@ -1,2 +0,0 @@
declare function warning(condition: any, message: string): void;
export default warning;

View File

@ -1,12 +0,0 @@
import { ReactNode } from 'react';
import { ConfirmationFunc } from '../history/types';
export type BaseRouterProps = {
basename: string;
getUserConfirmation: ConfirmationFunc;
children?: ReactNode;
};
export type BrowserRouterProps = BaseRouterProps & {
forceRefresh: boolean;
};
declare function BrowserRouter<P extends Partial<BrowserRouterProps>>(props: P): JSX.Element;
export default BrowserRouter;

View File

@ -1,7 +0,0 @@
import { BaseRouterProps } from './BrowserRouter';
import { urlHashType } from '../history/hashHistory';
export type HashRouterProps = BaseRouterProps & {
hashType: urlHashType;
};
declare function HashRouter<P extends Partial<HashRouterProps>>(props: P): JSX.Element;
export default HashRouter;

View File

@ -1,18 +0,0 @@
import * as React from 'react';
import { Location } from './index';
export type LinkProps = {
component?: React.ComponentType<any>;
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
replace?: boolean;
tag?: string;
/**
* @deprecated
* React16以后不再需要该属性
**/
innerRef?: React.Ref<HTMLAnchorElement>;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
declare function Link<P extends LinkProps>(props: P): React.DOMElement<{
href: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
} & Omit<P, "replace" | "to" | "component" | "onClick" | "target">, Element>;
export default Link;

View File

@ -1,10 +0,0 @@
import type { LinkProps } from './Link';
import { Location } from './index';
import { Matched } from './matcher/parser';
type NavLinkProps = {
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
isActive?: (match: Matched | null, location: Location) => boolean;
[key: string]: any;
} & LinkProps;
declare function NavLink<P extends NavLinkProps>(props: P): JSX.Element;
export default NavLink;

View File

@ -1,8 +0,0 @@
import { Location } from './index';
import { Action } from '../history/types';
type PromptProps = {
message?: string | ((location: Partial<Location>, action: Action) => void);
when?: boolean | ((location: Partial<Location>) => boolean);
};
declare function Prompt<P extends PromptProps>(props: P): JSX.Element;
export default Prompt;

View File

@ -1,13 +0,0 @@
import { Matched } from './matcher/parser';
import { Location } from './index';
export type RedirectProps = {
to: string | Partial<Location>;
push?: boolean;
path?: string;
from?: string;
exact?: boolean;
strict?: boolean;
readonly computed?: Matched | null;
};
declare function Redirect<P extends RedirectProps>(props: P): JSX.Element;
export default Redirect;

View File

@ -1,23 +0,0 @@
import * as React from 'react';
import { History, Location } from './index';
import { Matched } from './matcher/parser';
import { GetURLParams } from './matcher/types';
export type RouteComponentProps<P extends Record<string, any> = {}, S = unknown> = RouteChildrenProps<P, S>;
export type RouteChildrenProps<P extends Record<string, any> = {}, S = unknown> = {
history: History<S>;
location: Location<S>;
match: Matched<P> | null;
};
export type RouteProps<P extends Record<string, any> = {}, Path extends string = string> = {
location?: Location;
component?: React.ComponentType<RouteComponentProps<P>> | React.ComponentType<any> | undefined;
children?: ((props: RouteChildrenProps<P>) => React.ReactNode) | React.ReactNode;
render?: (props: RouteComponentProps<P>) => React.ReactNode;
path?: Path | Path[];
exact?: boolean;
sensitive?: boolean;
strict?: boolean;
computed?: Matched<P>;
};
declare function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>): JSX.Element;
export default Route;

View File

@ -1,8 +0,0 @@
import * as React from 'react';
import { History } from '../history/types';
export type RouterProps = {
history: History;
children?: React.ReactNode;
};
declare function Router<P extends RouterProps>(props: P): JSX.Element;
export default Router;

View File

@ -1,8 +0,0 @@
import * as React from 'react';
import { Location } from './index';
export type SwitchProps = {
location?: Location;
children?: React.ReactNode;
};
declare function Switch<P extends SwitchProps>(props: P): React.ReactElement | null;
export default Switch;

View File

@ -1 +0,0 @@
import '@testing-library/jest-dom';

View File

@ -1,8 +0,0 @@
import { History, Location } from '../index';
export declare let historyHook: History;
export declare let locationHook: Location;
export declare const LocationDisplay: () => JSX.Element;
export declare const Test_Demo: () => JSX.Element;
export declare const Test_Demo2: () => JSX.Element;
export declare const Test_Demo3: () => JSX.Element;
export declare const Test_Demo4: () => JSX.Element;

View File

@ -1,10 +0,0 @@
/// <reference types="react" />
import { History, Location } from './index';
import { Matched } from './matcher/parser';
export type RouterContextValue = {
history: History;
location: Location;
match: Matched | null;
};
declare const RouterContext: import("react").Context<RouterContextValue>;
export default RouterContext;

View File

@ -1,8 +0,0 @@
import { Matched, Params } from './matcher/parser';
import { History } from '../history/types';
import { Location } from './index';
declare function useHistory<S>(): History<S>;
declare function useLocation<S>(): Location<S>;
declare function useParams<P>(): Params<P> | {};
declare function useRouteMatch<P>(path?: string): Matched<P> | null;
export { useHistory, useLocation, useParams, useRouteMatch };

View File

@ -1,20 +0,0 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';

View File

@ -1,23 +0,0 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';
export { connectRouter, routerMiddleware } from '../connect-router';
export declare const ConnectedRouter: any;
export declare const ConnectedHRouter: any;

View File

@ -1,7 +0,0 @@
export type LifeCycleProps = {
onMount?: () => void;
onUpdate?: (prevProps?: LifeCycleProps) => void;
onUnmount?: () => void;
data?: any;
};
export declare function LifeCycle(props: LifeCycleProps): any;

View File

@ -1,2 +0,0 @@
import { Token } from './types';
export declare function lexer(path: string): Token[];

View File

@ -1,18 +0,0 @@
import { GetURLParams, Parser, ParserOption } from './types';
export type Params<P> = {
[K in keyof P]?: P[K];
};
export type Matched<P = any> = {
score: number[];
params: Params<P>;
path: string;
url: string;
isExact: boolean;
};
export declare function createPathParser<Str extends string>(pathname: Str, option?: ParserOption): Parser<GetURLParams<Str>>;
export declare function createPathParser<P = unknown>(pathname: string, option?: ParserOption): Parser<P>;
/**
* @description 使pathname与pattern进行匹配
*/
export declare function matchPath<P = any>(pathname: string, pattern: string | string[], option?: ParserOption): Matched<P> | null;
export declare function generatePath<P = any>(path: string, params: Params<P>): string;

View File

@ -1,36 +0,0 @@
import { Matched, Params } from './parser';
export type Token = {
type: TokenType;
value: string;
};
export declare enum TokenType {
Delimiter = "delimiter",
Static = "static",
Param = "param",
WildCard = "wildcard",
LBracket = "(",
RBracket = ")",
Pattern = "pattern"
}
export interface Parser<P> {
regexp: RegExp;
keys: string[];
parse(url: string): Matched<P> | null;
compile(params: Params<P>): string;
}
export type ParserOption = {
caseSensitive?: boolean;
strictMode?: boolean;
exact?: boolean;
};
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
type ParseParam<Param extends string> = Param extends `:${infer R}` ? {
[K in R]: string;
} : {};
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string, any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}` ? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>> : ParseParam<Str>;
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
export {};

View File

@ -1,6 +0,0 @@
/**
* @description url中的//转换为/
*/
export declare function cleanPath(path: string): string;
export declare function scoreCompare(score1: number[], score2: number[]): number;
export declare function escapeStr(str: string): string;

View File

@ -1,3 +0,0 @@
import * as React from 'react';
declare function withRouter<C extends React.ComponentType>(Component: C): (props: any) => JSX.Element;
export default withRouter;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,9 +0,0 @@
{
"module": "./esm/connectRouter.js",
"main": "./cjs/connectRouter.js",
"types": "./@types/router/index2.d.ts",
"peerDependencies": {
"react-redux": "^6.0.0 || ^7.1.0",
"redux": "^3.6.0 || ^4.0.0"
}
}

View File

@ -1,6 +1,6 @@
{
"name": "@cloudsop/horizon-router",
"version": "0.0.12",
"version": "1.0.5-alpha",
"description": "router for horizon framework, a part of horizon-ecosystem",
"main": "./router/cjs/router.js",
"module": "./router/esm/router.js",
@ -16,7 +16,7 @@
},
"scripts": {
"test": "jest",
"build": "rollup -c build.js",
"bulid": "rollup -c build.js",
"build-types-router": "tsc -p src/router/index.ts --emitDeclarationOnly --declaration --declarationDir ./router/@types --skipLibCheck",
"build-types-all": "tsc -p src/router/index2.ts --emitDeclarationOnly --declaration --declarationDir ./connectRouter/@types --skipLibCheck"
},
@ -67,10 +67,11 @@
"rollup": "2.79.1",
"rollup-plugin-execute": "^1.1.1",
"ts-jest": "29.0.3",
"typescript": "4.9.3"
"typescript": "4.9.3",
"rollup-plugin-terser": "^5.1.3"
},
"dependencies": {
"@cloudsop/horizon": "^0.0.52",
"@cloudsop/horizon": "*",
"react": "18.2.0",
"react-dom": "18.2.0"
},

View File

@ -1,25 +0,0 @@
import { Action, Path } from '../history/types';
type Location = Partial<Path>;
export declare enum ActionName {
LOCATION_CHANGE = "$horizon-router/LOCATION_CHANGE",
CALL_HISTORY_METHOD = "$horizon-router/CALL_HISTORY_METHOD"
}
export type ActionMessage = {
type: ActionName.LOCATION_CHANGE;
payload: {
location: Location;
action: Action;
isFirstRendering: boolean;
};
} | {
type: ActionName.CALL_HISTORY_METHOD;
payload: {
method: string;
args: any;
};
};
export declare const onLocationChanged: (location: Location, action: Action, isFirstRendering?: boolean) => ActionMessage;
export declare const push: (...args: any) => ActionMessage;
export declare const replace: (...args: any) => ActionMessage;
export declare const go: (...args: any) => ActionMessage;
export {};

View File

@ -1,3 +0,0 @@
import { ActionMessage } from './actions';
import { History } from '../history/types';
export declare function routerMiddleware(history: History): (_: any) => (next: any) => (action: ActionMessage) => any;

View File

@ -1,11 +0,0 @@
export { getConnectedRouter } from './connectedRouter';
export declare const connectRouter: (history: import("../router").History<unknown>) => (state?: {
location: Partial<import("../router").Location<unknown>> & {
query?: Record<string, any>;
};
action: import("../history/types").Action;
}, { type, payload }?: {
type?: import("./actions").ActionName;
payload?: any;
}) => any;
export { routerMiddleware } from './dispatch';

View File

@ -1,16 +0,0 @@
import { ActionName } from './actions';
import { Action, History } from '../history/types';
import { Location } from '../router';
type LocationWithQuery = Partial<Location> & {
query?: Record<string, any>;
};
type InitRouterState = {
location: LocationWithQuery;
action: Action;
};
type Payload = {
type?: ActionName;
payload?: any;
};
export declare function createConnectRouter(): (history: History) => (state?: InitRouterState, { type, payload }?: Payload) => any;
export {};

View File

@ -1,10 +0,0 @@
import { HistoryProps, Listener, Navigation, Prompt } from './types';
import transitionManager from './transitionManager';
export declare function getBaseHistory<S>(transitionManager: transitionManager<S>, setListener: (delta: number) => void, browserHistory: History): {
go: (step: number) => void;
goBack: () => void;
goForward: () => void;
listen: (listener: Listener<S>) => () => void;
block: (prompt?: Prompt<S>) => () => void;
getUpdateStateFunc: (historyProps: HistoryProps<S>) => (nextState: Navigation<S> | undefined) => void;
};

View File

@ -1,8 +0,0 @@
import { BaseOption, DefaultStateType, History } from './types';
export type BrowserHistoryOption = {
/**
* forceRefresh为True时跳转时会强制刷新页面
*/
forceRefresh?: boolean;
} & BaseOption;
export declare function createBrowserHistory<S = DefaultStateType>(options?: BrowserHistoryOption): History<S>;

View File

@ -1,4 +0,0 @@
export declare function isBrowser(): boolean;
export declare function getDefaultConfirmation(message: string, callBack: (result: boolean) => void): void;
export declare function isSupportHistory(): boolean;
export declare function isSupportsPopState(): boolean;

View File

@ -1,7 +0,0 @@
import { BaseOption, DefaultStateType, History } from './types';
export type urlHashType = 'slash' | 'noslash';
type HashHistoryOption = {
hashType?: urlHashType;
} & BaseOption;
export declare function createHashHistory<S = DefaultStateType>(option?: HashHistoryOption): History<S>;
export {};

View File

@ -1,11 +0,0 @@
import { Action, CallBackFunc, ConfirmationFunc, Listener, Location, Navigation, Prompt, TManager } from './types';
declare class TransitionManager<S> implements TManager<S> {
private prompt;
private listeners;
constructor();
setPrompt(prompt: Prompt<S>): () => void;
addListener(func: Listener<S>): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}
export default TransitionManager;

View File

@ -1,56 +0,0 @@
export type BaseOption = {
basename?: string;
getUserConfirmation?: ConfirmationFunc;
};
export interface HistoryProps<T = unknown> {
readonly action: Action;
readonly location: Location<T>;
length: number;
}
export interface History<T = unknown> extends HistoryProps<T> {
createHref(path: Partial<Path>): string;
push(to: To, state?: T): void;
replace(to: To, state?: T): void;
listen(listener: Listener<T>): () => void;
block(prompt: Prompt<T>): () => void;
go(index: number): void;
goBack(): void;
goForward(): void;
}
export declare enum Action {
pop = "POP",
push = "PUSH",
replace = "REPLACE"
}
export declare enum EventType {
PopState = "popstate",
HashChange = "hashchange"
}
export type Path = {
pathname: string;
search: string;
hash: string;
};
export type HistoryState<T> = {
state?: T;
key: string;
};
export type DefaultStateType = unknown;
export type Location<T = unknown> = Path & HistoryState<T>;
export type To = string | Partial<Path>;
export interface Listener<T = unknown> {
(navigation: Navigation<T>): void;
}
export interface Navigation<T = unknown> {
action: Action;
location: Location<T>;
}
export type Prompt<S> = string | boolean | null | ((location: Location<S>, action: Action) => void);
export type CallBackFunc = (isJump: boolean) => void;
export type ConfirmationFunc = (message: string, callBack: CallBackFunc) => void;
export interface TManager<S> {
setPrompt(next: Prompt<S>): () => void;
addListener(func: (navigation: Navigation<S>) => void): () => void;
notifyListeners(args: Navigation<S>): void;
confirmJumpTo(location: Location<S>, action: Action, userConfirmationFunc: ConfirmationFunc, callBack: CallBackFunc): void;
}

View File

@ -1,14 +0,0 @@
import { Action, Location, Path, To } from './types';
export declare function createPath(path: Partial<Path>): string;
export declare function parsePath(url: string): Partial<Path>;
export declare function createLocation<S>(current: string | Location, to: To, state?: S, key?: string): Readonly<Location<S>>;
export declare function isLocationEqual(p1: Partial<Path>, p2: Partial<Path>): boolean;
export declare function addHeadSlash(path: string): string;
export declare function stripHeadSlash(path: string): string;
export declare function normalizeSlash(path: string): string;
export declare function hasBasename(path: string, prefix: string): Boolean;
export declare function stripBasename(path: string, prefix: string): string;
export declare function createMemoryRecord<T, S>(initVal: S, fn: (arg: S) => T): {
getDelta: (to: S, form: S) => number;
addRecord: (current: S, newRecord: S, action: Action) => void;
};

View File

@ -1,2 +0,0 @@
declare function warning(condition: any, message: string): void;
export default warning;

View File

@ -1,12 +0,0 @@
import { ReactNode } from 'react';
import { ConfirmationFunc } from '../history/types';
export type BaseRouterProps = {
basename: string;
getUserConfirmation: ConfirmationFunc;
children?: ReactNode;
};
export type BrowserRouterProps = BaseRouterProps & {
forceRefresh: boolean;
};
declare function BrowserRouter<P extends Partial<BrowserRouterProps>>(props: P): JSX.Element;
export default BrowserRouter;

View File

@ -1,7 +0,0 @@
import { BaseRouterProps } from './BrowserRouter';
import { urlHashType } from '../history/hashHistory';
export type HashRouterProps = BaseRouterProps & {
hashType: urlHashType;
};
declare function HashRouter<P extends Partial<HashRouterProps>>(props: P): JSX.Element;
export default HashRouter;

View File

@ -1,18 +0,0 @@
import * as React from 'react';
import { Location } from './index';
export type LinkProps = {
component?: React.ComponentType<any>;
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
replace?: boolean;
tag?: string;
/**
* @deprecated
* React16以后不再需要该属性
**/
innerRef?: React.Ref<HTMLAnchorElement>;
} & React.AnchorHTMLAttributes<HTMLAnchorElement>;
declare function Link<P extends LinkProps>(props: P): React.DOMElement<{
href: string;
onClick: (event: React.MouseEvent<HTMLAnchorElement>) => void;
} & Omit<P, "replace" | "to" | "component" | "onClick" | "target">, Element>;
export default Link;

View File

@ -1,10 +0,0 @@
import type { LinkProps } from './Link';
import { Location } from './index';
import { Matched } from './matcher/parser';
type NavLinkProps = {
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
isActive?: (match: Matched | null, location: Location) => boolean;
[key: string]: any;
} & LinkProps;
declare function NavLink<P extends NavLinkProps>(props: P): JSX.Element;
export default NavLink;

View File

@ -1,8 +0,0 @@
import { Location } from './index';
import { Action } from '../history/types';
type PromptProps = {
message?: string | ((location: Partial<Location>, action: Action) => void);
when?: boolean | ((location: Partial<Location>) => boolean);
};
declare function Prompt<P extends PromptProps>(props: P): JSX.Element;
export default Prompt;

View File

@ -1,13 +0,0 @@
import { Matched } from './matcher/parser';
import { Location } from './index';
export type RedirectProps = {
to: string | Partial<Location>;
push?: boolean;
path?: string;
from?: string;
exact?: boolean;
strict?: boolean;
readonly computed?: Matched | null;
};
declare function Redirect<P extends RedirectProps>(props: P): JSX.Element;
export default Redirect;

View File

@ -1,23 +0,0 @@
import * as React from 'react';
import { History, Location } from './index';
import { Matched } from './matcher/parser';
import { GetURLParams } from './matcher/types';
export type RouteComponentProps<P extends Record<string, any> = {}, S = unknown> = RouteChildrenProps<P, S>;
export type RouteChildrenProps<P extends Record<string, any> = {}, S = unknown> = {
history: History<S>;
location: Location<S>;
match: Matched<P> | null;
};
export type RouteProps<P extends Record<string, any> = {}, Path extends string = string> = {
location?: Location;
component?: React.ComponentType<RouteComponentProps<P>> | React.ComponentType<any> | undefined;
children?: ((props: RouteChildrenProps<P>) => React.ReactNode) | React.ReactNode;
render?: (props: RouteComponentProps<P>) => React.ReactNode;
path?: Path | Path[];
exact?: boolean;
sensitive?: boolean;
strict?: boolean;
computed?: Matched<P>;
};
declare function Route<Path extends string, P extends Record<string, any> = GetURLParams<Path>>(props: RouteProps<P, Path>): JSX.Element;
export default Route;

View File

@ -1,8 +0,0 @@
import * as React from 'react';
import { History } from '../history/types';
export type RouterProps = {
history: History;
children?: React.ReactNode;
};
declare function Router<P extends RouterProps>(props: P): JSX.Element;
export default Router;

View File

@ -1,8 +0,0 @@
import * as React from 'react';
import { Location } from './index';
export type SwitchProps = {
location?: Location;
children?: React.ReactNode;
};
declare function Switch<P extends SwitchProps>(props: P): React.ReactElement | null;
export default Switch;

View File

@ -1 +0,0 @@
import '@testing-library/jest-dom';

View File

@ -1,8 +0,0 @@
import { History, Location } from '../index';
export declare let historyHook: History;
export declare let locationHook: Location;
export declare const LocationDisplay: () => JSX.Element;
export declare const Test_Demo: () => JSX.Element;
export declare const Test_Demo2: () => JSX.Element;
export declare const Test_Demo3: () => JSX.Element;
export declare const Test_Demo4: () => JSX.Element;

View File

@ -1,10 +0,0 @@
/// <reference types="react" />
import { History, Location } from './index';
import { Matched } from './matcher/parser';
export type RouterContextValue = {
history: History;
location: Location;
match: Matched | null;
};
declare const RouterContext: import("react").Context<RouterContextValue>;
export default RouterContext;

View File

@ -1,8 +0,0 @@
import { Matched, Params } from './matcher/parser';
import { History } from '../history/types';
import { Location } from './index';
declare function useHistory<S>(): History<S>;
declare function useLocation<S>(): Location<S>;
declare function useParams<P>(): Params<P> | {};
declare function useRouteMatch<P>(path?: string): Matched<P> | null;
export { useHistory, useLocation, useParams, useRouteMatch };

View File

@ -1,20 +0,0 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';

View File

@ -1,23 +0,0 @@
import { Location as HLocation } from '../history/types';
type Location<S = unknown> = Omit<HLocation<S>, 'key'>;
export { Location };
export type { History } from '../history/types';
export { createBrowserHistory } from '../history/browerHistory';
export { createHashHistory } from '../history/hashHistory';
export { default as __RouterContext } from './context';
export { matchPath, generatePath } from './matcher/parser';
export { useHistory, useLocation, useParams, useRouteMatch } from './hooks';
export { default as Route } from './Route';
export { default as Router } from './Router';
export { default as Switch } from './Switch';
export { default as Redirect } from './Redirect';
export { default as Prompt } from './Prompt';
export { default as withRouter } from './withRouter';
export { default as HashRouter } from './HashRouter';
export { default as BrowserRouter } from './BrowserRouter';
export { default as Link } from './Link';
export { default as NavLink } from './NavLink';
export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Route';
export { connectRouter, routerMiddleware } from '../connect-router';
export declare const ConnectedRouter: any;
export declare const ConnectedHRouter: any;

View File

@ -1,7 +0,0 @@
export type LifeCycleProps = {
onMount?: () => void;
onUpdate?: (prevProps?: LifeCycleProps) => void;
onUnmount?: () => void;
data?: any;
};
export declare function LifeCycle(props: LifeCycleProps): any;

View File

@ -1,2 +0,0 @@
import { Token } from './types';
export declare function lexer(path: string): Token[];

View File

@ -1,18 +0,0 @@
import { GetURLParams, Parser, ParserOption } from './types';
export type Params<P> = {
[K in keyof P]?: P[K];
};
export type Matched<P = any> = {
score: number[];
params: Params<P>;
path: string;
url: string;
isExact: boolean;
};
export declare function createPathParser<Str extends string>(pathname: Str, option?: ParserOption): Parser<GetURLParams<Str>>;
export declare function createPathParser<P = unknown>(pathname: string, option?: ParserOption): Parser<P>;
/**
* @description 使pathname与pattern进行匹配
*/
export declare function matchPath<P = any>(pathname: string, pattern: string | string[], option?: ParserOption): Matched<P> | null;
export declare function generatePath<P = any>(path: string, params: Params<P>): string;

View File

@ -1,36 +0,0 @@
import { Matched, Params } from './parser';
export type Token = {
type: TokenType;
value: string;
};
export declare enum TokenType {
Delimiter = "delimiter",
Static = "static",
Param = "param",
WildCard = "wildcard",
LBracket = "(",
RBracket = ")",
Pattern = "pattern"
}
export interface Parser<P> {
regexp: RegExp;
keys: string[];
parse(url: string): Matched<P> | null;
compile(params: Params<P>): string;
}
export type ParserOption = {
caseSensitive?: boolean;
strictMode?: boolean;
exact?: boolean;
};
type ClearLeading<U extends string> = U extends `/${infer R}` ? ClearLeading<R> : U;
type ClearTailing<U extends string> = U extends `${infer L}/` ? ClearTailing<L> : U;
type ParseParam<Param extends string> = Param extends `:${infer R}` ? {
[K in R]: string;
} : {};
type MergeParams<OneParam extends Record<string, any>, OtherParam extends Record<string, any>> = {
readonly [Key in keyof OneParam | keyof OtherParam]?: string;
};
type ParseURLString<Str extends string> = Str extends `${infer Param}/${infer Rest}` ? MergeParams<ParseParam<Param>, ParseURLString<ClearLeading<Rest>>> : ParseParam<Str>;
export type GetURLParams<U extends string> = ParseURLString<ClearLeading<ClearTailing<U>>>;
export {};

View File

@ -1,6 +0,0 @@
/**
* @description url中的//转换为/
*/
export declare function cleanPath(path: string): string;
export declare function scoreCompare(score1: number[], score2: number[]): number;
export declare function escapeStr(str: string): string;

View File

@ -1,3 +0,0 @@
import * as React from 'react';
declare function withRouter<C extends React.ComponentType>(Component: C): (props: any) => JSX.Element;
export default withRouter;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
import * as React from 'react';
import { useLayoutEffect } from 'react';
import { useLayoutEffect, useRef } from 'react';
import { connect, ReactReduxContext } from 'react-redux';
import { Store } from 'redux';
import { reduxAdapter } from '@cloudsop/horizon';
@ -28,7 +28,8 @@ function ConnectedRouterWithoutMemo<S>(props: ConnectedRouter<S>) {
const { getLocation } = stateReader(storeType);
// 监听store变化
const unsubscribe = store.subscribe(() => {
const unsubscribe = useRef<null | (() => void)>(
store.subscribe(() => {
// 获取redux State中的location信息
const {
pathname: pathnameInStore,
@ -62,7 +63,8 @@ function ConnectedRouterWithoutMemo<S>(props: ConnectedRouter<S>) {
stateInStore,
);
}
});
}),
);
const handleLocationChange = (args: Navigation<S>, isFirstRendering: boolean = false) => {
const { location, action } = args;
@ -70,12 +72,12 @@ function ConnectedRouterWithoutMemo<S>(props: ConnectedRouter<S>) {
};
// 监听history更新
const unListen = () => history.listen(handleLocationChange);
const unListen = useRef<null | (() => void)>(history.listen(handleLocationChange));
useLayoutEffect(() => {
return () => {
unListen();
unsubscribe();
unListen.current && unListen.current();
unsubscribe.current && unsubscribe.current();
};
}, []);

View File

@ -3,3 +3,4 @@ import { createConnectRouter } from './reducer';
export { getConnectedRouter } from './connectedRouter';
export const connectRouter = createConnectRouter();
export { routerMiddleware } from './dispatch';
export { push, go, replace } from './actions';

View File

@ -6,7 +6,6 @@ import { Location, matchPath } from './index';
import { Matched } from './matcher/parser';
import Context from './context';
import { parsePath } from '../history/utils';
import { escapeStr } from './matcher/utils';
type NavLinkProps = {
to: Partial<Location> | string | ((location: Location) => string | Partial<Location>);
@ -23,10 +22,9 @@ function NavLink<P extends NavLinkProps>(props: P) {
const toLocation = typeof to === 'function' ? to(context.location) : to;
const { pathname: path } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation;
// 把正则表达式的特殊符号加两个反斜杠进行转义
const escapedPath = path ? escapeStr(path) : '';
const match = escapedPath ? matchPath(context.location.pathname, escapedPath) : null;
const { pathname } = typeof toLocation === 'string' ? parsePath(toLocation) : toLocation;
const match = pathname ? matchPath(context.location.pathname, pathname) : null;
const isLinkActive = match && isActive ? isActive(match, context.location) : false;

View File

@ -16,17 +16,19 @@ function Router<P extends RouterProps>(props: P) {
const pendingLocation = useRef<Location | null>(null);
// 在Router加载时就监听history地址变化以保证在始渲染时重定向能正确触发
let unListen: null | (() => void) = history.listen(arg => {
const unListen = useRef<null | (() => void)>(
history.listen(arg => {
pendingLocation.current = arg.location;
});
}),
);
// 模拟componentDidMount和componentWillUnmount
useLayoutEffect(() => {
if (unListen) {
unListen();
if (unListen.current) {
unListen.current();
}
// 监听history中的位置变化
unListen = history.listen(arg => {
unListen.current = history.listen(arg => {
setLocation(arg.location);
});
@ -35,9 +37,9 @@ function Router<P extends RouterProps>(props: P) {
}
return () => {
if (unListen) {
unListen();
unListen = null;
if (unListen.current) {
unListen.current();
unListen.current = null;
pendingLocation.current = null;
}
};

View File

@ -31,15 +31,9 @@ function Switch<P extends SwitchProps>(props: P): React.ReactElement | null {
// node可能是Route和Redirect
if (node.type === Route) {
const props = node.props as RouteProps;
strict = props.strict;
sensitive = props.sensitive;
path = props.path;
({ strict, sensitive, path } = node.props as RouteProps);
} else if (node.type === Redirect) {
const props = node.props as RedirectProps;
path = props.path;
strict = props.strict;
from = props.from;
({ path, strict, from } = node.props as RedirectProps);
}
const exact = node.props.exact;

View File

@ -40,6 +40,6 @@ export type { RouteComponentProps, RouteChildrenProps, RouteProps } from './Rout
// ============================ Connect-router ============================
export { connectRouter, routerMiddleware } from '../connect-router';
export { connectRouter, routerMiddleware, push, go, replace } from '../connect-router';
export const ConnectedRouter = getConnectedRouter('Redux');
export const ConnectedHRouter = getConnectedRouter('HorizonXCompat');

View File

@ -53,6 +53,20 @@ describe('path lexer Test', () => {
]);
});
it('dynamic params with pattern 2', () => {
const tokens = lexer('/www.a.com/:b(abc|xyz)/*');
expect(tokens).toStrictEqual([
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'www.a.com' },
{ type: 'delimiter', value: '/' },
{ type: 'param', value: 'b' },
{ type: '(', value: '(' },
{ type: 'pattern', value: 'abc|xyz' },
{ type: ')', value: ')' },
{ type: 'delimiter', value: '/' },
{ type: 'wildcard', value: '*' },
]);
});
it('wildcard params test', () => {
const tokens = lexer('/www.a.com/:b');
expect(tokens).toStrictEqual([
@ -62,4 +76,45 @@ describe('path lexer Test', () => {
{ type: 'param', value: 'b' },
]);
});
it('wildcard in end of static param', () => {
const tokens = lexer('/abc*');
expect(tokens).toStrictEqual([
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'abc' },
{ type: 'pattern', value: '*' },
]);
});
it('wildcard in end of static param 2', () => {
const tokens = lexer('/abc*/xyz*');
expect(tokens).toStrictEqual([
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'abc' },
{ type: 'pattern', value: '*' },
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'xyz' },
{ type: 'pattern', value: '*' },
]);
});
it('url contain optional param at end', () => {
const tokens = lexer('/user/:name?');
expect(tokens).toEqual([
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'user' },
{ type: 'delimiter', value: '/' },
{ type: 'param', value: 'name' },
{ type: 'pattern', value: '?' },
]);
});
it('url contain optional param at middle', () => {
const tokens = lexer('/user/:name?/profile');
expect(tokens).toEqual([
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'user' },
{ type: 'delimiter', value: '/' },
{ type: 'param', value: 'name' },
{ type: 'pattern', value: '?' },
{ type: 'delimiter', value: '/' },
{ type: 'static', value: 'profile' },
]);
});
});

View File

@ -34,10 +34,13 @@ describe('parser test', () => {
const parser = createPathParser('/www.a.com/a/*', { exact: true });
const params = parser.parse('/www.a.com/a/b1/c1/d1');
const params1 = parser.parse('/www.a.com/a/b1/c1/');
const params2 = parser.parse('/www.a.com/a/b1/');
expect(params!.params).toStrictEqual({ '*': ['b1', 'c1', 'd1'] });
expect(params!.score).toStrictEqual([10, 10, 3, 3, 3]);
expect(params1!.params).toStrictEqual({ '*': ['b1', 'c1'] });
expect(params1!.score).toStrictEqual([10, 10, 3, 3]);
expect(params2!.params).toStrictEqual({ '*': ['b1'] });
expect(params2!.score).toStrictEqual([10, 10, 3]);
});
it('compile wildcard', function () {
@ -191,9 +194,30 @@ describe('parser test', () => {
});
});
it('wildcard after dynamic param with pattern', () => {
const parser = createPathParser('/detail/:action(info)/*');
const res = parser.parse('/detail/info/123');
expect(res).toEqual({
isExact: true,
path: '/detail/:action(info)/*',
url: '/detail/info/123',
score: [10, 6, 3],
params: { action: 'info', '*': ['123'] },
});
});
it('dynamic param with regexp pattern after wildcard', () => {
const parser = createPathParser('/detail/*/:action(\\d+)');
const res = parser.parse('/detail/abc/xyz/123');
expect(res).toEqual({
isExact: true,
path: '/detail/*/:action(\\d+)',
url: '/detail/abc/xyz/123',
score: [10, 3, 3, 6],
params: { action: '123', '*': ['abc', 'xyz'] },
});
});
it('dynamic param with regexp pattern', () => {
const parser = createPathParser('/detail/:action(\\d+)');
console.log(parser.regexp);
const res = parser.parse('/detail/123');
expect(res).toEqual({
isExact: true,
@ -245,4 +269,121 @@ describe('parser test', () => {
c: 'abc',
});
});
it('support wildcard "*" in end of static path 1', function () {
const parser = createPathParser('/home*');
const res = parser.parse('/homeAbc/a123');
expect(res).toEqual({
isExact: true,
path: '/home*',
url: '/homeAbc/a123',
score: [10],
params: { '0': 'Abc/a123' },
});
});
it('support wildcard "*" in url and dynamic param at end', function () {
const parser = createPathParser('/home*/:a+');
const res = parser.parse('/homeAbc/a');
expect(res).toEqual({
path: '/home*/:a+',
url: '/homeAbc/a',
isExact: true,
score: [10, 6],
params: { '0': 'Abc', a: 'a' },
});
});
it('parse url with optional param 1', () => {
const parser = createPathParser('/catalog/logical-view/:pageType/:viewName?');
const res = parser.parse('/catalog/logical-view/create');
expect(res).toStrictEqual({
isExact: true,
path: '/catalog/logical-view/:pageType/:viewName?',
url: '/catalog/logical-view/create',
score: [10, 10, 6, 6],
params: { pageType: 'create', viewName: undefined },
});
const res2 = parser.parse('/catalog/logical-view/create/view1');
expect(res2).toStrictEqual({
isExact: true,
path: '/catalog/logical-view/:pageType/:viewName?',
url: '/catalog/logical-view/create/view1',
score: [10, 10, 6, 6],
params: { pageType: 'create', viewName: 'view1' },
});
});
it('parse url with wildcard param 1', () => {
const parser = createPathParser('/home/:p*');
const res = parser.parse('/home/123');
expect(res).toStrictEqual({
path: '/home/:p*',
url: '/home/123',
isExact: true,
params: { p: '123' },
score: [10, 6],
});
const res2 = parser.parse('/home/123/456');
expect(res2).toStrictEqual({
path: '/home/:p*',
url: '/home/123/456',
isExact: true,
params: { p: '123/456' },
score: [10, 6],
});
});
it('parse url with wildcard param in middle of URL', () => {
const parser = createPathParser('/home/:p*/link');
const res = parser.parse('/home/123/link');
expect(res).toStrictEqual({
path: '/home/:p*/link',
url: '/home/123/link',
isExact: true,
params: { p: '123' },
score: [10, 6, 10],
});
const res2 = parser.parse('/home/link');
expect(res2).toStrictEqual({
path: '/home/:p*/link',
url: '/home/link',
isExact: true,
params: { p: undefined },
score: [10, 6, 10],
});
});
it('parse url with optional param 2', () => {
const parser = createPathParser('/user/:userid?/profile');
const res = parser.parse('/user/profile');
expect(res).toStrictEqual({
isExact: true,
params: { userid: undefined },
path: '/user/:userid?/profile',
score: [10, 6, 10],
url: '/user/profile',
});
const res2 = parser.parse('/user/123/profile');
expect(res2).toStrictEqual({
isExact: true,
params: { userid: '123' },
path: '/user/:userid?/profile',
score: [10, 6, 10],
url: '/user/123/profile',
});
});
it('complex url pattern test 1', function () {
const parser = createPathParser('/dump/taskList/:action(add|config)/lifecyclePolicy/:name?');
const res = parser.parse('/dump/taskList/add/lifecyclePolicy/');
expect(res).toStrictEqual({
isExact: true,
path: '/dump/taskList/:action(add|config)/lifecyclePolicy/:name?',
url: '/dump/taskList/add/lifecyclePolicy/',
score: [10, 10, 6, 10, 6],
params: { action: 'add', name: undefined },
});
const res1 = parser.parse('/dump/taskList/add/lifecyclePolicy/new');
expect(res1).toStrictEqual({
isExact: true,
path: '/dump/taskList/:action(add|config)/lifecyclePolicy/:name?',
url: '/dump/taskList/add/lifecyclePolicy/new',
score: [10, 10, 6, 10, 6],
params: { action: 'add', name: 'new' },
});
});
});

View File

@ -1,7 +1,7 @@
import { Token, TokenType } from './types';
import { cleanPath } from './utils';
const validChar = /[^/:*()]/;
const validChar = /[^/:()*?$^+]/;
// 对Url模板进行词法解析解析结果为Tokens
export function lexer(path: string): Token[] {
@ -66,6 +66,11 @@ export function lexer(path: string): Token[] {
skipChar(1);
continue;
}
if (['*', '?', '$', '^', '+'].includes(curChar)) {
tokens.push({ type: TokenType.Pattern, value: curChar });
skipChar(1);
continue;
}
if (validChar.test(curChar)) {
tokens.push({ type: TokenType.Pattern, value: getLiteral() });
continue;

View File

@ -39,6 +39,12 @@ const BASE_PARAM_PATTERN = '[^/]+';
const DefaultDelimiter = '/#?';
/**
* URL匹配整体流程
* 1.URL模板解析为Token
* 2.使Token生成正则表达式
* 3.URL中参数或填充URL模板
*/
export function createPathParser<Str extends string>(pathname: Str, option?: ParserOption): Parser<GetURLParams<Str>>;
export function createPathParser<P = unknown>(pathname: string, option?: ParserOption): Parser<P>;
export function createPathParser<P = unknown>(pathname: string, option: ParserOption = defaultOption): Parser<P> {
@ -47,12 +53,7 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
strictMode = defaultOption.strictMode,
exact = defaultOption.exact,
} = option;
/**
* URL匹配整体流程
* 1.URL模板解析为Token
* 2.使Token生成正则表达式
* 3.URL中参数或填充URL模板
*/
let pattern = '^';
const keys: string[] = [];
const scores: number[] = [];
@ -61,29 +62,61 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
const onlyHasWildCard = tokens.length === 1 && tokens[0].type === TokenType.WildCard;
const tokenCount = tokens.length;
const lastToken = tokens[tokenCount - 1];
let asteriskCount = 0;
/**
* URL中的可选参数/:parma?
* @description /?
* @param currentIdx
*/
const lookToNextDelimiter = (currentIdx: number): boolean => {
let hasOptionalParam = false;
while (currentIdx < tokens.length && tokens[currentIdx].type !== TokenType.Delimiter) {
if (tokens[currentIdx].value === '?' || tokens[currentIdx].value === '*') {
hasOptionalParam = true;
}
currentIdx++;
}
return hasOptionalParam;
};
for (let tokenIdx = 0; tokenIdx < tokenCount; tokenIdx++) {
const token = tokens[tokenIdx];
const nextToken = tokens[tokenIdx + 1];
switch (token.type) {
case TokenType.Delimiter:
pattern += '/';
const hasOptional = lookToNextDelimiter(tokenIdx + 1);
pattern += `/${hasOptional ? '?' : ''}`;
break;
case TokenType.Static:
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
if (nextToken && nextToken.type === TokenType.Pattern) {
pattern += `(.${nextToken.value})`;
keys.push(String(asteriskCount));
asteriskCount++;
}
scores.push(MatchScore.static);
break;
case TokenType.Param:
// 动态参数支持形如/:param、/:param*、/:param?、/:param(\\d+)的形式
let paramRegexp = '';
if (nextToken && nextToken.type === TokenType.LBracket) {
if (nextToken) {
switch (nextToken.type) {
case TokenType.LBracket:
// 跳过当前Token和左括号
tokenIdx += 2;
while (tokens[tokenIdx].type !== TokenType.RBracket) {
paramRegexp += tokens[tokenIdx].value;
tokenIdx++;
}
paramRegexp = `(${paramRegexp})`;
break;
case TokenType.Pattern:
tokenIdx++;
paramRegexp += `(${nextToken.value === '*' ? '.*' : BASE_PARAM_PATTERN})${nextToken.value}`;
break;
}
pattern += paramRegexp ? `((?:${paramRegexp}))` : `(${BASE_PARAM_PATTERN})`;
}
pattern += paramRegexp ? `(?:${paramRegexp})` : `(${BASE_PARAM_PATTERN})`;
keys.push(token.value);
scores.push(MatchScore.param);
break;
@ -139,7 +172,7 @@ export function createPathParser<P = unknown>(pathname: string, option: ParserOp
...new Array(value.length).fill(MatchScore.wildcard),
);
} else {
params[key] = param ? param : [];
params[key] = param ? param : undefined;
}
}