Skip to content

Commit

Permalink
Merge pull request #4 from rambler-digital-solutions/url
Browse files Browse the repository at this point in the history
feat(url): add url tools
  • Loading branch information
andrepolischuk authored Aug 14, 2024
2 parents 230cc52 + 3f0f342 commit 4a6d8ba
Show file tree
Hide file tree
Showing 8 changed files with 307 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .size-limit.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
{
"path": "packages/session-storage/dist/index.js",
"limit": "290 B"
},
{
"path": "packages/url/dist/index.js",
"limit": "675 B"
}
]
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ Common utils used by Rambler team
## Packages

- [@rambler-tech/cookie-storage](packages/cookie-storage)
- [@rambler-tech/lhci-report](packages/lhci-report)
- [@rambler-tech/local-storage](packages/local-storage)
- [@rambler-tech/session-storage](packages/session-storage)
- [@rambler-tech/lhci-report](packages/lhci-report)
- [@rambler-tech/url](packages/url)

## Contributing

Expand Down
15 changes: 15 additions & 0 deletions packages/url/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# URL tools

Enhanced tools for URL conversion

## Install

```
npm install -D @rambler-tech/url
```

or

```
yarn add -D @rambler-tech/url
```
130 changes: 130 additions & 0 deletions packages/url/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
getUrlParams,
getUrlHashParams,
addUrlParams,
removeUrlHash,
removeUrlNullableParams,
formatUrl,
encodeURIComponentRFC3986
} from '.'

test('getUrlParams: valid url', () => {
const href = 'https://foo.bar?action=login&provider=provider'

expect(getUrlParams(href)).toEqual({
action: 'login',
provider: 'provider'
})
})

test('getUrlParams: valid url with empty search params', () => {
const href = 'https://foo.bar'

expect(getUrlParams(href)).toEqual({})
})

test('getUrlHashParams: valid url', () => {
const href = 'https://foo.bar#action=login&provider=provider'

expect(getUrlHashParams(href)).toEqual({
action: 'login',
provider: 'provider'
})
})

test('getUrlHashParams: valid url with empty hash', () => {
const href = 'https://foo.baz'

expect(getUrlHashParams(href)).toEqual({})
})

test('addUrlParams with search params', () => {
expect(
addUrlParams('https://foo.bar/', {
logged: false,
action: 'login',
provider: 'provider'
})
).toEqual('https://foo.bar/?logged=false&action=login&provider=provider')
})

test('addUrlParams with cleaned search params', () => {
expect(
addUrlParams('https://foo.bar/', {
logged: false,
action: 'login',
provider: 'provider',
test: undefined,
foobar: null
})
).toEqual('https://foo.bar/?logged=false&action=login&provider=provider')
})

test('addUrlParams with hash', () => {
expect(
addUrlParams(
'https://foo.bar/?test=test#bar=baz',
{
logged: false,
action: 'login',
provider: 'provider'
},
{foo: 'bar'}
)
).toEqual(
'https://foo.bar/?test=test&logged=false&action=login&provider=provider#bar=baz&foo=bar'
)
})

test('removeUrlHash', () => {
const href = 'https://foo.bar/baz/?foo=bar'

expect(removeUrlHash(href)).toEqual(href)
expect(removeUrlHash('https://foo.bar/baz/?foo=bar#foo=bar')).toEqual(href)
})

test('should clean query', () => {
expect(
removeUrlNullableParams({test: undefined, foo: 'bar', bar: undefined})
).toEqual({
foo: 'bar'
})
})

test('should format url', () => {
expect(
formatUrl({
pathname: '/login',
query: {
email: 'testbar.ru',
rname: undefined,
src: null,
type: 'test',
back: 'back.ru'
}
})
).toBe('/login?email=testbar.ru&type=test&back=back.ru')
})

test('should format full url', () => {
expect(
formatUrl({
protocol: 'https:',
host: 'somehost.com',
pathname: '/login',
query: {
email: 'testbar.ru',
rname: undefined,
src: null,
type: 'test',
back: 'back.ru'
}
})
).toBe('https://somehost.com/login?email=testbar.ru&type=test&back=back.ru')
})

