├── CHANGELOG.md ├── .gitignore ├── .travis.yml ├── tsconfig.test.json ├── tsconfig.build.json ├── src ├── index.ts ├── param-types.ts ├── param-types.test.ts ├── route.test.ts └── route.ts ├── Makefile ├── tsconfig.json ├── package.json ├── tslint.json ├── LICENSE ├── README.md └── yarn.lock /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.1.0 2 | ====== 3 | Initial release -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .tmp 4 | .DS_Store 5 | npm-debug.log -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | - "8" 5 | script: 6 | - make 7 | - make test 8 | cache: 9 | yarn: true 10 | directories: 11 | - node_modules 12 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "noUnusedLocals": true 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "lib" 9 | ] 10 | } -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "noUnusedLocals": true 5 | }, 6 | "exclude": [ 7 | "node_modules", 8 | "example", 9 | "lib", 10 | "**/*.test.*" 11 | ] 12 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import createRoute from "./route"; 2 | 3 | export { 4 | createRoute, 5 | Route, 6 | OptRoute, 7 | RestRoute, 8 | RouteOpts 9 | } from "./route"; 10 | 11 | export { 12 | ParamType, 13 | StrParam, 14 | IntParam, 15 | FloatParam, 16 | DateTimeParam, 17 | ArrayParam 18 | } from "./param-types"; 19 | 20 | export default createRoute; -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: default setup clean build lint test 2 | 3 | # Put Node bins in path 4 | export PATH := node_modules/.bin:$(PATH) 5 | export SHELL := /bin/bash 6 | 7 | default: build 8 | 9 | setup: 10 | yarn install 11 | 12 | clean: 13 | rm -rf lib 14 | 15 | build: clean 16 | tsc -p tsconfig.build.json 17 | 18 | lint: 19 | tslint --type-check --project tsconfig.json 20 | 21 | test: 22 | ts-node --project tsconfig.test.json \ 23 | node_modules/.bin/tape 'src/**/*.test.*' | tap-spec 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./lib", 4 | "target": "es5", 5 | "declaration": true, 6 | "sourceMap": false, 7 | "importHelpers": true, 8 | "noEmitHelpers": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "alwaysStrict": true, 11 | "noImplicitAny": true, 12 | "noImplicitThis": true, 13 | "noImplicitReturns": true, 14 | "allowUnreachableCode": false, 15 | "allowUnusedLabels": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "strictNullChecks": true, 18 | "module": "commonjs", 19 | "moduleResolution": "node", 20 | "jsx": "react", 21 | "lib": ["dom", "es6"] 22 | }, 23 | "exclude":[ 24 | "node_modules", 25 | "lib" 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typed-routes", 3 | "version": "0.1.1", 4 | "description": "Routes with TypeScript support", 5 | "main": "lib/index.js", 6 | "typings": "./lib/index.d.ts", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/esperco/typed-routes.git" 10 | }, 11 | "author": "Andrew Fong ", 12 | "homepage": "https://github.com/esperco/typed-routes", 13 | "license": "MIT", 14 | "devDependencies": { 15 | "@types/blue-tape": "^0.1.31", 16 | "@types/sinon": "^2.3.2", 17 | "sinon": "^2.3.7", 18 | "tap-spec": "^4.1.1", 19 | "tape": "^4.8.0", 20 | "ts-node": "^3.1.0", 21 | "tslint": "^5.4.3", 22 | "typescript": "^2.4.1" 23 | }, 24 | "dependencies": { 25 | "tslib": "^1.7.1" 26 | }, 27 | "files": [ 28 | "lib" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [true, 5 | "check-space" 6 | ], 7 | "indent": [true, 8 | "spaces" 9 | ], 10 | "linebreak-style": [true, "LF"], 11 | "one-line": [true, 12 | "check-open-brace", 13 | "check-whitespace" 14 | ], 15 | "no-var-keyword": true, 16 | "quotemark": [true, 17 | "double", 18 | "avoid-escape" 19 | ], 20 | "semicolon": [true, "always"], 21 | "whitespace": [true, 22 | "check-branch", 23 | "check-decl", 24 | "check-operator", 25 | "check-module", 26 | "check-separator", 27 | "check-type" 28 | ], 29 | "typedef-whitespace": [true, { 30 | "call-signature": "nospace", 31 | "index-signature": "nospace", 32 | "parameter": "nospace", 33 | "property-declaration": "nospace", 34 | "variable-declaration": "nospace" 35 | }], 36 | "no-internal-module": true, 37 | "no-trailing-whitespace": true, 38 | "no-unused-variable": true 39 | }, 40 | "defaultSeverity": "warning" 41 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Esper Technologies, Inc. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /src/param-types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Structures for basic param typing 3 | */ 4 | 5 | // Base param type 6 | export interface ParamType { 7 | // Convert string to param type. May return undefined if type is invalid. 8 | parse(s: string): T|undefined; 9 | 10 | // Convert param to string, for use in path 11 | stringify(t: T): string; 12 | } 13 | 14 | export const StrParam: ParamType = { 15 | parse: (s) => s, 16 | stringify: (t) => t 17 | }; 18 | 19 | export const IntParam: ParamType = { 20 | parse: (s) => { 21 | let ret = parseInt(s); 22 | return isNaN(ret) ? undefined : ret; 23 | }, 24 | stringify: (t) => t.toString() 25 | }; 26 | 27 | export const FloatParam: ParamType = { 28 | parse: (s) => { 29 | let ret = parseFloat(s); 30 | return isNaN(ret) ? undefined : ret; 31 | }, 32 | stringify: (t) => t.toString() 33 | }; 34 | 35 | export const DateTimeParam: ParamType = { 36 | parse: (s) => { 37 | let ret = new Date(parseInt(s)); 38 | return isNaN(ret.getTime()) ? undefined : ret; 39 | }, 40 | stringify: (d) => d.getTime().toString() 41 | }; 42 | 43 | export const ArrayParam = ( 44 | p: ParamType, 45 | delimiter = "," 46 | ): ParamType => ({ 47 | parse: (s) => { 48 | let ret: T[] = []; 49 | if (s) { 50 | let parts = s.split(delimiter); 51 | for (let i in parts) { 52 | let t = p.parse(parts[i]); 53 | if (t === void 0) return undefined; 54 | ret.push(t); 55 | } 56 | } 57 | return ret; 58 | }, 59 | stringify: (t): string => { 60 | let ret: string[] = []; 61 | for (let i in t) { 62 | ret.push(p.stringify(t[i])); 63 | } 64 | return ret.join(delimiter); 65 | } 66 | }); -------------------------------------------------------------------------------- /src/param-types.test.ts: -------------------------------------------------------------------------------- 1 | import test = require("tape"); 2 | import { 3 | StrParam, 4 | IntParam, 5 | FloatParam, 6 | DateTimeParam, 7 | ArrayParam 8 | } from "./index"; 9 | 10 | test("StrParam", (assert) => { 11 | assert.equals(StrParam.parse("something"), "something", 12 | "parses to string"); 13 | assert.equals(StrParam.stringify("something"), "something", 14 | "stringifies string"); 15 | assert.end(); 16 | }); 17 | 18 | test("IntParam", (assert) => { 19 | assert.equals(IntParam.parse("123.1"), 123, 20 | "parses to integer"); 21 | assert.equals(IntParam.parse("x"), undefined, 22 | "rejects if cannot parse to integer"); 23 | assert.equals(IntParam.stringify(123), "123", 24 | "stringifies integer"); 25 | assert.end(); 26 | }); 27 | 28 | test("FloatParam", (assert) => { 29 | assert.equals(FloatParam.parse("123.1"), 123.1, 30 | "parses to float"); 31 | assert.equals(FloatParam.parse("x"), undefined, 32 | "rejects if cannot parse to float"); 33 | assert.equals(FloatParam.stringify(123.1), "123.1", 34 | "stringifies float"); 35 | assert.end(); 36 | }); 37 | 38 | test("DateTimeParam", (assert) => { 39 | assert.deepEquals( 40 | DateTimeParam.parse("1234567890000"), 41 | new Date(1234567890000), 42 | "parses to Date based on milliseconds since epoch" 43 | ); 44 | assert.deepEquals( 45 | DateTimeParam.parse("x"), 46 | undefined, 47 | "rejects if cannot parse to date" 48 | ); 49 | assert.equals( 50 | DateTimeParam.stringify(new Date(1234567890000)), 51 | "1234567890000", 52 | "stringifies float" 53 | ); 54 | assert.end(); 55 | }); 56 | 57 | test("ArrayParam", (assert) => { 58 | let ArrayInts = ArrayParam(IntParam, ","); 59 | assert.deepEquals(ArrayInts.parse("1,2,3"), [1, 2, 3], 60 | "parses to array of another type based on delimiter"); 61 | assert.deepEquals(ArrayInts.parse("123"), [123], 62 | "parses single instance of type"); 63 | assert.deepEquals(ArrayInts.parse(""), [], 64 | "parses empty array"); 65 | assert.equals(ArrayInts.parse("1,b,3"), undefined, 66 | "rejects if cannot parse any array element to type"); 67 | assert.equals(ArrayInts.stringify([1, 2, 3]), "1,2,3", 68 | "stringifies array"); 69 | assert.end(); 70 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | typed-routes 2 | ============ 3 | [![Build Status](https://travis-ci.org/esperco/typed-routes.svg?branch=master)](https://travis-ci.org/esperco/typed-routes) 4 | [![npm version](https://badge.fury.io/js/typed-routes.svg)](https://badge.fury.io/js/typed-routes) 5 | 6 | Routes with TypeScript support. 7 | 8 | This library is intended solely to help with pattern matching path-like 9 | strings. It makes no assumptions about location or browser history or any 10 | attempt to implement actual route change detection. 11 | 12 | Usage 13 | ----- 14 | 15 | ```ts 16 | import createRoute from "typed-routes"; 17 | 18 | // Like "/data/profiles/:userId/info" in Express 19 | let path = createRoute() 20 | .extend("data", "profiles") 21 | .param("userId") 22 | .extend("info"); 23 | ``` 24 | 25 | Typed routes can be matched against strings to return the extracted params 26 | (if there's match) or to convert a params object to a param string 27 | 28 | ```ts 29 | path.match("/data/profiles/xyz/info"); // => { userId: "xyz" } 30 | path.match("/daat/profylez/xyz/info"); // => undefined 31 | ``` 32 | 33 | Typed routes can also convert a set of params to a string route. 34 | 35 | ```ts 36 | path.from({ userId: "xyz" }); // => "/data/profiles/xyz/info" 37 | path.from({ uzerid: "xyz" }); // => Type error 38 | ``` 39 | 40 | Routes may have optional types and rest types as well 41 | 42 | ```ts 43 | let path = createRoute().param("groupId").opt("userId").rest(); 44 | path.match("/gid/uid/a/b/c"); 45 | // => { groupId: "gid", userId: "uid", rest: ["a", "b", "c"] } 46 | path.match("/gid"); 47 | // => { groupId: "gid", rest: [] } 48 | ``` 49 | 50 | Routes can specify types. 51 | 52 | ```ts 53 | import { default as createRoute, IntType } from "typed-routes"; 54 | 55 | let path = createRoute().extend("profile").param("uid", IntType); 56 | path.match("/profile/123"); // => { uid: 123 } 57 | path.match("/profile/abc"); // => undefined 58 | 59 | path.from({ uid: 123 }); // => "/profile/123" 60 | path.from({ uid: "abc" }); // => Type error 61 | ``` 62 | 63 | Types are just an object with parse and stringify functions. For example, 64 | this is the definition of the `DateTimeParam` type, which converts a Date 65 | to milliseconds since epoch. 66 | 67 | ```ts 68 | const DateTimeParam = { 69 | parse: (s: string): Date|undefined => { 70 | let ret = new Date(parseInt(s)); 71 | return isNaN(ret.getTime()) ? undefined : ret; 72 | }, 73 | stringify: (d: Date): string => d.getTime().toString() 74 | }; 75 | ``` 76 | 77 | You can provide your own types for more 78 | customized behavior (such as returning a default value is one is undefined). 79 | 80 | API 81 | --- 82 | 83 | ### createRoute 84 | 85 | ```ts 86 | createRoute({ 87 | // What kind of "slash" separates paths? Defaults to "/". 88 | delimiter: string; 89 | 90 | // Require that our route starts with a certain prefix. Defaults to "/". 91 | prefix: string; 92 | 93 | // Require that our route ends with a certain suffix. Defaults to 94 | // empty string. 95 | suffix: string; 96 | }): Route; 97 | ``` 98 | 99 | Creates a route object with certain settings. 100 | 101 | 102 | ### Route.extend 103 | 104 | ```ts 105 | route.extend(...parts: string[]): Route; 106 | ``` 107 | 108 | Adds static segments to route that must match exactly. 109 | 110 | 111 | ### Route.param 112 | 113 | ```ts 114 | route.param(name: string, type: ParamType = StringParam): Route; 115 | ``` 116 | 117 | Add a required parameter and optional type. 118 | 119 | 120 | ### Route.opt 121 | 122 | ```ts 123 | route.opt(name: string, type: ParamType = StringParam): OptRoute; 124 | ``` 125 | 126 | Add an optional parameter and type. `.extend` and `.param` cannot follow 127 | a `.opt` command. 128 | 129 | 130 | ### Route.rest 131 | 132 | ```ts 133 | route.rest(type = StringParam): Route; 134 | route.rest(name = "rest", type = StringParam): Route; 135 | ``` 136 | 137 | Add a field that captures multiple parts of the path as an array. Defaults 138 | to using `rest` as the property but this can be changed. Type specifies 139 | what kind of array we're working with (e.g. use `IntParam` or `FloatParam` 140 | to type as `number[]`). 141 | 142 | 143 | Built-In Param Types 144 | -------------------- 145 | 146 | * `StrParam` - Default parameter. Types as string. 147 | 148 | * `IntParam` - Type as integer using `parseInt`. 149 | 150 | * `FloatParam` - Type as float using `parseFloat`. 151 | 152 | * `DateTimeParam` - Type as Date by serializing as milliseconds since epoch. 153 | 154 | * `ArrayParam(ParamType, delimiter = ",")` - A function that takes another 155 | param type and returns as param type that parses as an array of the 156 | original type. For instance, `ArrayParam(IntParam, "::")` will parse 157 | `1::2::3` as `[1, 2, 3]`. 158 | -------------------------------------------------------------------------------- /src/route.test.ts: -------------------------------------------------------------------------------- 1 | import test = require("tape"); 2 | import createRoute, { IntParam } from "./index"; 3 | 4 | 5 | test("route with exact paths", (assert) => { 6 | let route = createRoute().extend("a", "b").extend("c"); 7 | 8 | assert.deepEquals(route.match("/a/b/c"), {}, 9 | "returns object on exact match"); 10 | 11 | assert.equals(route.match("/a/b"), undefined, 12 | "returns undefined for incomplete matches"); 13 | 14 | assert.equals(route.match("/a/b/c/"), undefined, 15 | "returns undefined for over-match"); 16 | 17 | assert.deepEquals(route.from({}), "/a/b/c", 18 | "returns exact path with from method"); 19 | 20 | assert.end(); 21 | }); 22 | 23 | 24 | test("with params", (assert) => { 25 | let route = createRoute() 26 | .extend("path", "to") 27 | .param("param1") 28 | .param("param2"); 29 | 30 | assert.deepEquals(route.match("/path/to/abc/def"), { 31 | param1: "abc", 32 | param2: "def" 33 | }, "returns object with extracted strings on match"); 34 | 35 | assert.equals(route.match("/path/to/abc"), undefined, 36 | "returns undefined for incomplete matches"); 37 | 38 | assert.equals(route.match("/path/to/abc/def/ghi"), undefined, 39 | "returns undefined for over-match"); 40 | 41 | assert.equals(route.from({ 42 | param1: "abc", 43 | param2: "def" 44 | }), "/path/to/abc/def", "stringfies params with from method"); 45 | 46 | assert.equals(route.toString(), "/path/to/:param1/:param2", 47 | "returns ExpressJS-like pattern on toString"); 48 | 49 | assert.end(); 50 | }); 51 | 52 | 53 | test("with optional params", (assert) => { 54 | let route = createRoute() 55 | .extend("path", "to") 56 | .param("param1") 57 | .opt("param2"); 58 | 59 | assert.deepEquals(route.match("/path/to/abc/def"), { 60 | param1: "abc", 61 | param2: "def" 62 | }, "returns object with extracted strings if matching optional params"); 63 | 64 | assert.deepEquals(route.match("/path/to/abc"), { 65 | param1: "abc" 66 | }, "returns object with extracted strings even " + 67 | "if optional params don't match"); 68 | 69 | assert.equals(route.match("/path/to/abc/def/ghi"), undefined, 70 | "returns undefined for over-match"); 71 | 72 | assert.equals(route.from({ 73 | param1: "abc", 74 | param2: "def" 75 | }), "/path/to/abc/def", "stringfies optional params with from method"); 76 | 77 | assert.equals(route.from({ 78 | param1: "abc" 79 | }), "/path/to/abc", "stringfies without optional params with from method"); 80 | 81 | assert.equals(route.toString(), "/path/to/:param1/:param2?", 82 | "returns ExpressJS-like pattern on toString"); 83 | 84 | assert.end(); 85 | }); 86 | 87 | 88 | test("with rest params", (assert) => { 89 | let route = createRoute() 90 | .extend("path", "to") 91 | .opt("param1") 92 | .rest(); 93 | 94 | assert.deepEquals(route.match("/path/to/abc/def/ghi"), { 95 | param1: "abc", 96 | rest: ["def", "ghi"] 97 | }, "returns object with rest pointing to string arrays if matching"); 98 | 99 | assert.deepEquals(route.match("/path/to"), { 100 | rest: [] 101 | }, "returns empty list if nothing to extract but otherwise valid"); 102 | 103 | assert.deepEquals(route.match("/path/from"), undefined, 104 | "returns undefined if not matching"); 105 | 106 | assert.deepEquals(route.from({ 107 | param1: "abc", 108 | rest: ["def", "ghi"] 109 | }), "/path/to/abc/def/ghi", "stringfies rest params with from method"); 110 | 111 | assert.deepEquals(route.from({ 112 | rest: [] 113 | }), "/path/to", "doesn't stringify empty array with from method"); 114 | 115 | assert.deepEquals(route.toString(), "/path/to/:param1?/*", 116 | "returns ExpressJS-like pattern on toString"); 117 | 118 | assert.end(); 119 | }); 120 | 121 | 122 | test("with typed params", (assert) => { 123 | let route = createRoute() 124 | .extend("a") 125 | .param("b", IntParam) 126 | .opt("c", IntParam) 127 | .rest(IntParam); 128 | 129 | assert.deepEquals(route.match("/a/1/2/3/4/5"), { 130 | b: 1, 131 | c: 2, 132 | rest: [3, 4, 5] 133 | }, "Converts to type when matching"); 134 | 135 | assert.deepEquals(route.match("/a/1"), { 136 | b: 1, 137 | rest: [] 138 | }, "Preserves opt- and rest-param behavior when matching"); 139 | 140 | assert.equals(route.match("/a/b/2/3/4/5"), undefined, 141 | "Rejects paths that don't match type conversion"); 142 | 143 | assert.equals(route.match("/a/1/c/3/4/5"), undefined, 144 | "Rejects paths that fail opt type conversion"); 145 | 146 | assert.equals(route.match("/a/1/2/3/e/5"), undefined, 147 | "Rejects paths that don't match rest type conversion"); 148 | 149 | assert.end(); 150 | }); 151 | 152 | 153 | test("with named rest params", (assert) => { 154 | let route1 = createRoute().rest("letters"); 155 | assert.deepEquals(route1.match("/a/b/c"), { 156 | letters: ["a", "b", "c"] 157 | }, "uses name for rest array"); 158 | 159 | let route2 = createRoute().rest("numbers", IntParam); 160 | assert.deepEquals(route2.match("/1/2/3"), { 161 | numbers: [1, 2, 3] 162 | }, "uses name for typed rest array"); 163 | 164 | assert.end(); 165 | }); 166 | 167 | 168 | test("with custom prefix", (assert) => { 169 | let route = createRoute({ prefix: "/#!/" }).extend("a").param("b"); 170 | 171 | assert.deepEquals(route.match("/#!/a/c"), { 172 | b: "c" 173 | }, "matches paths with prefix"); 174 | 175 | assert.equals(route.match("/a/c"), undefined, "rejects paths without prefix"); 176 | 177 | assert.equals(route.from({ 178 | b: "c" 179 | }), "/#!/a/c", "returns path with prefix with from method"); 180 | 181 | assert.equals(route.toString(), "/#!/a/:b", "includes prefix in toString"); 182 | 183 | assert.end(); 184 | }); 185 | 186 | 187 | test("with empty prefix", (assert) => { 188 | let route = createRoute({ prefix: "" }).extend("a").param("b"); 189 | 190 | assert.deepEquals(route.match("a/c"), { 191 | b: "c" 192 | }, "matches paths without prefix"); 193 | 194 | assert.equals(route.match("/a/c"), undefined, "rejects paths with prefix"); 195 | 196 | assert.equals(route.from({ 197 | b: "c" 198 | }), "a/c", "returns path without prefix with from method"); 199 | 200 | assert.end(); 201 | }); 202 | 203 | 204 | test("allows custom suffix", (assert) => { 205 | let route = createRoute({ suffix: "?x=1" }).extend("a").param("b"); 206 | 207 | assert.deepEquals(route.match("/a/c?x=1"), { 208 | b: "c" 209 | }, "matches paths with suffix"); 210 | 211 | assert.equals(route.match("/a/c"), undefined, "rejects paths without suffix"); 212 | 213 | assert.equals(route.from({ 214 | b: "c" 215 | }), "/a/c?x=1", "returns path with suffix with from method"); 216 | 217 | assert.equals(route.toString(), "/a/:b?x=1", "includes suffix in toString"); 218 | 219 | assert.end(); 220 | }); 221 | 222 | 223 | test("allows custom delimiter", (assert) => { 224 | let route = createRoute({ delimiter: "." }) 225 | .extend("a") 226 | .param("b") 227 | .rest(IntParam); 228 | 229 | assert.deepEquals(route.match("/a.c.1.2.3"), { 230 | b: "c", 231 | rest: [1, 2, 3] 232 | }, "matches path using custom delimiter"); 233 | 234 | assert.equals(route.from({ 235 | b: "c", 236 | rest: [1, 2, 3] 237 | }), "/a.c.1.2.3", "Uses custom delimiter when stringifying with from"); 238 | 239 | assert.end(); 240 | }); -------------------------------------------------------------------------------- /src/route.ts: -------------------------------------------------------------------------------- 1 | import { ParamType, StrParam } from "./param-types"; 2 | 3 | // Helper functions for extending mmap types 4 | export type MapParam = { [P in K]: T }; 5 | export type OptParam = { [P in K]?: T }; 6 | 7 | /* 8 | Internal types for representing parts of path 9 | */ 10 | 11 | export interface Param { 12 | type: "PARAM"; 13 | name: string; 14 | required: boolean; 15 | paramType: ParamType; 16 | } 17 | 18 | export interface RestParam { 19 | type: "REST"; 20 | name: string; 21 | paramType: ParamType; 22 | } 23 | 24 | // NB: string = match exactly 25 | export type Part = string|Param|RestParam; 26 | 27 | // Options for routes 28 | export interface FullRouteOpts { 29 | // What kind of "slash" separates paths? 30 | delimiter: string; 31 | 32 | // What does path start with? 33 | prefix: string; 34 | 35 | // What does path endwith 36 | suffix: string; 37 | } 38 | 39 | export type RouteOpts = Partial; 40 | 41 | const DEFAULT_ROUTE_OPTS: FullRouteOpts = { 42 | delimiter: "/", 43 | prefix: "/", // Leading slash 44 | suffix: "" // No trailing slash 45 | }; 46 | 47 | /* 48 | Base class for OptRoute and Route. Not created directly but with the rest 49 | method of OptRoute and Route. Once a Route or OptRoute becomes an RestRoute, 50 | it loses its ability to add additional params. 51 | */ 52 | export class RestRoute< 53 | P = {} // Type of params when converting to params from string 54 | > { /* tslint:disable-line */ 55 | opts: FullRouteOpts; 56 | parts: Part[] = []; 57 | 58 | // Returns the matching params for a given string. Undefined if no match. 59 | match(val: string): P|undefined { 60 | let { delimiter, prefix, suffix } = this.opts; 61 | if (val.slice(0, prefix.length) === prefix) { 62 | val = val.slice(prefix.length); 63 | } else { 64 | return undefined; 65 | } 66 | 67 | if (val.slice(val.length - suffix.length, val.length) === suffix) { 68 | // NB: val.length - suffix.length b/c str.slice(0, -0) === "" 69 | val = val.slice(0, val.length - suffix.length); 70 | } else { 71 | return undefined; 72 | } 73 | 74 | let ret: Partial

