├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .gitignore ├── CONTRIBUTING.md ├── DOCUMENTATION.md ├── LICENSE ├── README.md ├── SECURITY.md ├── dist ├── index.js └── index.mjs ├── example └── index.js ├── index.d.mts ├── index.d.ts ├── package-lock.json ├── package.json ├── src └── index.js └── test ├── index.mjs └── index.test-d.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ionicabizau 2 | patreon: ionicabizau 3 | open_collective: ionicabizau 4 | custom: https://www.buymeacoffee.com/h96wwchmy -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [12.x, 14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *~ 4 | *.log 5 | node_modules 6 | *.env 7 | .DS_Store 8 | package-lock.json 9 | .bloggify/* 10 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🌟 Contributing 2 | 3 | Want to contribute to this project? Great! Please read these quick steps to streamline the process and avoid unnecessary tasks. ✨ 4 | 5 | ## 💬 Discuss Changes 6 | Start by opening an issue in the repository using the [bug tracker][1]. Describe your proposed contribution or the bug you've found. If relevant, include platform info and screenshots. 🖼️ 7 | 8 | Wait for feedback before proceeding unless the fix is straightforward, like a typo. 📝 9 | 10 | ## 🔧 Fixing Issues 11 | 12 | Fork the project and create a branch for your fix, naming it `some-great-feature` or `some-issue-fix`. Commit changes while following the [code style][2]. If the project has tests, add one. ✅ 13 | 14 | If a `package.json` or `bower.json` exists, add yourself to the `contributors` array; create it if it doesn't. 🙌 15 | 16 | ```json 17 | { 18 | "contributors": [ 19 | "Your Name (http://your.website)" 20 | ] 21 | } 22 | ``` 23 | 24 | ## 📬 Creating a Pull Request 25 | Open a pull request and reference the initial issue (e.g., *fixes #*). Provide a clear title and consider adding visual aids for clarity. 📊 26 | 27 | ## ⏳ Wait for Feedback 28 | Your contributions will be reviewed. If feedback is given, update your branch as needed, and the pull request will auto-update. 🔄 29 | 30 | ## 🎉 Everyone Is Happy! 31 | Your contributions will be merged, and everyone will appreciate your effort! 😄❤️ 32 | 33 | Thanks! 🤩 34 | 35 | [1]: /issues 36 | [2]: https://github.com/IonicaBizau/code-style -------------------------------------------------------------------------------- /DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | ## Documentation 2 | 3 | You can see below the API reference of this module. 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-25 Ionică Bizău (https://ionicabizau.net) 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 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | # parse-url 21 | 22 | [![Support me on Patreon][badge_patreon]][patreon] [![Buy me a book][badge_amazon]][amazon] [![PayPal][badge_paypal_donate]][paypal-donations] [![Ask me anything](https://img.shields.io/badge/ask%20me-anything-1abc9c.svg)](https://github.com/IonicaBizau/ama) [![Version](https://img.shields.io/npm/v/parse-url.svg)](https://www.npmjs.com/package/parse-url) [![Downloads](https://img.shields.io/npm/dt/parse-url.svg)](https://www.npmjs.com/package/parse-url) [![Get help on Codementor](https://cdn.codementor.io/badges/get_help_github.svg)](https://www.codementor.io/@johnnyb?utm_source=github&utm_medium=button&utm_term=johnnyb&utm_campaign=github) 23 | 24 | Buy Me A Coffee 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | > An advanced url parser supporting git urls too. 33 | 34 | 35 | 36 | 37 | 38 | 39 | For low-level path parsing, check out [`parse-path`](https://github.com/IonicaBizau/parse-path). This very module is designed to parse urls. By default the urls are normalized. 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | ## :cloud: Installation 53 | 54 | ```sh 55 | # Using npm 56 | npm install --save parse-url 57 | 58 | # Using yarn 59 | yarn add parse-url 60 | ``` 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | ## :clipboard: Example 75 | 76 | 77 | 78 | ```js 79 | // Dependencies 80 | import parseUrl from "parse-url"; 81 | 82 | console.log(parseUrl("http://ionicabizau.net/blog")) 83 | // { 84 | // protocols: [ 'http' ], 85 | // protocol: 'http', 86 | // port: '', 87 | // resource: 'ionicabizau.net', 88 | // user: '', 89 | // password: '', 90 | // pathname: '/blog', 91 | // hash: '', 92 | // search: '', 93 | // href: 'http://ionicabizau.net/blog', 94 | // query: {} 95 | // } 96 | 97 | console.log(parseUrl("http://domain.com/path/name?foo=bar&bar=42#some-hash")) 98 | // { 99 | // protocols: [ 'http' ], 100 | // protocol: 'http', 101 | // port: '', 102 | // resource: 'domain.com', 103 | // user: '', 104 | // password: '', 105 | // pathname: '/path/name', 106 | // hash: 'some-hash', 107 | // search: 'foo=bar&bar=42', 108 | // href: 'http://domain.com/path/name?foo=bar&bar=42#some-hash', 109 | // query: { foo: 'bar', bar: '42' } 110 | // } 111 | 112 | // If you want to parse fancy Git urls, turn off the automatic url normalization 113 | console.log(parseUrl("git+ssh://git@host.xz/path/name.git", false)) 114 | // { 115 | // protocols: [ 'git', 'ssh' ], 116 | // protocol: 'git', 117 | // port: '', 118 | // resource: 'host.xz', 119 | // user: 'git', 120 | // password: '', 121 | // pathname: '/path/name.git', 122 | // hash: '', 123 | // search: '', 124 | // href: 'git+ssh://git@host.xz/path/name.git', 125 | // query: {} 126 | // } 127 | 128 | console.log(parseUrl("git@github.com:IonicaBizau/git-stats.git", false)) 129 | // { 130 | // protocols: [ 'ssh' ], 131 | // protocol: 'ssh', 132 | // port: '', 133 | // resource: 'github.com', 134 | // user: 'git', 135 | // password: '', 136 | // pathname: '/IonicaBizau/git-stats.git', 137 | // hash: '', 138 | // search: '', 139 | // href: 'git@github.com:IonicaBizau/git-stats.git', 140 | // query: {} 141 | // } 142 | ``` 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | ## :question: Get Help 161 | 162 | There are few ways to get help: 163 | 164 | 165 | 166 | 1. Please [post questions on Stack Overflow](https://stackoverflow.com/questions/ask). You can open issues with questions, as long you add a link to your Stack Overflow question. 167 | 2. For bug reports and feature requests, open issues. :bug: 168 | 3. For direct and quick help, you can [use Codementor](https://www.codementor.io/johnnyb). :rocket: 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | ## :yum: How to contribute 184 | Have an idea? Found a bug? See [how to contribute][contributing]. 185 | 186 | 187 | ## :sparkling_heart: Support my projects 188 | I open-source almost everything I can, and I try to reply to everyone needing help using these projects. Obviously, 189 | this takes time. You can integrate and use these projects in your applications *for free*! You can even change the source code and redistribute (even resell it). 190 | 191 | However, if you get some profit from this or just want to encourage me to continue creating stuff, there are few ways you can do it: 192 | 193 | 194 | - Starring and sharing the projects you like :rocket: 195 | - [![Buy me a book][badge_amazon]][amazon]—I love books! I will remember you after years if you buy me one. :grin: :book: 196 | - [![PayPal][badge_paypal]][paypal-donations]—You can make one-time donations via PayPal. I'll probably buy a ~~coffee~~ tea. :tea: 197 | - [![Support me on Patreon][badge_patreon]][patreon]—Set up a recurring monthly donation and you will get interesting news about what I'm doing (things that I don't share with everyone). 198 | - **Bitcoin**—You can send me bitcoins at this address (or scanning the code below): `1P9BRsmazNQcuyTxEqveUsnf5CERdq35V6` 199 | 200 | ![](https://i.imgur.com/z6OQI95.png) 201 | 202 | 203 | Thanks! :heart: 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | ## :dizzy: Where is this library used? 221 | If you are using this library in one of your projects, add it in this list. :sparkles: 222 | 223 | - `@_nomtek/react-native-shimmer-animation` 224 | - `@aabelmann/ui-layer` 225 | - `@abhinavoneuipoc/stencil-test` 226 | - `@adatechnology/react-native-android-getnet-pos` 227 | - `@adembacaj/react-native-google-pay` 228 | - `@ahmed_shaban123/react-native-currencyinput` 229 | - `@ali5049/react-native-buttons` 230 | - `@amirdiafi/react-native-ios-haptics` 231 | - `@amiruldev/wajs` 232 | - `@angga30prabu/wa-modified` 233 | - `@apardellass/react-native-audio-stream` 234 | - `@appconda/app` 235 | - `@appconda/next` 236 | - `@aysea/react-native-ui-library` 237 | - `@azalpacir/react-native-dhp-printer` 238 | - `@brantalikp/rn-resize` 239 | - `@buganto/client` 240 | - `@con-test/react-native-concent-common` 241 | - `@corelmax/react-native-my2c2p-sdk` 242 | - `@cs6/react-native-test-native-view-library` 243 | - `@damruravihara/react-native-testing-package` 244 | - `@dataparty/api` 245 | - `@enkeledi/react-native-week-month-date-picker` 246 | - `@felipesimmi/react-native-datalogic-module` 247 | - `@foundernetes/machines` 248 | - `@foundernetes/metal-debian` 249 | - `@geeky-apo/react-native-advanced-clipboard` 250 | - `@hawkingnetwork/react-native-tab-view` 251 | - `@hbglobal/react-native-actions-shortcuts` 252 | - `@hemith/react-native-tnk` 253 | - `@hieuquang2212/form` 254 | - `@hstech/utils` 255 | - `@idas1/ui-component-lib` 256 | - `@jfilipe-sparta/react-native-module_2` 257 | - `@jimengio/mocked-proxy` 258 | - `@jprustv/sulla-hotfix` 259 | - `@kgit/readability` 260 | - `@kgit/readbility` 261 | - `@klevn/solid-router` 262 | - `@kriblet/wa-automate` 263 | - `@labiebhn_/react-native-multiplier` 264 | - `@lakhlaifi/semantic-gitlab` 265 | - `@lakutata-module/service` 266 | - `@lehuyaa/my-assets` 267 | - `@logisticinfotech/react-native-geocoding-reversegeocoding` 268 | - `@mergulhao/wa-automate` 269 | - `@mockswitch/cli` 270 | - `@navabi/react-native-ssl-pinning` 271 | - `@notnuzzel/crawl` 272 | - `@oiti/rn-liveness2d` 273 | - `@open-wa/wa-automate` 274 | - `@openshift-assisted/ui-lib` 275 | - `@orgbluetooth/react-native-arunpayupayment` 276 | - `@orgbluetooth/react-native-payupayment` 277 | - `@parallelnft/web3modal` 278 | - `@phpboyscout/semantic-release-gitlab` 279 | - `@phuocnb/semrelease-gitlab` 280 | - `@pocket-tools/image-utils` 281 | - `@podpodium/common` 282 | - `@positionex/position-sdk` 283 | - `@praella/localisationist` 284 | - `@qiwi/sourcecrumbs` 285 | - `@react-18-pdf/root` 286 | - `@react-native-ui-design/button` 287 | - `@roq/ui-react` 288 | - `@roshub/api` 289 | - `@saad27/react-native-bottom-tab-tour` 290 | - `@safely-project/safely-ts` 291 | - `@semantic-release/gitlab` 292 | - `@sephriot/react-native-persistable-uri` 293 | - `@sidghimire/react-native-mapbox-navigation` 294 | - `@sridharetikala/react-native-rn-lib-custom-components` 295 | - `@status-im/react-native-transparent-video` 296 | - `@syedt/hellosdk` 297 | - `@taingo97/react-native-awesome-module` 298 | - `@taingo97/react-native-bluetooth-xprinter` 299 | - `@taingo97/react-native-expo-key-rsa-kt` 300 | - `@taingo97/react-native-expo-rsa` 301 | - `@taingo97/react-native-generate-key-rsa` 302 | - `@taingo97/react-native-key-rsa` 303 | - `@taingo97/react-native-print-xprinter` 304 | - `@taingo97/react-native-printer-imin` 305 | - `@taingo97/react-native-rsa` 306 | - `@taingo97/react-native-rsa-expo` 307 | - `@taingo97/react-native-sunmi-printer` 308 | - `@taingo97/react-native-telpo-printer` 309 | - `@teles1-semantic-release/gitlab` 310 | - `@thinxviewx/core-rn` 311 | - `@tlgeo/react-native-gdal` 312 | - `@tomw2w/my-nuxt-layer` 313 | - `@visioglobe/react-native-visioglobe` 314 | - `@wecraftapps/react-native-use-keyboard` 315 | - `@yplabs-ltd/react-native-detector` 316 | - `@zotasys/native` 317 | - `actttt` 318 | - `agent-get-agent` 319 | - `anakketiga` 320 | - `anaklanangtea` 321 | - `anakwadontea` 322 | - `angularvezba` 323 | - `apaas-track` 324 | - `api-reach-react-native-fix` 325 | - `archlibrary` 326 | - `arifbudixz` 327 | - `astra-ufo-sdk` 328 | - `awesome-module-kd` 329 | - `begg` 330 | - `bilibili2local` 331 | - `biometric-st` 332 | - `birken-react-native-community-image-editor` 333 | - `blitzzz` 334 | - `build-plugin-ssr` 335 | - `candlelabssdk` 336 | - `checkbox-component` 337 | - `cli-live-tutorial` 338 | - `connex-kakilang` 339 | - `connex_ram00nez` 340 | - `delta-screen` 341 | - `demo-test-scrn` 342 | - `deploy-versioning` 343 | - `design-system-trial-milyasbpa` 344 | - `dogandev-simple-toast` 345 | - `egg-muc-custom-loader` 346 | - `eval-spider` 347 | - `fawaterak-online-payment` 348 | - `fawatrak-online-payment` 349 | - `fixed_form_builder` 350 | - `fluent.adflow.reactnativesdk` 351 | - `fluent.adflow.reactnativesdk-alpha` 352 | - `fmsl` 353 | - `framework_test_library_sixdee` 354 | - `framework_test_library_sixdee_new` 355 | - `framework_test_library_sixdee_new_new` 356 | - `fuge-runner` 357 | - `gamification-integration-new` 358 | - `gaurav-react-native-loop` 359 | - `genz-native-elements` 360 | - `gerimismalamsenin` 361 | - `get-tarball-cli` 362 | - `gf-roq-ui-react` 363 | - `gh-monoproject-cli` 364 | - `git-up` 365 | - `gitlab-backup-util-harduino` 366 | - `gr-roq-ui-react` 367 | - `graphmilker` 368 | - `heroku-wp-environment-sync` 369 | - `hologit` 370 | - `hong1-utils` 371 | - `hubot-will-it-connect` 372 | - `hui-plugin-wss` 373 | - `iiif-manifest-editor` 374 | - `ipsamvel` 375 | - `jamuskalim` 376 | - `jesh-calculation` 377 | - `jnf-accesscontrol-rnttl` 378 | - `jordy-frijters-test-lib` 379 | - `jrennsoh88-react-native-scroll-indicator` 380 | - `jy-act` 381 | - `kakapo` 382 | - `khaled-salem-custom-components` 383 | - `l2forlerna` 384 | - `luojia-cli-dev` 385 | - `mangudinlagirajin` 386 | - `markdownalint-cli2` 387 | - `michael-stun` 388 | - `microbe.js` 389 | - `miguelcostero-ng2-toasty` 390 | - `native-apple-login` 391 | - `native-date-picker-module` 392 | - `native-google-login` 393 | - `native-kakao-login` 394 | - `native-modal-damage-vehicle` 395 | - `native-zip` 396 | - `ndla-source-map-resolver` 397 | - `new-awesome-4321` 398 | - `njs-wa-auto` 399 | - `normalize-id` 400 | - `normalize-ssh` 401 | - `normalize-ssh-url` 402 | - `npm_one_12_34_1_` 403 | - `npm_one_1_2_3` 404 | - `npm_one_2_2` 405 | - `npm_qwerty` 406 | - `nuxtpaginations` 407 | - `octopulse` 408 | - `parse-db-uri` 409 | - `pasbeaucoupmoinsrave` 410 | - `patepangdeui` 411 | - `payutesting` 412 | - `pileuleuyantea` 413 | - `pnm-yph-react-native-custom-components` 414 | - `project-wajs-dv` 415 | - `pyreswap-sdk` 416 | - `raact-native-arunramya151` 417 | - `reac-native-arun-ramya-test` 418 | - `react-native-adarsh_react_native_video_player` 419 | - `react-native-addition` 420 | - `react-native-android-native-view` 421 | - `react-native-android-video-player-view` 422 | - `react-native-animate-text` 423 | - `react-native-app-bubble` 424 | - `react-native-app-integrity-checksum` 425 | - `react-native-arps-authorize-net` 426 | - `react-native-arun-ramya-test` 427 | - `react-native-arunjeyam1987` 428 | - `react-native-arunmeena1987` 429 | - `react-native-arunramya151` 430 | - `react-native-auth-service-client` 431 | - `react-native-aventonfacetec-aventon` 432 | - `react-native-awesome-android-123` 433 | - `react-native-awesome-android-123-zeotap` 434 | - `react-native-awesome-module-dharmesh` 435 | - `react-native-awesome-module-latest` 436 | - `react-native-awesome-module-two` 437 | - `react-native-azure-communication-services` 438 | - `react-native-badge-control` 439 | - `react-native-basic-app` 440 | - `react-native-basic-screen` 441 | - `react-native-biometric-authenticate` 442 | - `react-native-bleccs-components` 443 | - `react-native-bluetooth-device-detect` 444 | - `react-native-bottom-tab-designs` 445 | - `react-native-bridge-package` 446 | - `react-native-bubble-chart` 447 | - `react-native-build-vesion-getter` 448 | - `react-native-check-component` 449 | - `react-native-chenaar` 450 | - `react-native-components-design` 451 | - `react-native-conekta-card-tokenizer` 452 | - `react-native-contact-list` 453 | - `react-native-cplus` 454 | - `react-native-create-video-thumbnail` 455 | - `react-native-ctp-odp` 456 | - `react-native-custom-image-carousel` 457 | - `react-native-custom-poccomponent` 458 | - `react-native-custom-poccomponent-next` 459 | - `react-native-datacapture-core` 460 | - `react-native-dff-components-demo` 461 | - `react-native-dhp-printer` 462 | - `react-native-dimensions-layout` 463 | - `react-native-dsphoto-module` 464 | - `react-native-dummy-view` 465 | - `react-native-escape` 466 | - `react-native-fedlight-dsm` 467 | - `react-native-get-countries` 468 | - `react-native-ghn-ekyc` 469 | - `react-native-ideo-rn-notifications` 470 | - `react-native-innity-2` 471 | - `react-native-innity-remaster` 472 | - `react-native-input-library` 473 | - `react-native-is7` 474 | - `react-native-jsi-device-info` 475 | - `react-native-kakao-maps` 476 | - `react-native-klarify-ios` 477 | - `react-native-klarify-ui` 478 | - `react-native-klc` 479 | - `react-native-lib-test-rn-1` 480 | - `react-native-library-testing-422522` 481 | - `react-native-line-login-android` 482 | - `react-native-login-demo-test` 483 | - `react-native-lowlatency` 484 | - `react-native-loyalty-platforms` 485 | - `react-native-manh-test` 486 | - `react-native-manual-ios-sdk` 487 | - `react-native-modal-progress-bar` 488 | - `react-native-module-arge` 489 | - `react-native-module-for-testing` 490 | - `react-native-multi-bluetooth-printer` 491 | - `react-native-multiplier-altroncoso` 492 | - `react-native-multiplier-component` 493 | - `react-native-multiplier-demo` 494 | - `react-native-multiplier2` 495 | - `react-native-multiply` 496 | - `react-native-multiply-component` 497 | - `react-native-multiselector` 498 | - `react-native-mun-kit` 499 | - `react-native-my-first-try-arun-ramya` 500 | - `react-native-native-audio-engine` 501 | - `react-native-native-ios-test1` 502 | - `react-native-nativewind` 503 | - `react-native-nghia-sharering` 504 | - `react-native-nice-learning` 505 | - `react-native-nyx-printer` 506 | - `react-native-offline-notice` 507 | - `react-native-onramp` 508 | - `react-native-opus` 509 | - `react-native-otp-custom-library` 510 | - `react-native-paynow-generator` 511 | - `react-native-payu-payment` 512 | - `react-native-payu-payment-testing` 513 | - `react-native-plugpag-wrapper` 514 | - `react-native-progress-arrow` 515 | - `react-native-pulsator-native` 516 | - `react-native-rabbitmq-all` 517 | - `react-native-radio-bic-group-lib` 518 | - `react-native-reanimated-sortable-list` 519 | - `react-native-recent-framework-update` 520 | - `react-native-remote-update` 521 | - `react-native-responsive-helper` 522 | - `react-native-responsive-size` 523 | - `react-native-return-usb-data` 524 | - `react-native-rn-app` 525 | - `react-native-rn-common-components-example` 526 | - `react-native-rn-icons-library` 527 | - `react-native-rn-tolkaplayer` 528 | - `react-native-rom-components` 529 | - `react-native-rtn-ips-poslin-test` 530 | - `react-native-s-airlines` 531 | - `react-native-sandycomponent` 532 | - `react-native-savczuk-feature-library` 533 | - `react-native-sayhello-module` 534 | - `react-native-screen-idle-timer` 535 | - `react-native-scroll-tab-to-index` 536 | - `react-native-shared-gesture` 537 | - `react-native-sharing-intent` 538 | - `react-native-simple-timeline` 539 | - `react-native-sp-test-common` 540 | - `react-native-sunmi-printer-hk` 541 | - `react-native-superapis-transbank-pos` 542 | - `react-native-syan-photo-picker` 543 | - `react-native-teads-sdk-module` 544 | - `react-native-tejab41097-sample-library` 545 | - `react-native-teknoctrl-components` 546 | - `react-native-test-comlibrary` 547 | - `react-native-test-module-hhh` 548 | - `react-native-test-multiplier-library` 549 | - `react-native-test-tooltip` 550 | - `react-native-test-view` 551 | - `react-native-ticker-tape` 552 | - `react-native-tilt-ble` 553 | - `react-native-tones` 554 | - `react-native-transtracker-library` 555 | - `react-native-ui-components-library` 556 | - `react-native-uvc-camera-android` 557 | - `react-native-vanguard-sdk` 558 | - `react-native-version-app` 559 | - `react-native-volume-phisical` 560 | - `react-native-withframework-check` 561 | - `react-native-wtf` 562 | - `react-native-xprinter-thermal-ble` 563 | - `react-native-ytximkit` 564 | - `reactnatively` 565 | - `reat-native-multiplierkpr` 566 | - `refinejs-repo` 567 | - `rn-adyen-dropin` 568 | - `rn-check-btn` 569 | - `rn-circular-chart` 570 | - `rn-counter-demo` 571 | - `rn-currency-formatter` 572 | - `rn-session-multiplier-demo` 573 | - `rn-tm-notify` 574 | - `rn_unique_device_id` 575 | - `rnttlock` 576 | - `robots-agent` 577 | - `rocomp` 578 | - `scout-chatbot-widget` 579 | - `sedanbosok` 580 | - `semantic-release-gitmoji-action` 581 | - `smart_one_connect` 582 | - `soajs.repositories` 583 | - `sourcecrumbs` 584 | - `stun` 585 | - `tehmusimhujan` 586 | - `test-haptik-lib` 587 | - `test-library-123` 588 | - `test-zeo-collect` 589 | - `tools_may_24` 590 | - `ts-scraper` 591 | - `tumblr-text` 592 | - `url-local` 593 | - `vantiq-react` 594 | - `verify-aws-sns-signature` 595 | - `vision-camera-base64-resized` 596 | - `vision-camera-plugin-face-detector` 597 | - `vision-camera-plugin-scan-faces` 598 | - `vrt-cli` 599 | - `vue-cli-plugin-ice-builder` 600 | - `vue-cli-plugin-ut-builder` 601 | - `wa-automate` 602 | - `wander-cli` 603 | - `warp-api` 604 | - `warp-server` 605 | - `wifi_configuration_package` 606 | - `winx-form-winx` 607 | - `workpad` 608 | - `xbuilder-forms` 609 | - `xl-git-up` 610 | - `yangtao-js` 611 | - `yarn-react-hook-form` 612 | - `zzzxxxyyy321123` 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | ## :scroll: License 625 | 626 | [MIT][license] © [Ionică Bizău][website] 627 | 628 | 629 | 630 | 631 | 632 | 633 | [license]: /LICENSE 634 | [website]: https://ionicabizau.net 635 | [contributing]: /CONTRIBUTING.md 636 | [docs]: /DOCUMENTATION.md 637 | [badge_patreon]: https://ionicabizau.github.io/badges/patreon.svg 638 | [badge_amazon]: https://ionicabizau.github.io/badges/amazon.svg 639 | [badge_paypal]: https://ionicabizau.github.io/badges/paypal.svg 640 | [badge_paypal_donate]: https://ionicabizau.github.io/badges/paypal_donate.svg 641 | [patreon]: https://www.patreon.com/ionicabizau 642 | [amazon]: http://amzn.eu/hRo9sIZ 643 | [paypal-donations]: https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=RVXDDLKKLQRJW 644 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting a Vulnerability 2 | 3 | If you discover a security vulnerability in parse-url please disclose it via [our huntr page](https://huntr.dev/repos/ionicabizau/parse-url/). Bounty eligibility, CVE assignment, response times and past reports are all there. 4 | 5 | Thank you for improving the security of parse-url. -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var parsePath = require('parse-path'); 4 | 5 | const DATA_URL_DEFAULT_MIME_TYPE = "text/plain"; 6 | const DATA_URL_DEFAULT_CHARSET = "us-ascii"; 7 | const testParameter = (name, filters) => filters.some((filter) => filter instanceof RegExp ? filter.test(name) : filter === name); 8 | const supportedProtocols = /* @__PURE__ */ new Set([ 9 | "https:", 10 | "http:", 11 | "file:" 12 | ]); 13 | const hasCustomProtocol = (urlString) => { 14 | try { 15 | const { protocol } = new URL(urlString); 16 | return protocol.endsWith(":") && !protocol.includes(".") && !supportedProtocols.has(protocol); 17 | } catch { 18 | return false; 19 | } 20 | }; 21 | const normalizeDataURL = (urlString, { stripHash }) => { 22 | const match = /^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec(urlString); 23 | if (!match) { 24 | throw new Error(`Invalid URL: ${urlString}`); 25 | } 26 | let { type, data, hash } = match.groups; 27 | const mediaType = type.split(";"); 28 | hash = stripHash ? "" : hash; 29 | let isBase64 = false; 30 | if (mediaType[mediaType.length - 1] === "base64") { 31 | mediaType.pop(); 32 | isBase64 = true; 33 | } 34 | const mimeType = mediaType.shift()?.toLowerCase() ?? ""; 35 | const attributes = mediaType.map((attribute) => { 36 | let [key, value = ""] = attribute.split("=").map((string) => string.trim()); 37 | if (key === "charset") { 38 | value = value.toLowerCase(); 39 | if (value === DATA_URL_DEFAULT_CHARSET) { 40 | return ""; 41 | } 42 | } 43 | return `${key}${value ? `=${value}` : ""}`; 44 | }).filter(Boolean); 45 | const normalizedMediaType = [ 46 | ...attributes 47 | ]; 48 | if (isBase64) { 49 | normalizedMediaType.push("base64"); 50 | } 51 | if (normalizedMediaType.length > 0 || mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE) { 52 | normalizedMediaType.unshift(mimeType); 53 | } 54 | return `data:${normalizedMediaType.join(";")},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ""}`; 55 | }; 56 | function normalizeUrl(urlString, options) { 57 | options = { 58 | defaultProtocol: "http", 59 | normalizeProtocol: true, 60 | forceHttp: false, 61 | forceHttps: false, 62 | stripAuthentication: true, 63 | stripHash: false, 64 | stripTextFragment: true, 65 | stripWWW: true, 66 | removeQueryParameters: [/^utm_\w+/i], 67 | removeTrailingSlash: true, 68 | removeSingleSlash: true, 69 | removeDirectoryIndex: false, 70 | removeExplicitPort: false, 71 | sortQueryParameters: true, 72 | ...options 73 | }; 74 | if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) { 75 | options.defaultProtocol = `${options.defaultProtocol}:`; 76 | } 77 | urlString = urlString.trim(); 78 | if (/^data:/i.test(urlString)) { 79 | return normalizeDataURL(urlString, options); 80 | } 81 | if (hasCustomProtocol(urlString)) { 82 | return urlString; 83 | } 84 | const hasRelativeProtocol = urlString.startsWith("//"); 85 | const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); 86 | if (!isRelativeUrl) { 87 | urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol); 88 | } 89 | const urlObject = new URL(urlString); 90 | if (options.forceHttp && options.forceHttps) { 91 | throw new Error("The `forceHttp` and `forceHttps` options cannot be used together"); 92 | } 93 | if (options.forceHttp && urlObject.protocol === "https:") { 94 | urlObject.protocol = "http:"; 95 | } 96 | if (options.forceHttps && urlObject.protocol === "http:") { 97 | urlObject.protocol = "https:"; 98 | } 99 | if (options.stripAuthentication) { 100 | urlObject.username = ""; 101 | urlObject.password = ""; 102 | } 103 | if (options.stripHash) { 104 | urlObject.hash = ""; 105 | } else if (options.stripTextFragment) { 106 | urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, ""); 107 | } 108 | if (urlObject.pathname) { 109 | const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g; 110 | let lastIndex = 0; 111 | let result = ""; 112 | for (; ; ) { 113 | const match = protocolRegex.exec(urlObject.pathname); 114 | if (!match) { 115 | break; 116 | } 117 | const protocol = match[0]; 118 | const protocolAtIndex = match.index; 119 | const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex); 120 | result += intermediate.replace(/\/{2,}/g, "/"); 121 | result += protocol; 122 | lastIndex = protocolAtIndex + protocol.length; 123 | } 124 | const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length); 125 | result += remnant.replace(/\/{2,}/g, "/"); 126 | urlObject.pathname = result; 127 | } 128 | if (urlObject.pathname) { 129 | try { 130 | urlObject.pathname = decodeURI(urlObject.pathname); 131 | } catch { 132 | } 133 | } 134 | if (options.removeDirectoryIndex === true) { 135 | options.removeDirectoryIndex = [/^index\.[a-z]+$/]; 136 | } 137 | if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) { 138 | let pathComponents = urlObject.pathname.split("/"); 139 | const lastComponent = pathComponents[pathComponents.length - 1]; 140 | if (testParameter(lastComponent, options.removeDirectoryIndex)) { 141 | pathComponents = pathComponents.slice(0, -1); 142 | urlObject.pathname = pathComponents.slice(1).join("/") + "/"; 143 | } 144 | } 145 | if (urlObject.hostname) { 146 | urlObject.hostname = urlObject.hostname.replace(/\.$/, ""); 147 | if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) { 148 | urlObject.hostname = urlObject.hostname.replace(/^www\./, ""); 149 | } 150 | } 151 | if (Array.isArray(options.removeQueryParameters)) { 152 | for (const key of [...urlObject.searchParams.keys()]) { 153 | if (testParameter(key, options.removeQueryParameters)) { 154 | urlObject.searchParams.delete(key); 155 | } 156 | } 157 | } 158 | if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) { 159 | urlObject.search = ""; 160 | } 161 | if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) { 162 | for (const key of [...urlObject.searchParams.keys()]) { 163 | if (!testParameter(key, options.keepQueryParameters)) { 164 | urlObject.searchParams.delete(key); 165 | } 166 | } 167 | } 168 | if (options.sortQueryParameters) { 169 | urlObject.searchParams.sort(); 170 | try { 171 | urlObject.search = decodeURIComponent(urlObject.search); 172 | } catch { 173 | } 174 | } 175 | if (options.removeTrailingSlash) { 176 | urlObject.pathname = urlObject.pathname.replace(/\/$/, ""); 177 | } 178 | if (options.removeExplicitPort && urlObject.port) { 179 | urlObject.port = ""; 180 | } 181 | const oldUrlString = urlString; 182 | urlString = urlObject.toString(); 183 | if (!options.removeSingleSlash && urlObject.pathname === "/" && !oldUrlString.endsWith("/") && urlObject.hash === "") { 184 | urlString = urlString.replace(/\/$/, ""); 185 | } 186 | if ((options.removeTrailingSlash || urlObject.pathname === "/") && urlObject.hash === "" && options.removeSingleSlash) { 187 | urlString = urlString.replace(/\/$/, ""); 188 | } 189 | if (hasRelativeProtocol && !options.normalizeProtocol) { 190 | urlString = urlString.replace(/^http:\/\//, "//"); 191 | } 192 | if (options.stripProtocol) { 193 | urlString = urlString.replace(/^(?:https?:)?\/\//, ""); 194 | } 195 | return urlString; 196 | } 197 | 198 | const parseUrl = (url, normalize = false) => { 199 | const GIT_RE = /^(?:([a-zA-Z_][a-zA-Z0-9_-]{0,31})@|https?:\/\/)([\w\.\-@]+)[\/:](([\~,\.\w,\-,\_,\/,\s]|%[0-9A-Fa-f]{2})+?(?:\.git|\/)?)$/; 200 | const throwErr = (msg) => { 201 | const err = new Error(msg); 202 | err.subject_url = url; 203 | throw err; 204 | }; 205 | if (typeof url !== "string" || !url.trim()) { 206 | throwErr("Invalid url."); 207 | } 208 | if (url.length > parseUrl.MAX_INPUT_LENGTH) { 209 | throwErr("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH."); 210 | } 211 | if (normalize) { 212 | if (typeof normalize !== "object") { 213 | normalize = { 214 | stripHash: false 215 | }; 216 | } 217 | url = normalizeUrl(url, normalize); 218 | } 219 | const parsed = parsePath(url); 220 | if (parsed.parse_failed) { 221 | const matched = parsed.href.match(GIT_RE); 222 | if (matched) { 223 | parsed.protocols = ["ssh"]; 224 | parsed.protocol = "ssh"; 225 | parsed.resource = matched[2]; 226 | parsed.host = matched[2]; 227 | parsed.user = matched[1]; 228 | parsed.pathname = `/${matched[3]}`; 229 | parsed.parse_failed = false; 230 | } else { 231 | throwErr("URL parsing failed."); 232 | } 233 | } 234 | return parsed; 235 | }; 236 | parseUrl.MAX_INPUT_LENGTH = 2048; 237 | 238 | module.exports = parseUrl; 239 | -------------------------------------------------------------------------------- /dist/index.mjs: -------------------------------------------------------------------------------- 1 | import parsePath from 'parse-path'; 2 | 3 | const DATA_URL_DEFAULT_MIME_TYPE = "text/plain"; 4 | const DATA_URL_DEFAULT_CHARSET = "us-ascii"; 5 | const testParameter = (name, filters) => filters.some((filter) => filter instanceof RegExp ? filter.test(name) : filter === name); 6 | const supportedProtocols = /* @__PURE__ */ new Set([ 7 | "https:", 8 | "http:", 9 | "file:" 10 | ]); 11 | const hasCustomProtocol = (urlString) => { 12 | try { 13 | const { protocol } = new URL(urlString); 14 | return protocol.endsWith(":") && !protocol.includes(".") && !supportedProtocols.has(protocol); 15 | } catch { 16 | return false; 17 | } 18 | }; 19 | const normalizeDataURL = (urlString, { stripHash }) => { 20 | const match = /^data:(?[^,]*?),(?[^#]*?)(?:#(?.*))?$/.exec(urlString); 21 | if (!match) { 22 | throw new Error(`Invalid URL: ${urlString}`); 23 | } 24 | let { type, data, hash } = match.groups; 25 | const mediaType = type.split(";"); 26 | hash = stripHash ? "" : hash; 27 | let isBase64 = false; 28 | if (mediaType[mediaType.length - 1] === "base64") { 29 | mediaType.pop(); 30 | isBase64 = true; 31 | } 32 | const mimeType = mediaType.shift()?.toLowerCase() ?? ""; 33 | const attributes = mediaType.map((attribute) => { 34 | let [key, value = ""] = attribute.split("=").map((string) => string.trim()); 35 | if (key === "charset") { 36 | value = value.toLowerCase(); 37 | if (value === DATA_URL_DEFAULT_CHARSET) { 38 | return ""; 39 | } 40 | } 41 | return `${key}${value ? `=${value}` : ""}`; 42 | }).filter(Boolean); 43 | const normalizedMediaType = [ 44 | ...attributes 45 | ]; 46 | if (isBase64) { 47 | normalizedMediaType.push("base64"); 48 | } 49 | if (normalizedMediaType.length > 0 || mimeType && mimeType !== DATA_URL_DEFAULT_MIME_TYPE) { 50 | normalizedMediaType.unshift(mimeType); 51 | } 52 | return `data:${normalizedMediaType.join(";")},${isBase64 ? data.trim() : data}${hash ? `#${hash}` : ""}`; 53 | }; 54 | function normalizeUrl(urlString, options) { 55 | options = { 56 | defaultProtocol: "http", 57 | normalizeProtocol: true, 58 | forceHttp: false, 59 | forceHttps: false, 60 | stripAuthentication: true, 61 | stripHash: false, 62 | stripTextFragment: true, 63 | stripWWW: true, 64 | removeQueryParameters: [/^utm_\w+/i], 65 | removeTrailingSlash: true, 66 | removeSingleSlash: true, 67 | removeDirectoryIndex: false, 68 | removeExplicitPort: false, 69 | sortQueryParameters: true, 70 | ...options 71 | }; 72 | if (typeof options.defaultProtocol === "string" && !options.defaultProtocol.endsWith(":")) { 73 | options.defaultProtocol = `${options.defaultProtocol}:`; 74 | } 75 | urlString = urlString.trim(); 76 | if (/^data:/i.test(urlString)) { 77 | return normalizeDataURL(urlString, options); 78 | } 79 | if (hasCustomProtocol(urlString)) { 80 | return urlString; 81 | } 82 | const hasRelativeProtocol = urlString.startsWith("//"); 83 | const isRelativeUrl = !hasRelativeProtocol && /^\.*\//.test(urlString); 84 | if (!isRelativeUrl) { 85 | urlString = urlString.replace(/^(?!(?:\w+:)?\/\/)|^\/\//, options.defaultProtocol); 86 | } 87 | const urlObject = new URL(urlString); 88 | if (options.forceHttp && options.forceHttps) { 89 | throw new Error("The `forceHttp` and `forceHttps` options cannot be used together"); 90 | } 91 | if (options.forceHttp && urlObject.protocol === "https:") { 92 | urlObject.protocol = "http:"; 93 | } 94 | if (options.forceHttps && urlObject.protocol === "http:") { 95 | urlObject.protocol = "https:"; 96 | } 97 | if (options.stripAuthentication) { 98 | urlObject.username = ""; 99 | urlObject.password = ""; 100 | } 101 | if (options.stripHash) { 102 | urlObject.hash = ""; 103 | } else if (options.stripTextFragment) { 104 | urlObject.hash = urlObject.hash.replace(/#?:~:text.*?$/i, ""); 105 | } 106 | if (urlObject.pathname) { 107 | const protocolRegex = /\b[a-z][a-z\d+\-.]{1,50}:\/\//g; 108 | let lastIndex = 0; 109 | let result = ""; 110 | for (; ; ) { 111 | const match = protocolRegex.exec(urlObject.pathname); 112 | if (!match) { 113 | break; 114 | } 115 | const protocol = match[0]; 116 | const protocolAtIndex = match.index; 117 | const intermediate = urlObject.pathname.slice(lastIndex, protocolAtIndex); 118 | result += intermediate.replace(/\/{2,}/g, "/"); 119 | result += protocol; 120 | lastIndex = protocolAtIndex + protocol.length; 121 | } 122 | const remnant = urlObject.pathname.slice(lastIndex, urlObject.pathname.length); 123 | result += remnant.replace(/\/{2,}/g, "/"); 124 | urlObject.pathname = result; 125 | } 126 | if (urlObject.pathname) { 127 | try { 128 | urlObject.pathname = decodeURI(urlObject.pathname); 129 | } catch { 130 | } 131 | } 132 | if (options.removeDirectoryIndex === true) { 133 | options.removeDirectoryIndex = [/^index\.[a-z]+$/]; 134 | } 135 | if (Array.isArray(options.removeDirectoryIndex) && options.removeDirectoryIndex.length > 0) { 136 | let pathComponents = urlObject.pathname.split("/"); 137 | const lastComponent = pathComponents[pathComponents.length - 1]; 138 | if (testParameter(lastComponent, options.removeDirectoryIndex)) { 139 | pathComponents = pathComponents.slice(0, -1); 140 | urlObject.pathname = pathComponents.slice(1).join("/") + "/"; 141 | } 142 | } 143 | if (urlObject.hostname) { 144 | urlObject.hostname = urlObject.hostname.replace(/\.$/, ""); 145 | if (options.stripWWW && /^www\.(?!www\.)[a-z\-\d]{1,63}\.[a-z.\-\d]{2,63}$/.test(urlObject.hostname)) { 146 | urlObject.hostname = urlObject.hostname.replace(/^www\./, ""); 147 | } 148 | } 149 | if (Array.isArray(options.removeQueryParameters)) { 150 | for (const key of [...urlObject.searchParams.keys()]) { 151 | if (testParameter(key, options.removeQueryParameters)) { 152 | urlObject.searchParams.delete(key); 153 | } 154 | } 155 | } 156 | if (!Array.isArray(options.keepQueryParameters) && options.removeQueryParameters === true) { 157 | urlObject.search = ""; 158 | } 159 | if (Array.isArray(options.keepQueryParameters) && options.keepQueryParameters.length > 0) { 160 | for (const key of [...urlObject.searchParams.keys()]) { 161 | if (!testParameter(key, options.keepQueryParameters)) { 162 | urlObject.searchParams.delete(key); 163 | } 164 | } 165 | } 166 | if (options.sortQueryParameters) { 167 | urlObject.searchParams.sort(); 168 | try { 169 | urlObject.search = decodeURIComponent(urlObject.search); 170 | } catch { 171 | } 172 | } 173 | if (options.removeTrailingSlash) { 174 | urlObject.pathname = urlObject.pathname.replace(/\/$/, ""); 175 | } 176 | if (options.removeExplicitPort && urlObject.port) { 177 | urlObject.port = ""; 178 | } 179 | const oldUrlString = urlString; 180 | urlString = urlObject.toString(); 181 | if (!options.removeSingleSlash && urlObject.pathname === "/" && !oldUrlString.endsWith("/") && urlObject.hash === "") { 182 | urlString = urlString.replace(/\/$/, ""); 183 | } 184 | if ((options.removeTrailingSlash || urlObject.pathname === "/") && urlObject.hash === "" && options.removeSingleSlash) { 185 | urlString = urlString.replace(/\/$/, ""); 186 | } 187 | if (hasRelativeProtocol && !options.normalizeProtocol) { 188 | urlString = urlString.replace(/^http:\/\//, "//"); 189 | } 190 | if (options.stripProtocol) { 191 | urlString = urlString.replace(/^(?:https?:)?\/\//, ""); 192 | } 193 | return urlString; 194 | } 195 | 196 | const parseUrl = (url, normalize = false) => { 197 | const GIT_RE = /^(?:([a-zA-Z_][a-zA-Z0-9_-]{0,31})@|https?:\/\/)([\w\.\-@]+)[\/:](([\~,\.\w,\-,\_,\/,\s]|%[0-9A-Fa-f]{2})+?(?:\.git|\/)?)$/; 198 | const throwErr = (msg) => { 199 | const err = new Error(msg); 200 | err.subject_url = url; 201 | throw err; 202 | }; 203 | if (typeof url !== "string" || !url.trim()) { 204 | throwErr("Invalid url."); 205 | } 206 | if (url.length > parseUrl.MAX_INPUT_LENGTH) { 207 | throwErr("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH."); 208 | } 209 | if (normalize) { 210 | if (typeof normalize !== "object") { 211 | normalize = { 212 | stripHash: false 213 | }; 214 | } 215 | url = normalizeUrl(url, normalize); 216 | } 217 | const parsed = parsePath(url); 218 | if (parsed.parse_failed) { 219 | const matched = parsed.href.match(GIT_RE); 220 | if (matched) { 221 | parsed.protocols = ["ssh"]; 222 | parsed.protocol = "ssh"; 223 | parsed.resource = matched[2]; 224 | parsed.host = matched[2]; 225 | parsed.user = matched[1]; 226 | parsed.pathname = `/${matched[3]}`; 227 | parsed.parse_failed = false; 228 | } else { 229 | throwErr("URL parsing failed."); 230 | } 231 | } 232 | return parsed; 233 | }; 234 | parseUrl.MAX_INPUT_LENGTH = 2048; 235 | 236 | export { parseUrl as default }; 237 | -------------------------------------------------------------------------------- /example/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | import parseUrl from "parse-url"; 3 | 4 | console.log(parseUrl("http://ionicabizau.net/blog")) 5 | // { 6 | // protocols: [ 'http' ], 7 | // protocol: 'http', 8 | // port: '', 9 | // resource: 'ionicabizau.net', 10 | // user: '', 11 | // password: '', 12 | // pathname: '/blog', 13 | // hash: '', 14 | // search: '', 15 | // href: 'http://ionicabizau.net/blog', 16 | // query: {} 17 | // } 18 | 19 | console.log(parseUrl("http://domain.com/path/name?foo=bar&bar=42#some-hash")) 20 | // { 21 | // protocols: [ 'http' ], 22 | // protocol: 'http', 23 | // port: '', 24 | // resource: 'domain.com', 25 | // user: '', 26 | // password: '', 27 | // pathname: '/path/name', 28 | // hash: 'some-hash', 29 | // search: 'foo=bar&bar=42', 30 | // href: 'http://domain.com/path/name?foo=bar&bar=42#some-hash', 31 | // query: { foo: 'bar', bar: '42' } 32 | // } 33 | 34 | // If you want to parse fancy Git urls, turn off the automatic url normalization 35 | console.log(parseUrl("git+ssh://git@host.xz/path/name.git", false)) 36 | // { 37 | // protocols: [ 'git', 'ssh' ], 38 | // protocol: 'git', 39 | // port: '', 40 | // resource: 'host.xz', 41 | // user: 'git', 42 | // password: '', 43 | // pathname: '/path/name.git', 44 | // hash: '', 45 | // search: '', 46 | // href: 'git+ssh://git@host.xz/path/name.git', 47 | // query: {} 48 | // } 49 | 50 | console.log(parseUrl("git@github.com:IonicaBizau/git-stats.git", false)) 51 | // { 52 | // protocols: [ 'ssh' ], 53 | // protocol: 'ssh', 54 | // port: '', 55 | // resource: 'github.com', 56 | // user: 'git', 57 | // password: '', 58 | // pathname: '/IonicaBizau/git-stats.git', 59 | // hash: '', 60 | // search: '', 61 | // href: 'git@github.com:IonicaBizau/git-stats.git', 62 | // query: {} 63 | // } 64 | -------------------------------------------------------------------------------- /index.d.mts: -------------------------------------------------------------------------------- 1 | import parseUrl = require("./index"); 2 | 3 | export type ParsedUrl = parseUrl.ParsedUrl; 4 | export type NormalizeOptions = parseUrl.NormalizeOptions; 5 | export type ParsingError = parseUrl.ParsingError; 6 | 7 | export default parseUrl; 8 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import parsePath = require("parse-path"); 2 | import normalizeUrl = require("normalize-url"); 3 | 4 | declare namespace parseUrl { 5 | const MAX_INPUT_LENGTH: 2048; 6 | 7 | type NormalizeOptions = normalizeUrl.Options; 8 | 9 | type ParsedUrl = parsePath.ParsedPath; 10 | 11 | interface ParsingError extends Error { 12 | readonly subject_url: string; 13 | } 14 | } 15 | 16 | declare function parseUrl( 17 | url: string, 18 | normalize?: boolean | parseUrl.NormalizeOptions 19 | ): parseUrl.ParsedUrl; 20 | 21 | export = parseUrl; 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-url", 3 | "version": "10.0.0", 4 | "description": "An advanced url parser supporting git urls too.", 5 | "main": "./dist/index.js", 6 | "module": "./dist/index.mjs", 7 | "types": "./index.d.ts", 8 | "exports": { 9 | "require": { 10 | "types": "./index.d.ts", 11 | "default": "./dist/index.js" 12 | }, 13 | "import": { 14 | "types": "./index.d.mts", 15 | "default": "./dist/index.mjs" 16 | } 17 | }, 18 | "directories": { 19 | "example": "example", 20 | "test": "test" 21 | }, 22 | "scripts": { 23 | "test": "node test/index.mjs && tsd", 24 | "build": "pkgroll" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/IonicaBizau/parse-url.git" 29 | }, 30 | "keywords": [ 31 | "parse", 32 | "url", 33 | "node", 34 | "git", 35 | "advanced" 36 | ], 37 | "author": "Ionică Bizău (https://ionicabizau.net)", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/IonicaBizau/parse-url/issues" 41 | }, 42 | "homepage": "https://github.com/IonicaBizau/parse-url", 43 | "devDependencies": { 44 | "normalize-url": "^8.0.1", 45 | "pkgroll": "^2.12.2", 46 | "tester": "^1.4.6", 47 | "tsd": "^0.32.0" 48 | }, 49 | "dependencies": { 50 | "parse-path": "^7.1.0" 51 | }, 52 | "files": [ 53 | "bin/", 54 | "app/", 55 | "lib/", 56 | "dist/", 57 | "src/", 58 | "scripts/", 59 | "resources/", 60 | "menu/", 61 | "cli.js", 62 | "index.js", 63 | "index.d.ts", 64 | "package-lock.json", 65 | "bloggify.js", 66 | "bloggify.json", 67 | "bloggify/" 68 | ], 69 | "engines": { 70 | "node": ">=14.13.0" 71 | }, 72 | "blah": { 73 | "description": [ 74 | "For low-level path parsing, check out [`parse-path`](https://github.com/IonicaBizau/parse-path). This very module is designed to parse urls. By default the urls are normalized." 75 | ] 76 | }, 77 | "tsd": { 78 | "directory": "test" 79 | } 80 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | 3 | import normalizeUrl from "normalize-url"; 4 | import parsePath from "parse-path"; 5 | 6 | /** 7 | * parseUrl 8 | * Parses the input url. 9 | * 10 | * **Note**: This *throws* if invalid urls are provided. 11 | * 12 | * @name parseUrl 13 | * @function 14 | * @param {String} url The input url. 15 | * @param {Boolean|Object} normalize Whether to normalize the url or not. 16 | * Default is `false`. If `true`, the url will 17 | * be normalized. If an object, it will be the 18 | * options object sent to [`normalize-url`](https://github.com/sindresorhus/normalize-url). 19 | * 20 | * For SSH urls, normalize won't work. 21 | * 22 | * @return {Object} An object containing the following fields: 23 | * 24 | * - `protocols` (Array): An array with the url protocols (usually it has one element). 25 | * - `protocol` (String): The first protocol, `"ssh"` (if the url is a ssh url) or `"file"`. 26 | * - `port` (String): The domain port. 27 | * - `resource` (String): The url domain (including subdomains). 28 | * - `host` (String): The fully qualified domain name of a network host, or its IP address. 29 | * - `user` (String): The authentication user (usually for ssh urls). 30 | * - `pathname` (String): The url pathname. 31 | * - `hash` (String): The url hash. 32 | * - `search` (String): The url querystring value. 33 | * - `href` (String): The input url. 34 | * - `query` (Object): The url querystring, parsed as object. 35 | * - `parse_failed` (Boolean): Whether the parsing failed or not. 36 | */ 37 | const parseUrl = (url, normalize = false) => { 38 | 39 | // Constants 40 | /** 41 | * ([a-zA-Z_][a-zA-Z0-9_-]{0,31}) Try to match the user 42 | * ([\w\.\-@]+) Match the host/resource 43 | * (([\~,\.\w,\-,\_,\/,\s]|%[0-9A-Fa-f]{2})+?(?:\.git|\/)?) Match the path, allowing spaces/white 44 | */ 45 | const GIT_RE = /^(?:([a-zA-Z_][a-zA-Z0-9_-]{0,31})@|https?:\/\/)([\w\.\-@]+)[\/:](([\~,\.\w,\-,\_,\/,\s]|%[0-9A-Fa-f]{2})+?(?:\.git|\/)?)$/; 46 | 47 | const throwErr = msg => { 48 | const err = new Error(msg) 49 | err.subject_url = url 50 | throw err 51 | } 52 | 53 | if (typeof url !== "string" || !url.trim()) { 54 | throwErr("Invalid url.") 55 | } 56 | 57 | if (url.length > parseUrl.MAX_INPUT_LENGTH) { 58 | throwErr("Input exceeds maximum length. If needed, change the value of parseUrl.MAX_INPUT_LENGTH.") 59 | } 60 | 61 | if (normalize) { 62 | if (typeof normalize !== "object") { 63 | normalize = { 64 | stripHash: false 65 | } 66 | } 67 | url = normalizeUrl(url, normalize) 68 | } 69 | 70 | const parsed = parsePath(url) 71 | 72 | // Potential git-ssh urls 73 | if (parsed.parse_failed) { 74 | const matched = parsed.href.match(GIT_RE) 75 | 76 | if (matched) { 77 | parsed.protocols = ["ssh"] 78 | parsed.protocol = "ssh" 79 | parsed.resource = matched[2] 80 | parsed.host = matched[2] 81 | parsed.user = matched[1] 82 | parsed.pathname = `/${matched[3]}` 83 | parsed.parse_failed = false 84 | } else { 85 | throwErr("URL parsing failed.") 86 | } 87 | } 88 | 89 | return parsed; 90 | } 91 | 92 | parseUrl.MAX_INPUT_LENGTH = 2048 93 | 94 | export default parseUrl; 95 | -------------------------------------------------------------------------------- /test/index.mjs: -------------------------------------------------------------------------------- 1 | // Dependencies 2 | 3 | import normalizeUrl from "normalize-url"; 4 | import parseUrl from "../dist/index.js"; 5 | import tester from "tester"; 6 | 7 | const INPUTS = [ 8 | [ 9 | "http://ionicabizau.net/blog" 10 | , { 11 | protocols: [ "http" ] 12 | , protocol: "http" 13 | , port: "" 14 | , resource: "ionicabizau.net" 15 | , host: "ionicabizau.net" 16 | , user: "" 17 | , pathname: "/blog" 18 | , hash: "" 19 | , search: "" 20 | , query: {} 21 | , parse_failed: false 22 | } 23 | ] 24 | , [ 25 | "//ionicabizau.net/foo.js" 26 | , { 27 | protocols: ["http"] 28 | , protocol: "http" 29 | , port: "" 30 | , resource: "ionicabizau.net" 31 | , host: "ionicabizau.net" 32 | , user: "" 33 | , pathname: "/foo.js" 34 | , hash: "" 35 | , search: "" 36 | , query: {} 37 | , parse_failed: false 38 | } 39 | ] 40 | , [ 41 | "http://domain.com/path/name#some-hash?foo=bar" 42 | , { 43 | protocols: ["http"] 44 | , protocol: "http" 45 | , port: "" 46 | , resource: "domain.com" 47 | , host: "domain.com" 48 | , user: "" 49 | , pathname: "/path/name" 50 | , hash: "some-hash?foo=bar" 51 | , search: "" 52 | , query: {} 53 | , parse_failed: false 54 | } 55 | ] 56 | , [ 57 | ["git+ssh://git@host.xz/path/name.git", false] 58 | , { 59 | protocols: ["git", "ssh"] 60 | , protocol: "git" 61 | , port: "" 62 | , resource: "host.xz" 63 | , host: "host.xz" 64 | , user: "git" 65 | , pathname: "/path/name.git" 66 | , hash: "" 67 | , search: "" 68 | , query: {} 69 | , parse_failed: false 70 | } 71 | ] 72 | , [ 73 | ["git@github.com:IonicaBizau/git-stats.git", false] 74 | , { 75 | protocols: ["ssh"] 76 | , protocol: "ssh" 77 | , port: "" 78 | , resource: "github.com" 79 | , host: "github.com" 80 | , user: "git" 81 | , pathname: "/IonicaBizau/git-stats.git" 82 | , hash: "" 83 | , search: "" 84 | , query: {} 85 | , parse_failed: false 86 | } 87 | ] 88 | , [ 89 | ["http://ionicabizau.net/with-true-normalize", true] 90 | , { 91 | protocols: [ "http" ] 92 | , protocol: "http" 93 | , port: "" 94 | , resource: "ionicabizau.net" 95 | , host: "ionicabizau.net" 96 | , user: "" 97 | , pathname: "/with-true-normalize" 98 | , hash: "" 99 | , search: "" 100 | , query: {} 101 | , parse_failed: false 102 | } 103 | ] 104 | , [ 105 | ["file:///etc/passwd?#http://a:1:1", false] 106 | , { 107 | protocols: [ "file" ] 108 | , protocol: "file" 109 | , port: "" 110 | , resource: "" 111 | , host: "" 112 | , user: "" 113 | , pathname: "/etc/passwd" 114 | , hash: "http://a:1:1" 115 | , search: "" 116 | , query: {} 117 | , parse_failed: false 118 | } 119 | ] 120 | , [ 121 | ["git@github.my-enterprise.com:my-org/my-repo.git", false], 122 | { 123 | protocols: [ 'ssh' ] 124 | , protocol: 'ssh' 125 | , port: '' 126 | , resource: 'github.my-enterprise.com' 127 | , host: 'github.my-enterprise.com' 128 | , user: 'git' 129 | , password: '' 130 | , pathname: '/my-org/my-repo.git' 131 | , hash: '' 132 | , search: '' 133 | , query: {} 134 | , parse_failed: false 135 | } 136 | ] 137 | , [ 138 | ["org-12345678@github.my-enterprise.com:my-org/my-repo.git", false], 139 | { 140 | protocols: [ 'ssh' ] 141 | , protocol: 'ssh' 142 | , port: '' 143 | , resource: 'github.my-enterprise.com' 144 | , host: 'github.my-enterprise.com' 145 | , user: 'org-12345678' 146 | , password: '' 147 | , pathname: '/my-org/my-repo.git' 148 | , hash: '' 149 | , search: '' 150 | , query: {} 151 | , parse_failed: false 152 | } 153 | ] 154 | , [ 155 | ["git@github.com:halup/Cloud.API.Gateway.git", false] 156 | , { 157 | protocols: [ "ssh" ] 158 | , protocol: "ssh" 159 | , port: "" 160 | , resource: "github.com" 161 | , host: "github.com" 162 | , user: "git" 163 | , pathname: "/halup/Cloud.API.Gateway.git" 164 | , hash: "" 165 | , search: "" 166 | , query: {} 167 | , parse_failed: false 168 | } 169 | ], 170 | [ 171 | [ 172 | "git@ssh.dev.azure.com:v3/ORG/My-Project/repo", 173 | false, 174 | ], 175 | { 176 | protocols: ["ssh"], 177 | protocol: "ssh", 178 | port: "", 179 | resource: "ssh.dev.azure.com", 180 | host: "ssh.dev.azure.com", 181 | user: "git", 182 | password: "", 183 | pathname: "/v3/ORG/My-Project/repo", 184 | hash: "", 185 | search: "", 186 | query: {}, 187 | parse_failed: false, 188 | }, 189 | ], 190 | [ 191 | [ 192 | "git@ssh.dev.azure.com:v3/ORG/My%20Project/repo", 193 | false, 194 | ], 195 | { 196 | protocols: ["ssh"], 197 | protocol: "ssh", 198 | port: "", 199 | resource: "ssh.dev.azure.com", 200 | host: "ssh.dev.azure.com", 201 | user: "git", 202 | password: "", 203 | pathname: "/v3/ORG/My%20Project/repo", 204 | hash: "", 205 | search: "", 206 | query: {}, 207 | parse_failed: false, 208 | }, 209 | ], 210 | [ 211 | [ 212 | "git@ssh.dev.azure.com:v3/ORG/My Project/repo", 213 | false, 214 | ], 215 | { 216 | protocols: ["ssh"], 217 | protocol: "ssh", 218 | port: "", 219 | resource: "ssh.dev.azure.com", 220 | host: "ssh.dev.azure.com", 221 | user: "git", 222 | password: "", 223 | pathname: "/v3/ORG/My Project/repo", 224 | hash: "", 225 | search: "", 226 | query: {}, 227 | parse_failed: false, 228 | }, 229 | ], 230 | [ 231 | [ 232 | "git@ssh.dev.azure.com:v3/ORG/My-Project/repo", 233 | false, 234 | ], 235 | { 236 | protocols: ["ssh"], 237 | protocol: "ssh", 238 | port: "", 239 | resource: "ssh.dev.azure.com", 240 | host: "ssh.dev.azure.com", 241 | user: "git", 242 | password: "", 243 | pathname: "/v3/ORG/My-Project/repo", 244 | hash: "", 245 | search: "", 246 | query: {}, 247 | parse_failed: false, 248 | }, 249 | ], 250 | [ 251 | [ 252 | "ORG@vs-ssh.visualstudio.com:v3/ORG/My-Project/repo", 253 | false, 254 | ], 255 | { 256 | protocols: ["ssh"], 257 | protocol: "ssh", 258 | port: "", 259 | resource: "vs-ssh.visualstudio.com", 260 | host: "vs-ssh.visualstudio.com", 261 | user: "ORG", 262 | password: "", 263 | pathname: "/v3/ORG/My-Project/repo", 264 | hash: "", 265 | search: "", 266 | query: {}, 267 | parse_failed: false, 268 | }, 269 | ], 270 | [ 271 | [ 272 | "https://ORG@dev.azure.com/ORG/My%20Project/_git/repo", 273 | false, 274 | ], 275 | { 276 | protocols: ["https"], 277 | protocol: "https", 278 | port: "", 279 | resource: "dev.azure.com", 280 | host: "dev.azure.com", 281 | user: "ORG", 282 | password: "", 283 | pathname: "/ORG/My%20Project/_git/repo", 284 | hash: "", 285 | search: "", 286 | query: {}, 287 | parse_failed: false, 288 | }, 289 | ], 290 | [ 291 | [ 292 | "https://ORG@dev.azure.com/ORG/My-Project/_git/repo", 293 | false, 294 | ], 295 | { 296 | protocols: ["https"], 297 | protocol: "https", 298 | port: "", 299 | resource: "dev.azure.com", 300 | host: "dev.azure.com", 301 | user: "ORG", 302 | password: "", 303 | pathname: "/ORG/My-Project/_git/repo", 304 | hash: "", 305 | search: "", 306 | query: {}, 307 | parse_failed: false, 308 | }, 309 | ], 310 | ]; 311 | 312 | tester.describe("check urls", test => { 313 | INPUTS.forEach(function (c) { 314 | let url = Array.isArray(c[0]) ? c[0][0] : c[0] 315 | test.should("support " + url, () => { 316 | const res = parseUrl(url, c[0][1] !== false); 317 | 318 | if (c[0][1] !== false) { 319 | url = normalizeUrl(url, { 320 | stripHash: false 321 | }) 322 | } 323 | 324 | c[1].href = c[1].href || url 325 | c[1].password = c[1].password || "" 326 | test.expect(res).toEqual(c[1]); 327 | }); 328 | }); 329 | 330 | test.should("throw if url is empty", () => { 331 | test.expect(() => { 332 | parseUrl("") 333 | }).toThrow(/invalid url/i) 334 | }) 335 | 336 | test.should("throw if url is too long", () => { 337 | parseUrl.MAX_INPUT_LENGTH = 10 338 | test.expect(() => { 339 | parseUrl("https://domain.com/") 340 | }).toThrow(/input exceeds maximum length/i) 341 | }) 342 | 343 | test.should("throw if url is invalid", () => { 344 | test.expect(() => { 345 | parseUrl("foo") 346 | }).toThrow(/url parsing failed/i) 347 | }) 348 | }); 349 | -------------------------------------------------------------------------------- /test/index.test-d.ts: -------------------------------------------------------------------------------- 1 | import { expectType } from "tsd"; 2 | import parseUrl from "../index"; 3 | 4 | // test type exports 5 | type PU = parseUrl.ParsedUrl; 6 | type NO = parseUrl.NormalizeOptions; 7 | type Err = parseUrl.ParsingError; 8 | 9 | expectType(parseUrl("http://ionicabizau.net/blog")); 10 | expectType(parseUrl("http://ionicabizau.net/blog", true)); 11 | expectType( 12 | parseUrl("http://ionicabizau.net/blog", { stripHash: true }) 13 | ); 14 | 15 | declare const err: parseUrl.ParsingError; 16 | expectType(err.subject_url); 17 | 18 | expectType<2048>(parseUrl.MAX_INPUT_LENGTH); 19 | --------------------------------------------------------------------------------