@shuvi/router 0.0.1-pre.1 → 0.0.1-pre.2

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.
Files changed (51) hide show
  1. package/esm/createRoutesFromArray.d.ts +2 -0
  2. package/esm/createRoutesFromArray.js +9 -0
  3. package/esm/getRedirectFromRoutes.d.ts +2 -0
  4. package/esm/getRedirectFromRoutes.js +8 -0
  5. package/esm/history/base.d.ts +53 -0
  6. package/esm/history/base.js +80 -0
  7. package/esm/history/browser.d.ts +12 -0
  8. package/esm/history/browser.js +104 -0
  9. package/esm/history/hash.d.ts +13 -0
  10. package/esm/history/hash.js +135 -0
  11. package/esm/history/index.d.ts +7 -0
  12. package/esm/history/index.js +13 -0
  13. package/esm/history/memory.d.ts +22 -0
  14. package/esm/history/memory.js +75 -0
  15. package/esm/index.d.ts +7 -0
  16. package/esm/index.js +7 -0
  17. package/esm/matchPathname.d.ts +15 -0
  18. package/esm/matchPathname.js +54 -0
  19. package/esm/matchRoutes.d.ts +6 -0
  20. package/esm/matchRoutes.js +92 -0
  21. package/esm/pathParserRanker.d.ts +65 -0
  22. package/esm/pathParserRanker.js +223 -0
  23. package/esm/pathTokenizer.d.ts +23 -0
  24. package/esm/pathTokenizer.js +149 -0
  25. package/esm/router.d.ts +10 -0
  26. package/esm/router.js +207 -0
  27. package/esm/types/history.d.ts +176 -0
  28. package/esm/types/history.js +0 -0
  29. package/esm/types/index.d.ts +2 -0
  30. package/esm/types/index.js +1 -0
  31. package/esm/types/router.d.ts +78 -0
  32. package/esm/types/router.js +0 -0
  33. package/esm/utils/async.d.ts +2 -0
  34. package/esm/utils/async.js +18 -0
  35. package/esm/utils/createRedirector.d.ts +8 -0
  36. package/esm/utils/createRedirector.js +30 -0
  37. package/esm/utils/dom.d.ts +1 -0
  38. package/esm/utils/dom.js +1 -0
  39. package/esm/utils/error.d.ts +1 -0
  40. package/esm/utils/error.js +3 -0
  41. package/esm/utils/extract-hooks.d.ts +3 -0
  42. package/esm/utils/extract-hooks.js +16 -0
  43. package/esm/utils/history.d.ts +12 -0
  44. package/esm/utils/history.js +51 -0
  45. package/esm/utils/index.d.ts +4 -0
  46. package/esm/utils/index.js +4 -0
  47. package/esm/utils/misc.d.ts +11 -0
  48. package/esm/utils/misc.js +41 -0
  49. package/esm/utils/path.d.ts +13 -0
  50. package/esm/utils/path.js +101 -0
  51. package/package.json +5 -4