test('should encode with RFC 3986 standard', () => {
expect(encodeURIComponentRFC3986('my file(2).txt')).toBe(
'my%20file%282%29.txt'
)
})
130 changes: 130 additions & 0 deletions packages/url/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/* eslint-disable import/no-unused-modules */

export type ParsedQuery<T = string> = Record<string, T | T[] | null | undefined>

export type Query = Record<string, any>

interface UrlObject {
protocol?: string | null
host?: string | null
pathname?: string | null
query?: string | Query | null
}

/** Format URL to string */
export function formatUrl(urlObject: UrlObject): string {
const {protocol, host, pathname, query} = urlObject

let cleanedQueryString

if (query && typeof query === 'object') {
// NOTE: remove undefined or null values from query
const cleanedQuery = removeUrlNullableParams(query)

cleanedQueryString = new URLSearchParams(cleanedQuery).toString()
} else {
cleanedQueryString = query
}

return (
(protocol ? `${protocol}//` : '') +
(host ?? '') +
pathname +
(cleanedQueryString ? `?${cleanedQueryString}` : '')
)
}

/** Get URL query params */
export function getUrlParams(url: string): ParsedQuery {
const searchParams: ParsedQuery = {}

const {searchParams: iterableSearchParams} = new URL(url)

iterableSearchParams.forEach((value, key) => {
searchParams[key] = value
})

return searchParams
}

/** Get URL hash params */
export function getUrlHashParams(url: string): ParsedQuery {
const hashParams: ParsedQuery = {}

const {hash} = new URL(url)
const iterableHashParams = new URLSearchParams(hash.substring(1))

iterableHashParams.forEach((value, key) => {
hashParams[key] = value
})

return hashParams
}

/** Add URL query params */
export function addUrlParams(
url: string,
searchParams: Query,
hashParams?: Query
): string {
if (
!Object.keys(searchParams).length &&
(!hashParams || !Object.keys(hashParams).length)
)
return url

const {protocol, host, pathname} = new URL(url)

const initialSearchParams = getUrlParams(url)
const mergedSearchParams = removeUrlNullableParams({
...initialSearchParams,
...searchParams
})

const search = new URLSearchParams(mergedSearchParams).toString()
const urlSearch = search ? `?${search}` : ''

const initialHashParams = getUrlHashParams(url)
const mergedHashParams = removeUrlNullableParams({
...initialHashParams,
...hashParams
})

const hash = new URLSearchParams(mergedHashParams).toString()
const urlHash = hash ? `#${hash}` : ''

return `${protocol}//${host}${pathname}${urlSearch}${urlHash}`
}

/** Remove URL nullable query params (`null` or `undefined`) */
export function removeUrlNullableParams<T extends Query>(query: T): T {
const resultQuery: Query = {}

Object.keys(query).forEach((key) => {
if (query[key] != null) {
resultQuery[key] = query[key]
}
})

return resultQuery as T
}

/** Remove URL hash */
export function removeUrlHash(url: string): string {
const {protocol, host, pathname, search} = new URL(url)

return `${protocol}//${host}${pathname}${search}`
}

/**
* Encode URI for RFC3986
*
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
*/
export function encodeURIComponentRFC3986(value: string): string {
return encodeURIComponent(value).replace(
/[!'()*]/g,
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
(c) => `%${c.charCodeAt(0).toString(16)}`
)
}
12 changes: 12 additions & 0 deletions packages/url/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@rambler-tech/url",
"version": "0.0.0",
"main": "dist",
"module": "dist",
"types": "dist/index.d.ts",
"license": "MIT",
"sideEffects": false,
"publishConfig": {
"access": "public"
}
}
9 changes: 9 additions & 0 deletions packages/url/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"baseUrl": ".",
"outDir": "dist"
},
"include": [".", "../../types"]
}
5 changes: 5 additions & 0 deletions packages/url/typedoc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "@rambler-tech/typedoc-config",
"readme": "../../README.md",
"entryPoints": ["./index.ts"]
}

0 comments on commit 4a6d8ba

Please sign in to comment.