vue-router
Version:
## Status: Alpha
1,365 lines (1,345 loc) • 73 kB
JavaScript
/*!
* vue-router v4.0.0-alpha.4
* (c) 2020 Eduardo San Martin Morote
* @license MIT
*/
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var vue = require('vue');
var NavigationType;
(function (NavigationType) {
NavigationType["pop"] = "pop";
NavigationType["push"] = "push";
})(NavigationType || (NavigationType = {}));
var NavigationDirection;
(function (NavigationDirection) {
NavigationDirection["back"] = "back";
NavigationDirection["forward"] = "forward";
NavigationDirection["unknown"] = "";
})(NavigationDirection || (NavigationDirection = {}));
// starting point for abstract history
const START_PATH = '';
const START = {
fullPath: START_PATH,
};
// Generic utils
/**
* Transforms an URI into a normalized history location
* @param parseQuery
* @param location - URI to normalize
* @returns a normalized history location
*/
function parseURL(parseQuery, location) {
let path = '', query = {}, searchString = '', hash = '';
// Could use URL and URLSearchParams but IE 11 doesn't support it
const searchPos = location.indexOf('?');
const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0);
if (searchPos > -1) {
path = location.slice(0, searchPos);
searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
query = parseQuery(searchString);
}
if (hashPos > -1) {
path = path || location.slice(0, hashPos);
// keep the # character
hash = location.slice(hashPos, location.length);
}
// no search and no query
path = path || location;
return {
fullPath: location,
path,
query,
hash,
};
}
/**
* Stringify a URL object
* @param stringifyQuery
* @param location
*/
function stringifyURL(stringifyQuery, location) {
let query = location.query ? stringifyQuery(location.query) : '';
return location.path + (query && '?') + query + (location.hash || '');
}
/**
* Strips off the base from the beginning of a location.pathname
* @param pathname - location.pathname
* @param base - base to strip off
*/
function stripBase(pathname, base) {
if (!base || pathname.indexOf(base) !== 0)
return pathname;
return pathname.replace(base, '') || '/';
}
function normalizeHistoryLocation(location) {
return {
// to avoid doing a typeof or in that is quite long
fullPath: location.fullPath || location,
};
}
// import { RouteLocationNormalized } from '../types'
function computeScrollPosition(el) {
return el
? {
x: el.scrollLeft,
y: el.scrollTop,
}
: {
x: window.pageXOffset,
y: window.pageYOffset,
};
}
function getElementPosition(el, offset) {
const docEl = document.documentElement;
const docRect = docEl.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
return {
x: elRect.left - docRect.left - offset.x,
y: elRect.top - docRect.top - offset.y,
};
}
const hashStartsWithNumberRE = /^#\d/;
function scrollToPosition(position) {
let normalizedPosition = null;
if ('selector' in position) {
// getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
// but at the same time, it doesn't make much sense to select an element with an id and an extra selector
const el = hashStartsWithNumberRE.test(position.selector)
? document.getElementById(position.selector.slice(1))
: document.querySelector(position.selector);
if (el) {
const offset = position.offset || { x: 0, y: 0 };
normalizedPosition = getElementPosition(el, offset);
}
// TODO: else dev warning?
}
else {
normalizedPosition = {
x: position.x,
y: position.y,
};
}
if (normalizedPosition) {
window.scrollTo(normalizedPosition.x, normalizedPosition.y);
}
}
/**
* Creates a normalized history location from a window.location object
* @param location
*/
function createCurrentLocation(base, location) {
const { pathname, search, hash } = location;
// allows hash based url
const hashPos = base.indexOf('#');
if (hashPos > -1) {
// prepend the starting slash to hash so the url starts with /#
let pathFromHash = hash.slice(1);
if (pathFromHash.charAt(0) !== '/')
pathFromHash = '/' + pathFromHash;
return normalizeHistoryLocation(stripBase(pathFromHash, ''));
}
const path = stripBase(pathname, base);
return normalizeHistoryLocation(path + search + hash);
}
function useHistoryListeners(base, historyState, location, replace) {
let listeners = [];
let teardowns = [];
// TODO: should it be a stack? a Dict. Check if the popstate listener
// can trigger twice
let pauseState = null;
const popStateHandler = ({ state, }) => {
const to = createCurrentLocation(base, window.location);
if (!state)
return replace(to.fullPath);
const from = location.value;
const fromState = historyState.value;
location.value = to;
historyState.value = state;
// ignore the popstate and reset the pauseState
if (pauseState && pauseState.fullPath === from.fullPath) {
pauseState = null;
return;
}
const deltaFromCurrent = fromState
? state.position - fromState.position
: '';
const distance = deltaFromCurrent || 0;
// console.log({ deltaFromCurrent })
// Here we could also revert the navigation by calling history.go(-distance)
// this listener will have to be adapted to not trigger again and to wait for the url
// to be updated before triggering the listeners. Some kind of validation function would also
// need to be passed to the listeners so the navigation can be accepted
// call all listeners
listeners.forEach(listener => {
listener(location.value, from, {
distance,
type: NavigationType.pop,
direction: distance
? distance > 0
? NavigationDirection.forward
: NavigationDirection.back
: NavigationDirection.unknown,
});
});
};
function pauseListeners() {
pauseState = location.value;
}
function listen(callback) {
// setup the listener and prepare teardown callbacks
listeners.push(callback);
const teardown = () => {
const index = listeners.indexOf(callback);
if (index > -1)
listeners.splice(index, 1);
};
teardowns.push(teardown);
return teardown;
}
function beforeUnloadListener() {
const { history } = window;
if (!history.state)
return;
history.replaceState({
...history.state,
scroll: computeScrollPosition(),
}, '');
}
function destroy() {
for (const teardown of teardowns)
teardown();
teardowns = [];
window.removeEventListener('popstate', popStateHandler);
window.removeEventListener('beforeunload', beforeUnloadListener);
}
// setup the listeners and prepare teardown callbacks
window.addEventListener('popstate', popStateHandler);
window.addEventListener('beforeunload', beforeUnloadListener);
return {
pauseListeners,
listen,
destroy,
};
}
/**
* Creates a state object
*/
function buildState(back, current, forward, replaced = false, computeScroll = false) {
return {
back,
current,
forward,
replaced,
position: window.history.length,
scroll: computeScroll ? computeScrollPosition() : null,
};
}
function useHistoryStateNavigation(base) {
const { history } = window;
// private variables
let location = {
value: createCurrentLocation(base, window.location),
};
let historyState = { value: history.state };
// build current history entry as this is a fresh navigation
if (!historyState.value) {
changeLocation(location.value, {
back: null,
current: location.value,
forward: null,
// the length is off by one, we need to decrease it
position: history.length - 1,
replaced: true,
scroll: computeScrollPosition(),
}, true);
}
function changeLocation(to, state, replace) {
const url = base + to.fullPath;
try {
// BROWSER QUIRK
// NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
history[replace ? 'replaceState' : 'pushState'](state, '', url);
historyState.value = state;
}
catch (err) {
vue.warn('[vue-router]: Error with push/replace State', err);
// Force the navigation, this also resets the call count
window.location[replace ? 'replace' : 'assign'](url);
}
}
function replace(to, data) {
const normalized = normalizeHistoryLocation(to);
const state = {
...buildState(historyState.value.back,
// keep back and forward entries but override current position
normalized, historyState.value.forward, true),
...history.state,
...data,
position: historyState.value.position,
};
changeLocation(normalized, state, true);
location.value = normalized;
}
function push(to, data) {
const normalized = normalizeHistoryLocation(to);
// Add to current entry the information of where we are going
// as well as saving the current position
// TODO: the scroll position computation should be customizable
const currentState = {
...history.state,
forward: normalized,
scroll: computeScrollPosition(),
};
changeLocation(currentState.current, currentState, true);
const state = {
...buildState(location.value, normalized, null),
position: currentState.position + 1,
...data,
};
changeLocation(normalized, state, false);
location.value = normalized;
}
return {
location,
state: historyState,
push,
replace,
};
}
function createWebHistory(base = '') {
const historyNavigation = useHistoryStateNavigation(base);
const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
function back(triggerListeners = true) {
go(-1, triggerListeners);
}
function forward(triggerListeners = true) {
go(1, triggerListeners);
}
function go(distance, triggerListeners = true) {
if (!triggerListeners)
historyListeners.pauseListeners();
history.go(distance);
}
const routerHistory = {
// it's overriden right after
// @ts-ignore
location: historyNavigation.location.value,
base,
back,
forward,
go,
...historyNavigation,
...historyListeners,
};
Object.defineProperty(routerHistory, 'location', {
get: () => historyNavigation.location.value,
});
Object.defineProperty(routerHistory, 'state', {
get: () => historyNavigation.state.value,
});
return routerHistory;
}
/**
* Creates a in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere.
* It's up to the user to replace that location with the starter location.
* @param base - Base applied to all urls, defaults to '/'
* @returns a history object that can be passed to the router constructor
*/
function createMemoryHistory(base = '') {
let listeners = [];
// TODO: make sure this is right as the first location is nowhere so maybe this should be empty instead
let queue = [START];
let position = 0;
function setLocation(location) {
position++;
if (position === queue.length) {
// we are at the end, we can simply append a new entry
queue.push(location);
}
else {
// we are in the middle, we remove everything from here in the queue
queue.splice(position);
queue.push(location);
}
}
function triggerListeners(to, from, { direction, distance, }) {
const info = {
direction,
distance,
type: NavigationType.pop,
};
for (let callback of listeners) {
callback(to, from, info);
}
}
const routerHistory = {
// rewritten by Object.defineProperty
location: START,
// TODO:
state: {},
base,
replace(to) {
const toNormalized = normalizeHistoryLocation(to);
// remove current entry and decrement position
queue.splice(position--, 1);
setLocation(toNormalized);
},
push(to, data) {
setLocation(normalizeHistoryLocation(to));
},
listen(callback) {
listeners.push(callback);
return () => {
const index = listeners.indexOf(callback);
if (index > -1)
listeners.splice(index, 1);
};
},
destroy() {
listeners = [];
},
back(shouldTrigger = true) {
this.go(-1, shouldTrigger);
},
forward(shouldTrigger = true) {
this.go(1, shouldTrigger);
},
go(distance, shouldTrigger = true) {
const from = this.location;
const direction =
// we are considering distance === 0 going forward, but in abstract mode
// using 0 for the distance doesn't make sense like it does in html5 where
// it reloads the page
distance < 0 ? NavigationDirection.back : NavigationDirection.forward;
position = Math.max(0, Math.min(position + distance, queue.length - 1));
if (shouldTrigger) {
triggerListeners(this.location, from, {
direction,
distance,
});
}
},
};
Object.defineProperty(routerHistory, 'location', {
get: () => queue[position],
});
return routerHistory;
}
function createWebHashHistory(base = '') {
// Make sure this implementation is fine in terms of encoding, specially for IE11
return createWebHistory(base + '/#');
}
const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';
const PolySymbol = (name) =>
// vr = vue router
hasSymbol ? Symbol(name) : `_vr_` + name;
// rvlm = Router View Location Matched
const matchedRouteKey = PolySymbol('rvlm');
// rvd = Router View Depth
const viewDepthKey = PolySymbol('rvd');
// r = router
const routerKey = PolySymbol('r');
// rt = route location
const routeLocationKey = PolySymbol('rl');
/**
* Encoding Rules
* ␣ = Space
* Path: ␣ " < > # ? { }
* Query: ␣ " < > # & =
* Hash: ␣ " < > `
*
* On top of that the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2) defines some extra characters to be encoded. Most browsers do not encode them in encodeURI https://github.com/whatwg/url/issues/369 so it may be safer to also encode !'()*. Leaving unencoded only ASCII alphanum plus -._~
* This extra safety should be applied to query by patching the string returned by encodeURIComponent
* encodeURI also encodes [\]^
* \ should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a \ into a / if directly typed in
* ` should also be encoded everywhere because some browsers like FF encode it when directly written while others don't
* Safari and IE don't encode "<>{}` in hash
*/
// const EXTRA_RESERVED_RE = /[!'()*]/g
// const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
const HASH_RE = /#/g; // %23
const AMPERSAND_RE = /&/g; // %26
const SLASH_RE = /\//g; // %2F
const EQUAL_RE = /=/g; // %3D
const IM_RE = /\?/g; // %3F
const ENC_BRACKET_OPEN_RE = /%5B/g; // [
const ENC_BRACKET_CLOSE_RE = /%5D/g; // ]
const ENC_CARET_RE = /%5E/g; // ^
const ENC_BACKTICK_RE = /%60/g; // `
const ENC_CURLY_OPEN_RE = /%7B/g; // {
const ENC_PIPE_RE = /%7C/g; // |
const ENC_CURLY_CLOSE_RE = /%7D/g; // }
function commonEncode(text) {
return encodeURI('' + text)
.replace(ENC_PIPE_RE, '|')
.replace(ENC_BRACKET_OPEN_RE, '[')
.replace(ENC_BRACKET_CLOSE_RE, ']');
}
function encodeQueryProperty(text) {
return commonEncode(text)
.replace(HASH_RE, '%23')
.replace(AMPERSAND_RE, '%26')
.replace(EQUAL_RE, '%3D')
.replace(ENC_BACKTICK_RE, '`')
.replace(ENC_CURLY_OPEN_RE, '{')
.replace(ENC_CURLY_CLOSE_RE, '}')
.replace(ENC_CARET_RE, '^');
}
function encodePath(text) {
return commonEncode(text)
.replace(HASH_RE, '%23')
.replace(IM_RE, '%3F');
}
function encodeParam(text) {
return encodePath(text).replace(SLASH_RE, '%2F');
}
function decode(text) {
try {
return decodeURIComponent(text);
}
catch (err) {
}
return text;
}
/**
* Transforms a queryString into a {@link LocationQuery} object. Accept both, a
* version with the leading `?` and without Should work as URLSearchParams
*
* @param search - search string to parse
* @returns a query object
*/
function parseQuery(search) {
const query = {};
// avoid creating an object with an empty key and empty value
// because of split('&')
if (search === '' || search === '?')
return query;
const hasLeadingIM = search[0] === '?';
const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&');
for (let i = 0; i < searchParams.length; ++i) {
let [key, rawValue] = searchParams[i].split('=');
key = decode(key);
// avoid decoding null
let value = rawValue == null ? null : decode(rawValue);
if (key in query) {
// an extra variable for ts types
let currentValue = query[key];
if (!Array.isArray(currentValue)) {
currentValue = query[key] = [currentValue];
}
currentValue.push(value);
}
else {
query[key] = value;
}
}
return query;
}
/**
* Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it
* doesn't prepend a `?`
*
* @param query - query object to stringify
* @returns string verion of the query without the leading `?`
*/
function stringifyQuery(query) {
let search = '';
for (let key in query) {
if (search.length)
search += '&';
const value = query[key];
key = encodeQueryProperty(key);
if (value == null) {
// only null adds the value
if (value !== undefined)
search += key;
continue;
}
// keep null values
let values = Array.isArray(value)
? value.map(v => v && encodeQueryProperty(v))
: [value && encodeQueryProperty(value)];
for (let i = 0; i < values.length; i++) {
// only append & with i > 0
search += (i ? '&' : '') + key;
if (values[i] != null)
search += ('=' + values[i]);
}
}
return search;
}
/**
* Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting
* numbers into strings, removing keys with an undefined value and replacing
* undefined with null in arrays
*
* @param query - query object to normalize
* @returns a normalized query object
*/
function normalizeQuery(query) {
const normalizedQuery = {};
for (let key in query) {
let value = query[key];
if (value !== undefined) {
normalizedQuery[key] = Array.isArray(value)
? value.map(v => (v == null ? null : '' + v))
: value == null
? value
: '' + value;
}
}
return normalizedQuery;
}
function isRouteLocation(route) {
return typeof route === 'string' || (route && typeof route === 'object');
}
const START_LOCATION_NORMALIZED = vue.markNonReactive({
path: '/',
name: undefined,
params: {},
query: {},
hash: '',
fullPath: '/',
matched: [],
meta: {},
redirectedFrom: undefined,
});
// DEV only debug messages
const ErrorTypeMessages = {
[0 /* MATCHER_NOT_FOUND */]({ location, currentLocation }) {
return `No match for\n ${JSON.stringify(location)}${currentLocation
? '\nwhile being at\n' + JSON.stringify(currentLocation)
: ''}`;
},
[1 /* NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard`;
},
[2 /* NAVIGATION_ABORTED */]({ from, to }) {
return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard`;
},
[3 /* NAVIGATION_CANCELLED */]({ from, to }) {
return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new \`push\` or \`replace\``;
},
};
// Public errors, TBD
// export type PublicRouterError = NavigationError
function createRouterError(type, params) {
{
return Object.assign(new Error(ErrorTypeMessages[type](params)), { type }, params);
}
}
const propertiesToLog = ['params', 'query', 'hash'];
function stringifyRoute(to) {
if (typeof to === 'string')
return to;
if ('path' in to)
return to.path;
const location = {};
for (const key of propertiesToLog) {
if (key in to)
location[key] = to[key];
}
return JSON.stringify(location, null, 2);
}
// default pattern for a param: non greedy everything but /
const BASE_PARAM_PATTERN = '[^/]+?';
const BASE_PATH_PARSER_OPTIONS = {
sensitive: false,
strict: false,
start: true,
end: true,
};
// Special Regex characters that must be escaped in static tokens
const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
/**
* Creates a path parser from an array of Segments (a segment is an array of Tokens)
*
* @param segments - array of segments returned by tokenizePath
* @param extraOptions - optional options for the regexp
* @returns a PathParser
*/
function tokensToParser(segments, extraOptions) {
const options = {
...BASE_PATH_PARSER_OPTIONS,
...extraOptions,
};
// the amount of scores is the same as the length of segments except for the root segment "/"
let score = [];
// the regexp as a string
let pattern = options.start ? '^' : '';
// extracted keys
const keys = [];
for (const segment of segments) {
// the root segment needs special treatment
const segmentScores = segment.length ? [] : [90 /* Root */];
// allow trailing slash
if (options.strict && !segment.length)
pattern += '/';
for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
const token = segment[tokenIndex];
// resets the score if we are inside a sub segment /:a-other-:b
let subSegmentScore = 40 /* Segment */ +
(options.sensitive ? 0.25 /* BonusCaseSensitive */ : 0);
if (token.type === 0 /* Static */) {
// prepend the slash if we are starting a new segment
if (!tokenIndex)
pattern += '/';
pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
subSegmentScore += 40 /* Static */;
}
else if (token.type === 1 /* Param */) {
const { value, repeatable, optional, regexp } = token;
keys.push({
name: value,
repeatable,
optional,
});
const re = regexp ? regexp : BASE_PARAM_PATTERN;
// the user provided a custom regexp /:id(\\d+)
if (re !== BASE_PARAM_PATTERN) {
subSegmentScore += 10 /* BonusCustomRegExp */;
// make sure the regexp is valid before using it
try {
new RegExp(`(${re})`);
}
catch (err) {
throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
err.message);
}
}
// when we repeat we must take care of the repeating leading slash
let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
// prepend the slash if we are starting a new segment
if (!tokenIndex)
subPattern = optional ? `(?:/${subPattern})` : '/' + subPattern;
if (optional)
subPattern += '?';
pattern += subPattern;
subSegmentScore += 20 /* Dynamic */;
if (optional)
subSegmentScore += -8 /* BonusOptional */;
if (repeatable)
subSegmentScore += -20 /* BonusRepeatable */;
if (re === '.*')
subSegmentScore += -50 /* BonusWildcard */;
}
segmentScores.push(subSegmentScore);
}
// an empty array like /home/ -> [[{home}], []]
// if (!segment.length) pattern += '/'
score.push(segmentScores);
}
// only apply the strict bonus to the last score
if (options.strict && options.end) {
const i = score.length - 1;
score[i][score[i].length - 1] += 0.7000000000000001 /* BonusStrict */;
}
// TODO: dev only warn double trailing slash
if (!options.strict)
pattern += '/?';
if (options.end)
pattern += '$';
// allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_somethingelse
else if (options.strict)
pattern += '(?:/|$)';
const re = new RegExp(pattern, options.sensitive ? '' : 'i');
function parse(path) {
const match = path.match(re);
const params = {};
if (!match)
return null;
for (let i = 1; i < match.length; i++) {
const value = match[i] || '';
const key = keys[i - 1];
params[key.name] = value && key.repeatable ? value.split('/') : value;
}
return params;
}
function stringify(params) {
let path = '';
// for optional parameters to allow to be empty
let avoidDuplicatedSlash = false;
for (const segment of segments) {
if (!avoidDuplicatedSlash || path[path.length - 1] !== '/')
path += '/';
avoidDuplicatedSlash = false;
for (const token of segment) {
if (token.type === 0 /* Static */) {
path += token.value;
}
else if (token.type === 1 /* Param */) {
const { value, repeatable, optional } = token;
const param = value in params ? params[value] : '';
if (Array.isArray(param) && !repeatable)
throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
const text = Array.isArray(param) ? param.join('/') : param;
if (!text) {
// do not append a slash on the next iteration
if (optional)
avoidDuplicatedSlash = true;
else
throw new Error(`Missing required param "${value}"`);
}
path += text;
}
}
}
return path;
}
return {
re,
score,
keys,
parse,
stringify,
};
}
/**
* Compares an array of numbers as used in PathParser.score and returns a
* number. This function can be used to `sort` an array
* @param a - first array of numbers
* @param b - second array of numbers
* @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
* should be sorted first
*/
function compareScoreArray(a, b) {
let i = 0;
while (i < a.length && i < b.length) {
const diff = b[i] - a[i];
// only keep going if diff === 0
if (diff)
return diff;
i++;
}
// if the last subsegment was Static, the shorter segments should be sorted first
// otherwise sort the longest segment first
if (a.length < b.length) {
return a.length === 1 && a[0] === 40 /* Static */ + 40 /* Segment */
? -1
: 1;
}
else if (a.length > b.length) {
return b.length === 1 && b[0] === 40 /* Static */ + 40 /* Segment */
? 1
: -1;
}
return 0;
}
/**
* Compare function that can be used with `sort` to sort an array of PathParser
* @param a - first PathParser
* @param b - second PathParser
* @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
*/
function comparePathParserScore(a, b) {
let i = 0;
const aScore = a.score;
const bScore = b.score;
while (i < aScore.length && i < bScore.length) {
const comp = compareScoreArray(aScore[i], bScore[i]);
// do not return if both are equal
if (comp)
return comp;
i++;
}
// if a and b share the same score entries but b has more, sort b first
return bScore.length - aScore.length;
// this is the ternary version
// return aScore.length < bScore.length
// ? 1
// : aScore.length > bScore.length
// ? -1
// : 0
}
const ROOT_TOKEN = {
type: 0 /* Static */,
value: '',
};
const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
function tokenizePath(path) {
if (!path)
return [[]];
if (path === '/')
return [[ROOT_TOKEN]];
// remove the leading slash
if (path[0] !== '/')
throw new Error('A non-empty path must start with "/"');
function crash(message) {
throw new Error(`ERR (${state})/"${buffer}": ${message}`);
}
let state = 0 /* Static */;
let previousState = state;
const tokens = [];
// the segment will always be valid because we get into the initial state
// with the leading /
let segment;
function finalizeSegment() {
if (segment)
tokens.push(segment);
segment = [];
}
// index on the path
let i = 0;
// char at index
let char;
// buffer of the value read
let buffer = '';
// custom regexp for a param
let customRe = '';
function consumeBuffer() {
if (!buffer)
return;
if (state === 0 /* Static */) {
segment.push({
type: 0 /* Static */,
value: buffer,
});
}
else if (state === 1 /* Param */ ||
state === 2 /* ParamRegExp */ ||
state === 3 /* ParamRegExpEnd */) {
if (segment.length > 1 && (char === '*' || char === '+'))
crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`);
segment.push({
type: 1 /* Param */,
value: buffer,
regexp: customRe,
repeatable: char === '*' || char === '+',
optional: char === '*' || char === '?',
});
}
else {
crash('Invalid state to consume buffer');
}
buffer = '';
}
function addCharToBuffer() {
buffer += char;
}
while (i < path.length) {
char = path[i++];
if (char === '\\' && state !== 2 /* ParamRegExp */) {
previousState = state;
state = 4 /* EscapeNext */;
continue;
}
switch (state) {
case 0 /* Static */:
if (char === '/') {
if (buffer) {
consumeBuffer();
}
finalizeSegment();
}
else if (char === ':') {
consumeBuffer();
state = 1 /* Param */;
// } else if (char === '{') {
// TODO: handle group (or drop it)
// addCharToBuffer()
}
else {
addCharToBuffer();
}
break;
case 4 /* EscapeNext */:
addCharToBuffer();
state = previousState;
break;
case 1 /* Param */:
if (char === '(') {
state = 2 /* ParamRegExp */;
customRe = '';
}
else if (VALID_PARAM_RE.test(char)) {
addCharToBuffer();
}
else {
consumeBuffer();
state = 0 /* Static */;
// go back one character if we were not modifying
if (char !== '*' && char !== '?' && char !== '+')
i--;
}
break;
case 2 /* ParamRegExp */:
if (char === ')') {
// handle the escaped )
if (customRe[customRe.length - 1] == '\\')
customRe = customRe.slice(0, -1) + char;
else
state = 3 /* ParamRegExpEnd */;
}
else {
customRe += char;
}
break;
case 3 /* ParamRegExpEnd */:
// same as finalizing a param
consumeBuffer();
state = 0 /* Static */;
// go back one character if we were not modifying
if (char !== '*' && char !== '?' && char !== '+')
i--;
break;
default:
crash('Unknown state');
break;
}
}
if (state === 2 /* ParamRegExp */)
crash(`Unfinished custom RegExp for param "${buffer}"`);
consumeBuffer();
finalizeSegment();
return tokens;
}
function createRouteRecordMatcher(record, parent, options) {
const parser = tokensToParser(tokenizePath(record.path), options);
const matcher = {
...parser,
record,
parent,
// these needs to be populated by the parent
children: [],
alias: [],
};
if (parent) {
// both are aliases or both are not aliases
// we don't want to mix them because the order is used when
// passing originalRecord in Matcher.addRoute
if (!matcher.record.aliasOf === !parent.record.aliasOf)
parent.children.push(matcher);
// else TODO: save alias children to be able to remove them
}
return matcher;
}
let noop = () => { };
function createRouterMatcher(routes, globalOptions) {
// normalized ordered array of matchers
const matchers = [];
const matcherMap = new Map();
function getRecordMatcher(name) {
return matcherMap.get(name);
}
// TODO: add routes to children of parent
function addRoute(record, parent, originalRecord) {
let mainNormalizedRecord = normalizeRouteRecord(record);
// we might be the child of an alias
mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
const options = { ...globalOptions, ...record.options };
// generate an array of records to correctly handle aliases
const normalizedRecords = [mainNormalizedRecord];
if ('alias' in record) {
const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
for (const alias of aliases) {
normalizedRecords.push({
...mainNormalizedRecord,
// this allows us to hold a copy of the `components` option
// so that async components cache is hold on the original record
components: originalRecord
? originalRecord.record.components
: mainNormalizedRecord.components,
path: alias,
// we might be the child of an alias
aliasOf: originalRecord
? originalRecord.record
: mainNormalizedRecord,
});
}
}
let matcher;
let originalMatcher;
for (const normalizedRecord of normalizedRecords) {
let { path } = normalizedRecord;
// Build up the path for nested routes if the child isn't an absolute
// route. Only add the / delimiter if the child path isn't empty and if the
// parent path doesn't have a trailing slash
if (parent && path[0] !== '/') {
let parentPath = parent.record.path;
let connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
normalizedRecord.path =
parent.record.path + (path && connectingSlash + path);
}
// create the object before hand so it can be passed to children
matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
// if we are an alias we must tell the original record that we exist
// so we can be removed
if (originalRecord) {
originalRecord.alias.push(matcher);
}
else {
// otherwise, the first record is the original and others are aliases
originalMatcher = originalMatcher || matcher;
if (originalMatcher !== matcher)
originalMatcher.alias.push(matcher);
}
let children = mainNormalizedRecord.children;
for (let i = 0; i < children.length; i++) {
addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
}
// if there was no original record, then the first one was not an alias and all
// other alias (if any) need to reference this record when adding children
originalRecord = originalRecord || matcher;
insertMatcher(matcher);
}
return originalMatcher
? () => {
// since other matchers are aliases, they should be removed by the original matcher
removeRoute(originalMatcher);
}
: noop;
}
function removeRoute(matcherRef) {
if (typeof matcherRef === 'string') {
const matcher = matcherMap.get(matcherRef);
if (matcher) {
matcherMap.delete(matcherRef);
matchers.splice(matchers.indexOf(matcher), 1);
matcher.children.forEach(removeRoute);
matcher.alias.forEach(removeRoute);
}
}
else {
let index = matchers.indexOf(matcherRef);
if (index > -1) {
matchers.splice(index, 1);
if (matcherRef.record.name)
matcherMap.delete(matcherRef.record.name);
matcherRef.children.forEach(removeRoute);
matcherRef.alias.forEach(removeRoute);
}
}
}
function getRoutes() {
return matchers;
}
function insertMatcher(matcher) {
let i = 0;
// console.log('i is', { i })
while (i < matchers.length &&
comparePathParserScore(matcher, matchers[i]) >= 0)
i++;
// console.log('END i is', { i })
// while (i < matchers.length && matcher.score <= matchers[i].score) i++
matchers.splice(i, 0, matcher);
// only add the original record to the name map
if (matcher.record.name && !isAliasRecord(matcher))
matcherMap.set(matcher.record.name, matcher);
}
/**
* Resolves a location. Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
* @param location - MatcherLocation to resolve to a url
* @param currentLocation - MatcherLocationNormalized of the current location
*/
function resolve(location, currentLocation) {
let matcher;
let params = {};
let path;
let name;
if ('name' in location && location.name) {
matcher = matcherMap.get(location.name);
if (!matcher)
throw createRouterError(0 /* MATCHER_NOT_FOUND */, {
location,
});
name = matcher.record.name;
// TODO: merge params with current location. Should this be done by name. I think there should be some kind of relationship between the records like children of a parent should keep parent props but not the rest
// needs an RFC if breaking change
params = location.params || currentLocation.params;
// throws if cannot be stringified
path = matcher.stringify(params);
}
else if ('path' in location) {
matcher = matchers.find(m => m.re.test(location.path));
// matcher should have a value after the loop
// no need to resolve the path with the matcher as it was provided
// this also allows the user to control the encoding
path = location.path;
if (matcher) {
// TODO: dev warning of unused params if provided
params = matcher.parse(location.path);
name = matcher.record.name;
}
// location is a relative path
}
else {
// match by name or path of current route
matcher = currentLocation.name
? matcherMap.get(currentLocation.name)
: matchers.find(m => m.re.test(currentLocation.path));
if (!matcher)
throw createRouterError(0 /* MATCHER_NOT_FOUND */, {
location,
currentLocation,
});
name = matcher.record.name;
params = location.params || currentLocation.params;
path = matcher.stringify(params);
}
const matched = [];
let parentMatcher = matcher;
while (parentMatcher) {
// reversed order so parents are at the beginning
// const { record } = parentMatcher
// TODO: check resolving child routes by path when parent has an alias
matched.unshift(parentMatcher.record);
parentMatcher = parentMatcher.parent;
}
return {
name,
path,
params,
matched,
meta: matcher ? matcher.record.meta : {},
};
}
// add initial routes
routes.forEach(route => addRoute(route));
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
}
/**
* Normalizes a RouteRecord. Transforms the `redirect` option into a `beforeEnter`
* @param record
* @returns the normalized version
*/
function normalizeRouteRecord(record) {
let components;
let beforeEnter;
if ('redirect' in record) {
components = {};
let { redirect } = record;
beforeEnter = (to, from, next) => {
next(typeof redirect === 'function' ? redirect(to) : redirect);
};
}
else {
components =
'components' in record ? record.components : { default: record.component };
beforeEnter = record.beforeEnter;
}
return {
path: record.path,
components,
// record is an object and if it has a children property, it's an array
children: record.children || [],
name: record.name,
beforeEnter,
props: record.props || false,
meta: record.meta || {},
leaveGuards: [],
instances: {},
aliasOf: undefined,
};
}
/**
* Checks if a record or any of its parent is an alias
* @param record
*/
function isAliasRecord(record) {
while (record) {
if (record.record.aliasOf)
return true;
record = record.parent;
}
return false;
}
function guardToPromiseFn(guard, to, from, instance) {
return () => new Promise((resolve, reject) => {
const next = (valid) => {
if (valid === false)
reject(createRouterError(2 /* NAVIGATION_ABORTED */, {
from,
to,
}));
else if (isRouteLocation(valid)) {
reject(createRouterError(1 /* NAVIGATION_GUARD_REDIRECT */, {
from: to,
to: valid,
}));
}
else if (!valid || valid === true) {
resolve();
}
else {
// TODO: call the in component enter callbacks. Maybe somewhere else
// record && record.enterCallbacks.push(valid)
resolve();
}
};
guard.call(instance, to, from, next);
});
}
function isESModule(obj) {
return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module');
}
function extractComponentsGuards(matched, guardType, to, from) {
const guards = [];
for (const record of matched) {
for (const name in record.components) {
const rawComponent = record.components[name];
if (typeof rawComponent === 'function') {
// start requesting the chunk already
const componentPromise = rawComponent().catch(() => null);
guards.push(async () => {
const resolved = await componentPromise;
if (!resolved)
throw new Error('TODO: error while fetching');
const resolvedComponent = isESModule(resolved)
? resolved.default
: resolved;
// replace the function with the resolved component
record.components[name] = resolvedComponent;
const guard = resolvedComponent[guardType];
return (
// @ts-ignore: the guard matcheds the instance type
guard && guardToPromiseFn(guard, to, from, record.instances[name])());
});
}
else {
const guard = rawComponent[guardType];
guard &&
// @ts-ignore: the guard matcheds the instance type
guards.push(guardToPromiseFn(guard, to, from, record.instances[name]));
}
}
}
return guards;
}
function applyToParams(fn, params) {
const newParams = {};
for (const key in params) {
const value = params[key];
newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value);
}
return newParams;
}
function isSameRouteRecord(a, b) {
// since the original record has an undefined value for aliasOf
// but all aliases point to the original record, this will always compare
// the original record
return (a.aliasOf || a) === (b.aliasOf || b);
}
function isSameLocationObject(a, b) {
const aKeys = Object.keys(a);
const bKeys = Object.keys(b);
if (aKeys.length !== bKeys.length)
return false;
let i = 0;
let key;
while (i < aKeys.length) {
key = aKeys[i];
if (key !== bKeys[i])
return false;
if (!isSameLocationObjectValue(a[key], b[key]))
return false;
i++;
}
return true;
}
function isSameLocationObjectValue(a, b) {
if (typeof a !== typeof b)
return false;
// both a and b are arrays
if (Array.isArray(a))
return (a.length === b.length &&