@@ -0,0 +1,92 @@
1
+ import { matchPathname } from './matchPathname';
2
+ import { joinPaths, resolvePath } from './utils';
3
+ import { tokensToParser, comparePathParserScore } from './pathParserRanker';
4
+ import { tokenizePath } from './pathTokenizer';
5
+ function matchRouteBranch(branch, pathname) {
6
+ let routes = branch[1];
7
+ let matchedPathname = '/';
8
+ let matchedParams = {};
9
+ let matches = [];
10
+ for (let i = 0; i < routes.length; ++i) {
11
+ let route = routes[i];
12
+ let remainingPathname = matchedPathname === '/'
13
+ ? pathname
14
+ : pathname.slice(matchedPathname.length) || '/';
15
+ let routeMatch = matchPathname({
16
+ path: route.path,
17
+ caseSensitive: route.caseSensitive,
18
+ end: i === routes.length - 1
19
+ }, remainingPathname);
20
+ if (!routeMatch)
21
+ return null;
22
+ matchedPathname = joinPaths([matchedPathname, routeMatch.pathname]);
23
+ matchedParams = Object.assign(Object.assign({}, matchedParams), routeMatch.params);
24
+ matches.push({
25
+ route,
26
+ pathname: matchedPathname,
27
+ params: Object.freeze(matchedParams)
28
+ });
29
+ }
30
+ return matches;
31
+ }
32
+ export function rankRouteBranches(branches) {
33
+ if (branches.length <= 1) {
34
+ return branches;
35
+ }
36
+ const normalizedPaths = branches.map((branch, index) => {
37
+ const [path] = branch;
38
+ return Object.assign(Object.assign({}, tokensToParser(tokenizePath(path))), { path,
39
+ index });
40
+ });
41
+ normalizedPaths.sort((a, b) => comparePathParserScore(a, b));
42
+ const newBranches = [];
43
+ // console.log(
44
+ // normalizedPaths
45
+ // .map(parser => `${parser.path} -> ${JSON.stringify(parser.score)}`)
46
+ // .join('\n')
47
+ // )
48
+ normalizedPaths.forEach((branch, newBranchesIndex) => {
49
+ const { index } = branch;
50
+ newBranches[newBranchesIndex] = branches[index];
51
+ });
52
+ return newBranches;
53
+ }
54
+ function flattenRoutes(routes, branches = [], parentPath = '', parentRoutes = [], parentIndexes = []) {
55
+ routes.forEach((route, index) => {
56
+ let path = joinPaths([parentPath, route.path]);
57
+ let routes = parentRoutes.concat(route);
58
+ let indexes = parentIndexes.concat(index);
59
+ // Add the children before adding this route to the array so we traverse the
60
+ // route tree depth-first and child routes appear before their parents in
61
+ // the "flattened" version.
62
+ if (route.children) {
63
+ flattenRoutes(route.children, branches, path, routes, indexes);
64
+ }
65
+ branches.push([path, routes, indexes]);
66
+ });
67
+ return branches;
68
+ }
69
+ export function matchRoutes(routes, location, basename = '') {
70
+ if (typeof location === 'string') {
71
+ location = resolvePath(location);
72
+ }
73
+ let pathname = location.pathname || '/';
74
+ if (basename) {
75
+ let base = basename.replace(/^\/*/, '/').replace(/\/+$/, '');
76
+ if (pathname.startsWith(base)) {
77
+ pathname = pathname === base ? '/' : pathname.slice(base.length);
78
+ }
79
+ else {
80
+ // Pathname does not start with the basename, no match.
81
+ return null;
82
+ }
83
+ }
84
+ let branches = flattenRoutes(routes);
85
+ branches = rankRouteBranches(branches);
86
+ let matches = null;
87
+ for (let i = 0; matches == null && i < branches.length; ++i) {
88
+ // TODO: Match on search, state too?
89
+ matches = matchRouteBranch(branches[i], pathname);
90
+ }
91
+ return matches;
92
+ }
@@ -0,0 +1,65 @@
1
+ import { Token } from './pathTokenizer';
2
+ declare type PathParams = Record<string, string | string[]>;
3
+ export declare type MatchPathParams = {
4
+ match: string;
5
+ params: PathParams;
6
+ };
7
+ /**
8
+ * A param in a url like `/users/:id`
9
+ */
10
+ export interface PathParserParamKey {
11
+ name: string;
12
+ repeatable: boolean;
13
+ optional: boolean;
14
+ }
15
+ export interface PathParser {
16
+ /**
17
+ * The regexp used to match a url
18
+ */
19
+ re: RegExp;
20
+ /**
21
+ * The score of the parser
22
+ */
23
+ score: Array<number[]>;
24
+ /**
25
+ * Keys that appeared in the path
26
+ */
27
+ keys: PathParserParamKey[];
28
+ /**
29
+ * Parses a url and returns the matched params or nul if it doesn't match. An
30
+ * optional param that isn't preset will be an empty string. A repeatable
31
+ * param will be an array if there is at least one value.
32
+ *
33
+ * @param path - url to parse
34
+ * @returns a Params object, empty if there are no params. `null` if there is
35
+ * no match
36
+ */
37
+ parse(path: string): MatchPathParams | null;
38
+ /**
39
+ * Creates a string version of the url
40
+ *
41
+ * @param params - object of params
42
+ * @returns a url
43
+ */
44
+ stringify(params: PathParams): string;
45
+ }
46
+ export declare type PathParserOptions = Pick<_PathParserOptions, 'end' | 'sensitive' | 'strict'>;
47
+ /**
48
+ * Creates a path parser from an array of Segments (a segment is an array of Tokens)
49
+ *
50
+ * @param segments - array of segments returned by tokenizePath
51
+ * @param extraOptions - optional options for the regexp
52
+ * @returns a PathParser
53
+ */
54
+ export declare function tokensToParser(segments: Array<Token[]>, extraOptions?: _PathParserOptions): PathParser;
55
+ /**
56
+ * Compare function that can be used with `sort` to sort an array of PathParser
57
+ * @param a - first PathParser
58
+ * @param b - second PathParser
59
+ * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b, last sort by index
60
+ */
61
+ declare type PathParserIndex = PathParser & {
62
+ index: number;
63
+ };
64
+ export declare function comparePathParserScore(a: PathParserIndex, b: PathParserIndex): number;
65
+ export {};
@@ -0,0 +1,223 @@
1
+ // default pattern for a param: non greedy everything but /
2
+ const BASE_PARAM_PATTERN = '[^/]+?';
3
+ const BASE_PATH_PARSER_OPTIONS = {
4
+ sensitive: false,
5
+ strict: false,
6
+ start: true,
7
+ end: true,
8
+ };
9
+ // Special Regex characters that must be escaped in static tokens
10
+ const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
11
+ /**
12
+ * Creates a path parser from an array of Segments (a segment is an array of Tokens)
13
+ *
14
+ * @param segments - array of segments returned by tokenizePath
15
+ * @param extraOptions - optional options for the regexp
16
+ * @returns a PathParser
17
+ */
18
+ export function tokensToParser(segments, extraOptions) {
19
+ const options = Object.assign({}, BASE_PATH_PARSER_OPTIONS, extraOptions);
20
+ // the amount of scores is the same as the length of segments except for the root segment "/"
21
+ let score = [];
22
+ // the regexp as a string
23
+ let pattern = options.start ? '^' : '';
24
+ // extracted keys
25
+ const keys = [];
26
+ for (const segment of segments) {
27
+ // the root segment needs special treatment
28
+ const segmentScores = segment.length ? [] : [90 /* Root */];
29
+ // allow trailing slash
30
+ if (options.strict && !segment.length)
31
+ pattern += '/';
32
+ for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
33
+ const token = segment[tokenIndex];
34
+ // resets the score if we are inside a sub segment /:a-other-:b
35
+ let subSegmentScore = 40 /* Segment */ +
36
+ (options.sensitive ? 0.25 /* BonusCaseSensitive */ : 0);
37
+ if (token.type === 0 /* Static */) {
38
+ // prepend the slash if we are starting a new segment
39
+ if (!tokenIndex)
40
+ pattern += '/';
41
+ pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
42
+ subSegmentScore += 40 /* Static */;
43
+ }
44
+ else if (token.type === 1 /* Param */) {
45
+ const { value, repeatable, optional, regexp } = token;
46
+ keys.push({
47
+ name: value,
48
+ repeatable,
49
+ optional,
50
+ });
51
+ const re = regexp ? regexp : BASE_PARAM_PATTERN;
52
+ // the user provided a custom regexp /:id(\\d+)
53
+ if (re !== BASE_PARAM_PATTERN) {
54
+ subSegmentScore += 10 /* BonusCustomRegExp */;
55
+ // make sure the regexp is valid before using it
56
+ try {
57
+ new RegExp(`(${re})`);
58
+ }
59
+ catch (err) {
60
+ throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
61
+ err.message);
62
+ }
63
+ }
64
+ // when we repeat we must take care of the repeating leading slash
65
+ let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
66
+ // prepend the slash if we are starting a new segment
67
+ if (!tokenIndex)
68
+ subPattern =
69
+ // avoid an optional / if there are more segments e.g. /:p?-static
70
+ // or /:p?-:p2
71
+ optional && segment.length < 2
72
+ ? `(?:/${subPattern})`
73
+ : '/' + subPattern;
74
+ if (optional)
75
+ subPattern += '?';
76
+ pattern += subPattern;
77
+ if (!options.end)
78
+ pattern += '(?=\/|$)';
79
+ subSegmentScore += 20 /* Dynamic */;
80
+ if (optional)
81
+ subSegmentScore += -8 /* BonusOptional */;
82
+ if (repeatable)
83
+ subSegmentScore += -20 /* BonusRepeatable */;
84
+ if (re === '.*')
85
+ subSegmentScore += -50 /* BonusWildcard */;
86
+ }
87
+ segmentScores.push(subSegmentScore);
88
+ }
89
+ // an empty array like /home/ -> [[{home}], []]
90
+ // if (!segment.length) pattern += '/'
91
+ score.push(segmentScores);
92
+ }
93
+ // only apply the strict bonus to the last score
94
+ if (options.strict && options.end) {
95
+ const i = score.length - 1;
96
+ score[i][score[i].length - 1] += 0.7000000000000001 /* BonusStrict */;
97
+ }
98
+ // TODO: dev only warn double trailing slash
99
+ if (!options.strict)
100
+ pattern += '/*?';
101
+ if (options.end)
102
+ pattern += '$';
103
+ // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
104
+ else if (options.strict)
105
+ pattern += '(?:/*|$)';
106
+ const re = new RegExp(pattern, options.sensitive ? '' : 'i');
107
+ function parse(path) {
108
+ const match = path.match(re);
109
+ const params = {};
110
+ if (!match)
111
+ return null;
112
+ for (let i = 1; i < match.length; i++) {
113
+ const value = match[i] || '';
114
+ const key = keys[i - 1];
115
+ params[key.name] = value && key.repeatable ? value.split('/') : value;
116
+ }
117
+ return {
118
+ match: match[0],
119
+ params,
120
+ };
121
+ }
122
+ function stringify(params) {
123
+ let path = '';
124
+ // for optional parameters to allow to be empty
125
+ let avoidDuplicatedSlash = false;
126
+ for (const segment of segments) {
127
+ if (!avoidDuplicatedSlash || !path.endsWith('/'))
128
+ path += '/';
129
+ avoidDuplicatedSlash = false;
130
+ for (const token of segment) {
131
+ if (token.type === 0 /* Static */) {
132
+ path += token.value;
133
+ }
134
+ else if (token.type === 1 /* Param */) {
135
+ const { value, repeatable, optional } = token;
136
+ const param = params[value];
137
+ if (Array.isArray(param) && !repeatable)
138
+ throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
139
+ if (param === undefined && !optional) {
140
+ throw new Error(`Missing required param "${value}"`);
141
+ }
142
+ const text = Array.isArray(param) ? param.join('/') : (param || '');
143
+ if (!text && optional) {
144
+ // if we have more than one optional param like /:a?-static we
145
+ // don't need to care about the optional param
146
+ if (segment.length < 2) {
147
+ // remove the last slash as we could be at the end
148
+ if (path.endsWith('/'))
149
+ path = path.slice(0, -1);
150
+ // do not append a slash on the next iteration
151
+ else
152
+ avoidDuplicatedSlash = true;
153
+ }
154
+ }
155
+ path += text;
156
+ }
157
+ }
158
+ }
159
+ return path;
160
+ }
161
+ return {
162
+ re,
163
+ score,
164
+ keys,
165
+ parse,
166
+ stringify,
167
+ };
168
+ }
169
+ /**
170
+ * Compares an array of numbers as used in PathParser.score and returns a
171
+ * number. This function can be used to `sort` an array
172
+ * @param a - first array of numbers
173
+ * @param b - second array of numbers
174
+ * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
175
+ * should be sorted first
176
+ */
177
+ function compareScoreArray(a, b) {
178
+ let i = 0;
179
+ while (i < a.length && i < b.length) {
180
+ const diff = b[i] - a[i];
181
+ // only keep going if diff === 0
182
+ if (diff)
183
+ return diff;
184
+ i++;
185
+ }
186
+ // if the last subsegment was Static, the shorter segments should be sorted first
187
+ // otherwise sort the longest segment first
188
+ if (a.length < b.length) {
189
+ return a.length === 1 && a[0] === 40 /* Static */ + 40 /* Segment */
190
+ ? -1
191
+ : 1;
192
+ }
193
+ else if (a.length > b.length) {
194
+ return b.length === 1 && b[0] === 40 /* Static */ + 40 /* Segment */
195
+ ? 1
196
+ : -1;
197
+ }
198
+ return 0;
199
+ }
200
+ export function comparePathParserScore(a, b) {
201
+ let i = 0;
202
+ const aScore = a.score;
203
+ const bScore = b.score;
204
+ while (i < aScore.length && i < bScore.length) {
205
+ const comp = compareScoreArray(aScore[i], bScore[i]);
206
+ // do not return if both are equal
207
+ if (comp)
208
+ return comp;
209
+ i++;
210
+ }
211
+ // if a and b share the same score entries but b has more, sort b first
212
+ const lengthDiff = bScore.length - aScore.length;
213
+ if (lengthDiff)
214
+ return lengthDiff;
215
+ // this is the ternary version
216
+ // return aScore.length < bScore.length
217
+ // ? 1
218
+ // : aScore.length > bScore.length
219
+ // ? -1
220
+ // : 0
221
+ //
222
+ return a.index - b.index;
223
+ }
@@ -0,0 +1,23 @@
1
+ export declare const enum TokenType {
2
+ Static = 0,
3
+ Param = 1,
4
+ Group = 2
5
+ }
6
+ interface TokenStatic {
7
+ type: TokenType.Static;
8
+ value: string;
9
+ }
10
+ interface TokenParam {
11
+ type: TokenType.Param;
12
+ regexp?: string;
13
+ value: string;
14
+ optional: boolean;
15
+ repeatable: boolean;
16
+ }
17
+ interface TokenGroup {
18
+ type: TokenType.Group;
19
+ value: Exclude<Token, TokenGroup>[];
20
+ }
21
+ export declare type Token = TokenStatic | TokenParam | TokenGroup;
22
+ export declare function tokenizePath(path: string): Array<Token[]>;
23
+ export {};
@@ -0,0 +1,149 @@
1
+ const ROOT_TOKEN = {
2
+ type: 0 /* Static */,
3
+ value: ''
4
+ };
5
+ const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
6
+ // After some profiling, the cache seems to be unnecessary because tokenizePath
7
+ // (the slowest part of adding a route) is very fast
8
+ // const tokenCache = new Map<string, Token[][]>()
9
+ export function tokenizePath(path) {
10
+ if (!path)
11
+ return [[]];
12
+ if (path === '/')
13
+ return [[ROOT_TOKEN]];
14
+ if (!path.startsWith('/')) {
15
+ path = path.replace(/^\/*/, '/'); // Make sure it has a leading /
16
+ }
17
+ // if (tokenCache.has(path)) return tokenCache.get(path)!
18
+ function crash(message) {
19
+ throw new Error(`ERR (${state})/"${buffer}": ${message}`);
20
+ }
21
+ let state = 0 /* Static */;
22
+ let previousState = state;
23
+ const tokens = [];
24
+ // the segment will always be valid because we get into the initial state
25
+ // with the leading /
26
+ let segment;
27
+ function finalizeSegment() {
28
+ if (segment)
29
+ tokens.push(segment);
30
+ segment = [];
31
+ }
32
+ // index on the path
33
+ let i = 0;
34
+ // char at index
35
+ let char;
36
+ // buffer of the value read
37
+ let buffer = '';
38
+ // custom regexp for a param
39
+ let customRe = '';
40
+ function consumeBuffer() {
41
+ if (!buffer)
42
+ return;
43
+ if (state === 0 /* Static */) {
44
+ segment.push({
45
+ type: 0 /* Static */,
46
+ value: buffer
47
+ });
48
+ }
49
+ else if (state === 1 /* Param */ ||
50
+ state === 2 /* ParamRegExp */ ||
51
+ state === 3 /* ParamRegExpEnd */) {
52
+ if (segment.length > 1 && (char === '*' || char === '+'))
53
+ crash(`A repeatable param (${buffer}) must be alone in its segment. `);
54
+ segment.push({
55
+ type: 1 /* Param */,
56
+ value: buffer,
57
+ regexp: customRe,
58
+ repeatable: char === '*' || char === '+',
59
+ optional: char === '*' || char === '?'
60
+ });
61
+ }
62
+ else {
63
+ crash('Invalid state to consume buffer');
64
+ }
65
+ buffer = '';
66
+ }
67
+ function addCharToBuffer() {
68
+ buffer += char;
69
+ }
70
+ while (i < path.length) {
71
+ char = path[i++];
72
+ if (char === '\\' && state !== 2 /* ParamRegExp */) {
73
+ previousState = state;
74
+ state = 4 /* EscapeNext */;
75
+ continue;
76
+ }
77
+ switch (state) {
78
+ case 0 /* Static */:
79
+ if (char === '/') {
80
+ if (buffer) {
81
+ consumeBuffer();
82
+ }
83
+ finalizeSegment();
84
+ }
85
+ else if (char === ':') {
86
+ consumeBuffer();
87
+ state = 1 /* Param */;
88
+ }
89
+ else {
90
+ addCharToBuffer();
91
+ }
92
+ break;
93
+ case 4 /* EscapeNext */:
94
+ addCharToBuffer();
95
+ state = previousState;
96
+ break;
97
+ case 1 /* Param */:
98
+ if (char === '(') {
99
+ state = 2 /* ParamRegExp */;
100
+ }
101
+ else if (VALID_PARAM_RE.test(char)) {
102
+ addCharToBuffer();
103
+ }
104
+ else {
105
+ consumeBuffer();
106
+ state = 0 /* Static */;
107
+ // go back one character if we were not modifying
108
+ if (char !== '*' && char !== '?' && char !== '+')
109
+ i--;
110
+ }
111
+ break;
112
+ case 2 /* ParamRegExp */:
113
+ // TODO: is it worth handling nested regexp? like :p(?:prefix_([^/]+)_suffix)
114
+ // it already works by escaping the closing )
115
+ // https://paths.esm.dev/?p=AAMeJbiAwQEcDKbAoAAkP60PG2R6QAvgNaA6AFACM2ABuQBB#
116
+ // is this really something people need since you can also write
117
+ // /prefix_:p()_suffix
118
+ if (char === ')') {
119
+ // handle the escaped )
120
+ if (customRe[customRe.length - 1] == '\\')
121
+ customRe = customRe.slice(0, -1) + char;
122
+ else
123
+ state = 3 /* ParamRegExpEnd */;
124
+ }
125
+ else {
126
+ customRe += char;
127
+ }
128
+ break;
129
+ case 3 /* ParamRegExpEnd */:
130
+ // same as finalizing a param
131
+ consumeBuffer();
132
+ state = 0 /* Static */;
133
+ // go back one character if we were not modifying
134
+ if (char !== '*' && char !== '?' && char !== '+')
135
+ i--;
136
+ customRe = '';
137
+ break;
138
+ default:
139
+ crash('Unknown state');
140
+ break;
141
+ }
142
+ }
143
+ if (state === 2 /* ParamRegExp */)
144
+ crash(`Unfinished custom RegExp for param "${buffer}"`);
145
+ consumeBuffer();
146
+ finalizeSegment();
147
+ // tokenCache.set(path, tokens)
148
+ return tokens;
149
+ }
@@ -0,0 +1,10 @@
1
+ import { IRouter, IRouteRecord, IPartialRouteRecord } from './types';
2
+ import History from './history/base';
3
+ interface IRouterOptions<RouteRecord extends IPartialRouteRecord> {
4
+ history: History;
5
+ routes: RouteRecord[];
6
+ caseSensitive?: boolean;
7
+ basename?: string;
8
+ }
9
+ export declare const createRouter: <RouteRecord extends IRouteRecord<any>>(options: IRouterOptions<RouteRecord>) => IRouter<RouteRecord>;
10
+ export {};