@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
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
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)