= {}; 75 | let strParts = val.split(delimiter); 76 | for (let i in this.parts) { 77 | let expected = this.parts[i]; 78 | let actual = strParts.shift(); 79 | 80 | // If param, assign param to key. 81 | if (typeof expected !== "string") { 82 | let { paramType, name } = expected; 83 | 84 | // Rest params consume remainder 85 | if (expected.type === "REST") { 86 | let rest: any[] = (ret as any)[name] = []; 87 | while (actual) { 88 | let v = paramType.parse(actual); 89 | if (v === void 0) { 90 | return undefined; 91 | } 92 | rest.push(v); 93 | actual = strParts.shift(); 94 | } 95 | } 96 | 97 | // Parse single param 98 | else { 99 | let v = actual && paramType.parse(actual); 100 | if (v === void 0) { 101 | // Reject if required or truthy actual (implies parsing failed) 102 | if (expected.required || actual) { 103 | return undefined; 104 | } 105 | } else { 106 | ret[name] = v; 107 | } 108 | } 109 | } 110 | 111 | // Not param, return null if not exact match 112 | else if (actual !== expected) { 113 | return undefined; 114 | } 115 | } 116 | 117 | // Did not match all parts, reject 118 | if (strParts.length) return undefined; 119 | 120 | return ret as P; 121 | } 122 | 123 | // Converts param objects to string 124 | from(params: P): string { 125 | let retParts: string[] = []; 126 | for (let i in this.parts) { 127 | let part = this.parts[i]; 128 | if (typeof part === "string") { 129 | retParts.push(part); 130 | } else if (part.type === "REST") { 131 | let val = (params as any)[part.name]; 132 | if (val instanceof Array) { 133 | for (let i in val) { 134 | retParts.push(part.paramType.stringify(val[i])); 135 | } 136 | } else { 137 | throw new Error("Expected array for " + part.name); 138 | } 139 | } else { 140 | let val = (params as any)[part.name]; 141 | if (val !== void 0) { 142 | retParts.push(part.paramType.stringify(val)); 143 | } else if (part.required === true) { 144 | throw new Error("Expected value for " + part.name); 145 | } 146 | } 147 | } 148 | return this.join(retParts); 149 | } 150 | 151 | // ExpressJS-style path 152 | toString(): string { 153 | let retParts: string[] = []; 154 | for (let i in this.parts) { 155 | let part = this.parts[i]; 156 | if (typeof part === "string") { 157 | retParts.push(part); 158 | } else if (part.type === "REST") { 159 | retParts.push("*") 160 | } else { 161 | retParts.push(":" + part.name + (part.required ? "" : "?")); 162 | } 163 | } 164 | return this.join(retParts); 165 | } 166 | 167 | // Helper function that adds leading and trailing delimiters 168 | protected join(parts: string[]) { 169 | let { prefix, suffix, delimiter } = this.opts; 170 | return prefix + parts.join(delimiter) + suffix; 171 | } 172 | } 173 | 174 | /* 175 | Base class for Route. Not created directly but with the opt method of a 176 | Route. Once a Route becomes an OptRoute, it loses its ability to add 177 | required params and other parts. 178 | */ 179 | export class OptRoute

extends RestRoute

{ /* tslint:disable-line */ 180 | 181 | // clone (with new part) 182 | protected add< 183 | R extends RestRoute, 184 | C extends typeof RestRoute 185 | > (part: Part, cls: C): R { 186 | return Object.create(cls.prototype, { 187 | opts: { 188 | value: this.opts 189 | }, 190 | parts: { 191 | value: this.parts.concat([part]) 192 | } 193 | }); 194 | } 195 | 196 | /* 197 | Add an optional part to route. Returns an OptRoute (i.e. no more extend or 198 | non-optional params because optional params always follows string and 199 | required params. 200 | */ 201 | opt( 202 | name: K, 203 | paramType?: ParamType 204 | ): OptRoute

> { 205 | return this.add({ 206 | type: "PARAM", 207 | name, 208 | required: false, 209 | paramType: paramType || StrParam 210 | }, OptRoute); 211 | } 212 | 213 | /* 214 | Add a remainder capture to route. Returns a RestRoute (i.e. no more params 215 | or anything else because remaidner is always a the end) 216 | */ 217 | rest( 218 | paramType?: ParamType 219 | ): RestRoute

>; 220 | rest( 221 | name: K, 222 | paramType?: ParamType 223 | ): RestRoute

>; 224 | rest( 225 | first?: K|ParamType, 226 | second?: ParamType 227 | ): RestRoute

> { 228 | let name: K|undefined; 229 | let paramType: ParamType|undefined; 230 | if (typeof first === "string") { 231 | name = first; 232 | if (second) { 233 | paramType = second; 234 | } 235 | } else if (first) { 236 | paramType = first; 237 | } 238 | return this.add({ 239 | type: "REST", 240 | name: name || "rest", 241 | paramType: paramType || StrParam 242 | }, RestRoute); 243 | } 244 | } 245 | 246 | /* 247 | Chainable method for creating routes. 248 | */ 249 | export class Route

extends OptRoute

{ /* tslint:disable-line */ 250 | opts: FullRouteOpts; 251 | parts: Part[] = []; 252 | 253 | constructor(opts: RouteOpts = {}) { 254 | super(); 255 | this.opts = { ...DEFAULT_ROUTE_OPTS, ...opts }; 256 | } 257 | 258 | // Extend route with a new part that does not correspond to some part 259 | extend(...names: string[]): this { 260 | let t = this; 261 | for (let i in names) { 262 | t = t.add(names[i], Route); 263 | } 264 | return t; 265 | } 266 | 267 | // Add a required param to route 268 | param( 269 | name: K, 270 | paramType?: ParamType 271 | ): Route

> { 272 | return this.add({ 273 | type: "PARAM", 274 | name, 275 | required: true, 276 | paramType: paramType || StrParam 277 | }, Route); 278 | } 279 | } 280 | 281 | 282 | /* Syntactic sugar for not having to write "new" */ 283 | export const createRoute = (opts: RouteOpts = {}) => new Route(opts); 284 | 285 | export default createRoute; -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@types/blue-tape@^0.1.31": 6 | version "0.1.31" 7 | resolved "https://registry.yarnpkg.com/@types/blue-tape/-/blue-tape-0.1.31.tgz#67e0aa9062015fc5fc40db5834162723327ad915" 8 | dependencies: 9 | "@types/node" "*" 10 | "@types/tape" "*" 11 | 12 | "@types/node@*": 13 | version "8.0.17" 14 | resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.17.tgz#677bc8c118cfb76013febb62ede1f31d2c7222a1" 15 | 16 | "@types/sinon@^2.3.2": 17 | version "2.3.3" 18 | resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-2.3.3.tgz#1f20b96f954b4997a09c1c0a20264aaba6b00147" 19 | 20 | "@types/tape@*": 21 | version "4.2.30" 22 | resolved "https://registry.yarnpkg.com/@types/tape/-/tape-4.2.30.tgz#3c1917c4dfd6f27271b9922772513515bc6c46b4" 23 | dependencies: 24 | "@types/node" "*" 25 | 26 | ansi-regex@^2.0.0: 27 | version "2.1.1" 28 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" 29 | 30 | ansi-styles@^2.2.1: 31 | version "2.2.1" 32 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" 33 | 34 | ansi-styles@^3.1.0: 35 | version "3.2.0" 36 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" 37 | dependencies: 38 | color-convert "^1.9.0" 39 | 40 | arrify@^1.0.0: 41 | version "1.0.1" 42 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 43 | 44 | babel-code-frame@^6.22.0: 45 | version "6.22.0" 46 | resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4" 47 | dependencies: 48 | chalk "^1.1.0" 49 | esutils "^2.0.2" 50 | js-tokens "^3.0.0" 51 | 52 | balanced-match@^1.0.0: 53 | version "1.0.0" 54 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 55 | 56 | brace-expansion@^1.1.7: 57 | version "1.1.8" 58 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" 59 | dependencies: 60 | balanced-match "^1.0.0" 61 | concat-map "0.0.1" 62 | 63 | chalk@^1.0.0, chalk@^1.1.0: 64 | version "1.1.3" 65 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" 66 | dependencies: 67 | ansi-styles "^2.2.1" 68 | escape-string-regexp "^1.0.2" 69 | has-ansi "^2.0.0" 70 | strip-ansi "^3.0.0" 71 | supports-color "^2.0.0" 72 | 73 | chalk@^2.0.0: 74 | version "2.0.1" 75 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.0.1.tgz#dbec49436d2ae15f536114e76d14656cdbc0f44d" 76 | dependencies: 77 | ansi-styles "^3.1.0" 78 | escape-string-regexp "^1.0.5" 79 | supports-color "^4.0.0" 80 | 81 | color-convert@^1.9.0: 82 | version "1.9.0" 83 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.0.tgz#1accf97dd739b983bf994d56fec8f95853641b7a" 84 | dependencies: 85 | color-name "^1.1.1" 86 | 87 | color-name@^1.1.1: 88 | version "1.1.3" 89 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 90 | 91 | colors@^1.1.2: 92 | version "1.1.2" 93 | resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63" 94 | 95 | commander@^2.9.0: 96 | version "2.11.0" 97 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" 98 | 99 | concat-map@0.0.1: 100 | version "0.0.1" 101 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 102 | 103 | core-util-is@~1.0.0: 104 | version "1.0.2" 105 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 106 | 107 | deep-equal@~1.0.1: 108 | version "1.0.1" 109 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 110 | 111 | define-properties@^1.1.2: 112 | version "1.1.2" 113 | resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.2.tgz#83a73f2fea569898fb737193c8f873caf6d45c94" 114 | dependencies: 115 | foreach "^2.0.5" 116 | object-keys "^1.0.8" 117 | 118 | defined@~1.0.0: 119 | version "1.0.0" 120 | resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" 121 | 122 | diff@^3.1.0, diff@^3.2.0: 123 | version "3.3.0" 124 | resolved "https://registry.yarnpkg.com/diff/-/diff-3.3.0.tgz#056695150d7aa93237ca7e378ac3b1682b7963b9" 125 | 126 | duplexer@^0.1.1: 127 | version "0.1.1" 128 | resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.1.tgz#ace6ff808c1ce66b57d1ebf97977acb02334cfc1" 129 | 130 | es-abstract@^1.5.0: 131 | version "1.7.0" 132 | resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.7.0.tgz#dfade774e01bfcd97f96180298c449c8623fb94c" 133 | dependencies: 134 | es-to-primitive "^1.1.1" 135 | function-bind "^1.1.0" 136 | is-callable "^1.1.3" 137 | is-regex "^1.0.3" 138 | 139 | es-to-primitive@^1.1.1: 140 | version "1.1.1" 141 | resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.1.1.tgz#45355248a88979034b6792e19bb81f2b7975dd0d" 142 | dependencies: 143 | is-callable "^1.1.1" 144 | is-date-object "^1.0.1" 145 | is-symbol "^1.0.1" 146 | 147 | escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: 148 | version "1.0.5" 149 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 150 | 151 | esutils@^2.0.2: 152 | version "2.0.2" 153 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 154 | 155 | figures@^1.4.0: 156 | version "1.7.0" 157 | resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" 158 | dependencies: 159 | escape-string-regexp "^1.0.5" 160 | object-assign "^4.1.0" 161 | 162 | for-each@~0.3.2: 163 | version "0.3.2" 164 | resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.2.tgz#2c40450b9348e97f281322593ba96704b9abd4d4" 165 | dependencies: 166 | is-function "~1.0.0" 167 | 168 | foreach@^2.0.5: 169 | version "2.0.5" 170 | resolved "https://registry.yarnpkg.com/foreach/-/foreach-2.0.5.tgz#0bee005018aeb260d0a3af3ae658dd0136ec1b99" 171 | 172 | formatio@1.2.0: 173 | version "1.2.0" 174 | resolved "https://registry.yarnpkg.com/formatio/-/formatio-1.2.0.tgz#f3b2167d9068c4698a8d51f4f760a39a54d818eb" 175 | dependencies: 176 | samsam "1.x" 177 | 178 | fs.realpath@^1.0.0: 179 | version "1.0.0" 180 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 181 | 182 | function-bind@^1.0.2, function-bind@^1.1.0, function-bind@~1.1.0: 183 | version "1.1.0" 184 | resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.0.tgz#16176714c801798e4e8f2cf7f7529467bb4a5771" 185 | 186 | glob@^7.1.1, glob@~7.1.2: 187 | version "7.1.2" 188 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 189 | dependencies: 190 | fs.realpath "^1.0.0" 191 | inflight "^1.0.4" 192 | inherits "2" 193 | minimatch "^3.0.4" 194 | once "^1.3.0" 195 | path-is-absolute "^1.0.0" 196 | 197 | has-ansi@^2.0.0: 198 | version "2.0.0" 199 | resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" 200 | dependencies: 201 | ansi-regex "^2.0.0" 202 | 203 | has-flag@^2.0.0: 204 | version "2.0.0" 205 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" 206 | 207 | has@^1.0.1, has@~1.0.1: 208 | version "1.0.1" 209 | resolved "https://registry.yarnpkg.com/has/-/has-1.0.1.tgz#8461733f538b0837c9361e39a9ab9e9704dc2f28" 210 | dependencies: 211 | function-bind "^1.0.2" 212 | 213 | inflight@^1.0.4: 214 | version "1.0.6" 215 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 216 | dependencies: 217 | once "^1.3.0" 218 | wrappy "1" 219 | 220 | inherits@2, inherits@~2.0.3: 221 | version "2.0.3" 222 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 223 | 224 | is-callable@^1.1.1, is-callable@^1.1.3: 225 | version "1.1.3" 226 | resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.3.tgz#86eb75392805ddc33af71c92a0eedf74ee7604b2" 227 | 228 | is-date-object@^1.0.1: 229 | version "1.0.1" 230 | resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" 231 | 232 | is-finite@^1.0.1: 233 | version "1.0.2" 234 | resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" 235 | dependencies: 236 | number-is-nan "^1.0.0" 237 | 238 | is-function@~1.0.0: 239 | version "1.0.1" 240 | resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" 241 | 242 | is-regex@^1.0.3: 243 | version "1.0.4" 244 | resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" 245 | dependencies: 246 | has "^1.0.1" 247 | 248 | is-symbol@^1.0.1: 249 | version "1.0.1" 250 | resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.1.tgz#3cc59f00025194b6ab2e38dbae6689256b660572" 251 | 252 | isarray@0.0.1: 253 | version "0.0.1" 254 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" 255 | 256 | isarray@~1.0.0: 257 | version "1.0.0" 258 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 259 | 260 | js-tokens@^3.0.0: 261 | version "3.0.2" 262 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" 263 | 264 | lodash@^3.6.0: 265 | version "3.10.1" 266 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" 267 | 268 | lolex@^1.6.0: 269 | version "1.6.0" 270 | resolved "https://registry.yarnpkg.com/lolex/-/lolex-1.6.0.tgz#3a9a0283452a47d7439e72731b9e07d7386e49f6" 271 | 272 | make-error@^1.1.1: 273 | version "1.3.0" 274 | resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.0.tgz#52ad3a339ccf10ce62b4040b708fe707244b8b96" 275 | 276 | minimatch@^3.0.4: 277 | version "3.0.4" 278 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 279 | dependencies: 280 | brace-expansion "^1.1.7" 281 | 282 | minimist@0.0.8: 283 | version "0.0.8" 284 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 285 | 286 | minimist@^1.2.0, minimist@~1.2.0: 287 | version "1.2.0" 288 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 289 | 290 | mkdirp@^0.5.1: 291 | version "0.5.1" 292 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 293 | dependencies: 294 | minimist "0.0.8" 295 | 296 | native-promise-only@^0.8.1: 297 | version "0.8.1" 298 | resolved "https://registry.yarnpkg.com/native-promise-only/-/native-promise-only-0.8.1.tgz#20a318c30cb45f71fe7adfbf7b21c99c1472ef11" 299 | 300 | number-is-nan@^1.0.0: 301 | version "1.0.1" 302 | resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" 303 | 304 | object-assign@^4.1.0: 305 | version "4.1.1" 306 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 307 | 308 | object-inspect@~1.3.0: 309 | version "1.3.0" 310 | resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.3.0.tgz#5b1eb8e6742e2ee83342a637034d844928ba2f6d" 311 | 312 | object-keys@^1.0.8: 313 | version "1.0.11" 314 | resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.0.11.tgz#c54601778ad560f1142ce0e01bcca8b56d13426d" 315 | 316 | once@^1.3.0: 317 | version "1.4.0" 318 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 319 | dependencies: 320 | wrappy "1" 321 | 322 | parse-ms@^1.0.0: 323 | version "1.0.1" 324 | resolved "https://registry.yarnpkg.com/parse-ms/-/parse-ms-1.0.1.tgz#56346d4749d78f23430ca0c713850aef91aa361d" 325 | 326 | path-is-absolute@^1.0.0: 327 | version "1.0.1" 328 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 329 | 330 | path-parse@^1.0.5: 331 | version "1.0.5" 332 | resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" 333 | 334 | path-to-regexp@^1.7.0: 335 | version "1.7.0" 336 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d" 337 | dependencies: 338 | isarray "0.0.1" 339 | 340 | plur@^1.0.0: 341 | version "1.0.0" 342 | resolved "https://registry.yarnpkg.com/plur/-/plur-1.0.0.tgz#db85c6814f5e5e5a3b49efc28d604fec62975156" 343 | 344 | pretty-ms@^2.1.0: 345 | version "2.1.0" 346 | resolved "https://registry.yarnpkg.com/pretty-ms/-/pretty-ms-2.1.0.tgz#4257c256df3fb0b451d6affaab021884126981dc" 347 | dependencies: 348 | is-finite "^1.0.1" 349 | parse-ms "^1.0.0" 350 | plur "^1.0.0" 351 | 352 | process-nextick-args@~1.0.6: 353 | version "1.0.7" 354 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 355 | 356 | re-emitter@^1.0.0: 357 | version "1.1.3" 358 | resolved "https://registry.yarnpkg.com/re-emitter/-/re-emitter-1.1.3.tgz#fa9e319ffdeeeb35b27296ef0f3d374dac2f52a7" 359 | 360 | readable-stream@^2.0.0, readable-stream@^2.1.5: 361 | version "2.3.3" 362 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" 363 | dependencies: 364 | core-util-is "~1.0.0" 365 | inherits "~2.0.3" 366 | isarray "~1.0.0" 367 | process-nextick-args "~1.0.6" 368 | safe-buffer "~5.1.1" 369 | string_decoder "~1.0.3" 370 | util-deprecate "~1.0.1" 371 | 372 | repeat-string@^1.5.2: 373 | version "1.6.1" 374 | resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" 375 | 376 | resolve@^1.3.2, resolve@~1.4.0: 377 | version "1.4.0" 378 | resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.4.0.tgz#a75be01c53da25d934a98ebd0e4c4a7312f92a86" 379 | dependencies: 380 | path-parse "^1.0.5" 381 | 382 | resumer@~0.0.0: 383 | version "0.0.0" 384 | resolved "https://registry.yarnpkg.com/resumer/-/resumer-0.0.0.tgz#f1e8f461e4064ba39e82af3cdc2a8c893d076759" 385 | dependencies: 386 | through "~2.3.4" 387 | 388 | safe-buffer@~5.1.0, safe-buffer@~5.1.1: 389 | version "5.1.1" 390 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 391 | 392 | samsam@1.x, samsam@^1.1.3: 393 | version "1.2.1" 394 | resolved "https://registry.yarnpkg.com/samsam/-/samsam-1.2.1.tgz#edd39093a3184370cb859243b2bdf255e7d8ea67" 395 | 396 | semver@^5.3.0: 397 | version "5.4.1" 398 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" 399 | 400 | sinon@^2.3.7: 401 | version "2.4.1" 402 | resolved "https://registry.yarnpkg.com/sinon/-/sinon-2.4.1.tgz#021fd64b54cb77d9d2fb0d43cdedfae7629c3a36" 403 | dependencies: 404 | diff "^3.1.0" 405 | formatio "1.2.0" 406 | lolex "^1.6.0" 407 | native-promise-only "^0.8.1" 408 | path-to-regexp "^1.7.0" 409 | samsam "^1.1.3" 410 | text-encoding "0.6.4" 411 | type-detect "^4.0.0" 412 | 413 | source-map-support@^0.4.0: 414 | version "0.4.15" 415 | resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.15.tgz#03202df65c06d2bd8c7ec2362a193056fef8d3b1" 416 | dependencies: 417 | source-map "^0.5.6" 418 | 419 | source-map@^0.5.6: 420 | version "0.5.6" 421 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 422 | 423 | split@^1.0.0: 424 | version "1.0.0" 425 | resolved "https://registry.yarnpkg.com/split/-/split-1.0.0.tgz#c4395ce683abcd254bc28fe1dabb6e5c27dcffae" 426 | dependencies: 427 | through "2" 428 | 429 | string.prototype.trim@~1.1.2: 430 | version "1.1.2" 431 | resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.1.2.tgz#d04de2c89e137f4d7d206f086b5ed2fae6be8cea" 432 | dependencies: 433 | define-properties "^1.1.2" 434 | es-abstract "^1.5.0" 435 | function-bind "^1.0.2" 436 | 437 | string_decoder@~1.0.3: 438 | version "1.0.3" 439 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 440 | dependencies: 441 | safe-buffer "~5.1.0" 442 | 443 | strip-ansi@^3.0.0: 444 | version "3.0.1" 445 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" 446 | dependencies: 447 | ansi-regex "^2.0.0" 448 | 449 | strip-bom@^3.0.0: 450 | version "3.0.0" 451 | resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" 452 | 453 | strip-json-comments@^2.0.0: 454 | version "2.0.1" 455 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 456 | 457 | supports-color@^2.0.0: 458 | version "2.0.0" 459 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" 460 | 461 | supports-color@^4.0.0: 462 | version "4.2.1" 463 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.2.1.tgz#65a4bb2631e90e02420dba5554c375a4754bb836" 464 | dependencies: 465 | has-flag "^2.0.0" 466 | 467 | tap-out@^1.4.1: 468 | version "1.4.2" 469 | resolved "https://registry.yarnpkg.com/tap-out/-/tap-out-1.4.2.tgz#c907ec1bf9405111d088263e92f5608b88cbb37a" 470 | dependencies: 471 | re-emitter "^1.0.0" 472 | readable-stream "^2.0.0" 473 | split "^1.0.0" 474 | trim "0.0.1" 475 | 476 | tap-spec@^4.1.1: 477 | version "4.1.1" 478 | resolved "https://registry.yarnpkg.com/tap-spec/-/tap-spec-4.1.1.tgz#e2e9f26f5208232b1f562288c97624d58a88f05a" 479 | dependencies: 480 | chalk "^1.0.0" 481 | duplexer "^0.1.1" 482 | figures "^1.4.0" 483 | lodash "^3.6.0" 484 | pretty-ms "^2.1.0" 485 | repeat-string "^1.5.2" 486 | tap-out "^1.4.1" 487 | through2 "^2.0.0" 488 | 489 | tape@^4.8.0: 490 | version "4.8.0" 491 | resolved "https://registry.yarnpkg.com/tape/-/tape-4.8.0.tgz#f6a9fec41cc50a1de50fa33603ab580991f6068e" 492 | dependencies: 493 | deep-equal "~1.0.1" 494 | defined "~1.0.0" 495 | for-each "~0.3.2" 496 | function-bind "~1.1.0" 497 | glob "~7.1.2" 498 | has "~1.0.1" 499 | inherits "~2.0.3" 500 | minimist "~1.2.0" 501 | object-inspect "~1.3.0" 502 | resolve "~1.4.0" 503 | resumer "~0.0.0" 504 | string.prototype.trim "~1.1.2" 505 | through "~2.3.8" 506 | 507 | text-encoding@0.6.4: 508 | version "0.6.4" 509 | resolved "https://registry.yarnpkg.com/text-encoding/-/text-encoding-0.6.4.tgz#e399a982257a276dae428bb92845cb71bdc26d19" 510 | 511 | through2@^2.0.0: 512 | version "2.0.3" 513 | resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.3.tgz#0004569b37c7c74ba39c43f3ced78d1ad94140be" 514 | dependencies: 515 | readable-stream "^2.1.5" 516 | xtend "~4.0.1" 517 | 518 | through@2, through@~2.3.4, through@~2.3.8: 519 | version "2.3.8" 520 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 521 | 522 | trim@0.0.1: 523 | version "0.0.1" 524 | resolved "https://registry.yarnpkg.com/trim/-/trim-0.0.1.tgz#5858547f6b290757ee95cccc666fb50084c460dd" 525 | 526 | ts-node@^3.1.0: 527 | version "3.3.0" 528 | resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-3.3.0.tgz#c13c6a3024e30be1180dd53038fc209289d4bf69" 529 | dependencies: 530 | arrify "^1.0.0" 531 | chalk "^2.0.0" 532 | diff "^3.1.0" 533 | make-error "^1.1.1" 534 | minimist "^1.2.0" 535 | mkdirp "^0.5.1" 536 | source-map-support "^0.4.0" 537 | tsconfig "^6.0.0" 538 | v8flags "^3.0.0" 539 | yn "^2.0.0" 540 | 541 | tsconfig@^6.0.0: 542 | version "6.0.0" 543 | resolved "https://registry.yarnpkg.com/tsconfig/-/tsconfig-6.0.0.tgz#6b0e8376003d7af1864f8df8f89dd0059ffcd032" 544 | dependencies: 545 | strip-bom "^3.0.0" 546 | strip-json-comments "^2.0.0" 547 | 548 | tslib@^1.7.1: 549 | version "1.7.1" 550 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.7.1.tgz#bc8004164691923a79fe8378bbeb3da2017538ec" 551 | 552 | tslint@^5.4.3: 553 | version "5.5.0" 554 | resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.5.0.tgz#10e8dab3e3061fa61e9442e8cee3982acf20a6aa" 555 | dependencies: 556 | babel-code-frame "^6.22.0" 557 | colors "^1.1.2" 558 | commander "^2.9.0" 559 | diff "^3.2.0" 560 | glob "^7.1.1" 561 | minimatch "^3.0.4" 562 | resolve "^1.3.2" 563 | semver "^5.3.0" 564 | tslib "^1.7.1" 565 | tsutils "^2.5.1" 566 | 567 | tsutils@^2.5.1: 568 | version "2.8.0" 569 | resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-2.8.0.tgz#0160173729b3bf138628dd14a1537e00851d814a" 570 | dependencies: 571 | tslib "^1.7.1" 572 | 573 | type-detect@^4.0.0: 574 | version "4.0.3" 575 | resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.3.tgz#0e3f2670b44099b0b46c284d136a7ef49c74c2ea" 576 | 577 | typescript@^2.4.1: 578 | version "2.4.2" 579 | resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.2.tgz#f8395f85d459276067c988aa41837a8f82870844" 580 | 581 | user-home@^1.1.1: 582 | version "1.1.1" 583 | resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" 584 | 585 | util-deprecate@~1.0.1: 586 | version "1.0.2" 587 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 588 | 589 | v8flags@^3.0.0: 590 | version "3.0.0" 591 | resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-3.0.0.tgz#4be9604488e0c4123645def705b1848d16b8e01f" 592 | dependencies: 593 | user-home "^1.1.1" 594 | 595 | wrappy@1: 596 | version "1.0.2" 597 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 598 | 599 | xtend@~4.0.1: 600 | version "4.0.1" 601 | resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" 602 | 603 | yn@^2.0.0: 604 | version "2.0.0" 605 | resolved "https://registry.yarnpkg.com/yn/-/yn-2.0.0.tgz#e5adabc8acf408f6385fc76495684c88e6af689a" 606 | --------------------------------------------------------------------------------