Skip to content

Commit a6c06f0

Browse files
committed
Extract router path extras into a separate module
1 parent f052795 commit a6c06f0

File tree

16 files changed

+401
-38
lines changed

16 files changed

+401
-38
lines changed

β€ŽLICENSEβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
Copyright Β© REPLACE_ME Erin Millard
1+
Copyright Β© 2021 Erin Millard
22

33
Permission is hereby granted, free of charge, to any person obtaining a copy
44
of this software and associated documentation files (the "Software"), to deal

β€ŽREADME.mdβ€Ž

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,15 @@
1-
# Packula template repo
2-
3-
<!-- Uncomment this section
1+
# Packula router path extras
42

53
[![Current version][badge-version-image]][badge-version-link]
64
[![Bundle size][badge-bundle-image]][badge-bundle-link]
75
[![Build status][badge-build-image]][badge-build-link]
86
[![Test coverage][badge-coverage-image]][badge-coverage-link]
97

10-
[badge-build-image]: https://img.shields.io/github/workflow/status/packula/GITHUB_REPO_NAME/CI?style=for-the-badge
11-
[badge-build-link]: https://github.com/packula/GITHUB_REPO_NAME/actions/workflows/ci.yml
12-
[badge-bundle-image]: https://img.shields.io/bundlephobia/minzip/@packula/GITHUB_REPO_NAME?style=for-the-badge
13-
[badge-bundle-link]: https://bundlephobia.com/result?p=@packula/GITHUB_REPO_NAME
14-
[badge-coverage-image]: https://img.shields.io/codecov/c/gh/packula/GITHUB_REPO_NAME?style=for-the-badge
15-
[badge-coverage-link]: https://codecov.io/gh/packula/GITHUB_REPO_NAME
16-
[badge-version-image]: https://img.shields.io/npm/v/@packula/GITHUB_REPO_NAME?label=%40packula%2FGITHUB_REPO_NAME&logo=npm&style=for-the-badge
17-
[badge-version-link]: https://npmjs.com/package/@packula/GITHUB_REPO_NAME
18-
19-
-->
20-
21-
This repository is a template for Packula TypeScript projects. After creating a
22-
repository from this template, follow these steps:
23-
24-
- Uncomment the badges in this `README.md` file
25-
- Replace the string `GITHUB_REPO_NAME` in all files with the actual repo name.
26-
- Search for `REPLACE_ME` in all files to find areas that need manual input.
27-
- On the settings page (https://github.com/packula/GITHUB_REPO_NAME/settings):
28-
- Disable the "Wikis" feature
29-
- Disable the "Projects" feature
30-
- Enable "Automatically delete head branches" under the "Merge button" section
8+
[badge-build-image]: https://img.shields.io/github/workflow/status/packula/router-path-extras/CI?style=for-the-badge
9+
[badge-build-link]: https://github.com/packula/router-path-extras/actions/workflows/ci.yml
10+
[badge-bundle-image]: https://img.shields.io/bundlephobia/minzip/@packula/router-path-extras?style=for-the-badge
11+
[badge-bundle-link]: https://bundlephobia.com/result?p=@packula/router-path-extras
12+
[badge-coverage-image]: https://img.shields.io/codecov/c/gh/packula/router-path-extras?style=for-the-badge
13+
[badge-coverage-link]: https://codecov.io/gh/packula/router-path-extras
14+
[badge-version-image]: https://img.shields.io/npm/v/@packula/router-path-extras?label=%40packula%2Frouter-path-extras&logo=npm&style=for-the-badge
15+
[badge-version-link]: https://npmjs.com/package/@packula/router-path-extras

β€Žpackage.jsonβ€Ž

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
2-
"name": "@packula/GITHUB_REPO_NAME",
2+
"name": "@packula/router-path-extras",
33
"version": "0.0.0",
4-
"description": "REPLACE_ME",
5-
"repository": "packula/GITHUB_REPO_NAME",
6-
"bugs": "https://github.com/packula/GITHUB_REPO_NAME/issues",
7-
"homepage": "https://packula.dev/",
4+
"description": "Additional parameter types for Packula router path",
5+
"repository": "packula/router-path-extras",
6+
"bugs": "https://github.com/packula/router-path-extras/issues",
7+
"homepage": "https://packula.dev",
88
"author": "Erin Millard <[email protected]>",
99
"license": "MIT",
1010
"publishConfig": {
@@ -23,11 +23,18 @@
2323
"scripts": {
2424
"prepare": "rollup --config rollup.config.js"
2525
},
26+
"dependencies": {
27+
"@packula/regexp": "^0.1.1"
28+
},
29+
"peerDependencies": {
30+
"@packula/router-path": "^0.2.0"
31+
},
2632
"devDependencies": {
2733
"@packula/eslint-config": "^1.1.1",
2834
"@packula/jest-config": "^1.0.1",
29-
"@packula/rollup-config": "^1.0.1",
30-
"@packula/tsconfig": "^1.0.0",
35+
"@packula/rollup-config": "^1.1.1",
36+
"@packula/router-path": "^0.2.0",
37+
"@packula/tsconfig": "^1.0.2",
3138
"@typescript-eslint/eslint-plugin": "^4.18.0",
3239
"@typescript-eslint/parser": "^4.18.0",
3340
"codecov": "^3.0.2",

β€Žsrc/coercion.tsβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {Param} from '@packula/router-path'
2+
3+
export function int<Name extends string> (name: Name): Param<Name, number> {
4+
return {
5+
name,
6+
exp: /(0|[1-9]\d*)/,
7+
build: arg => `${Math.floor(arg)}`,
8+
parse: match => parseInt(match, 10),
9+
}
10+
}

β€Žsrc/index.tsβ€Ž

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
1-
export const placeholder = 'REPLACE_ME'
1+
export {int} from './coercion'
2+
export {optional} from './optional'
3+
export {any, some} from './repeating'
4+
export {createSlash, slash} from './slash'

β€Žsrc/optional.tsβ€Ž

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {AnyParam, NormalizeParam, Param, param, ParamArg, ParamOrString} from '@packula/router-path'
2+
import {escape, unwrap} from '@packula/regexp'
3+
4+
export function optional<InnerParam extends ParamOrString> (
5+
literals: TemplateStringsArray,
6+
inner: InnerParam,
7+
): OptionalParam<NormalizeParam<InnerParam>> {
8+
type NormalizedParam = NormalizeParam<InnerParam>
9+
type Arg = ParamArg<OptionalParam<NormalizedParam>>
10+
11+
if (literals.length !== 2) throw new Error('Invalid param count')
12+
13+
const [start, end] = literals
14+
const {build, exp, name, parse} = typeof inner === 'string'
15+
? param(inner) as NormalizedParam
16+
: inner as NormalizedParam
17+
18+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
19+
return {
20+
name,
21+
exp: new RegExp(`(?:${escape(start)}${unwrap(exp)}${escape(end)})?`),
22+
build: (arg: Arg) => arg == null ? '' : `${start}${build(arg)}${end}`,
23+
parse: match => match === '' ? undefined : parse(match),
24+
} as OptionalParam<NormalizeParam<InnerParam>>
25+
}
26+
27+
type OptionalParam<InnerParam extends AnyParam> = InnerParam extends Param<infer Name, infer Arg, infer Result>
28+
? Param<Name, Arg | undefined, Result | undefined>
29+
: AnyParam

β€Žsrc/repeating.tsβ€Ž

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import {Param} from '@packula/router-path'
2+
import {escape, unwrap} from '@packula/regexp'
3+
4+
export function any<Name extends string> (
5+
name: Name,
6+
exp: RegExp = /[^/]+/,
7+
separator: string = '/',
8+
prefix: string = separator,
9+
): Param<Name, string[], Partial<string[]>> {
10+
return {
11+
name,
12+
exp: new RegExp(`${arrayParamPattern(exp, prefix, separator)}?`),
13+
build: arg => arg.length > 0 ? `${prefix}${arg.join(separator)}` : '',
14+
parse: match => match === '' ? [] : match.split(separator),
15+
}
16+
}
17+
18+
export function some<Name extends string> (
19+
name: Name,
20+
exp: RegExp = /[^/]+/,
21+
separator: string = '/',
22+
prefix: string = separator,
23+
): Param<Name, string[], SomeParamResult> {
24+
return {
25+
name,
26+
exp: new RegExp(arrayParamPattern(exp, prefix, separator)),
27+
build: arg => `${prefix}${arg.join(separator)}`,
28+
parse: match => match.split(separator) as SomeParamResult,
29+
}
30+
}
31+
32+
type SomeParamResult = { [0]: string } & Partial<string[]>
33+
34+
function arrayParamPattern (exp: RegExp, prefix: string, separator: string): string {
35+
const segmentExp = `(?:${unwrap(exp)})`
36+
37+
return `(?:${escape(prefix)}(${segmentExp}(?:${escape(separator)}${segmentExp})*))`
38+
}

β€Žsrc/slash.tsβ€Ž

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Param} from '@packula/router-path'
2+
3+
export function createSlash<Name extends string> (name: Name): Param<Name, boolean> {
4+
return {
5+
name,
6+
exp: /(\/)?/,
7+
build: arg => arg ? '/' : '',
8+
parse: match => match !== '',
9+
}
10+
}
11+
12+
export const slash = createSlash('hasSlash')

β€Žtest/.eslintrc.jsβ€Ž

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports = {
2+
extends: [
3+
'plugin:jest/recommended',
4+
'plugin:jest/style',
5+
],
6+
plugins: [
7+
'jest',
8+
],
9+
env: {
10+
jest: true,
11+
},
12+
rules: {
13+
'jest/no-focused-tests': 'warn',
14+
},
15+
}

β€Žtest/unit/any.spec.tsβ€Ž

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {path} from '@packula/router-path'
2+
3+
import {any} from '../../src/repeating'
4+
5+
describe('any()', () => {
6+
it('should allow building from an array', () => {
7+
const subject = path`/a${any('p1')}/b`
8+
9+
expect(subject.build({p1: ['wx', 'yz']})).toBe('/a/wx/yz/b')
10+
expect(subject.build({p1: []})).toBe('/a/b')
11+
})
12+
13+
it('should allow building with alternate expressions, separators, and prefixes', () => {
14+
expect(path`/a${any('p1', /x/, '<sep>')}/b`.build({p1: ['wx', 'yz']})).toBe('/a<sep>wx<sep>yz/b')
15+
expect(path`/a${any('p1', /x/, '<sep>', '<pre>')}/b`.build({p1: ['wx', 'yz']})).toBe('/a<pre>wx<sep>yz/b')
16+
})
17+
18+
it('should match zero to many path segments', () => {
19+
const subject = path`/a${any('p1')}/b`
20+
21+
expect(subject.match('/a/b')).toStrictEqual({p1: []})
22+
expect(subject.match('/a/xy/b')).toStrictEqual({p1: ['xy']})
23+
expect(subject.match('/a/wx/yz/b')).toStrictEqual({p1: ['wx', 'yz']})
24+
})
25+
26+
it('should allow matching with alternate expressions, separators, and prefixes', () => {
27+
const subjectA = path`/a${any('p1', /xy|yz|[π“π“Ž]/u, '<sep>')}/b`
28+
29+
expect(subjectA.match('/a/b')).toStrictEqual({p1: []})
30+
expect(subjectA.match('/a<sep>xy/b')).toStrictEqual({p1: ['xy']})
31+
expect(subjectA.match('/a<sep>xy<sep>yz<sep>𝓍<sep>π“Ž/b')).toStrictEqual({p1: ['xy', 'yz', '𝓍', 'π“Ž']})
32+
33+
const subjectB = path`/a${any('p1', /xy|yz|[π“π“Ž]/u, '<sep>', '<pre>')}/b`
34+
35+
expect(subjectB.match('/a/b')).toStrictEqual({p1: []})
36+
expect(subjectB.match('/a<pre>xy/b')).toStrictEqual({p1: ['xy']})
37+
expect(subjectB.match('/a<pre>xy<sep>yz<sep>𝓍<sep>π“Ž/b')).toStrictEqual({p1: ['xy', 'yz', '𝓍', 'π“Ž']})
38+
})
39+
40+
it('should not match empty path segments', () => {
41+
const subject = path`/a${any('p1')}/b`
42+
43+
expect(subject.match('/a//b')).toBeUndefined()
44+
expect(subject.match('/a///b')).toBeUndefined()
45+
})
46+
47+
it('should complain about keys being possibly undefined', () => {
48+
const result = path`/a${any('p1')}/b`.match('/a/xy/b')
49+
50+
expect(result).toBeDefined()
51+
if (result == null) return // TypeScript guard
52+
53+
// @ts-expect-error
54+
expect(result.p1[0].toUpperCase()).toBe('XY')
55+
})
56+
})

0 commit comments

Comments
Β (0)