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;
|
84348047 |
let findWildcard = R.compose(
R.findIndex.bind(R, isWildcard),
pathSplit
);
|
30db14b1 |
|
84348047 |
let result = paramLength1 > paramLength2 ? match2 : match1;
|
28d2ab95 |
if (paramLength1 === paramLength2) {
|
10f5ef83 |
let path1WildcardIdx = findWildcard(match1.path);
let path2WildcardIdx = findWildcard(match2.path);
|
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;
|
84348047 |
const buildMatch = (extractedParams, route) =>
Object.assign({ extractedParams }, route);
|
30db14b1 |
|
84348047 |
return R.toPairs(matchers).reduce(
(match, [path, { type: matcherType, route }]) => {
const pathMatcher = route.routeMatcher;
const matchedParams = pathMatcher(inputPath);
|
30db14b1 |
|
84348047 |
if (matchedParams) {
if (matcherType === "exact") {
return buildMatch(matchedParams, route);
} else {
return mostSpecificRouteMatch(
match,
buildMatch(matchedParams, route)
);
}
|
27fb05ae |
} else {
|
84348047 |
return match;
|
27fb05ae |
}
|
84348047 |
},
null
);
|
30db14b1 |
}
function mostSpecificActionMatch(match1, match2) {
if (!match1) {
return match2;
}
|
84348047 |
let countExtraParams = ({ extraParams: obj }) => Object.keys(obj).length;
|
41e430d7 |
return countExtraParams(match1) >= countExtraParams(match2) ? match1 : match2;
|
30db14b1 |
}
// matchers is {action : [routeMatcher]} structure
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)) {
|
84348047 |
match = Object.assign({ extractedParams: {} }, route);
|
30db14b1 |
// case 3
break; // most specific
} else if (matcherType === "wildcard") {
// case 1+2
|
84348047 |
const unallocatedArgKeys = R.difference(
Object.keys(args),
Object.keys(route.extraParams)
);
|
30db14b1 |
// if all keys ^ are equal to all keys in route
|
84348047 |
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);
|
84348047 |
match = mostSpecificActionMatch(
match,
Object.assign({ 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("/");
let params = [];
|
41e430d7 |
for (const part of pathParts.filter(isWildcard)) {
const name = part.slice(1);
|
30db14b1 |
|
41e430d7 |
if (params.indexOf(name) !== -1) {
throw new Error("duplicate param");
|
30db14b1 |
}
|
41e430d7 |
params.push(name);
|
30db14b1 |
}
return params;
}
function normalizePathParts(path) {
|
84348047 |
const rawPathParts = R.split("/", path);
|
30db14b1 |
const normalizedPathParts = R.filter(p => p !== "", rawPathParts);
return normalizedPathParts;
}
function makeRoute(path, action, extraParams) {
let type = "exact";
if (path.indexOf(":") !== -1) {
type = "wildcard";
}
const normalizedPathParts = normalizePathParts(path);
|
41e430d7 |
const updateWildcard = (wildcards, match, input) => {
|
84348047 |
const wildcardName = match.replace(":", "");
return Object.assign(wildcards, { [wildcardName]: input });
|
27fb05ae |
};
|
41e430d7 |
|
84348047 |
const routeMatcher = function(inputPath) {
|
41e430d7 |
let result = null;
|
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) {
const f = (acc, [match, input]) => {
if (acc === null) {
return null;
}
|
84348047 |
if (match === input) {
|
27fb05ae |
return acc;
|
41e430d7 |
} else if (match[0] === ":") {
return updateWildcard(acc, match, input);
} else {
return null;
}
};
|
27fb05ae |
result = R.zip(normMatchPath, normInputPath).reduce(f, {});
|
30db14b1 |
}
|
41e430d7 |
return result;
|
30db14b1 |
};
let routeParams = extractParams(path);
return {
type,
route: {
routeMatcher,
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);
for (const otherPath of Object.keys(compiledRouteMatchers)) {
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) {
let compiledActionMatchers = {};
let compiledRouteMatchers = {};
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) {
|
84348047 |
let parts = match.path.split("/");
|
30db14b1 |
let resultParts = [];
for (let part of parts) {
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) {
|
84348047 |
let { compiledActionMatchers, compiledRouteMatchers } = compileRoutes(
routesConfig
);
|
30db14b1 |
|
478c0afb |
function pathForAction(action) {
const match = matchAction(action, compiledActionMatchers);
return match ? constructPath(match) : null;
}
|
30db14b1 |
let actionDispatcher = {
|
de8a1429 |
currentLocation: null,
|
30db14b1 |
store: null,
activateDispatcher(store) {
|
84348047 |
window.addEventListener("urlchanged", this);
|
30db14b1 |
this.store = store;
},
enhanceStore(nextStoreCreator) {
|
bab394ca |
let middleware = buildMiddleware(this);
|
30db14b1 |
return (reducer, finalInitialState, enhancer) => {
let theStore = nextStoreCreator(reducer, finalInitialState, enhancer);
this.activateDispatcher(theStore);
|
478c0afb |
theStore.pathForAction = pathForAction;
|
84348047 |
theStore.dispatch = middleware(theStore)(
theStore.dispatch.bind(theStore)
);
|
30db14b1 |
return theStore;
|
27fb05ae |
};
|
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) {
|
bab394ca |
let result = undefined;
|
7b1d07fc |
if (this.currentLocation !== newLoc) {
this.currentLocation = newLoc;
|
bab394ca |
result = cb();
|
7b1d07fc |
}
|
bab394ca |
return result;
|
7b1d07fc |
},
|
4b39d368 |
receiveLocation(location) {
|
7b1d07fc |
this.onLocationChanged(location.pathname, () => {
|
de8a1429 |
const match = matchRoute(location, compiledRouteMatchers);
|
84348047 |
if (match) {
|
de8a1429 |
const action = constructAction(match);
|
30db14b1 |
|
de8a1429 |
this.store.dispatch(action);
}
|
7b1d07fc |
});
|
30db14b1 |
},
|
7b1d07fc |
|
30db14b1 |
receiveAction(action) {
let matcher = matchAction(action, compiledActionMatchers);
|
84348047 |
if (matcher) {
|
30db14b1 |
let path = constructPath(matcher);
|
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 => {
console.warn(
|
84348047 |
"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 |
}
|