@sanity/state-router
Advanced tools
Comparing version 0.0.3 to 0.1.0
@@ -76,3 +76,2 @@ 'use strict'; | ||
value: function render() { | ||
return _react2.default.createElement('a', _extends({}, (0, _omit2.default)(this.props, 'replace'), { onClick: this.handleClick })); | ||
@@ -85,5 +84,2 @@ } | ||
exports.default = Link; | ||
Link.defaultProps = { | ||
@@ -97,2 +93,3 @@ replace: false | ||
__internalRouter: _react.PropTypes.object | ||
}; | ||
}; | ||
exports.default = Link; |
@@ -13,4 +13,2 @@ 'use strict'; | ||
var _ = require('../'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
@@ -24,48 +22,24 @@ | ||
var didWarn = false; | ||
function validateProps(props) { | ||
if (didWarn) { | ||
return; | ||
} | ||
if (props.state && props.location) { | ||
// eslint-disable-next-line no-console | ||
console.error(new Error("[Warning] You passed both state and location to RouterProvider. If you pass 'state' you don't need to pass 'location' and vice versa")); | ||
didWarn = true; | ||
} | ||
} | ||
var RouterProvider = function (_React$Component) { | ||
_inherits(RouterProvider, _React$Component); | ||
function RouterProvider(props) { | ||
function RouterProvider() { | ||
var _ref; | ||
var _temp, _this, _ret; | ||
_classCallCheck(this, RouterProvider); | ||
for (var _len = arguments.length, rest = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { | ||
rest[_key - 1] = arguments[_key]; | ||
for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { | ||
args[_key] = arguments[_key]; | ||
} | ||
var _this = _possibleConstructorReturn(this, (_ref = RouterProvider.__proto__ || Object.getPrototypeOf(RouterProvider)).call.apply(_ref, [this, props].concat(rest))); | ||
_this.navigateUrl = _this.navigateUrl.bind(_this); | ||
_this.navigateState = _this.navigateState.bind(_this); | ||
_this.resolvePathFromState = _this.resolvePathFromState.bind(_this); | ||
validateProps(props); | ||
return _this; | ||
} | ||
_createClass(RouterProvider, [{ | ||
key: 'navigateUrl', | ||
value: function navigateUrl(url) { | ||
return _ret = (_temp = (_this = _possibleConstructorReturn(this, (_ref = RouterProvider.__proto__ || Object.getPrototypeOf(RouterProvider)).call.apply(_ref, [this].concat(args))), _this), _this.navigateUrl = function (url) { | ||
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
var replace = _ref2.replace; | ||
var onNavigate = this.props.onNavigate; | ||
var onNavigate = _this.props.onNavigate; | ||
onNavigate(url, { replace: replace }); | ||
} | ||
}, { | ||
key: 'navigateState', | ||
value: function navigateState(nextState) { | ||
}, _this.navigateState = function (nextState) { | ||
var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; | ||
@@ -75,15 +49,9 @@ | ||
this.navigateUrl(this.resolvePathFromState(nextState), { replace: replace }); | ||
} | ||
}, { | ||
key: 'resolvePathFromState', | ||
value: function resolvePathFromState(state) { | ||
return (0, _.resolvePathFromState)(this.props.router, state); | ||
} | ||
}, { | ||
key: 'componentWillReceiveProps', | ||
value: function componentWillReceiveProps(nextProps) { | ||
validateProps(nextProps); | ||
} | ||
}, { | ||
_this.navigateUrl(_this.resolvePathFromState(nextState), { replace: replace }); | ||
}, _this.resolvePathFromState = function (state) { | ||
return _this.props.router.encode(state); | ||
}, _temp), _possibleConstructorReturn(_this, _ret); | ||
} | ||
_createClass(RouterProvider, [{ | ||
key: 'getChildContext', | ||
@@ -94,3 +62,2 @@ value: function getChildContext() { | ||
var location = _props.location; | ||
var state = _props.state; | ||
@@ -104,3 +71,3 @@ return { | ||
navigate: this.navigateState, | ||
state: state || (0, _.resolveStateFromPath)(router, location.pathname) | ||
state: router.decode(location.pathname) | ||
} | ||
@@ -119,13 +86,12 @@ }; | ||
exports.default = RouterProvider; | ||
RouterProvider.propTypes = { | ||
state: _react.PropTypes.object, | ||
children: _react.PropTypes.node, | ||
onNavigate: _react.PropTypes.func, | ||
router: _react.PropTypes.object, | ||
onNavigate: _react.PropTypes.func, | ||
location: _react.PropTypes.shape({ | ||
pathname: _react.PropTypes.string | ||
}) | ||
}), | ||
children: _react.PropTypes.node | ||
}; | ||
exports.default = RouterProvider; | ||
RouterProvider.childContextTypes = { | ||
@@ -132,0 +98,0 @@ __internalRouter: _react.PropTypes.object, |
@@ -43,3 +43,4 @@ 'use strict'; | ||
resolvePathFromState: function resolvePathFromState(nextState) { | ||
return __internalRouter.resolvePathFromState(getUnscopedState(nextState)); | ||
var empty = Object.keys(nextState).length === 0; | ||
return __internalRouter.resolvePathFromState(empty ? {} : addScope(nextState)); | ||
}, | ||
@@ -50,10 +51,10 @@ navigateUrl: __internalRouter.navigateUrl | ||
navigate: function navigate(nextState, options) { | ||
router.navigate(getUnscopedState(nextState), options); | ||
router.navigate(addScope(nextState), options); | ||
}, | ||
state: router.state[scope] || {} | ||
state: router.state[scope] | ||
} | ||
}; | ||
function getUnscopedState(nextState) { | ||
return Object.assign({}, router.state, _defineProperty({}, scope, nextState)); | ||
function addScope(nextState) { | ||
return nextState && Object.assign({}, router.state, _defineProperty({}, scope, nextState)); | ||
} | ||
@@ -71,12 +72,10 @@ } | ||
exports.default = RouteScope; | ||
RouteScope.propTypes = { | ||
scope: _react.PropTypes.string | ||
scope: _react.PropTypes.string, | ||
children: _react.PropTypes.node | ||
}; | ||
RouteScope.childContextTypes = RouteScope.contextTypes = { | ||
__internalRouter: _react.PropTypes.object, | ||
router: _react.PropTypes.object | ||
}; | ||
}; | ||
exports.default = RouteScope; |
@@ -58,3 +58,2 @@ 'use strict'; | ||
var nextState = toIndex ? EMPTY_STATE : state || EMPTY_STATE; | ||
return this.context.__internalRouter.resolvePathFromState(nextState); | ||
@@ -73,5 +72,7 @@ } | ||
exports.default = StateLink; | ||
StateLink.propTypes = { | ||
state: _react.PropTypes.object, | ||
replace: _react.PropTypes.bool, | ||
toIndex: _react.PropTypes.bool | ||
}; | ||
StateLink.defaultProps = { | ||
@@ -81,8 +82,5 @@ replace: false, | ||
}; | ||
StateLink.propTypes = { | ||
state: _react.PropTypes.object, | ||
replace: _react.PropTypes.bool | ||
}; | ||
StateLink.contextTypes = { | ||
__internalRouter: _react.PropTypes.object | ||
}; | ||
}; | ||
exports.default = StateLink; |
@@ -6,75 +6,24 @@ 'use strict'; | ||
}); | ||
exports.resolvePathFromState = exports.resolveStateFromPath = exports.route = undefined; | ||
var _resolveStateFromPath = require('./resolveStateFromPath'); | ||
var _route = require('./route'); | ||
Object.defineProperty(exports, 'resolveStateFromPath', { | ||
Object.defineProperty(exports, 'route', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_resolveStateFromPath).default; | ||
return _interopRequireDefault(_route).default; | ||
} | ||
}); | ||
var _resolvePathFromState = require('./resolvePathFromState'); | ||
var _resolveStateFromPath = require('./resolveStateFromPath'); | ||
Object.defineProperty(exports, 'resolvePathFromState', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_resolvePathFromState).default; | ||
} | ||
}); | ||
var _resolveStateFromPath2 = _interopRequireDefault(_resolveStateFromPath); | ||
var _createRoute = require('./createRoute'); | ||
var _resolvePathFromState = require('./resolvePathFromState'); | ||
Object.defineProperty(exports, 'createRoute', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_createRoute).default; | ||
} | ||
}); | ||
var _resolvePathFromState2 = _interopRequireDefault(_resolvePathFromState); | ||
var _createScope = require('./createScope'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
Object.defineProperty(exports, 'createScope', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_createScope).default; | ||
} | ||
}); | ||
var _RouterProvider = require('./components/RouterProvider'); | ||
Object.defineProperty(exports, 'RouterProvider', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_RouterProvider).default; | ||
} | ||
}); | ||
var _Link = require('./components/Link'); | ||
Object.defineProperty(exports, 'Link', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_Link).default; | ||
} | ||
}); | ||
var _StateLink = require('./components/StateLink'); | ||
Object.defineProperty(exports, 'StateLink', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_StateLink).default; | ||
} | ||
}); | ||
var _RouteScope = require('./components/RouteScope'); | ||
Object.defineProperty(exports, 'RouteScope', { | ||
enumerable: true, | ||
get: function get() { | ||
return _interopRequireDefault(_RouteScope).default; | ||
} | ||
}); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
exports.resolveStateFromPath = _resolveStateFromPath2.default; | ||
exports.resolvePathFromState = _resolvePathFromState2.default; |
@@ -6,57 +6,49 @@ 'use strict'; | ||
}); | ||
exports.default = resolvePathFromState; | ||
var _flatten = require('lodash/flatten'); | ||
var _flatten2 = require('lodash/flatten'); | ||
var _flatten2 = _interopRequireDefault(_flatten); | ||
var _flatten3 = _interopRequireDefault(_flatten2); | ||
var _difference = require('lodash/difference'); | ||
exports.default = resolvePathFromState; | ||
var _difference2 = _interopRequireDefault(_difference); | ||
var _findMatchingNodes = require('./findMatchingNodes'); | ||
var _extractParams = require('./utils/extractParams'); | ||
var _findMatchingNodes2 = _interopRequireDefault(_findMatchingNodes); | ||
var _extractParams2 = _interopRequireDefault(_extractParams); | ||
var _debug = require('./utils/debug'); | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
function insertParams(pattern, params, splat) { | ||
if (!pattern) { | ||
return []; | ||
} | ||
return (0, _flatten2.default)(pattern.split('/').filter(Boolean).map(function (segment) { | ||
if (segment.startsWith(':')) { | ||
var paramName = segment.substring(1); | ||
return params[paramName]; | ||
} | ||
if (segment === '*') { | ||
return splat; | ||
} | ||
return segment; | ||
})).filter(Boolean); | ||
} | ||
function resolvePathFromState(node, state) { | ||
(0, _debug.debug)('Resolving path from state %o', state); | ||
function _resolvePath(routeNode, state) { | ||
if (routeNode.isScope) { | ||
return _resolvePath(routeNode.node, state[routeNode.name]); | ||
var match = (0, _findMatchingNodes2.default)(node, state); | ||
if (match.remaining.length > 0) { | ||
var formatted = match.remaining.map(function (key) { | ||
return key + ' (=' + JSON.stringify(state[key]) + ')'; | ||
}).join(', '); | ||
throw new Error('State key' + (match.remaining.length == 1 ? '' : 's') + ' not mapped to url params: ' + formatted); | ||
} | ||
var requiredParams = (0, _extractParams2.default)(routeNode.pattern); | ||
var missingParams = (0, _difference2.default)(requiredParams, Object.keys(state || {})); | ||
if (missingParams.length > 0) { | ||
return null; | ||
if (match.nodes.length === 0) { | ||
throw new Error('Unable to resolve path from given state: ' + JSON.stringify(state)); | ||
} | ||
var childPath = null; | ||
routeNode.children(state).some(function (childNode) { | ||
childPath = _resolvePath(childNode, state); | ||
return !!childPath; | ||
}); | ||
var scopedState = state; | ||
var relative = (0, _flatten3.default)(match.nodes.map(function (matchNode) { | ||
if (matchNode.scope) { | ||
scopedState = scopedState[matchNode.scope]; | ||
} | ||
return matchNode.route.segments.map(function (segment) { | ||
if (segment.type === 'dir') { | ||
return segment.name; | ||
} | ||
var transform = node.transform && node.transform[segment.name]; | ||
return transform ? transform.toPath(scopedState[segment.name]) : scopedState[segment.name]; | ||
}); | ||
})).join('/'); | ||
return insertParams(routeNode.pattern, state).concat(childPath || []).join('/'); | ||
} | ||
(0, _debug.debug)('Resolved to /%s', relative); | ||
function resolvePathFromState(routeNode, state) { | ||
return '/' + (_resolvePath(routeNode, state) || ''); | ||
return '/' + relative; | ||
} |
@@ -6,45 +6,57 @@ 'use strict'; | ||
}); | ||
exports.default = resolveStateFromPath; | ||
var _httpHash = require('http-hash'); | ||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; | ||
var _httpHash2 = _interopRequireDefault(_httpHash); | ||
exports.default = resolveStateFromPath; | ||
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } | ||
var _debug = require('./utils/debug'); | ||
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } | ||
function resolveStateFromPath(routeNode, path) { | ||
var result = {}; | ||
var hash = (0, _httpHash2.default)(); | ||
hash.set(routeNode.pattern, routeNode); | ||
var match = hash.get(path); | ||
function matchPath(node, path) { | ||
var parts = path.split('/').filter(Boolean); | ||
var segmentsLength = node.route.segments.length; | ||
if (parts.length < segmentsLength) { | ||
return null; | ||
} | ||
if (!match.handler) { | ||
return {}; | ||
var state = {}; | ||
var isMatching = node.route.segments.every(function (segment, i) { | ||
if (segment.type === 'dir') { | ||
return segment.name === parts[i]; | ||
} | ||
var transform = node.transform && node.transform[segment.name]; | ||
state[segment.name] = transform ? transform.toState(parts[i]) : parts[i]; | ||
return true; | ||
}); | ||
if (!isMatching) { | ||
return null; | ||
} | ||
Object.assign(result, match.params); | ||
var rest = parts.slice(segmentsLength); | ||
var childState = null; | ||
var children = typeof node.children === 'function' ? node.children(state) : node.children; | ||
if (children) { | ||
children.some(function (childNode) { | ||
childState = matchPath(childNode, rest.join('/')); | ||
return childState; | ||
}); | ||
} | ||
if (match.splat != null) { | ||
(function () { | ||
// get matching child routes | ||
var childRoutes = (0, _httpHash2.default)(); | ||
var childRouteNodes = routeNode.children(match.params); | ||
childRouteNodes.forEach(function (childRouteNode) { | ||
var pattern = (childRouteNode.isScope ? childRouteNode.node : childRouteNode).pattern; | ||
childRoutes.set(pattern, childRouteNode); | ||
}); | ||
var childMatch = childRoutes.get(match.splat || '/'); | ||
if (rest.length > 0 && !childState) { | ||
return null; | ||
} | ||
if (childMatch.handler) { | ||
var childNode = childMatch.handler; | ||
var mergedState = _extends({}, state, childState); | ||
return node.scope ? _defineProperty({}, node.scope, mergedState) : mergedState; | ||
} | ||
var childState = childNode.isScope ? _defineProperty({}, childNode.name, resolveStateFromPath(childNode.node, match.splat)) : resolveStateFromPath(childNode, match.splat); | ||
function resolveStateFromPath(node, path) { | ||
(0, _debug.debug)('resolving state from path %s', path); | ||
Object.assign(result, childState); | ||
} | ||
})(); | ||
} | ||
return result; | ||
var pathMatch = matchPath(node, path.split('?')[0]); | ||
(0, _debug.debug)('resolved: %o', pathMatch || null); | ||
return pathMatch || null; | ||
} |
{ | ||
"name": "@sanity/state-router", | ||
"version": "0.0.3", | ||
"version": "0.1.0", | ||
"description": "A path pattern => state object bidirectional mapper", | ||
@@ -9,4 +9,8 @@ "main": "lib/index.js", | ||
"test": "tap test/*.test.js --node-arg --require --node-arg ./.babel-init.js", | ||
"compile": "babel --out-dir lib/ src/", | ||
"test:lib:compile": "babel . -q --ignore node_modules,demo-server,perf --out-dir test-compiled", | ||
"test:lib": "npm run test:lib:compile && cd test-compiled && tap test/**.test.js && cd - && npm run test:lib:clean", | ||
"test:lib:clean": "rimraf test-compiled", | ||
"compile": "npm run clean && babel --out-dir lib/ src/", | ||
"compile:watch": "babel --watch --out-dir lib/ src/", | ||
"benchmark": "npm run compile && node --prof --logfile=benchmarks.log perf/benchmark.js", | ||
"prepublish": "in-publish && npm run compile || not-in-publish", | ||
@@ -20,3 +24,2 @@ "postpublish": "in-publish && npm run clean || not-in-publish", | ||
"dependencies": { | ||
"http-hash": "^2.0.0", | ||
"lodash": "^4.15.0" | ||
@@ -27,4 +30,9 @@ }, | ||
"babel-core": "^6.14.0", | ||
"babel-eslint": "^6.1.2", | ||
"babel-eslint": "^7.0.0", | ||
"babel-plugin-lodash": "^3.2.9", | ||
"babel-plugin-syntax-class-properties": "^6.13.0", | ||
"babel-plugin-syntax-flow": "^6.13.0", | ||
"babel-plugin-syntax-object-rest-spread": "^6.13.0", | ||
"babel-plugin-transform-class-properties": "^6.16.0", | ||
"babel-plugin-transform-flow-strip-types": "^6.14.0", | ||
"babel-plugin-transform-object-rest-spread": "^6.8.0", | ||
@@ -40,8 +48,9 @@ "babel-polyfill": "^6.13.0", | ||
"eslint-config-bengler": "^2.0.0", | ||
"eslint-plugin-import": "^1.14.0", | ||
"eslint-plugin-flowtype-errors": "^1.4.1", | ||
"eslint-plugin-import": "^2.0.1", | ||
"eslint-plugin-react": "^6.2.0", | ||
"express": "^4.14.0", | ||
"history": "^3.2.1", | ||
"flow-bin": "^0.33.0", | ||
"history": "^4.3.0", | ||
"in-publish": "^2.0.0", | ||
"jest": "^15.1.1", | ||
"object-inspect": "^1.2.1", | ||
@@ -48,0 +57,0 @@ "quickreload": "^2.1.2", |
@@ -13,2 +13,12 @@ import React, {PropTypes} from 'react' | ||
export default class Link extends React.Component { | ||
static defaultProps = { | ||
replace: false, | ||
} | ||
static propTypes = { | ||
replace: PropTypes.bool | ||
} | ||
static contextTypes = { | ||
__internalRouter: PropTypes.object | ||
} | ||
constructor() { | ||
@@ -40,15 +50,4 @@ super() | ||
render() { | ||
return <a {...omit(this.props, 'replace')} onClick={this.handleClick} /> | ||
} | ||
} | ||
Link.defaultProps = { | ||
replace: false, | ||
} | ||
Link.propTypes = { | ||
replace: PropTypes.bool | ||
} | ||
Link.contextTypes = { | ||
__internalRouter: PropTypes.object | ||
} |
import React, {PropTypes} from 'react' | ||
import {resolvePathFromState, resolveStateFromPath} from '../' | ||
let didWarn = false | ||
function validateProps(props) { | ||
if (didWarn) { | ||
return | ||
} | ||
if (props.state && props.location) { | ||
// eslint-disable-next-line no-console | ||
console.error(new Error( | ||
"[Warning] You passed both state and location to RouterProvider. If you pass 'state' you don't need to pass 'location' and vice versa" | ||
)) | ||
didWarn = true | ||
} | ||
} | ||
export default class RouterProvider extends React.Component { | ||
constructor(props, ...rest) { | ||
super(props, ...rest) | ||
this.navigateUrl = this.navigateUrl.bind(this) | ||
this.navigateState = this.navigateState.bind(this) | ||
this.resolvePathFromState = this.resolvePathFromState.bind(this) | ||
validateProps(props) | ||
static propTypes = { | ||
onNavigate: PropTypes.func, | ||
router: PropTypes.object, | ||
location: PropTypes.shape({ | ||
pathname: PropTypes.string | ||
}), | ||
children: PropTypes.node | ||
} | ||
navigateUrl(url, {replace} = {}) { | ||
navigateUrl = (url, {replace} = {}) => { | ||
const {onNavigate} = this.props | ||
@@ -32,16 +18,12 @@ onNavigate(url, {replace}) | ||
navigateState(nextState, {replace} = {}) { | ||
navigateState = (nextState, {replace} = {}) => { | ||
this.navigateUrl(this.resolvePathFromState(nextState), {replace}) | ||
} | ||
resolvePathFromState(state) { | ||
return resolvePathFromState(this.props.router, state) | ||
resolvePathFromState = state => { | ||
return this.props.router.encode(state) | ||
} | ||
componentWillReceiveProps(nextProps) { | ||
validateProps(nextProps) | ||
} | ||
getChildContext() { | ||
const {router, location, state} = this.props | ||
const {router, location} = this.props | ||
return { | ||
@@ -54,3 +36,3 @@ __internalRouter: { | ||
navigate: this.navigateState, | ||
state: state || resolveStateFromPath(router, location.pathname) | ||
state: router.decode(location.pathname) | ||
} | ||
@@ -64,11 +46,2 @@ } | ||
} | ||
RouterProvider.propTypes = { | ||
state: PropTypes.object, | ||
children: PropTypes.node, | ||
router: PropTypes.object, | ||
onNavigate: PropTypes.func, | ||
location: PropTypes.shape({ | ||
pathname: PropTypes.string | ||
}) | ||
} | ||
RouterProvider.childContextTypes = { | ||
@@ -75,0 +48,0 @@ __internalRouter: PropTypes.object, |
import React, {PropTypes} from 'react' | ||
export default class RouteScope extends React.Component { | ||
static propTypes = { | ||
scope: PropTypes.string, | ||
children: PropTypes.node | ||
} | ||
static childContextTypes = RouteScope.contextTypes = { | ||
__internalRouter: PropTypes.object, | ||
router: PropTypes.object | ||
} | ||
getChildContext() { | ||
@@ -10,3 +20,4 @@ const {scope} = this.props | ||
resolvePathFromState: nextState => { | ||
return __internalRouter.resolvePathFromState(getUnscopedState(nextState)) | ||
const empty = Object.keys(nextState).length === 0 | ||
return __internalRouter.resolvePathFromState(empty ? {} : addScope(nextState)) | ||
}, | ||
@@ -16,11 +27,11 @@ navigateUrl: __internalRouter.navigateUrl | ||
router: { | ||
navigate: (nextState, options)=> { | ||
router.navigate(getUnscopedState(nextState), options) | ||
navigate: (nextState, options) => { | ||
router.navigate(addScope(nextState), options) | ||
}, | ||
state: router.state[scope] || {} | ||
state: router.state[scope] | ||
} | ||
} | ||
function getUnscopedState(nextState) { | ||
return Object.assign({}, router.state, {[scope]: nextState}) | ||
function addScope(nextState) { | ||
return nextState && Object.assign({}, router.state, {[scope]: nextState}) | ||
} | ||
@@ -33,10 +44,1 @@ | ||
} | ||
RouteScope.propTypes = { | ||
scope: PropTypes.string | ||
} | ||
RouteScope.childContextTypes = RouteScope.contextTypes = { | ||
__internalRouter: PropTypes.object, | ||
router: PropTypes.object | ||
} |
@@ -8,2 +8,17 @@ import React, {PropTypes} from 'react' | ||
export default class StateLink extends React.Component { | ||
static propTypes = { | ||
state: PropTypes.object, | ||
replace: PropTypes.bool, | ||
toIndex: PropTypes.bool | ||
} | ||
static defaultProps = { | ||
replace: false, | ||
toIndex: false, | ||
} | ||
static contextTypes = { | ||
__internalRouter: PropTypes.object | ||
} | ||
resolveUrl() { | ||
@@ -20,3 +35,2 @@ const {toIndex, state} = this.props | ||
const nextState = toIndex ? EMPTY_STATE : (state || EMPTY_STATE) | ||
return this.context.__internalRouter.resolvePathFromState(nextState) | ||
@@ -29,13 +43,1 @@ } | ||
} | ||
StateLink.defaultProps = { | ||
replace: false, | ||
toIndex: false, | ||
} | ||
StateLink.propTypes = { | ||
state: PropTypes.object, | ||
replace: PropTypes.bool | ||
} | ||
StateLink.contextTypes = { | ||
__internalRouter: PropTypes.object | ||
} |
@@ -1,8 +0,7 @@ | ||
export {default as resolveStateFromPath} from './resolveStateFromPath' | ||
export {default as resolvePathFromState} from './resolvePathFromState' | ||
export {default as createRoute} from './createRoute' | ||
export {default as createScope} from './createScope' | ||
export {default as RouterProvider} from './components/RouterProvider' | ||
export {default as Link} from './components/Link' | ||
export {default as StateLink} from './components/StateLink' | ||
export {default as RouteScope} from './components/RouteScope' | ||
import resolveStateFromPath from './resolveStateFromPath' | ||
import resolvePathFromState from './resolvePathFromState' | ||
export {default as route} from './route' | ||
export {resolveStateFromPath} | ||
export {resolvePathFromState} |
@@ -1,50 +0,39 @@ | ||
import flatten from 'lodash/flatten' | ||
import difference from 'lodash/difference' | ||
import extractParams from './utils/extractParams' | ||
// @flow | ||
import type {Node, MatchResult} from './types' | ||
import findMatchingNodes from './findMatchingNodes' | ||
import {flatten} from 'lodash' | ||
import {debug} from './utils/debug' | ||
function insertParams(pattern, params, splat) { | ||
if (!pattern) { | ||
return [] | ||
} | ||
return flatten( | ||
pattern.split('/') | ||
.filter(Boolean) | ||
.map(segment => { | ||
if (segment.startsWith(':')) { | ||
const paramName = segment.substring(1) | ||
return params[paramName] | ||
} | ||
if (segment === '*') { | ||
return splat | ||
} | ||
return segment | ||
})) | ||
.filter(Boolean) | ||
} | ||
export default function resolvePathFromState(node: Node, state: Object): string { | ||
debug('Resolving path from state %o', state) | ||
function _resolvePath(routeNode, state) { | ||
if (routeNode.isScope) { | ||
return _resolvePath(routeNode.node, state[routeNode.name]) | ||
const match: MatchResult = findMatchingNodes(node, state) | ||
if (match.remaining.length > 0) { | ||
const formatted = match.remaining.map(key => `${key} (=${JSON.stringify(state[key])})`).join(', ') | ||
throw new Error(`State key${match.remaining.length == 1 ? '' : 's'} not mapped to url params: ${formatted}`) | ||
} | ||
const requiredParams = extractParams(routeNode.pattern) | ||
const missingParams = difference(requiredParams, Object.keys(state || {})) | ||
if (missingParams.length > 0) { | ||
return null | ||
if (match.nodes.length === 0) { | ||
throw new Error(`Unable to resolve path from given state: ${JSON.stringify(state)}`) | ||
} | ||
let childPath = null | ||
routeNode.children(state).some(childNode => { | ||
childPath = _resolvePath(childNode, state) | ||
return !!childPath | ||
}) | ||
let scopedState = state | ||
const relative = flatten( | ||
match.nodes.map(matchNode => { | ||
if (matchNode.scope) { | ||
scopedState = scopedState[matchNode.scope] | ||
} | ||
return matchNode.route.segments.map(segment => { | ||
if (segment.type === 'dir') { | ||
return segment.name | ||
} | ||
const transform = node.transform && node.transform[segment.name] | ||
return transform ? transform.toPath(scopedState[segment.name]) : scopedState[segment.name] | ||
}) | ||
}) | ||
).join('/') | ||
return insertParams(routeNode.pattern, state) | ||
.concat(childPath || []) | ||
.join('/') | ||
} | ||
debug('Resolved to /%s', relative) | ||
export default function resolvePathFromState(routeNode, state) { | ||
return `/${_resolvePath(routeNode, state) || ''}` | ||
return `/${relative}` | ||
} |
@@ -1,36 +0,52 @@ | ||
import HttpHash from 'http-hash' | ||
// @flow | ||
import type {Node} from './types' | ||
import {debug} from './utils/debug' | ||
export default function resolveStateFromPath(routeNode, path) { | ||
const result = {} | ||
const hash = HttpHash() | ||
hash.set(routeNode.pattern, routeNode) | ||
const match = hash.get(path) | ||
function matchPath(node : Node, path : string) : ?{[key: string]: string} { | ||
const parts = path.split('/').filter(Boolean) | ||
const segmentsLength = node.route.segments.length | ||
if (parts.length < segmentsLength) { | ||
return null | ||
} | ||
if (!match.handler) { | ||
return {} | ||
const state = {} | ||
const isMatching = node.route.segments.every((segment, i) => { | ||
if (segment.type === 'dir') { | ||
return segment.name === parts[i] | ||
} | ||
const transform = node.transform && node.transform[segment.name] | ||
state[segment.name] = transform ? transform.toState(parts[i]) : parts[i] | ||
return true | ||
}) | ||
if (!isMatching) { | ||
return null | ||
} | ||
Object.assign(result, match.params) | ||
if (match.splat != null) { | ||
// get matching child routes | ||
const childRoutes = HttpHash() | ||
const childRouteNodes = routeNode.children(match.params) | ||
childRouteNodes.forEach(childRouteNode => { | ||
const pattern = (childRouteNode.isScope ? childRouteNode.node : childRouteNode).pattern | ||
childRoutes.set(pattern, childRouteNode) | ||
const rest = parts.slice(segmentsLength) | ||
let childState = null | ||
const children = typeof node.children === 'function' ? node.children(state) : node.children | ||
if (children) { | ||
children.some(childNode => { | ||
childState = matchPath(childNode, rest.join('/')) | ||
return childState | ||
}) | ||
const childMatch = childRoutes.get(match.splat || '/') | ||
} | ||
if (childMatch.handler) { | ||
const childNode = childMatch.handler | ||
if (rest.length > 0 && !childState) { | ||
return null | ||
} | ||
const childState = childNode.isScope | ||
? {[childNode.name]: resolveStateFromPath(childNode.node, match.splat)} | ||
: resolveStateFromPath(childNode, match.splat) | ||
const mergedState = {...state, ...childState} | ||
return node.scope ? {[node.scope]: mergedState} : mergedState | ||
} | ||
Object.assign(result, childState) | ||
} | ||
} | ||
return result | ||
export default function resolveStateFromPath(node : Node, path : string) : ?Object { | ||
debug('resolving state from path %s', path) | ||
const pathMatch = matchPath(node, path.split('?')[0]) | ||
debug('resolved: %o', pathMatch || null) | ||
return pathMatch || null | ||
} | ||
@@ -0,10 +1,11 @@ | ||
// @flow | ||
import test from './_util/test' | ||
import createRoute from '../src/createRoute' | ||
import createScope from '../src/createScope' | ||
import route from '../src/route' | ||
import type {Router} from '../src/types' | ||
import resolvePathFromState from '../src/resolvePathFromState' | ||
test('resolves empty state to fixed base path', t => { | ||
const rootRoute = createRoute('/root/*', [ | ||
createRoute('/:page', [ | ||
createRoute('/:productId') | ||
const rootRoute : Router = route('/root', [ | ||
route('/:page', [ | ||
route('/:productId') | ||
]) | ||
@@ -15,20 +16,20 @@ ]) | ||
test('throws on unresolvable state', {todo: true}, t => { | ||
const rootRoute = createRoute('/root/*', [ | ||
createRoute('/:page', [ | ||
createRoute('/:productId') | ||
test('throws on unresolvable state', {todo: false}, t => { | ||
const rootRoute = route('/root', [ | ||
route('/:page', [ | ||
route('/:productId') | ||
]) | ||
]) | ||
t.throws(() => resolvePathFromState(rootRoute, {foo: 'bar'}), new Error('Unmappable state keys remaining: foo')) | ||
t.throws(() => resolvePathFromState(rootRoute, {foo: 'bar'}), /.*not mapped .* params.*foo.*/) | ||
}) | ||
test('Resolves this', t => { | ||
const routesDef = createRoute('/:dataset/*', [ | ||
createRoute('/settings/:setting'), | ||
createRoute('/tools/:tool/*', params => { | ||
const routesDef = route('/:dataset', [ | ||
route('/settings/:setting'), | ||
route('/tools/:tool', params => { | ||
if (params.tool === 'desk') { | ||
return createScope('desk', createRoute('/collections/:collection')) | ||
return [route.scope('desk', '/collections/:collection')] | ||
} | ||
if (params.tool === 'another-tool') { | ||
return createScope('foo', createRoute('/omg/:nope')) | ||
return [route.scope('foo', '/omg/:nope')] | ||
} | ||
@@ -40,3 +41,3 @@ }) | ||
tool: 'another-tool', | ||
'foo': { | ||
foo: { | ||
nope: 'foo' | ||
@@ -43,0 +44,0 @@ }, |
@@ -0,48 +1,115 @@ | ||
// @flow | ||
import type {Node} from '../src/types' | ||
import test from './_util/test' | ||
import createRoute from '../src/createRoute' | ||
import createScope from '../src/createScope' | ||
import resolveStateFromPath from '../src/resolveStateFromPath' | ||
test('matches a flat route', t => { | ||
const rootRoute = createRoute('/products/:productSlug') | ||
t.same(resolveStateFromPath(rootRoute, '/products/nyan-cat'), {productSlug: 'nyan-cat'}) | ||
t.same(resolveStateFromPath(rootRoute, '/'), {}) | ||
}) | ||
const node : Node = { | ||
route: { | ||
raw: '/foo/:bar', | ||
segments: [ | ||
{type: 'dir', name: 'foo'}, | ||
{type: 'param', name: 'bar'}, | ||
], | ||
}, | ||
transform: {}, | ||
children: [ | ||
{ | ||
route: { | ||
raw: '/dynamic/:foo', | ||
segments: [ | ||
{type: 'dir', name: 'dynamic'}, | ||
{type: 'param', name: 'foo'}, | ||
], | ||
}, | ||
transform: {}, | ||
children(state) { | ||
if (state.foo === 'foo') { | ||
return [{ | ||
route: { | ||
raw: '/:whenfoo', | ||
segments: [ | ||
{type: 'param', name: 'whenfoo'}, | ||
] | ||
}, | ||
transform: {}, | ||
children: [] | ||
}] | ||
} | ||
return [{ | ||
route: { | ||
raw: '/:notfoo', | ||
segments: [ | ||
{type: 'param', name: 'notfoo'}, | ||
], | ||
}, | ||
transform: {}, | ||
children: [] | ||
}] | ||
} | ||
}, | ||
{ | ||
route: { | ||
raw: '/nix/:animal', | ||
segments: [ | ||
{type: 'dir', name: 'nix'}, | ||
{type: 'param', name: 'animal'}, | ||
], | ||
}, | ||
transform: {}, | ||
children: [] | ||
}, | ||
{ | ||
route: { | ||
raw: '/qux/:animal', | ||
segments: [ | ||
{type: 'dir', name: 'qux'}, | ||
{type: 'param', name: 'animal'}, | ||
] | ||
}, | ||
transform: { | ||
animal: { | ||
toState: value => ({name: value.toUpperCase()}), | ||
toPath: animal => animal.name.toLowerCase() | ||
} | ||
}, | ||
children: [] | ||
}, | ||
] | ||
} | ||
test('matches all levels in a nested route definition', t => { | ||
const rootRoute = createRoute('/*', [ | ||
createRoute('/products/:productSlug') | ||
]) | ||
t.same(resolveStateFromPath(rootRoute, '/products/nyan-cat'), {productSlug: 'nyan-cat'}) | ||
t.same(resolveStateFromPath(rootRoute, '/'), {}) | ||
}) | ||
const examples = [ | ||
['/foo/bar', {bar: 'bar'}], | ||
['foo/bar', {bar: 'bar'}], | ||
['foo/bar/baz', null], | ||
['/foo/bar/qux/cat', { | ||
animal: {name: 'CAT'}, | ||
bar: 'bar' | ||
}], | ||
['/foo/bar/nix/cat', { | ||
animal: 'cat', | ||
bar: 'bar' | ||
}], | ||
['/foo/bar/nix/cat', { | ||
animal: 'cat', | ||
bar: 'bar' | ||
}], | ||
['/nope/bar', null], | ||
['/foo/bar/dynamic/foo/thisisfoo', { | ||
bar: 'bar', | ||
foo: 'foo', | ||
whenfoo: 'thisisfoo', | ||
}], | ||
['/foo', null] | ||
].filter(Boolean) | ||
test('fails if two routes defines the same parameter', {todo: true}, t => { | ||
const rootRoute = createRoute('/products/:foo/:foo') | ||
t.throws(resolveStateFromPath(rootRoute, '/products/foo/foo')) | ||
examples.forEach(([path, state]) => { | ||
test(`path ${path} => ${JSON.stringify(state)}`, t => { | ||
t.deepEqual(resolveStateFromPath(node, path), state) | ||
}) | ||
}) | ||
test('matches according to router scope', t => { | ||
const rootRoute = createRoute('/:category/*', [ | ||
createScope('product', createRoute('/products/:productSlug/*', [ | ||
createRoute('/:section') | ||
])) | ||
]) | ||
t.same(resolveStateFromPath(rootRoute, '/'), {}) | ||
t.same(resolveStateFromPath(rootRoute, '/imaginary-pets/products/nyan-cat/purchase'), { | ||
category: 'imaginary-pets', | ||
product: { | ||
productSlug: 'nyan-cat', | ||
section: 'purchase' | ||
} | ||
}) | ||
t.same(resolveStateFromPath(rootRoute, '/imaginary-pets/products/nyan-cat'), { | ||
category: 'imaginary-pets', | ||
product: { | ||
productSlug: 'nyan-cat' | ||
} | ||
}) | ||
}) | ||
// IDEA! Can/should not map from a global param space to state because conflicts | ||
// assert.deepEqual(resolveStateFromPath(node, '/foo/bar;flabla=flarb/nix/cat;family=mammal'), { | ||
// | ||
// }) |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 2 instances in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
64737
1
54
1611
35
5
- Removedhttp-hash@^2.0.0
- Removedhttp-hash@2.0.1(transitive)