84348047 |
import * as R from "ramda";
|
30db14b1 |
|
28d2ab95 |
function pathSplit(path) {
|
84348047 |
return path.split("/");
|
28d2ab95 |
}
|
30db14b1 |
function mostSpecificRouteMatch(match1, match2) {
if (!match1) {
return match2;
}
|
28d2ab95 |
const paramLength1 = match1.routeParams.length;
const paramLength2 = match2.routeParams.length;
|
f62307c9 |
|
72e96b4d |
const findWildcard = R.compose(
|
f62307c9 |
R.findIndex(isWildcard),
|
84348047 |
pathSplit
);
|
30db14b1 |
|
84348047 |
let result = paramLength1 > paramLength2 ? match2 : match1;
|
28d2ab95 |
if (paramLength1 === paramLength2) {
|
72e96b4d |
const path1WildcardIdx = findWildcard(match1.path);
const path2WildcardIdx = findWildcard(match2.path);
|
10f5ef83 |
|
84348047 |
result =
path1WildcardIdx !== -1 && path1WildcardIdx < path2WildcardIdx
? match2
: match1;
|
30db14b1 |
}
if (result === null) {
throw new Error("routes should have been disambiguated at compile time");
}
return result;
}
// do something with routes.
function matchRoute(loc, matchers) {
const inputPath = loc.pathname;
|
f62307c9 |
const buildMatch = (extractedParams, route) => ({ extractedParams, ...route });
|
30db14b1 |
|
dae2ec8a |
return R.reduce(
|
7a0c8d3f |
(match, [_, { type: matcherType, route }]) => {
|
dae2ec8a |
const { pathMatcher } = route;
|
84348047 |
const matchedParams = pathMatcher(inputPath);
|
30db14b1 |
|
84348047 |
if (matchedParams) {
|
dae2ec8a |
return matcherType === 'exact'
? buildMatch(matchedParams, route)
: mostSpecificRouteMatch(match, buildMatch(matchedParams, route));
|
27fb05ae |
} else {
|
84348047 |
return match;
|
27fb05ae |
}
|
84348047 |
},
|
dae2ec8a |
null,
R.toPairs(matchers)
|
84348047 |
);
|
30db14b1 |
}
function mostSpecificActionMatch(match1, match2) {
if (!match1) {
return match2;
}
|
72e96b4d |
const countExtraParams = ({ extraParams: obj }) => R.keys(obj).length;
|
41e430d7 |
return countExtraParams(match1) >= countExtraParams(match2) ? match1 : match2;
|
30db14b1 |
}
|
dae2ec8a |
// matchers is {action : [pathMatcher]} structure
|
30db14b1 |
function matchAction(action, matchers) {
// match on params in action vs possible actions if more than 1
let match = null;
|
84348047 |
const { type: actionType, ...args } = action;
|
41e430d7 |
const routes = matchers[actionType];
|
30db14b1 |
// Specificity:
// 1. wildcard(s) / no extra param /route/:id || /route/me
// 2. wildcards / exact extra params match with remaining wildcard
// 3. no-wildcard / exact extra params match
|
84348047 |
for (const { type: matcherType, route } of routes) {
|
30db14b1 |
if (matcherType === "exact" && R.equals(route.extraParams, args)) {
// case 3
|
dae2ec8a |
match = { extractedParams: {}, ...route};
|
30db14b1 |
break; // most specific
} else if (matcherType === "wildcard") {
// case 1+2
|
84348047 |
const unallocatedArgKeys = R.difference(
|
f62307c9 |
R.keys(args),
R.keys(route.extraParams)
|
84348047 |
);
|
30db14b1 |
// if all keys ^ are equal to all keys in route
|
dae2ec8a |
const intersectCount = R.intersection(unallocatedArgKeys, route.routeParams).length;
|
41e430d7 |
const unionCount = R.union(unallocatedArgKeys, route.routeParams).length;
|
30db14b1 |
|
41e430d7 |
if (intersectCount === unionCount) {
|
30db14b1 |
const extractedParams = R.pick(unallocatedArgKeys, args);
|
dae2ec8a |
match = mostSpecificActionMatch(match, { extractedParams, ...route });
|
30db14b1 |
}
}
}
return match;
}
function matchesAction(action, matchers) {
return !!matchers[action.type];
}
|
41e430d7 |
function isWildcard(segment) {
return segment && segment[0] === ":";
}
|
478c0afb |
|
30db14b1 |
function extractParams(path) {
const pathParts = path.split("/");
|
dae2ec8a |
const params = R.compose(
R.map(x => x.substr(1)),
R.filter(isWildcard)
)(pathParts);
|
30db14b1 |
|
dae2ec8a |
if(R.uniq(params).length !== params.length) {
throw new Error("duplicate param");
|
30db14b1 |
}
|
dae2ec8a |
|
30db14b1 |
return params;
}
function normalizePathParts(path) {
|
dae2ec8a |
const splitAndFilterEmpty = R.compose(
R.filter(p => p !== ""),
R.split('/')
|
f62307c9 |
);
return splitAndFilterEmpty(path);
|
30db14b1 |
}
function makeRoute(path, action, extraParams) {
|
f62307c9 |
const type = (R.includes(':', path) ? 'wildcard': 'exact');
|
30db14b1 |
const normalizedPathParts = normalizePathParts(path);
|
dae2ec8a |
const pathMatcher = function(inputPath) {
|
41e430d7 |
let result = null;
|
f62307c9 |
|
30db14b1 |
const normMatchPath = normalizedPathParts;
const normInputPath = normalizePathParts(inputPath);
|
41e430d7 |
// exact match
|
30db14b1 |
if (R.equals(normalizedPathParts, normInputPath)) {
return {};
}
|
41e430d7 |
//wildcard match
|
30db14b1 |
const inputLength = normInputPath.length;
const matchLength = normMatchPath.length;
|
41e430d7 |
if (inputLength === matchLength) {
|
dae2ec8a |
result = R.reduce((extractedValues, [match, input]) => {
if (extractedValues === null) {
|
41e430d7 |
return null;
}
|
84348047 |
if (match === input) {
|
dae2ec8a |
return extractedValues;
} else if (R.startsWith(":", match)) {
const wildcardName = R.replace(':', '', match);
return {...extractedValues, [wildcardName]: input};
|
41e430d7 |
} else {
return null;
}
|
dae2ec8a |
}, {}, R.zip(normMatchPath, normInputPath));
|
30db14b1 |
}
|
41e430d7 |
return result;
|
30db14b1 |
};
let routeParams = extractParams(path);
return {
type,
route: {
|
dae2ec8a |
pathMatcher,
|
30db14b1 |
path,
action,
routeParams,
extraParams
}
|
27fb05ae |
};
|
30db14b1 |
}
|
10f5ef83 |
function normalizeWildcards(path) {
let curIdx = 0;
|
84348047 |
return path.map(el => {
|
10f5ef83 |
if (isWildcard(el)) {
|
27fb05ae |
return `:wildcard${curIdx}`;
|
10f5ef83 |
} else {
return el;
}
});
}
function routeAlreadyExists(compiledRouteMatchers, path) {
|
27fb05ae |
let result = compiledRouteMatchers.hasOwnProperty(path);
|
10f5ef83 |
if (!result) {
|
84348047 |
const normalizingSplit = R.compose(
normalizeWildcards,
pathSplit
);
|
10f5ef83 |
const pathParts = normalizingSplit(path);
|
f62307c9 |
for (const otherPath of R.keys(compiledRouteMatchers)) {
|
10f5ef83 |
const otherPathParts = normalizingSplit(otherPath);
if (R.equals(pathParts, otherPathParts)) {
|
84348047 |
throw new Error(
`invalid routing configuration — route ${path} overlaps with route ${otherPath}`
);
|
10f5ef83 |
}
}
}
return result;
}
|
30db14b1 |
function compileRoutes(routesConfig) {
|
f62307c9 |
const compiledActionMatchers = {};
const compiledRouteMatchers = {};
|
30db14b1 |
for (let [path, action, extraParams] of routesConfig) {
|
84348047 |
if (typeof path !== "string" || typeof action !== "string") {
throw new Error(
"invalid routing configuration - path and action must both be strings"
);
|
30db14b1 |
}
if (!compiledActionMatchers[action]) {
compiledActionMatchers[action] = [];
}
const route = makeRoute(path, action, extraParams);
compiledActionMatchers[action].push(route);
|
10f5ef83 |
if (routeAlreadyExists(compiledRouteMatchers, path)) {
|
30db14b1 |
throw new Error("overlapping paths");
}
compiledRouteMatchers[path] = route;
}
return {
compiledActionMatchers, // { ACTION: [Route] }
|
84348047 |
compiledRouteMatchers // { PATH: Route }
|
27fb05ae |
};
|
30db14b1 |
}
function constructAction(match) {
|
84348047 |
return { type: match.action, ...match.extractedParams, ...match.extraParams };
|
30db14b1 |
}
function constructPath(match) {
|
72e96b4d |
const parts = match.path.split("/");
const resultParts = [];
|
30db14b1 |
|
72e96b4d |
for (const part of parts) {
|
30db14b1 |
if (part[0] === ":") {
const name = part.slice(1);
const val = match.extractedParams.hasOwnProperty(name)
|
84348047 |
? match.extractedParams[name]
: match.extraParams[name];
|
30db14b1 |
resultParts.push(val);
} else {
resultParts.push(part);
}
}
|
84348047 |
return resultParts.join("/");
|
30db14b1 |
}
function createActionDispatcher(routesConfig, window) {
|
72e96b4d |
const { compiledActionMatchers, compiledRouteMatchers } = compileRoutes(
|
84348047 |
routesConfig
);
|
30db14b1 |
|
478c0afb |
function pathForAction(action) {
const match = matchAction(action, compiledActionMatchers);
return match ? constructPath(match) : null;
}
|
f62307c9 |
function actionForLocation(location) {
const match = matchRoute(location, compiledRouteMatchers);
return match ? constructAction(match) : null;
}
|
72e96b4d |
const actionDispatcher = {
|
de8a1429 |
currentLocation: null,
|
30db14b1 |
store: null,
|
f62307c9 |
|
30db14b1 |
activateDispatcher(store) {
|
84348047 |
window.addEventListener("urlchanged", this);
|
30db14b1 |
this.store = store;
},
|
f62307c9 |
|
30db14b1 |
enhanceStore(nextStoreCreator) {
|
72e96b4d |
const middleware = buildMiddleware(this);
|
f62307c9 |
|
30db14b1 |
return (reducer, finalInitialState, enhancer) => {
|
72e96b4d |
const theStore = nextStoreCreator(reducer, finalInitialState, enhancer);
|
f62307c9 |
|
30db14b1 |
this.activateDispatcher(theStore);
|
f62307c9 |
|
478c0afb |
theStore.pathForAction = pathForAction;
|
f62307c9 |
|
84348047 |
theStore.dispatch = middleware(theStore)(
theStore.dispatch.bind(theStore)
);
|
30db14b1 |
return theStore;
|
27fb05ae |
};
|
30db14b1 |
},
|
f62307c9 |
|
30db14b1 |
handleEvent(ev) {
if (!this.store) {
|
84348047 |
throw new Error(
"You must call activateDispatcher with redux store as argument"
);
|
30db14b1 |
}
const location = ev.detail;
|
4b39d368 |
this.receiveLocation(location);
},
|
7b1d07fc |
onLocationChanged(newLoc, cb) {
if (this.currentLocation !== newLoc) {
this.currentLocation = newLoc;
|
f62307c9 |
cb();
|
7b1d07fc |
}
},
|
4b39d368 |
receiveLocation(location) {
|
7b1d07fc |
this.onLocationChanged(location.pathname, () => {
|
30db14b1 |
|
f62307c9 |
const action = actionForLocation(location);
if (action) {
|
de8a1429 |
this.store.dispatch(action);
}
|
7b1d07fc |
});
|
30db14b1 |
},
|
7b1d07fc |
|
30db14b1 |
receiveAction(action) {
|
f62307c9 |
const path = pathForAction(action);
if (path) {
|
7b1d07fc |
this.onLocationChanged(path, () => {
|
84348047 |
window.history.pushState({}, "", path);
|
7b1d07fc |
});
|
30db14b1 |
}
},
|
7b1d07fc |
|
30db14b1 |
handlesAction(action) {
return matchesAction(action, compiledActionMatchers);
}
};
|
84348047 |
actionDispatcher.enhanceStore = actionDispatcher.enhanceStore.bind(
actionDispatcher
);
|
30db14b1 |
return actionDispatcher;
}
function buildMiddleware(actionDispatcher) {
return store => next => action => {
if (actionDispatcher.handlesAction(action)) {
|
27fb05ae |
actionDispatcher.receiveAction(action, store);
|
30db14b1 |
}
return next(action);
};
}
export default function installBrowserRouter(routesConfig, window) {
const actionDispatcher = createActionDispatcher(routesConfig, window);
|
bab394ca |
const middleware = x => {
|
7a0c8d3f |
//eslint-disable-next-line no-console
|
bab394ca |
console.warn(
|
7a0c8d3f |
"Using the routedux middleware directly is deprecated, the enhancer now" +
" applies it automatically and the middleware is now a no-op that" +
" will be removed in later versions."
|
bab394ca |
);
return y => y;
};
|
30db14b1 |
|
84348047 |
return {
middleware,
enhancer: actionDispatcher.enhanceStore,
init: actionDispatcher.receiveLocation.bind(
actionDispatcher,
window.location
)
};
|
30db14b1 |
}
|