├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── package-lock.json
├── package.json
├── src
└── index.js
└── test
└── index.spec.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["babel-preset-moxy/lib"]
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 4
6 | end_of_line = lf
7 | charset = utf-8
8 | trim_trailing_whitespace = true
9 | insert_final_newline = true
10 |
11 | [{*.md,*.snap}]
12 | trim_trailing_whitespace = false
13 |
14 | [package.json]
15 | indent_size = 2
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "extends": [
4 | "eslint-config-moxy/es9",
5 | "eslint-config-moxy/addons/es-modules",
6 | "eslint-config-moxy/addons/node",
7 | "eslint-config-moxy/addons/jest"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | npm-debug.*
3 | coverage/
4 | lib/
5 | es/
6 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | - "lts/*"
5 | # Report coverage
6 | after_success:
7 | - "npm i codecov"
8 | - "node_modules/.bin/codecov"
9 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4 |
5 | ### [2.1.2](https://github.com/moxystudio/js-proper-url-join/compare/v2.1.1...v2.1.2) (2024-10-15)
6 |
7 | ### [2.1.1](https://github.com/moxystudio/js-proper-url-join/compare/v2.1.0...v2.1.1) (2019-11-03)
8 |
9 | ## [2.1.0](https://github.com/moxystudio/js-proper-url-join/compare/v2.0.1...v2.1.0) (2019-11-03)
10 |
11 |
12 | ### Features
13 |
14 | * add support to keep leading and trailing slashes ([#32](https://github.com/moxystudio/js-proper-url-join/issues/32)) ([6008865](https://github.com/moxystudio/js-proper-url-join/commit/6008865b1899be04c170eec2710ecadd1ae9aea6))
15 |
16 |
17 | ## [2.0.1](https://github.com/moxystudio/js-proper-url-join/compare/v1.2.0...v2.0.1) (2019-03-13)
18 |
19 |
20 | # [2.0.0](https://github.com/moxystudio/js-proper-url-join/compare/v1.2.0...v2.0.0) (2019-03-13)
21 |
22 |
23 | ### Features
24 |
25 | * compile to both cjs and es ([16a284f](https://github.com/moxystudio/js-proper-url-join/commit/16a284f))
26 |
27 |
28 |
29 | # [1.2.0](https://github.com/moxystudio/js-proper-url-join/compare/v1.1.2...v1.2.0) (2018-01-16)
30 |
31 |
32 | ### Features
33 |
34 | * add query string object support ([#12](https://github.com/moxystudio/js-proper-url-join/issues/12)) ([ee58d16](https://github.com/moxystudio/js-proper-url-join/commit/ee58d16))
35 |
36 |
37 |
38 |
39 | ## [1.1.2](https://github.com/moxystudio/js-proper-url-join/compare/v1.1.1...v1.1.2) (2017-12-07)
40 |
41 |
42 | ### Bug Fixes
43 |
44 | * fix concatenation not allowing type number ([180965b](https://github.com/moxystudio/js-proper-url-join/commit/180965b)), closes [#7](https://github.com/moxystudio/js-proper-url-join/issues/7)
45 |
46 |
47 |
48 |
49 | ## [1.1.1](https://github.com/moxystudio/js-proper-url-join/compare/v1.1.0...v1.1.1) (2017-11-23)
50 |
51 |
52 | ### Bug Fixes
53 |
54 | * fix readme installation instructions ([ec932a2](https://github.com/moxystudio/js-proper-url-join/commit/ec932a2))
55 |
56 |
57 |
58 |
59 | # [1.1.0](https://github.com/moxystudio/js-proper-url-join/compare/v1.0.0...v1.1.0) (2017-11-22)
60 |
61 |
62 | ### Features
63 |
64 | * add support for absolute URLs and query strings. ([0c82a59](https://github.com/moxystudio/js-proper-url-join/commit/0c82a59)), closes [#2](https://github.com/moxystudio/js-proper-url-join/issues/2) [#3](https://github.com/moxystudio/js-proper-url-join/issues/3)
65 |
66 |
67 |
68 |
69 | # 1.0.0 (2017-11-22)
70 |
71 |
72 | ### Features
73 |
74 | * initial commit ([dcb4777](https://github.com/moxystudio/js-proper-url-join/commit/dcb4777))
75 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Made With MOXY Lda
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # proper-url-join
2 |
3 | [![NPM version][npm-image]][npm-url] [![Downloads][downloads-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][codecov-image]][codecov-url] [![Dependency status][david-dm-image]][david-dm-url] [![Dev Dependency status][david-dm-dev-image]][david-dm-dev-url]
4 |
5 | [npm-url]:https://npmjs.org/package/proper-url-join
6 | [npm-image]:https://img.shields.io/npm/v/proper-url-join.svg
7 | [downloads-image]:https://img.shields.io/npm/dm/proper-url-join.svg
8 | [travis-url]:https://travis-ci.org/moxystudio/js-proper-url-join
9 | [travis-image]:https://img.shields.io/travis/moxystudio/js-proper-url-join/master.svg
10 | [codecov-url]:https://codecov.io/gh/moxystudio/js-proper-url-join
11 | [codecov-image]:https://img.shields.io/codecov/c/github/moxystudio/js-proper-url-join/master.svg
12 | [david-dm-url]:https://david-dm.org/moxystudio/js-proper-url-join
13 | [david-dm-image]:https://img.shields.io/david/moxystudio/js-proper-url-join.svg
14 | [david-dm-dev-url]:https://david-dm.org/moxystudio/js-proper-url-join?type=dev
15 | [david-dm-dev-image]:https://img.shields.io/david/dev/moxystudio/js-proper-url-join.svg
16 |
17 | Like `path.join` but for a URL.
18 |
19 |
20 | ## Installation
21 |
22 | `$ npm install proper-url-join`
23 |
24 | This library expects the host environment to be up-to-date or polyfilled with [core-js](https://github.com/zloirock/core-js) or similar.
25 |
26 | This library is written in ES9 and is using ES modules. You must compile the source code to support older browsers.
27 |
28 |
29 | ## Motivation
30 |
31 | There are a lot of packages that attempt to provide this functionality but they all have issues.
32 | This package exists with the hope to do it right:
33 |
34 | - Consistent behavior
35 | - Support adding/removing leading and trailing slashes
36 | - Supports absolute URLs, e.g.: http//google.com
37 | - Supports protocol relative URLs, e.g.: //google.com
38 | - Supports query strings
39 |
40 |
41 | ## Usage
42 |
43 | ```js
44 | import urlJoin from 'proper-url-join';
45 |
46 | urlJoin('foo', 'bar'); // /foo/bar
47 | urlJoin('/foo/', '/bar/'); // /foo/bar
48 | urlJoin('foo', '', 'bar'); // /foo/bar
49 | urlJoin('foo', undefined, 'bar'); // /foo/bar
50 | urlJoin('foo', null, 'bar'); // /foo/bar
51 |
52 | // With leading & trailing slash options
53 | urlJoin('foo', 'bar', { leadingSlash: false }); // foo/bar
54 | urlJoin('foo', 'bar', { trailingSlash: true }); // /foo/bar/
55 | urlJoin('foo', 'bar', { leadingSlash: false, trailingSlash: true }); // foo/bar/
56 |
57 | // Absolute URLs
58 | urlJoin('http://google.com', 'foo'); // http://google.com/foo
59 |
60 | // Protocol relative URLs
61 | urlJoin('//google.com', 'foo', { protocolRelative: true }); // //google.com/foo
62 |
63 | // With query string as an url part
64 | urlJoin('foo', 'bar?queryString'); // /foo/bar?queryString
65 | urlJoin('foo', 'bar?queryString', { trailingSlash: true }); // /foo/bar/?queryString
66 |
67 | // With query string as an object
68 | urlJoin('foo', { query: { biz: 'buz', foo: 'bar' } }); // /foo?biz=buz&foo=bar
69 |
70 | // With both query string as an url part and an object
71 | urlJoin('foo', 'bar?queryString', { query: { biz: 'buz', foo: 'bar' } }); // /foo/bar?biz=buz&foo=bar&queryString
72 | ```
73 |
74 | #### options
75 |
76 | ###### leadingSlash
77 |
78 | Type: `boolean` / `string`
79 | Default: `true`
80 |
81 | Adds or removes leading `/`. You may pass `keep` to preserve what the leading slash only if it's present on the input.
82 |
83 | ###### trailingSlash
84 |
85 | Type: `boolean` / `string`
86 | Default: `false`
87 |
88 | Adds or removes trailing `/`. You may pass `keep` to preserve what the trailing slash only if it's present on the input.
89 |
90 | ###### protocolRelative
91 |
92 | Type: `boolean`
93 | Default: `false`
94 |
95 | Enables support for protocol relative URLs
96 |
97 | ###### query
98 |
99 | Type: `object`
100 |
101 | Query string object that will be properly stringified and appended to the url. It will be merged with the query string in the url, if it exists.
102 |
103 | ###### queryOptions
104 |
105 | Type: `object`
106 |
107 | [Options](https://github.com/sindresorhus/query-string#stringifyobject-options) to be considered when stringifying the query
108 |
109 |
110 |
111 | ## Tests
112 |
113 | `$ npm test`
114 | `$ npm test -- --watch` during development
115 |
116 |
117 | ## License
118 |
119 | [MIT License](http://opensource.org/licenses/MIT)
120 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "proper-url-join",
3 | "description": "Like `path.join` but for a URL",
4 | "version": "2.1.2",
5 | "keywords": [
6 | "url",
7 | "join",
8 | "path",
9 | "pathname",
10 | "normalize"
11 | ],
12 | "author": "André Cruz ",
13 | "homepage": "https://github.com/moxystudio/js-proper-url-join",
14 | "repository": {
15 | "type": "git",
16 | "url": "git@github.com:moxystudio/js-proper-url-join.git"
17 | },
18 | "license": "MIT",
19 | "main": "lib/index.js",
20 | "module": "es/index.js",
21 | "files": [
22 | "lib",
23 | "es"
24 | ],
25 | "scripts": {
26 | "build:commonjs": "BABEL_ENV=commonjs babel src -d lib",
27 | "build:es": "BABEL_ENV=es babel src -d es",
28 | "build": "npm run build:commonjs && npm run build:es",
29 | "lint": "eslint --ignore-path .gitignore .",
30 | "test": "jest --env node --coverage",
31 | "prerelease": "npm t && npm run lint && npm run build",
32 | "release": "standard-version",
33 | "postrelease": "git push --follow-tags origin HEAD && npm publish"
34 | },
35 | "lint-staged": {
36 | "*.js": [
37 | "eslint --fix",
38 | "git add"
39 | ]
40 | },
41 | "commitlint": {
42 | "extends": [
43 | "@commitlint/config-conventional"
44 | ]
45 | },
46 | "devDependencies": {
47 | "@babel/cli": "^7.2.3",
48 | "@babel/core": "^7.3.4",
49 | "@commitlint/cli": "^8.2.0",
50 | "@commitlint/config-conventional": "^8.2.0",
51 | "babel-jest": "^24.5.0",
52 | "babel-preset-moxy": "^3.0.4",
53 | "eslint": "^6.6.0",
54 | "eslint-config-moxy": "^9.1.0",
55 | "husky": "^3.0.9",
56 | "jest": "^24.5.0",
57 | "lint-staged": "^9.4.2",
58 | "standard-version": "^7.0.0"
59 | },
60 | "dependencies": {
61 | "query-string": "^7.1.3"
62 | },
63 | "husky": {
64 | "hooks": {
65 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS",
66 | "pre-commit": "lint-staged"
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import queryString from 'query-string';
2 |
3 | const defaultUrlRegExp = /^(\w+:\/\/[^/?]+)?(.*?)(\?.+)?$/;
4 | const protocolRelativeUrlRegExp = /^(\/\/[^/?]+)(.*?)(\?.+)?$/;
5 |
6 | const normalizeParts = (parts) => (
7 | parts
8 | // Filter non-string or non-numeric values
9 | .filter((part) => typeof part === 'string' || typeof part === 'number')
10 | // Convert to strings
11 | .map((part) => `${part}`)
12 | // Remove empty parts
13 | .filter((part) => part)
14 | );
15 |
16 | const parseParts = (parts, options) => {
17 | const { protocolRelative } = options;
18 |
19 | const partsStr = parts.join('/');
20 | const urlRegExp = protocolRelative ? protocolRelativeUrlRegExp : defaultUrlRegExp;
21 | const [, prefix = '', pathname = '', suffix = ''] = partsStr.match(urlRegExp) || [];
22 |
23 | return {
24 | prefix,
25 | pathname: {
26 | parts: pathname.split('/').filter((part) => part !== ''),
27 | hasLeading: suffix ? /^\/\/+/.test(pathname) : /^\/+/.test(pathname),
28 | hasTrailing: suffix ? /\/\/+$/.test(pathname) : /\/+$/.test(pathname),
29 | },
30 | suffix,
31 | };
32 | };
33 |
34 | const buildUrl = (parsedParts, options) => {
35 | const { prefix, pathname, suffix } = parsedParts;
36 | const { parts: pathnameParts, hasLeading, hasTrailing } = pathname;
37 | const { leadingSlash, trailingSlash } = options;
38 |
39 | const addLeading = leadingSlash === true || (leadingSlash === 'keep' && hasLeading);
40 | const addTrailing = trailingSlash === true || (trailingSlash === 'keep' && hasTrailing);
41 |
42 | // Start with prefix if not empty (http://google.com)
43 | let url = prefix;
44 |
45 | // Add the parts
46 | if (pathnameParts.length > 0) {
47 | if (url || addLeading) {
48 | url += '/';
49 | }
50 |
51 | url += pathnameParts.join('/');
52 | }
53 |
54 | // Add trailing to the end
55 | if (addTrailing) {
56 | url += '/';
57 | }
58 |
59 | // Add leading if URL is still empty
60 | if (!url && addLeading) {
61 | url += '/';
62 | }
63 |
64 | // Build a query object based on the url query string and options query object
65 | const query = { ...queryString.parse(suffix, options.queryOptions), ...options.query };
66 | const queryStr = queryString.stringify(query, options.queryOptions);
67 |
68 | if (queryStr) {
69 | url += `?${queryStr}`;
70 | }
71 |
72 | return url;
73 | };
74 |
75 | const urlJoin = (...parts) => {
76 | const lastArg = parts[parts.length - 1];
77 | let options;
78 |
79 | // If last argument is an object, then it's the options
80 | // Note that null is an object, so we verify if is truthy
81 | if (lastArg && typeof lastArg === 'object') {
82 | options = lastArg;
83 | parts = parts.slice(0, -1);
84 | }
85 |
86 | // Parse options
87 | options = {
88 | leadingSlash: true,
89 | trailingSlash: false,
90 | protocolRelative: false,
91 | ...options,
92 | };
93 |
94 | // Normalize parts before parsing them
95 | parts = normalizeParts(parts);
96 |
97 | // Split the parts into prefix, pathname, and suffix
98 | // (scheme://host)(/pathnameParts.join('/'))(?queryString)
99 | const parsedParts = parseParts(parts, options);
100 |
101 | // Finaly build the url based on the parsedParts
102 | return buildUrl(parsedParts, options);
103 | };
104 |
105 | export default urlJoin;
106 |
--------------------------------------------------------------------------------
/test/index.spec.js:
--------------------------------------------------------------------------------
1 | import urlJoin from '../src';
2 |
3 | it('should add leading slash and no trailing slash by default', () => {
4 | expect(urlJoin()).toBe('/');
5 | expect(urlJoin(undefined, 'foo')).toBe('/foo');
6 | expect(urlJoin('foo', null, 'bar')).toBe('/foo/bar');
7 | expect(urlJoin('foo', '', 'bar')).toBe('/foo/bar');
8 | expect(urlJoin('foo')).toBe('/foo');
9 | expect(urlJoin('/foo')).toBe('/foo');
10 | expect(urlJoin('/', '/foo')).toBe('/foo');
11 | expect(urlJoin('/', '//foo')).toBe('/foo');
12 | expect(urlJoin('/', '/foo//')).toBe('/foo');
13 | expect(urlJoin('/', '/foo/', '')).toBe('/foo');
14 | expect(urlJoin('/', '/foo/', '/')).toBe('/foo');
15 | expect(urlJoin('foo', 'bar')).toBe('/foo/bar');
16 | expect(urlJoin('/foo', 'bar')).toBe('/foo/bar');
17 | expect(urlJoin('/foo', '/bar')).toBe('/foo/bar');
18 | expect(urlJoin('/foo/', '/bar/')).toBe('/foo/bar');
19 | expect(urlJoin('/foo/', '/bar/baz')).toBe('/foo/bar/baz');
20 | expect(urlJoin('/foo/', '/bar//baz')).toBe('/foo/bar/baz');
21 |
22 | expect(urlJoin('http://google.com')).toBe('http://google.com');
23 | expect(urlJoin('http://google.com/')).toBe('http://google.com');
24 | expect(urlJoin('http://google.com', '')).toBe('http://google.com');
25 | expect(urlJoin('http://google.com', 'foo')).toBe('http://google.com/foo');
26 | expect(urlJoin('http://google.com/', 'foo')).toBe('http://google.com/foo');
27 | expect(urlJoin('http://google.com/', '/foo')).toBe('http://google.com/foo');
28 | expect(urlJoin('http://google.com//', '/foo')).toBe('http://google.com/foo');
29 | expect(urlJoin('http://google.com/foo', 'bar')).toBe('http://google.com/foo/bar');
30 |
31 | expect(urlJoin('http://google.com', '?queryString')).toBe('http://google.com?queryString');
32 | expect(urlJoin('http://google.com', 'foo?queryString')).toBe('http://google.com/foo?queryString');
33 | expect(urlJoin('http://google.com', 'foo', '?queryString')).toBe('http://google.com/foo?queryString');
34 | expect(urlJoin('http://google.com', 'foo/', '?queryString')).toBe('http://google.com/foo?queryString');
35 | expect(urlJoin('http://google.com?queryString')).toBe('http://google.com?queryString');
36 | });
37 |
38 | it('should add leading slash and trailing slash', () => {
39 | const options = { trailingSlash: true };
40 |
41 | expect(urlJoin(options)).toBe('/');
42 | expect(urlJoin(undefined, 'foo', options)).toBe('/foo/');
43 | expect(urlJoin('foo', null, 'bar', options)).toBe('/foo/bar/');
44 | expect(urlJoin('foo', '', 'bar', options)).toBe('/foo/bar/');
45 | expect(urlJoin('foo', options)).toBe('/foo/');
46 | expect(urlJoin('/foo', options)).toBe('/foo/');
47 | expect(urlJoin('/', '/foo', options)).toBe('/foo/');
48 | expect(urlJoin('/', '//foo', options)).toBe('/foo/');
49 | expect(urlJoin('/', '/foo//', options)).toBe('/foo/');
50 | expect(urlJoin('/', '/foo/', '', options)).toBe('/foo/');
51 | expect(urlJoin('/', '/foo/', '/', options)).toBe('/foo/');
52 | expect(urlJoin('foo', 'bar', options)).toBe('/foo/bar/');
53 | expect(urlJoin('/foo', 'bar', options)).toBe('/foo/bar/');
54 | expect(urlJoin('/foo', '/bar', options)).toBe('/foo/bar/');
55 | expect(urlJoin('/foo/', '/bar/', options)).toBe('/foo/bar/');
56 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('/foo/bar/baz/');
57 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('/foo/bar/baz/');
58 |
59 | expect(urlJoin('http://google.com', options)).toBe('http://google.com/');
60 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com/');
61 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com/');
62 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo/');
63 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo/');
64 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo/');
65 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo/');
66 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar/');
67 |
68 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com/?queryString');
69 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo/?queryString');
70 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo/?queryString');
71 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo/?queryString');
72 | expect(urlJoin('http://google.com?queryString', options)).toBe('http://google.com/?queryString');
73 | });
74 |
75 | it('should remove leading slash and add trailing slash', () => {
76 | const options = { leadingSlash: false, trailingSlash: true };
77 |
78 | expect(urlJoin(options)).toBe('/');
79 | expect(urlJoin(undefined, 'foo', options)).toBe('foo/');
80 | expect(urlJoin('foo', null, 'bar', options)).toBe('foo/bar/');
81 | expect(urlJoin('foo', '', 'bar', options)).toBe('foo/bar/');
82 | expect(urlJoin('foo', options)).toBe('foo/');
83 | expect(urlJoin('/foo', options)).toBe('foo/');
84 | expect(urlJoin('/', '/foo', options)).toBe('foo/');
85 | expect(urlJoin('/', '//foo', options)).toBe('foo/');
86 | expect(urlJoin('/', '/foo//', options)).toBe('foo/');
87 | expect(urlJoin('/', '/foo/', '', options)).toBe('foo/');
88 | expect(urlJoin('/', '/foo/', '/', options)).toBe('foo/');
89 | expect(urlJoin('foo', 'bar', options)).toBe('foo/bar/');
90 | expect(urlJoin('/foo', 'bar', options)).toBe('foo/bar/');
91 | expect(urlJoin('/foo', '/bar', options)).toBe('foo/bar/');
92 | expect(urlJoin('/foo/', '/bar/', options)).toBe('foo/bar/');
93 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('foo/bar/baz/');
94 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('foo/bar/baz/');
95 |
96 | expect(urlJoin('http://google.com', options)).toBe('http://google.com/');
97 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com/');
98 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com/');
99 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo/');
100 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo/');
101 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo/');
102 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo/');
103 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar/');
104 |
105 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com/?queryString');
106 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo/?queryString');
107 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo/?queryString');
108 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo/?queryString');
109 | expect(urlJoin('http://google.com?queryString', options)).toBe('http://google.com/?queryString');
110 | });
111 |
112 | it('should remove leading slash and trailing slash', () => {
113 | const options = { leadingSlash: false, trailingSlash: false };
114 |
115 | expect(urlJoin(options)).toBe('');
116 | expect(urlJoin(undefined, 'foo', options)).toBe('foo');
117 | expect(urlJoin('foo', null, 'bar', options)).toBe('foo/bar');
118 | expect(urlJoin('foo', '', 'bar', options)).toBe('foo/bar');
119 | expect(urlJoin('foo', options)).toBe('foo');
120 | expect(urlJoin('/foo', options)).toBe('foo');
121 | expect(urlJoin('/', '/foo', options)).toBe('foo');
122 | expect(urlJoin('/', '//foo', options)).toBe('foo');
123 | expect(urlJoin('/', '/foo//', options)).toBe('foo');
124 | expect(urlJoin('/', '/foo/', '', options)).toBe('foo');
125 | expect(urlJoin('/', '/foo/', '/', options)).toBe('foo');
126 | expect(urlJoin('foo', 'bar', options)).toBe('foo/bar');
127 | expect(urlJoin('/foo', 'bar', options)).toBe('foo/bar');
128 | expect(urlJoin('/foo', '/bar', options)).toBe('foo/bar');
129 | expect(urlJoin('/foo/', '/bar/', options)).toBe('foo/bar');
130 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('foo/bar/baz');
131 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('foo/bar/baz');
132 |
133 | expect(urlJoin('http://google.com', options)).toBe('http://google.com');
134 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com');
135 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com');
136 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo');
137 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo');
138 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo');
139 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo');
140 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar');
141 |
142 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com?queryString');
143 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo?queryString');
144 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo?queryString');
145 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo?queryString');
146 | expect(urlJoin('http://google.com?queryString', options)).toBe('http://google.com?queryString');
147 | });
148 |
149 | it('should support URLs with relative protocol according to options.protocolRelative', () => {
150 | const options = { protocolRelative: true };
151 |
152 | expect(urlJoin('//google.com', 'foo', options)).toBe('//google.com/foo');
153 | expect(urlJoin('//google.com/', 'foo', options)).toBe('//google.com/foo');
154 | expect(urlJoin('//google.com/foo', 'bar', options)).toBe('//google.com/foo/bar');
155 | expect(urlJoin('//google.com/foo//', 'bar', options)).toBe('//google.com/foo/bar');
156 |
157 | options.protocolRelative = false;
158 |
159 | expect(urlJoin('//google.com', 'foo', options)).toBe('/google.com/foo');
160 | expect(urlJoin('//google.com/', 'foo', options)).toBe('/google.com/foo');
161 | expect(urlJoin('//google.com/foo', 'bar', options)).toBe('/google.com/foo/bar');
162 | expect(urlJoin('//google.com/foo//', 'bar', options)).toBe('/google.com/foo/bar');
163 | });
164 |
165 | it('should include numbers', () => {
166 | expect(urlJoin(undefined, 1)).toBe('/1');
167 | expect(urlJoin(1, null, 2)).toBe('/1/2');
168 | expect(urlJoin(1, '', 2)).toBe('/1/2');
169 | expect(urlJoin(1)).toBe('/1');
170 | expect(urlJoin('/1')).toBe('/1');
171 | expect(urlJoin('/', '/1')).toBe('/1');
172 | expect(urlJoin('/', '//1')).toBe('/1');
173 | expect(urlJoin('/', '/1//')).toBe('/1');
174 | expect(urlJoin('/', '/1/', '')).toBe('/1');
175 | expect(urlJoin('/', '/1/', '/')).toBe('/1');
176 | expect(urlJoin(1, 2)).toBe('/1/2');
177 | expect(urlJoin('/1', 2)).toBe('/1/2');
178 | expect(urlJoin('/1', '/2')).toBe('/1/2');
179 | expect(urlJoin('/1/', '/2/')).toBe('/1/2');
180 | expect(urlJoin('/1/', '/2/3')).toBe('/1/2/3');
181 | expect(urlJoin('/1/', '/2//3')).toBe('/1/2/3');
182 |
183 | expect(urlJoin('http://google.com', 1)).toBe('http://google.com/1');
184 | expect(urlJoin('http://google.com/', 1)).toBe('http://google.com/1');
185 | expect(urlJoin('http://google.com/', '/1')).toBe('http://google.com/1');
186 | expect(urlJoin('http://google.com//', '/1')).toBe('http://google.com/1');
187 | expect(urlJoin('http://google.com/1', 2)).toBe('http://google.com/1/2');
188 |
189 | expect(urlJoin('http://google.com', '?1')).toBe('http://google.com?1');
190 | expect(urlJoin('http://google.com', 'foo?1')).toBe('http://google.com/foo?1');
191 | expect(urlJoin('http://google.com', 'foo', '?1')).toBe('http://google.com/foo?1');
192 | expect(urlJoin('http://google.com', 'foo/', '?1')).toBe('http://google.com/foo?1');
193 | expect(urlJoin('http://google.com?1')).toBe('http://google.com?1');
194 | });
195 |
196 | it('should handle the provided query object and append it to the url', () => {
197 | const options = { query: { biz: 'buz', foo: 'bar' } };
198 |
199 | expect(urlJoin('/google.com', options)).toBe('/google.com?biz=buz&foo=bar');
200 | expect(urlJoin('google.com', options)).toBe('/google.com?biz=buz&foo=bar');
201 |
202 | options.protocolRelative = false;
203 |
204 | expect(urlJoin('//google.com', options)).toBe('/google.com?biz=buz&foo=bar');
205 |
206 | options.leadingSlash = false;
207 |
208 | expect(urlJoin('google.com', options)).toBe('google.com?biz=buz&foo=bar');
209 |
210 | options.trailingSlash = true;
211 |
212 | expect(urlJoin('google.com', options)).toBe('google.com/?biz=buz&foo=bar');
213 |
214 | options.trailingSlash = false;
215 |
216 | expect(urlJoin('google.com', 'qux?tux=baz', options)).toBe('google.com/qux?biz=buz&foo=bar&tux=baz');
217 | });
218 |
219 | it('should handle the provided query and query options objects', () => {
220 | const options = { query: { foo: [1, 2, 3] }, queryOptions: {} };
221 |
222 | expect(urlJoin('/google.com', options)).toBe('/google.com?foo=1&foo=2&foo=3');
223 |
224 | options.queryOptions.arrayFormat = 'bracket';
225 |
226 | expect(urlJoin('/google.com', options)).toBe('/google.com?foo[]=1&foo[]=2&foo[]=3');
227 |
228 | options.queryOptions.arrayFormat = 'index';
229 |
230 | expect(urlJoin('/google.com', options)).toBe('/google.com?foo[0]=1&foo[1]=2&foo[2]=3');
231 | });
232 |
233 | it('should keep leading slash and remove trailing slash', () => {
234 | const options = { leadingSlash: 'keep' };
235 |
236 | expect(urlJoin(options)).toBe('');
237 | expect(urlJoin(undefined, 'foo', options)).toBe('foo');
238 | expect(urlJoin('foo', null, 'bar', options)).toBe('foo/bar');
239 | expect(urlJoin('foo', '', 'bar', options)).toBe('foo/bar');
240 | expect(urlJoin('foo', options)).toBe('foo');
241 | expect(urlJoin('/foo', options)).toBe('/foo');
242 | expect(urlJoin('/', '/foo', options)).toBe('/foo');
243 | expect(urlJoin('/', '//foo', options)).toBe('/foo');
244 | expect(urlJoin('/', '/foo//', options)).toBe('/foo');
245 | expect(urlJoin('/', '/foo/', '', options)).toBe('/foo');
246 | expect(urlJoin('/', '/foo/', '/', options)).toBe('/foo');
247 | expect(urlJoin('foo', 'bar', options)).toBe('foo/bar');
248 | expect(urlJoin('/foo', 'bar', options)).toBe('/foo/bar');
249 | expect(urlJoin('/foo', '/bar', options)).toBe('/foo/bar');
250 | expect(urlJoin('/foo/', '/bar/', options)).toBe('/foo/bar');
251 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('/foo/bar/baz');
252 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('/foo/bar/baz');
253 |
254 | expect(urlJoin('http://google.com', options)).toBe('http://google.com');
255 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com');
256 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com');
257 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo');
258 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo');
259 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo');
260 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo');
261 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar');
262 |
263 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com?queryString');
264 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo?queryString');
265 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo?queryString');
266 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo?queryString');
267 | });
268 |
269 | it('should add leading slash and keep trailing slash', () => {
270 | const options = { trailingSlash: 'keep' };
271 |
272 | expect(urlJoin(options)).toBe('/');
273 | expect(urlJoin(undefined, 'foo', options)).toBe('/foo');
274 | expect(urlJoin('foo', null, 'bar', options)).toBe('/foo/bar');
275 | expect(urlJoin('foo', '', 'bar', options)).toBe('/foo/bar');
276 | expect(urlJoin('foo', options)).toBe('/foo');
277 | expect(urlJoin('/foo', options)).toBe('/foo');
278 | expect(urlJoin('/', '/foo', options)).toBe('/foo');
279 | expect(urlJoin('/', '//foo', options)).toBe('/foo');
280 | expect(urlJoin('/', '/foo//', options)).toBe('/foo/');
281 | expect(urlJoin('/', '/foo/', '', options)).toBe('/foo/');
282 | expect(urlJoin('/', '/foo/', '/', options)).toBe('/foo/');
283 | expect(urlJoin('foo', 'bar', options)).toBe('/foo/bar');
284 | expect(urlJoin('/foo', 'bar', options)).toBe('/foo/bar');
285 | expect(urlJoin('/foo', '/bar', options)).toBe('/foo/bar');
286 | expect(urlJoin('/foo/', '/bar/', options)).toBe('/foo/bar/');
287 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('/foo/bar/baz');
288 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('/foo/bar/baz');
289 |
290 | expect(urlJoin('http://google.com', options)).toBe('http://google.com');
291 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com/');
292 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com');
293 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo');
294 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo');
295 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo');
296 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo');
297 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar');
298 |
299 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com?queryString');
300 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo?queryString');
301 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo?queryString');
302 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo/?queryString');
303 | });
304 |
305 | it('should keep leading slash and add trailing slash', () => {
306 | const options = { leadingSlash: 'keep', trailingSlash: true };
307 |
308 | expect(urlJoin(options)).toBe('/');
309 | expect(urlJoin(undefined, 'foo', options)).toBe('foo/');
310 | expect(urlJoin('foo', null, 'bar', options)).toBe('foo/bar/');
311 | expect(urlJoin('foo', '', 'bar', options)).toBe('foo/bar/');
312 | expect(urlJoin('foo', options)).toBe('foo/');
313 | expect(urlJoin('/foo', options)).toBe('/foo/');
314 | expect(urlJoin('/', '/foo', options)).toBe('/foo/');
315 | expect(urlJoin('/', '//foo', options)).toBe('/foo/');
316 | expect(urlJoin('/', '/foo//', options)).toBe('/foo/');
317 | expect(urlJoin('/', '/foo/', '', options)).toBe('/foo/');
318 | expect(urlJoin('/', '/foo/', '/', options)).toBe('/foo/');
319 | expect(urlJoin('foo', 'bar', options)).toBe('foo/bar/');
320 | expect(urlJoin('/foo', 'bar', options)).toBe('/foo/bar/');
321 | expect(urlJoin('/foo', '/bar', options)).toBe('/foo/bar/');
322 | expect(urlJoin('/foo/', '/bar/', options)).toBe('/foo/bar/');
323 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('/foo/bar/baz/');
324 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('/foo/bar/baz/');
325 |
326 | expect(urlJoin('http://google.com', options)).toBe('http://google.com/');
327 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com/');
328 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com/');
329 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo/');
330 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo/');
331 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo/');
332 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo/');
333 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar/');
334 |
335 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com/?queryString');
336 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo/?queryString');
337 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo/?queryString');
338 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo/?queryString');
339 | expect(urlJoin('http://google.com?queryString', options)).toBe('http://google.com/?queryString');
340 | });
341 |
342 | it('should remove leading slash and keep trailing slash', () => {
343 | const options = { leadingSlash: false, trailingSlash: 'keep' };
344 |
345 | expect(urlJoin(options)).toBe('');
346 | expect(urlJoin(undefined, 'foo', options)).toBe('foo');
347 | expect(urlJoin('foo', null, 'bar', options)).toBe('foo/bar');
348 | expect(urlJoin('foo', '', 'bar', options)).toBe('foo/bar');
349 | expect(urlJoin('foo', options)).toBe('foo');
350 | expect(urlJoin('/foo', options)).toBe('foo');
351 | expect(urlJoin('/', '/foo', options)).toBe('foo');
352 | expect(urlJoin('/', '//foo', options)).toBe('foo');
353 | expect(urlJoin('/', '/foo//', options)).toBe('foo/');
354 | expect(urlJoin('/', '/foo/', '', options)).toBe('foo/');
355 | expect(urlJoin('/', '/foo/', '/', options)).toBe('foo/');
356 | expect(urlJoin('foo', 'bar', options)).toBe('foo/bar');
357 | expect(urlJoin('/foo', 'bar', options)).toBe('foo/bar');
358 | expect(urlJoin('/foo', '/bar', options)).toBe('foo/bar');
359 | expect(urlJoin('/foo/', '/bar/', options)).toBe('foo/bar/');
360 | expect(urlJoin('/foo/', '/bar/baz', options)).toBe('foo/bar/baz');
361 | expect(urlJoin('/foo/', '/bar//baz', options)).toBe('foo/bar/baz');
362 |
363 | expect(urlJoin('http://google.com', options)).toBe('http://google.com');
364 | expect(urlJoin('http://google.com/', options)).toBe('http://google.com/');
365 |
366 | expect(urlJoin('http://google.com', '', options)).toBe('http://google.com');
367 | expect(urlJoin('http://google.com', 'foo', options)).toBe('http://google.com/foo');
368 | expect(urlJoin('http://google.com/', 'foo', options)).toBe('http://google.com/foo');
369 | expect(urlJoin('http://google.com/', '/foo', options)).toBe('http://google.com/foo');
370 | expect(urlJoin('http://google.com//', '/foo', options)).toBe('http://google.com/foo');
371 | expect(urlJoin('http://google.com/foo', 'bar', options)).toBe('http://google.com/foo/bar');
372 |
373 | expect(urlJoin('http://google.com', '?queryString', options)).toBe('http://google.com?queryString');
374 | expect(urlJoin('http://google.com', 'foo?queryString', options)).toBe('http://google.com/foo?queryString');
375 | expect(urlJoin('http://google.com', 'foo', '?queryString', options)).toBe('http://google.com/foo?queryString');
376 | expect(urlJoin('http://google.com', 'foo/', '?queryString', options)).toBe('http://google.com/foo/?queryString');
377 | expect(urlJoin('http://google.com?queryString', options)).toBe('http://google.com?queryString');
378 | });
379 |
--------------------------------------------------------------------------------