-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1 from leancodepl/feature/initial-implementation
Feature/initial implementation
- Loading branch information
Showing
26 changed files
with
7,683 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* eslint-env node */ | ||
module.exports = { | ||
parser: "@typescript-eslint/parser", | ||
plugins: ["@typescript-eslint", "prettier"], | ||
extends: [ | ||
"eslint:recommended", | ||
"plugin:@typescript-eslint/eslint-recommended", | ||
"plugin:@typescript-eslint/recommended", | ||
"plugin:prettier/recommended", | ||
"prettier/@typescript-eslint", | ||
], | ||
parserOptions: { | ||
ecmaVersion: 2018, | ||
sourceType: "module", | ||
ecmaFeatures: { | ||
jsx: true, | ||
}, | ||
}, | ||
rules: { | ||
"no-var": "off", | ||
"no-console": ["warn", { allow: ["warn", "error", "assert"] }], | ||
"max-params": ["error", { max: 4 }], | ||
|
||
"prettier/prettier": "warn", | ||
|
||
"@typescript-eslint/explicit-function-return-type": "off", | ||
"@typescript-eslint/no-explicit-any": "off", | ||
"@typescript-eslint/prefer-interface": "off", | ||
"@typescript-eslint/explicit-member-accessibility": "off", | ||
"@typescript-eslint/no-parameter-properties": "off", | ||
"@typescript-eslint/no-empty-function": "off", | ||
"@typescript-eslint/no-use-before-define": "off", | ||
"@typescript-eslint/no-var-requires": "off", | ||
"@typescript-eslint/camelcase": "off", | ||
"@typescript-eslint/no-empty-interface": "off", | ||
"@typescript-eslint/no-inferrable-types": "off", | ||
"@typescript-eslint/explicit-module-boundary-types": "off", | ||
"@typescript-eslint/ban-types": "off", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
name: build | ||
|
||
on: | ||
push: | ||
branches: | ||
- master | ||
pull_request: | ||
|
||
jobs: | ||
build: | ||
name: Build | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
- name: Install modules | ||
run: npm ci | ||
- name: Run ESLint | ||
run: npm run lint | ||
- name: Run tests | ||
run: npm test | ||
- name: Test types | ||
run: npm run test:types | ||
- name: Build | ||
run: npm run build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
# Logs | ||
logs | ||
*.log | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
lerna-debug.log* | ||
|
||
# Runtime data | ||
pids | ||
*.pid | ||
*.seed | ||
*.pid.lock | ||
|
||
# Directory for instrumented libs generated by jscoverage/JSCover | ||
lib-cov | ||
|
||
# Coverage directory used by tools like istanbul | ||
coverage | ||
|
||
# nyc test coverage | ||
.nyc_output | ||
|
||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) | ||
.grunt | ||
|
||
# Bower dependency directory (https://bower.io/) | ||
bower_components | ||
|
||
# node-waf configuration | ||
.lock-wscript | ||
|
||
# Compiled binary addons (http://nodejs.org/api/addons.html) | ||
build/Release | ||
|
||
# Dependency directories | ||
node_modules/ | ||
jspm_packages/ | ||
|
||
# Typescript v1 declaration files | ||
typings/ | ||
|
||
# Optional npm cache directory | ||
.npm | ||
|
||
# Optional eslint cache | ||
.eslintcache | ||
|
||
# Optional REPL history | ||
.node_repl_history | ||
|
||
# Output of 'npm pack' | ||
*.tgz | ||
|
||
# Yarn | ||
.yarn-integrity | ||
.yarn/* | ||
!.yarn/releases | ||
!.yarn/plugins | ||
.pnp.* | ||
|
||
# Dotenv environment variables file | ||
.env | ||
|
||
# Compiled binaries | ||
lib |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
module.exports = { | ||
printWidth: 120, | ||
useTabs: false, | ||
arrowParens: "avoid", | ||
trailingComma: "all", | ||
tabWidth: 4, | ||
jsxBracketSameLine: true, | ||
endOfLine: "lf", | ||
overrides: [ | ||
{ | ||
files: ["*.json", ".prettierrc", ".eslintrc", ".stylelintrc", ".yml"], | ||
options: { | ||
tabWidth: 2, | ||
}, | ||
}, | ||
], | ||
proseWrap: "always", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
# ts-routes | ||
|
||
[![npm](https://img.shields.io/npm/v/ts-routes)](https://www.npmjs.com/package/ts-routes) | ||
[![Actions Status](https://github.com/leancodepl/ts-routes/workflows/build/badge.svg)](https://github.com/leancodepl/ts-routes/actions) | ||
|
||
Helper library for constructing strongly typed parameterized routing paths. It prevents you from passing hardcoded | ||
strings with routes across the app. | ||
|
||
ts-routes is independent on routing libraries and can be used together with e.g. React Router DOM or Vue Router. | ||
|
||
## Installation | ||
|
||
``` | ||
npm install ts-routes | ||
yarn add ts-routes | ||
``` | ||
|
||
## Quick start | ||
|
||
```js | ||
import { createRouting, number, query, uuid } from 'ts-routes'; | ||
|
||
const routes = createRouting({ | ||
products: segment`/products`, | ||
users: segment`/users/${number('userId')}`, | ||
items: { | ||
...segment`/items${query({ filter: false })}`, | ||
children: { | ||
item: segment`/${uuid('itemId')}`, | ||
}, | ||
}, | ||
} as const); | ||
|
||
routes.products(); // '/products' | ||
routes.products.pattern // '/products' | ||
|
||
routes.users({ userId: '10' }) // '/users/10' | ||
routes.users.pattern // '/users/:userId([0-9]+) | ||
|
||
routes.items({}, { filter: 'new' }) // '/items?filter=new' | ||
routes.items.pattern // '/items' | ||
|
||
routes.items.item({ itemId: '12d66718-e47c-4a2a-ad5b-8897def2f6a7' }) // '/items/12d66718-e47c-4a2a-ad5b-8897def2f6a7' | ||
routes.items.item.pattern // `/items/:itemId(${uuidRegex})` | ||
``` | ||
## Usage | ||
### Routing | ||
To use strongly typed paths, you first have to create the routing object by calling `createRouting` and providing an | ||
object defining segments. Segments represent single routing paths and are implemented as tagged template literals: | ||
```js | ||
const routes = createRouting({ | ||
users: segment`/users` | ||
} as const); | ||
``` | ||
### Parameters | ||
You can define route params (i.e. parts of the path that are variable) by interpolating the `arg` function inside a | ||
segment: | ||
```js | ||
segment`/users/${arg("userId")}`; | ||
``` | ||
This will enable you to create paths like `/users/1` or `/users/username`. | ||
By default route parameters are treated as required. You can make them optional by providing extra configuration. It is | ||
also possible to limit possible parameter values by passing a regex string. While trying to create a route which doesn't | ||
satisfy the pattern, an exception will be thrown. | ||
```js | ||
segment`/users/${arg("userId", { | ||
optional: true, | ||
pattern: "[0-9]", | ||
})}`; | ||
``` | ||
When creating a route, path parameters can be passed in the first argument: | ||
```js | ||
routes.users({ userId: "10" }); | ||
``` | ||
There are some predefined convenience parameter types provided: | ||
- `number(name: string, optional?: boolean)` for number strings | ||
- `uuid(name: string, optional?: boolean)` for UUID strings | ||
### Query string | ||
Query string parameters can be specified by interpolating `query` function inside a segment string. The `query` function | ||
expects an object where keys are names of parameters and values specify whether those params are required in the path. | ||
```js | ||
segment`/product${query({ | ||
productId: true, | ||
details: false, | ||
})}`; | ||
``` | ||
The above segment defines a path which expects the `productId` URL param and the optional `details` URL param. | ||
When creating a route query strings can be passed in the second argument: | ||
```js | ||
routes.products( | ||
{}, | ||
{ | ||
productId: "10", | ||
details: "false", | ||
}, | ||
); | ||
``` | ||
which will return `/product?details=false&productId=10`. | ||
### Nested routes | ||
Routes can be nested by providing an optional `children` property to segments: | ||
```js | ||
const routes = createRouting({ | ||
parent: { | ||
...segment`/parent`, | ||
children: { | ||
child: segment`/child`, | ||
}, | ||
}, | ||
} as const); | ||
``` | ||
Child routes are attached to the parent route object so that to construct a child route you can call | ||
`routes.parent.child()` (which will return `/parent/child`). | ||
Routes can be deeply nested and child routes will include all required and optional route parameters and query string | ||
parameters from parent routes. | ||
### Patterns | ||
While creating a routing, alongside path string generators, patterns for those paths compatible with | ||
[path-to-regexp](https://github.com/pillarjs/path-to-regexp) are generated. You can access them via the `pattern` | ||
property: | ||
``` | ||
routes.products.pattern | ||
``` | ||
Those patterns are useful for integration with routing libraries which support | ||
[path-to-regexp](https://github.com/pillarjs/path-to-regexp)-style syntax (e.g. React Router DOM, Vue Router). | ||
### React Router DOM | ||
You can use patterns for defining routes: | ||
```jsx | ||
<Route exact component={ProductsPage} path={routes.products.pattern} /> | ||
``` | ||
With React it's also useful to add some helper types which can be used for typing routing props for components: | ||
```ts | ||
import { FunctionComponent } from "react"; | ||
import { RouteComponentProps } from "react-router-dom"; | ||
import { RouteParamsFor } from "ts-routes"; | ||
|
||
type PageProps<TPathParams extends (...args: any[]) => string> = RouteComponentProps<RouteParamsFor<TPathParams>>; | ||
|
||
type PageComponent<TPathParams extends (...args: any[]) => string> = FunctionComponent<PageProps<TPathParams>>; | ||
``` | ||
Which you can then use like so: | ||
```tsx | ||
type ProductsPageProps = PageProps<typeof routes.products>; | ||
|
||
const ProductPage: PageComponent<typeof routes.products> = ({ | ||
match: { | ||
params: { productId }, | ||
}, | ||
}) => <div>{productId}</div>; | ||
``` | ||
And for query string params: | ||
```ts | ||
import { useMemo } from "react"; | ||
import { useLocation } from "react-router-dom"; | ||
import { QueryParamsFor } from "ts-routes"; | ||
|
||
function useQueryParams() { | ||
const location = useLocation(); | ||
return useMemo(() => new URLSearchParams(location.search), [location.search]); | ||
} | ||
|
||
function useQueryParamsGuarded<T extends (...args: any) => any>() { | ||
return useQueryParams() as URLSearchParams & { get(name: keyof NonNullable<QueryParamsFor<T>>): string | null }; | ||
} | ||
|
||
const redirectUrl = useQueryParamsGuarded<typeof routes.login>().get("redirect"); | ||
``` | ||
### Vue Router | ||
You can use patterns for defining routes: | ||
```js | ||
const router = new VueRouter({ | ||
routes: [ | ||
{ | ||
path: routes.products.pattern, | ||
component: ProductsPage, | ||
}, | ||
], | ||
}); | ||
``` |
Oops, something went wrong.