├── .eslintrc ├── .gitignore ├── .prettierignore ├── .size-snapshot.json ├── .travis.yml ├── LICENSE ├── README.md ├── index.js ├── jest.config.js ├── modules ├── .babelrc ├── __tests__ │ ├── .eslintrc │ └── resolvePathname-test.js └── index.js ├── package-lock.json ├── package.json └── rollup.config.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "shared-node-browser": true 4 | }, 5 | "extends": ["eslint:recommended"], 6 | "parser": "babel-eslint" 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | 3 | /cjs/ 4 | /esm/ 5 | /umd/ 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | package.json 2 | -------------------------------------------------------------------------------- /.size-snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "esm/resolve-pathname.js": { 3 | "bundled": 1764, 4 | "minified": 722, 5 | "gzipped": 404, 6 | "treeshaked": { 7 | "rollup": { 8 | "code": 0, 9 | "import_statements": 0 10 | }, 11 | "webpack": { 12 | "code": 951 13 | } 14 | } 15 | }, 16 | "umd/resolve-pathname.js": { 17 | "bundled": 2148, 18 | "minified": 795, 19 | "gzipped": 465 20 | }, 21 | "umd/resolve-pathname.min.js": { 22 | "bundled": 2148, 23 | "minified": 795, 24 | "gzipped": 465 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: node 3 | cache: npm 4 | env: 5 | - TEST_ENV=cjs BUILD_ENV=cjs 6 | - TEST_ENV=umd BUILD_ENV=umd 7 | - TEST_ENV=source 8 | before_script: 9 | - ([[ -z "$BUILD_ENV" ]] || npm run build) 10 | script: 11 | - npm run lint 12 | - npm test 13 | jobs: 14 | include: 15 | - stage: Release 16 | if: tag =~ ^v[0-9] 17 | env: NPM_TAG=$([[ "$TRAVIS_TAG" == *-* ]] && echo "next" || echo "latest") 18 | script: echo "Releasing $TRAVIS_TAG to npm with tag \"$NPM_TAG\" ..." 19 | deploy: 20 | provider: npm 21 | skip_cleanup: true 22 | tag: "$NPM_TAG" 23 | email: npm@mjackson.me 24 | api_key: 25 | secure: FFo4kATFfuGW7IhODQr6PaqxrwCzAyQd3o3EU3irJkfA/PAMVg8Ccd8LeCFOZQZ0Lf1xMwGnQaaw9a3MQBukIXTyX7gzuXfq0PFH3w4gxQbLiiG6DKX+vIvC9dm0yuELfIzfq2znqEtK0mmGKT6y1ZYUqYe+BeQkaksD3cBfVaK5UVCg6ktratfnPtlvPoErRmn6yR8BydQPw75p10ofAkRf/EPfJ1q6fEpSODDs88/I5WO1s3XldIycSw+x2xc49p+7CFx1whU4vZ8MHp9ccFIv16WPCaSQ77Gnkk2FSUXH2+swGH8+5b8PD2WunDvrTM6dq1FOrqhliu9UUgNUrWW2Zi2CymbYtG/S7AxJB4S7+zTCkdThjIkgKVIknMglG/d7Fa0Q+tAvU7/KkyMAYCCXl+ntbVuBl36SpxgqnWo8N5BJfWR0J2SwKqaKXk1Bj9IKrB+zwVnZW982V3BmaUvMqfkA/lHaw0urRy/fdtepWE4ax8nLAMkWjP9rOOPn6GUjcaiBz8DrdZ1Bz1ytvlkLS6xrbyxW3iLfkAvWB26RB/nD2y/L896ciNLx6rG/ZPM7Iy8aQtxKs6RrNcvHCdjD/77nDqh+kBzsCYApF2p/Mmyd+GPqM9N3NlI+jznYvNrpCOxN79zz12lj+w3NqBA0/TFed1TrLHPHzAMx38Q= 26 | on: 27 | tags: true 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Michael Jackson 2016-2018 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # resolve-pathname [![Travis][build-badge]][build] [![npm package][npm-badge]][npm] 2 | 3 | [build-badge]: https://img.shields.io/travis/mjackson/resolve-pathname/master.svg?style=flat-square 4 | [build]: https://travis-ci.org/mjackson/resolve-pathname 5 | [npm-badge]: https://img.shields.io/npm/v/resolve-pathname.svg?style=flat-square 6 | [npm]: https://www.npmjs.org/package/resolve-pathname 7 | 8 | [resolve-pathname](https://www.npmjs.com/package/resolve-pathname) resolves URL pathnames identical to the way browsers resolve the pathname of an `` value. The goals are: 9 | 10 | - 100% compatibility with browser pathname resolution 11 | - Pure JavaScript implementation (no DOM dependency) 12 | 13 | ## Installation 14 | 15 | Using [npm](https://www.npmjs.com/): 16 | 17 | $ npm install --save resolve-pathname 18 | 19 | Then, use as you would anything else: 20 | 21 | ```js 22 | // using ES6 modules 23 | import resolvePathname from 'resolve-pathname'; 24 | 25 | // using CommonJS modules 26 | var resolvePathname = require('resolve-pathname'); 27 | ``` 28 | 29 | The UMD build is also available on [unpkg](https://unpkg.com): 30 | 31 | ```html 32 | 33 | ``` 34 | 35 | You can find the library on `window.resolvePathname`. 36 | 37 | ## Usage 38 | 39 | ```js 40 | import resolvePathname from 'resolve-pathname'; 41 | 42 | // Simply pass the pathname you'd like to resolve. Second 43 | // argument is the path we're coming from, or the current 44 | // pathname. It defaults to "/". 45 | resolvePathname('about', '/company/jobs'); // /company/about 46 | resolvePathname('../jobs', '/company/team/ceo'); // /company/jobs 47 | resolvePathname('about'); // /about 48 | resolvePathname('/about'); // /about 49 | 50 | // Index paths (with a trailing slash) are also supported and 51 | // work the same way as browsers. 52 | resolvePathname('about', '/company/info/'); // /company/info/about 53 | 54 | // In browsers, it's easy to resolve a URL pathname relative to 55 | // the current page. Just use window.location! e.g. if 56 | // window.location.pathname == '/company/team/ceo' then 57 | resolvePathname('cto', window.location.pathname); // /company/team/cto 58 | resolvePathname('../jobs', window.location.pathname); // /company/jobs 59 | ``` 60 | 61 | ## Prior Work 62 | 63 | - [url.resolve](https://nodejs.org/api/url.html#url_url_resolve_from_to) - node's `url.resolve` implementation for full URLs 64 | - [resolve-url](https://www.npmjs.com/package/resolve-url) - A DOM-dependent implementation of the same algorithm 65 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (process.env.NODE_ENV === 'production') { 4 | module.exports = require('./cjs/resolve-pathname.min.js'); 5 | } else { 6 | module.exports = require('./cjs/resolve-pathname.js'); 7 | } 8 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | let mappedModule; 2 | switch (process.env.TEST_ENV) { 3 | case 'cjs': 4 | mappedModule = '/cjs/resolve-pathname.js'; 5 | break; 6 | case 'umd': 7 | mappedModule = '/umd/resolve-pathname.js'; 8 | break; 9 | case 'source': 10 | default: 11 | mappedModule = '/modules/index.js'; 12 | } 13 | 14 | module.exports = { 15 | moduleNameMapper: { 16 | '^resolve-pathname$': mappedModule 17 | }, 18 | testMatch: ['**/__tests__/**/*-test.js'], 19 | testURL: 'http://localhost/' 20 | }; 21 | -------------------------------------------------------------------------------- /modules/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env"] 3 | } 4 | -------------------------------------------------------------------------------- /modules/__tests__/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /modules/__tests__/resolvePathname-test.js: -------------------------------------------------------------------------------- 1 | import resolvePathname from 'resolve-pathname'; 2 | 3 | describe('resolvePathname', () => { 4 | it('works when from is not given', () => { 5 | expect(resolvePathname('c')).toEqual('c'); 6 | }); 7 | 8 | it('works when from is relative', () => { 9 | expect(resolvePathname('c', 'a/b')).toEqual('a/c'); 10 | }); 11 | 12 | it('works when to is absolute', () => { 13 | expect(resolvePathname('/c', '/a/b')).toEqual('/c'); 14 | }); 15 | 16 | it('works when to is empty', () => { 17 | expect(resolvePathname('', '/a/b')).toEqual('/a/b'); 18 | }); 19 | 20 | it('works when to is a sibling of the parent', () => { 21 | expect(resolvePathname('../c', '/a/b')).toEqual('/c'); 22 | }); 23 | 24 | it('works when to is a sibling path', () => { 25 | expect(resolvePathname('c', '/a/b')).toEqual('/a/c'); 26 | }); 27 | 28 | it('works when from is an index path', () => { 29 | expect(resolvePathname('c', '/a/')).toEqual('/a/c'); 30 | }); 31 | 32 | it('works when to points to the parent directory', () => { 33 | expect(resolvePathname('..', '/a/b')).toEqual('/'); 34 | }); 35 | 36 | // Copied from node's test/parallel/test-url.js 37 | const nodeURLResolveTestCases = [ 38 | ['/foo/bar/baz', 'quux', '/foo/bar/quux'], 39 | ['/foo/bar/baz', 'quux/asdf', '/foo/bar/quux/asdf'], 40 | ['/foo/bar/baz', 'quux/baz', '/foo/bar/quux/baz'], 41 | ['/foo/bar/baz', '../quux/baz', '/foo/quux/baz'], 42 | ['/foo/bar/baz', '/bar', '/bar'], 43 | ['/foo/bar/baz/', 'quux', '/foo/bar/baz/quux'], 44 | ['/foo/bar/baz/', 'quux/baz', '/foo/bar/baz/quux/baz'], 45 | ['/foo/bar/baz', '../../../../../../../../quux/baz', '/quux/baz'], 46 | ['/foo/bar/baz', '../../../../../../../quux/baz', '/quux/baz'], 47 | ['/foo', '.', '/'], 48 | ['/foo', '..', '/'], 49 | ['/foo/', '.', '/foo/'], 50 | ['/foo/', '..', '/'], 51 | ['/foo/bar', '.', '/foo/'], 52 | ['/foo/bar', '..', '/'], 53 | ['/foo/bar/', '.', '/foo/bar/'], 54 | ['/foo/bar/', '..', '/foo/'], 55 | ['foo/bar', '../../../baz', '../../baz'], 56 | ['foo/bar/', '../../../baz', '../baz'], 57 | //['http://example.com/b//c//d;p?q#blarg', 'https:#hash2', 'https:///#hash2'], 58 | //['http://example.com/b//c//d;p?q#blarg', 59 | //'https:/p/a/t/h?s#hash2', 60 | //'https://p/a/t/h?s#hash2'], 61 | //['http://example.com/b//c//d;p?q#blarg', 62 | //'https://u:p@h.com/p/a/t/h?s#hash2', 63 | //'https://u:p@h.com/p/a/t/h?s#hash2'], 64 | //['http://example.com/b//c//d;p?q#blarg', 65 | //'https:/a/b/c/d', 66 | //'https://a/b/c/d'], 67 | //['http://example.com/b//c//d;p?q#blarg', 68 | //'http:#hash2', 69 | //'http://example.com/b//c//d;p?q#hash2'], 70 | //['http://example.com/b//c//d;p?q#blarg', 71 | //'http:/p/a/t/h?s#hash2', 72 | //'http://example.com/p/a/t/h?s#hash2'], 73 | //['http://example.com/b//c//d;p?q#blarg', 74 | //'http://u:p@h.com/p/a/t/h?s#hash2', 75 | //'http://u:p@h.com/p/a/t/h?s#hash2'], 76 | //['http://example.com/b//c//d;p?q#blarg', 77 | //'http:/a/b/c/d', 78 | //'http://example.com/a/b/c/d'], 79 | ['/foo/bar/baz', '/../etc/passwd', '/etc/passwd'] 80 | //['http://localhost', 'file:///Users/foo', 'file:///Users/foo'], 81 | //['http://localhost', 'file://foo/Users', 'file://foo/Users'] 82 | ]; 83 | 84 | nodeURLResolveTestCases.forEach(([from, to, expected]) => { 85 | it(`resolvePathname('${to}', '${from}') == '${expected}'`, () => { 86 | expect(resolvePathname(to, from)).toEqual(expected); 87 | }); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /modules/index.js: -------------------------------------------------------------------------------- 1 | function isAbsolute(pathname) { 2 | return pathname.charAt(0) === '/'; 3 | } 4 | 5 | // About 1.5x faster than the two-arg version of Array#splice() 6 | function spliceOne(list, index) { 7 | for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) { 8 | list[i] = list[k]; 9 | } 10 | 11 | list.pop(); 12 | } 13 | 14 | // This implementation is based heavily on node's url.parse 15 | function resolvePathname(to, from) { 16 | if (from === undefined) from = ''; 17 | 18 | var toParts = (to && to.split('/')) || []; 19 | var fromParts = (from && from.split('/')) || []; 20 | 21 | var isToAbs = to && isAbsolute(to); 22 | var isFromAbs = from && isAbsolute(from); 23 | var mustEndAbs = isToAbs || isFromAbs; 24 | 25 | if (to && isAbsolute(to)) { 26 | // to is absolute 27 | fromParts = toParts; 28 | } else if (toParts.length) { 29 | // to is relative, drop the filename 30 | fromParts.pop(); 31 | fromParts = fromParts.concat(toParts); 32 | } 33 | 34 | if (!fromParts.length) return '/'; 35 | 36 | var hasTrailingSlash; 37 | if (fromParts.length) { 38 | var last = fromParts[fromParts.length - 1]; 39 | hasTrailingSlash = last === '.' || last === '..' || last === ''; 40 | } else { 41 | hasTrailingSlash = false; 42 | } 43 | 44 | var up = 0; 45 | for (var i = fromParts.length; i >= 0; i--) { 46 | var part = fromParts[i]; 47 | 48 | if (part === '.') { 49 | spliceOne(fromParts, i); 50 | } else if (part === '..') { 51 | spliceOne(fromParts, i); 52 | up++; 53 | } else if (up) { 54 | spliceOne(fromParts, i); 55 | up--; 56 | } 57 | } 58 | 59 | if (!mustEndAbs) for (; up--; up) fromParts.unshift('..'); 60 | 61 | if ( 62 | mustEndAbs && 63 | fromParts[0] !== '' && 64 | (!fromParts[0] || !isAbsolute(fromParts[0])) 65 | ) 66 | fromParts.unshift(''); 67 | 68 | var result = fromParts.join('/'); 69 | 70 | if (hasTrailingSlash && result.substr(-1) !== '/') result += '/'; 71 | 72 | return result; 73 | } 74 | 75 | export default resolvePathname; 76 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resolve-pathname", 3 | "version": "3.0.0", 4 | "description": "Resolve URL pathnames using JavaScript", 5 | "repository": "mjackson/resolve-pathname", 6 | "license": "MIT", 7 | "author": "Michael Jackson", 8 | "files": [ 9 | "cjs", 10 | "esm", 11 | "index.js", 12 | "umd" 13 | ], 14 | "main": "index.js", 15 | "module": "esm/resolve-pathname.js", 16 | "unpkg": "umd/resolve-pathname.js", 17 | "scripts": { 18 | "build": "rollup -c", 19 | "clean": "git clean -fdX .", 20 | "lint": "eslint modules", 21 | "prepublishOnly": "npm run build", 22 | "test": "jest" 23 | }, 24 | "devDependencies": { 25 | "@babel/core": "^7.1.6", 26 | "@babel/preset-env": "^7.1.6", 27 | "babel-core": "^7.0.0-bridge.0", 28 | "babel-eslint": "^10.0.1", 29 | "babel-jest": "^23.6.0", 30 | "eslint": "^5.9.0", 31 | "jest": "^23.6.0", 32 | "rollup": "^0.67.3", 33 | "rollup-plugin-replace": "^2.1.0", 34 | "rollup-plugin-size-snapshot": "^0.7.0", 35 | "rollup-plugin-uglify": "^6.0.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import replace from 'rollup-plugin-replace'; 2 | import { sizeSnapshot } from 'rollup-plugin-size-snapshot'; 3 | import { uglify } from 'rollup-plugin-uglify'; 4 | 5 | import pkg from './package.json'; 6 | 7 | const input = './modules/index.js'; 8 | const globalName = 'resolvePathname'; 9 | 10 | const cjs = [ 11 | { 12 | input, 13 | output: { file: `cjs/${pkg.name}.js`, format: 'cjs' }, 14 | plugins: [ 15 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }) 16 | ] 17 | }, 18 | { 19 | input, 20 | output: { file: `cjs/${pkg.name}.min.js`, format: 'cjs' }, 21 | plugins: [ 22 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 23 | uglify() 24 | ] 25 | } 26 | ]; 27 | 28 | const esm = [ 29 | { 30 | input, 31 | output: { file: `esm/${pkg.name}.js`, format: 'esm' }, 32 | plugins: [sizeSnapshot()] 33 | } 34 | ]; 35 | 36 | const umd = [ 37 | { 38 | input, 39 | output: { file: `umd/${pkg.name}.js`, format: 'umd', name: globalName }, 40 | plugins: [ 41 | replace({ 'process.env.NODE_ENV': JSON.stringify('development') }), 42 | sizeSnapshot() 43 | ] 44 | }, 45 | { 46 | input, 47 | output: { file: `umd/${pkg.name}.min.js`, format: 'umd', name: globalName }, 48 | plugins: [ 49 | replace({ 'process.env.NODE_ENV': JSON.stringify('production') }), 50 | sizeSnapshot(), 51 | uglify() 52 | ] 53 | } 54 | ]; 55 | 56 | let config; 57 | switch (process.env.BUILD_ENV) { 58 | case 'cjs': 59 | config = cjs; 60 | break; 61 | case 'esm': 62 | config = esm; 63 | break; 64 | case 'umd': 65 | config = umd; 66 | break; 67 | default: 68 | config = cjs.concat(esm).concat(umd); 69 | } 70 | 71 | export default config; 72 | --------------------------------------------------------------------------------