import {compile, pathToRegexp} from 'path-to-regexp' import erre from 'erre' // check whether the window object is defined const isNode = typeof process !== 'undefined' const isString = str => typeof str === 'string' // the url parsing function depends on the platform, on node we rely on the 'url' module /* istanbul ignore next */ const parseURL = (...args) => isNode ? require('url').parse(...args) : new URL(...args) /** * Replace the base path from a path * @param {string} path - router path string * @returns {string} path cleaned up without the base */ const replaceBase = path => path.replace(defaults.base, '') /** * Try to match the current path or skip it * @param {RegEx} pathRegExp - target path transformed by pathToRegexp * @returns {string|Symbol} if the path match we return it otherwise we cancel the stream */ const matchOrSkip = pathRegExp => path => match(path, pathRegExp) ? path : erre.cancel() /** * Combine 2 streams connecting the events of dispatcherStream to the receiverStream * @param {Stream} dispatcherStream - main stream dispatching events * @param {Stream} receiverStream - sub stream receiving events from the dispatcher * @returns {Stream} receiverStream */ const joinStreams = (dispatcherStream, receiverStream) => { dispatcherStream.on.value(receiverStream.push) receiverStream.on.end(() => { dispatcherStream.off.value(receiverStream.push) }) return receiverStream } /** * Error handling function * @param {Error} error - error to catch * @returns {void} */ const panic = error => { if (defaults.silentErrors) return throw new Error(error) } // make sure that the router will always receive strings params export const filterStrings = str => isString(str) ? str : erre.cancel() // create the streaming router export const router = erre(filterStrings).on.error(panic) // cast the values of this stream always to string /* @type {object} general configuration object */ export const defaults = { // custom option base: '', silentErrors: false, // pathToRegexp options sensitive: false, strict: false, end: true, start: true, delimiter: '/#?', encode: undefined, endsWith: undefined, prefixes: './' } /** * Merge the user options with the defaults * @param {Object} options - custom user options * @returns {Object} options object merged with defaults */ export const mergeOptions = options => ({...defaults, ...options}) /* {@link https://github.com/pillarjs/path-to-regexp#usage} */ export const toRegexp = (path, keys, options) => pathToRegexp(path, keys, mergeOptions(options)) /** * Convert a router entry to a real path computing the url parameters * @param {string} path - router path string * @param {Object} params - named matched parameters * @param {Object} options - pathToRegexp options object * @returns {string} computed url string */ export const toPath = (path, params, options) => compile(path, mergeOptions(options))(params) /** * Parse a string path generating an object containing * @param {string} path - target path * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @param {Object} options - object containing the base path * @returns {URL} url object enhanced with the `match` attribute */ export const toURL = (path, pathRegExp, options = {}) => { const {base} = mergeOptions(options) const [, ...params] = pathRegExp.exec(path) const url = parseURL(path, base) // extend the url object adding the matched params url.params = params.reduce((acc, param, index) => { const key = options.keys && options.keys[index] if (key) acc[key.name] = param ? decodeURIComponent(param) : param return acc }, {}) return url } /** * Return true if a path will be matched * @param {string} path - target path * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @returns {boolean} true if the path matches the regexp */ export const match = (path, pathRegExp) => pathRegExp.test(path) /** * Factory function to create an sequence of functions to pass to erre.js * This function will be used in the erre stream * @param {RegExp} pathRegExp - path transformed to regexp via pathToRegexp * @param {Object} options - pathToRegexp options object * @returns {Array} a functions array that will be used as stream pipe for erre.js */ export const createURLStreamPipe = (pathRegExp, options) => [ decodeURI, replaceBase, matchOrSkip(pathRegExp, options), path => toURL(path, pathRegExp, options) ] /** * Create a fork of the main router stream * @param {string} path - route to match * @param {Object} options - pathToRegexp options object * @returns {Stream} new route stream */ export default function createRoute(path, options) { const keys = [] const pathRegExp = pathToRegexp(path, keys, options) const URLStream = erre(...createURLStreamPipe(pathRegExp, { ...options, keys })) return joinStreams(router, URLStream).on.error(panic) }