├── .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 | --------------------------------------------------------------------------------